rpg-dialogue-js
Version:
A simple roleplay game dialogue engine and editor.
429 lines (398 loc) • 13.6 kB
text/typescript
/**
* @author Ikaros Kappler
* @date 2013-11-27
* @modified 2014-04-05 Ikaros Kappler (member array outerTilePolygons added).
* @modified 2015-03-19 Ikaros Kappler (added toSVG()).
* @modified 2020-10-30 Refactored the this super class to work with PlotBoilerplate.
* @modified 2020-11-11 Ported the class from vanilla JS to TypeScript.
* @version 2.0.1-alpha
* @name GirihTile
**/
import { Bounds } from "../../Bounds";
import { Line } from "../../Line";
import { Polygon } from "../../Polygon";
import { Vertex } from "../../Vertex";
import { XYCoords } from "../../interfaces";
export enum TileType {
UNKNOWN = "UNKNOWN",
DECAGON = "DECAGON",
PENTAGON = "PENTAGON",
IRREGULAR_HEXAGON = "IRREGULAR_HEXAGON",
RHOMBUS = "RHOMBUS",
BOW_TIE = "BOW_TIE",
// This is not part of the actual girih tile set!
PENROSE_RHOMBUS = "PENROSE_RHOMBUS"
}
export interface IAdjacency {
edgeIndex: number;
offset: XYCoords;
}
/**
* @classdesc This is a general tile superclass. All other tile classes extends this one.
*
* Rule:
* * the outer and the inner sub polygons must be inside the main polygon's bounds.
*
* @requires Bounds
* @requires Polyon
* @requires Vertex
* @requires XYCoords
*/
export abstract class GirihTile extends Polygon {
/**
* The center of this tile.
*
* @name position
* @member {Vertex}
* @memberof GirihTile
* @type {Vertex}
* @instance
*/
public position: Vertex;
/**
* The edge length of this tile (all edges of a Girih tile have same length).
*
* @name edgeLength
* @member {number}
* @memberof GirihTile
* @type {number}
* @readonly
* @instance
*/
public readonly edgeLength: number;
/**
* The rotation of this tile. This is stored to make cloning easier.
*
* @name rotation.
* @member {number}
* @memberof GirihTile
* @type {number}
* @instance
*/
public rotation: number;
/**
* The symmetry (=order) of this tile. This is the number of steps used for a full
* rotation (in this Girih case: 10). Future Girih implementations might have other symmetries.
*
* @name symmetry
* @member {number}
* @memberof GirihTile
* @type {number}
* @instance
*/
public symmetry: number; // Todo: rename to 'symmetries'?
/**
* The unique symmetries. This must be an nth part of the global `symmetry`.
* Rotating this tile `uniqueSymmetries' times results in the same visual tile (flipped around
* a symmetry axis).
*
* @name uniqueSymmetries
* @member {number}
* @memberof GirihTile
* @type {number}
* @instance
*/
public uniqueSymmetries: number;
/**
* The inner tile polygons.
*
* @name innerTilePolygons
* @member {Array<Polygon>}
* @memberof GirihTile
* @type {Array<Polygon>}
* @instance
*/
public innerTilePolygons: Array<Polygon>;
/**
* The outer tile polygons.
*
* @name outerTilePolygons
* @member {Array<Polygon>}
* @memberof GirihTile
* @type {Array<Polygon>}
* @instance
*/
public outerTilePolygons: Array<Polygon>;
/**
* An identifier for the tile type.
*
* @name tileType
* @member {TileType}
* @memberof GirihTile
* @type {TileType}
* @instance
*/
public tileType: TileType;
/**
* The initial bounds (of the un-rotated tile). These are required to calculate the
* correct texture mapping.
*
* @name baseBounds
* @member {Bounds}
* @memberof GirihTile
* @type {Bounds}
* @instance
*/
public baseBounds: Bounds;
/**
* A rectangle on the shipped texture image (`girihtexture-500px.png`) marking the
* texture position. The bounds are relative, so each component must be in [0..1].
* The texture is a square.
*
* @name textureSource
* @member {Bounds}
* @memberof GirihTile
* @type {Bounds}
* @instance
*/
public readonly textureSource: Bounds;
/**
* An epsilon to use for detecting adjacent edges. 0.001 seems to be a good value.
* Adjust if needed.
*
* @name epsilon
* @member {number}
* @memberof GirihTile
* @type {number}
* @static
*/
public static epsilon: number = 0.001;
/**
* The default edge length.
*
* @name DEFAULT_EDGE_LENGTH
* @member {number}
* @memberof GirihTile
* @type {number}
* @readonly
* @static
*/
public static readonly DEFAULT_EDGE_LENGTH: number = 58;
/**
* @constructor
* @memberof GirihTile
* @abstract class
* @param {Vertex} position - The position of the tile.
* @param {number} edgeLength - The edge length (default is GirihTile.DEFAULT_EDGE_LENGTH).
* @param {TileType} tileType - One of `TileType.*` enum members.
**/
constructor(position: Vertex, edgeLength?: number, tileType?: TileType) {
super([], false); // vertices, isOpen
if (typeof edgeLength === "undefined") edgeLength = GirihTile.DEFAULT_EDGE_LENGTH;
if (typeof tileType == "undefined") tileType = TileType.UNKNOWN;
this.edgeLength = edgeLength;
this.position = position;
this.rotation = 0.0; // angle;
this.symmetry = 10;
this.uniqueSymmetries = 10;
// An array of polygons.
// The inner tile polygons are those that do not share edges with the outer
// tile bounds (vertices are OK).
this.innerTilePolygons = [];
// A second array of polygons.
// The outer tile polygons are those that make up the whole tile area
// _together with the inner tile polygons (!)_; the union of the
// inner tile polygons and the outer tile polygons covers exactly
// the whole tile polygon.
// The intersection of both sets is empty.
// Outer tile polygon share at least one (partial) edge with the complete
// tile polygon (length > 0).
this.outerTilePolygons = [];
// this.imageProperties = null;
this.textureSource = new Bounds(new Vertex(), new Vertex());
this.tileType = tileType;
}
/**
* @abstract Subclasses must override this.
*/
abstract clone(): GirihTile;
/**
* Move this tile around (together will all inner polygons).
* As this function overrides Polygon.move(...), the returned
* instance (this) must be of type `Polygon`.
*
* @name move
* @instance
* @override
* @memberof GirihTile
* @param {XYCoords} amount
* @return {Polygon} this
*/
move(amount: XYCoords): Polygon {
super.move.call(this, amount);
for (var i in this.innerTilePolygons) this.innerTilePolygons[i].move(amount);
for (var i in this.outerTilePolygons) this.outerTilePolygons[i].move(amount);
this.position.add(amount);
return this;
}
/**
* Find the adjacent tile (given by the template tile)
* Note that the tile itself will be modified (rotated and moved to the correct position).
*
* @name findAdjacentTilePosition
* @memberof GirihTile
* @instance
* @param {number} edgeIndex - The edge number of the you you want to find adjacency for.
* @param {Polygon} tile - The polygon (or tile) you want to find adjacency for at the specified edge.
* @return {IAdjacency|null} Adjacency information or null if the passed tile does not match.
*/
findAdjacentTilePosition(edgeIndex: number, tile: Polygon): IAdjacency | null {
const edgeA: Line = new Line(
this.vertices[edgeIndex % this.vertices.length],
this.vertices[(edgeIndex + 1) % this.vertices.length]
);
// Find adjacent edge
for (var i = 0; i < tile.vertices.length; i++) {
const edgeB: Line = new Line(
tile.vertices[i % tile.vertices.length].clone(),
tile.vertices[(i + 1) % tile.vertices.length].clone()
);
// Goal: edgeA.a==edgeB.b && edgeA.b==edgeB.a
// So move edgeB
const offset: XYCoords = edgeB.b.difference(edgeA.a);
edgeB.add(offset);
if (edgeB.a.distance(edgeA.b) < GirihTile.epsilon) {
return { edgeIndex: i, offset: offset } as IAdjacency;
}
}
return null;
}
/**
* Find all possible adjacent tile positions (and rotations) for `neighbourTile`.
*
* @name transformTileToAdjacencies
* @memberof GirihTile
* @instance
* @param {number} baseEdgeIndex - The edge number of the you you want to find adjacencies for.
* @param {GirihTile} neighbourTile - The polygon (or tile) you want to find adjacencies for at the specified edge.
* @return {IAdjacency|null} Adjacency information or null if the passed tile does not match.
*/
transformTileToAdjacencies(baseEdgeIndex: number, neighbourTile: GirihTile): Array<GirihTile> {
// Find a rotation for that tile to match
let i: number = 0;
const foundAlignments: Array<GirihTile> = [];
let positionedTile: GirihTile | null = null;
while (i < neighbourTile.uniqueSymmetries) {
positionedTile = this.transformTilePositionToAdjacency(baseEdgeIndex, neighbourTile);
if (positionedTile != null) {
positionedTile = positionedTile.clone();
foundAlignments.push(positionedTile);
}
neighbourTile.rotate((Math.PI * 2) / neighbourTile.symmetry);
i++;
}
return foundAlignments;
}
/**
* Apply adjacent tile position to `neighbourTile`.
*
* @name transformTilePositionToAdjacencies
* @memberof GirihTile
* @instance
* @param {number} baseEdgeIndex - The edge number of the you you want to apply adjacent position for.
* @param {Polygon} neighbourTile - The polygon (or tile) you want to find adjacency for at the specified edge.
* @return {Polygon|null} the passed tile itself if adjacency was found, null otherwise.
*/
transformTilePositionToAdjacency<P extends Polygon>(baseEdgeIndex: number, neighbourTile: P): P | null {
// Find the position for that tile to match (might not exist)
// { edgeIndex:number, offset:XYCoords }
var adjacency = this.findAdjacentTilePosition(baseEdgeIndex, neighbourTile);
if (adjacency != null) {
neighbourTile.move(adjacency.offset);
return neighbourTile;
}
return null;
}
/**
* Get the inner tile polygon at the given index.
* This function applies MOD to the index.
*
* @name getInnerTilePolygonAt
* @instance
* @memberof GirihTile
* @param {number} index
* @return {Polygon} The sub polygon (inner tile) at the given index.
**/
getInnerTilePolygonAt(index: number): Polygon {
if (index < 0)
return this.innerTilePolygons[this.innerTilePolygons.length - (Math.abs(index) % this.innerTilePolygons.length)];
else return this.innerTilePolygons[index % this.innerTilePolygons.length];
}
/**
* Get the outer tile polygon at the given index.
* This function applies MOD to the index.
*
* @name getOuterTilePolygonAt
* @instance
* @memberof GirihTile
* @param {number} index
* @return {Polygon} The sub polygon (outer tile) at the given index.
**/
getOuterTilePolygonAt(index: number): Polygon {
if (index < 0)
return this.outerTilePolygons[this.outerTilePolygons.length - (Math.abs(index) % this.outerTilePolygons.length)];
else return this.outerTilePolygons[index % this.outerTilePolygons.length];
}
/**
* Rotate this tile
* Note: this function behaves a bitdifferent than the genuine Polygon.rotate function!
* Polygon has the default center of rotation at (0,0).
* The GirihTile rotates around its center (position) by default.
*
* @name rotate
* @instance
* @memberof GirihTile
* @param {number} angle - The angle to use for rotation.
* @param {Vertex?} center - The center of rotation (default is this.position).
* @return {Polygon} this
**/
rotate(angle: number, center?: Vertex): GirihTile {
if (typeof center === "undefined") center = this.position;
super.rotate(angle, center);
for (var i in this.innerTilePolygons) {
this.innerTilePolygons[i].rotate(angle, center);
}
for (var i in this.outerTilePolygons) {
this.outerTilePolygons[i].rotate(angle, center);
}
this.rotation += angle;
return this;
}
/**
* This function locates the closest tile edge (polygon edge)
* to the passed point.
*
* Currently the edge distance to a point is measured by the
* euclidian distance from the edge's middle point.
*
* Idea: move this function to Polygon?
*
* @name locateEdgeAtPoint
* @instance
* @memberof GirihTile
* @param {XYCoords} point - The point to detect the closest edge for.
* @param {number} tolerance - The tolerance (=max distance) the detected edge
* must be inside.
* @return {nmber} the edge index (index of the starting vertex, so [index,index+1] is the edge ) or -1 if not found.
**/
locateEdgeAtPoint(point: XYCoords, tolerance: number): number {
if (this.vertices.length == 0) return -1;
const middle: Vertex = new Vertex(0, 0);
let tmpDistance: number = 0;
let resultDistance: number = tolerance * 2; // definitely outside the tolerance :)
let resultIndex: number = -1;
for (var i = 0; i < this.vertices.length; i++) {
const vertI: Vertex = this.getVertexAt(i);
const vertJ: Vertex = this.getVertexAt(i + 1);
// Create a point in the middle of the edge
middle.x = vertI.x + (vertJ.x - vertI.x) / 2.0;
middle.y = vertI.y + (vertJ.y - vertI.y) / 2.0;
tmpDistance = middle.distance(point);
if (tmpDistance <= tolerance && (resultIndex == -1 || tmpDistance < resultDistance)) {
resultDistance = tmpDistance;
resultIndex = i;
}
}
return resultIndex;
}
} // END class