UNPKG

rpg-dialogue-js

Version:

A simple roleplay game dialogue engine and editor.

429 lines (398 loc) 13.6 kB
/** * @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