UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

452 lines (363 loc) • 12.4 kB
import { assert } from "../../../core/assert.js"; import Signal from "../../../core/events/signal/Signal.js"; import { noop } from "../../../core/function/noop.js"; import Vector2 from "../../../core/geom/Vector2.js"; import { computeTileGridMove } from "../../../engine/ui/tiles2d/computeTileGridMove.js"; import { DragAndDropContext } from '../../common/dnd/DragAndDropContext.js'; import dom from "../../DOM.js"; import View from "../../View.js"; import TileView from './Tile.js'; const slotElementPrototype = dom('div').addClass('marker').el; class SlotView extends View { constructor() { super(); this.el = slotElementPrototype.cloneNode(false); } } class TileGridView extends View { /** * @param {TileGrid} model * @param {DragAndDropContext} [dragAndDropContext] * @param {boolean} [enableDragAndDrop] * @param {Vector2} [tileSize] * @param {Vector2} [tileSpacing] * @param {function(View, Rectangle)} [hookTileAdded] * @param {function(View, Rectangle)} [hookTileRemoved] * @param {function()} [hookInvalidDrop] * @param {boolean} [captureEventTap] * @constructor */ constructor( { model, dragAndDropContext = new DragAndDropContext(), enableDragAndDrop = false, tileSize = new Vector2(50, 50), tileSpacing = Vector2.zero, hookTileAdded, hookTileRemoved, hookInvalidDrop = noop, captureEventTap = true, } ) { super(); /** * * @type {TileGrid} */ this.model = model; /** * * @type {DragAndDropContext} * @private */ this.__dragAndDropContext = dragAndDropContext; /** * * @type {boolean} * @private */ this.__enableDragAndDrop = enableDragAndDrop; /** * * @type {Vector2} * @private */ this.__tileSize = tileSize; /** * * @type {Vector2} * @private */ this.__tileSpacing = tileSpacing; /** * * @type {Function} * @private */ this.__hookTileAdded = hookTileAdded; /** * * @type {Function} * @private */ this.__hookTileRemoved = hookTileRemoved; /** * * @type {boolean} * @private */ this.__captureEventTap = captureEventTap; /** * * @type {function()} * @private */ this.__hookInvalidDrop = hookInvalidDrop; const dRoot = dom("div").addClass("ui-tile-grid-view"); this.el = dRoot.el; const dMarkers = dom('div').addClass('marker-container'); const vMarkers = new View(); this.__markerCountainer = vMarkers; vMarkers.el = dMarkers.el; const dTiles = dom('div').addClass('tile-container'); const vTiles = new View(); vTiles.el = dTiles.el; this.addChild(vMarkers); this.addChild(vTiles); this.on.tap = new Signal(); this.slots = []; //place markers this.generateSlots(model.size.x, model.size.y); model.tiles.forEach(this.addTile, this); model.tiles.on.added.add(this.addTile, this); model.tiles.on.removed.add(this.removeTile, this); const areaForTiles = TileView.calculateSize(tileSize, tileSpacing, model.size); this.size.copy(areaForTiles); } /** * * @param {Draggable} draggable * @param {number} cell_x * @param {number} cell_y * @return {boolean} * @private */ __validateDrop(draggable, cell_x, cell_y) { assert.defined(draggable, 'draggable'); assert.isNumber(cell_x, 'cell_x'); assert.isNumber(cell_y, 'cell_y'); /** * * @type {TileGrid} */ const model = this.model; const view = draggable.view; /** * @type {Rectangle} */ const tile = view.model; //check if tile will fit in the desired position if ( cell_x < 0 || tile.size.x + cell_x > model.size.x || cell_y < 0 || tile.size.y + cell_y > model.size.y ) { return false; } const domain = draggable.parent.domain; if (domain === this) { //all well return true; } if (view.lockDragContext === true) { //item is not allowed to move to another context return false; } /** * * @type {TileGrid} */ const targetGrid = model; /** * @type {TileGrid} */ const sourceGrid = domain.model; const overlappingTiles = []; const overlappingTileCount = model.getOverlappingTilesRaw(overlappingTiles, cell_x, cell_y, tile.size.x, tile.size.y); const tileArea = tile.computeArea(); //construct move plan const moveProgram = computeTileGridMove(tile, cell_x, cell_y, sourceGrid, targetGrid); if (moveProgram === null) { //move is impossible return false; } if (overlappingTileCount === 0) { // slot is unoccupied, validation depends on capacity constraint return model.capacity.getValue() >= model.computeTotalTileArea() + tileArea; } else { // slot is occupied // compute total overlap area let overlappingTileArea = 0; for (let j = 0; j < overlappingTileCount; j++) { const rectangle = overlappingTiles[j]; overlappingTileArea += rectangle.computeArea(); } if (model.capacity.getValue() < model.computeTotalTileArea() + tileArea - overlappingTileArea) { //grid can't take this tile return false; } if (sourceGrid.capacity.getValue() < sourceGrid.computeTotalTileArea() - tileArea + overlappingTileArea) { //old grid can't fit swapped tiles return false; } const instructions = moveProgram.instructions; const instructionCount = instructions.length; for (let i = 0; i < instructionCount; i++) { /** * * @type {TileMoveInstruction} */ const moveInstruction = instructions[i]; /** * * @type {Rectangle} */ const moveTile = moveInstruction.tile; if (moveTile.lockDragContext === true && moveInstruction.source !== moveInstruction.target) { //attempting to move a tile across contexts, but the tile doesn't allow that return false; } } //it's a tile swap, capacity will not be affected after transaction return true; } } /** * * @param {Draggable} draggable What was being dragged * @param {DropTarget} dropTarget Where the drop was attempted * @private */ __handleInvalidDrop(draggable, dropTarget) { this.__hookInvalidDrop(draggable, dropTarget); } __makeSlotView(x, y) { const v = new SlotView(); v.size.copy(this.__tileSize); const discretePosition = new Vector2(x, y); const pos = TileView.calculatePosition(this.__tileSize, this.__tileSpacing, discretePosition); v.position.copy(pos); /** * * @type {TileGrid} */ const model = this.model; const self = this; const validateDrop = (draggable) => { return this.__validateDrop(draggable, discretePosition.x, discretePosition.y); } if (this.__enableDragAndDrop) { /** * * @type {DropTarget} */ const target = this.__dragAndDropContext.addTarget(v, self, validateDrop); target.on.invalidDrop.add(this.__handleInvalidDrop, this); target.on.added.add(function (draggable, oldParent) { /** * @type {TileGrid} */ const otherGrid = oldParent.domain.model; const tile = draggable.view.model; const move = computeTileGridMove(tile, discretePosition.x, discretePosition.y, otherGrid, model); if (move !== null) { move.execute(); move.instructions.forEach(function (instruction) { const tile = instruction.tile; //re-introduce tiles to trigger various updates in visualization chain instruction.target.tiles.removeOneOf(tile); instruction.target.tiles.add(tile); }); } else { //move is not possible console.error('move is not possible'); } }); v.dropTarget = target; } return v; } /** * * @param {number} width * @param {number} height */ generateSlots(width, height) { let i = 0; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const v = this.__makeSlotView(x, y); this.__markerCountainer.addChild(v); this.slots[i++] = v; } } } /** * * @param {Rectangle} tile */ addTile(tile) { const tileView = new TileView({ model: tile, spacing: this.__tileSpacing, size: this.__tileSize }); if (typeof this.__hookTileAdded === "function") { this.__hookTileAdded(tileView, tile); } this.addChild(tileView); const tileViewEl = tileView.el; //sign up drag and drop if (this.__enableDragAndDrop) { const slot = this.getSlot(tile.position.x, tile.position.y); const dropTarget = slot.dropTarget; tileView.draggable = this.__dragAndDropContext.addElement(tileView, dropTarget); } if (this.__captureEventTap === true) { tileViewEl.addEventListener('click', (event) => { this.on.tap.dispatch(tile, tileView, event); }); } } /** * * @param {number} x * @param {number} y * @return {View} */ getSlot(x, y) { /** * * @type {TileGrid} */ const model = this.model; const size = model.size; return this.slots[size.x * y + x]; } /** * * @param {Rectangle} tile */ removeTile(tile) { const view = this.findTileView(tile); this.removeChild(view); if (typeof this.__hookTileRemoved === "function") { this.__hookTileRemoved(view, tile); } } /** * * @param {Rectangle} tile * @return {View} */ findTileView(tile) { const children = this.children; for (let i = 0; i < children.length; i++) { const view = children[i]; if (view.model === tile) { return view; } } } getChildByUUID(uuid) { const children = this.children; const numChildren = children.length; for (let i = 0; i < numChildren; i++) { const child = children[i]; if (child.uuid === uuid) { return child; } } //not found return null; } } export default TileGridView;