diff --git a/packages/phaser3-drag-select/src/js/lib/mouse-camera-drag.js b/packages/phaser3-drag-select/src/js/lib/mouse-camera-drag.js index b59d195..b752ef1 100644 --- a/packages/phaser3-drag-select/src/js/lib/mouse-camera-drag.js +++ b/packages/phaser3-drag-select/src/js/lib/mouse-camera-drag.js @@ -1,5 +1,7 @@ -import PluginConfig, { MOUSE_BUTTONS } from './plugin-config'; import Phaser from 'phaser'; +import { MOUSE_BUTTONS } from '@pixelburp/phaser3-utils'; + +import PluginConfig from './plugin-config'; const EMPTY_VALUES = [undefined, null]; diff --git a/packages/phaser3-drag-select/src/js/lib/mouse-interface.js b/packages/phaser3-drag-select/src/js/lib/mouse-interface.js index b619836..faef816 100644 --- a/packages/phaser3-drag-select/src/js/lib/mouse-interface.js +++ b/packages/phaser3-drag-select/src/js/lib/mouse-interface.js @@ -1,6 +1,8 @@ -import PluginConfig, { MOUSE_BUTTONS } from './plugin-config'; -import MouseCameraDrag from './mouse-camera-drag'; import Phaser from 'phaser'; +import { MOUSE_BUTTONS } from '@pixelburp/phaser3-utils'; + +import PluginConfig from './plugin-config'; +import MouseCameraDrag from './mouse-camera-drag'; const PREVENT_DEFAULT = e => e.preventDefault(); diff --git a/packages/phaser3-drag-select/src/js/lib/plugin-config.js b/packages/phaser3-drag-select/src/js/lib/plugin-config.js index e27476a..6836e7c 100644 --- a/packages/phaser3-drag-select/src/js/lib/plugin-config.js +++ b/packages/phaser3-drag-select/src/js/lib/plugin-config.js @@ -1,21 +1,6 @@ -const NOOP = () => {}; - -// 0: No button or un-initialized -// 1: Left button -// 2: Right button -// 4: Wheel button or middle button -// 8: 4th button (typically the "Browser Back" button) -// 16: 5th button (typically the "Browser Forward" button) -export const MOUSE_BUTTONS = { - NO_BUTTON: 0, - LEFT: 1, - RIGHT: 2, - MIDDLE: 4, - FOURTH_BUTTON: 8, - FIFTH_BUTTON: 16, -}; +import { MOUSE_BUTTONS } from '@pixelburp/phaser3-utils'; -const MOUSE_BUTTONS_VALUES = Object.values(MOUSE_BUTTONS); +const NOOP = () => {}; export const IS_INTERACTIVE_CHILD = child => child.input?.enabled; diff --git a/packages/phaser3-formation-generator/src/assets/disabled-sprite-50x50.png b/packages/phaser3-formation-generator/src/assets/disabled-sprite-50x50.png new file mode 100644 index 0000000..46aa536 Binary files /dev/null and b/packages/phaser3-formation-generator/src/assets/disabled-sprite-50x50.png differ diff --git a/packages/phaser3-formation-generator/src/assets/enabled-sprite-50x50.png b/packages/phaser3-formation-generator/src/assets/enabled-sprite-50x50.png new file mode 100644 index 0000000..7cfd425 Binary files /dev/null and b/packages/phaser3-formation-generator/src/assets/enabled-sprite-50x50.png differ diff --git a/packages/phaser3-formation-generator/src/demo/scene/demo-scene.js b/packages/phaser3-formation-generator/src/demo/scene/demo-scene.js index 7697591..80775c3 100644 --- a/packages/phaser3-formation-generator/src/demo/scene/demo-scene.js +++ b/packages/phaser3-formation-generator/src/demo/scene/demo-scene.js @@ -1,5 +1,5 @@ import Phaser, { Scene } from 'phaser'; -import { forEach } from '@pixelburp/phaser3-utils'; +import { forEach, MOUSE_BUTTONS } from '@pixelburp/phaser3-utils'; import FormationGeneratorPlugin from 'src/js/formation-generator-plugin'; @@ -7,6 +7,7 @@ const { KeyCodes } = Phaser.Input.Keyboard; export default class DemoScene extends Scene { fpsText; + mySprites; constructor() { super({ key: 'DemoScene', active: true }); @@ -31,18 +32,71 @@ export default class DemoScene extends Scene { this.myControls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig); } + createInputEvents() { + const { formationMovement, mySprites } = this; + + this.input.on('pointerdown', () => { + console.log('mySprites', mySprites); + formationMovement.calculate(mySprites, { + maxCols: 15, + minCols: 3 + }); + }); + } + + createSprites() { + let sprite, setAsInteractive, y, spriteKey, worldX, worldY; + this.mySprites = []; + let x = 1; + const length = 5; + const OFFSET = 200; + + for (x; x <= length; x += 1) { + y = 1; + for (y; y <= length; y += 1) { + // Every even numbered item will be set as "interactive" + setAsInteractive = x % 2 === 0; + spriteKey = setAsInteractive ? 'enabled-sprite' : 'disabled-sprite'; + worldX = x * 100 + OFFSET; + worldY = y * 100 + OFFSET; + sprite = new Phaser.GameObjects.Sprite(this, worldX, worldY, spriteKey); + sprite.isSelected = setAsInteractive; // @TODO temp setting as "selected" + + if (setAsInteractive) { + sprite.setInteractive(); + } + + this.add.existing(sprite); + this.mySprites.push(sprite); + } + } + } + preload() { + this.load.image('disabled-sprite', 'src/assets/disabled-sprite-50x50.png'); + this.load.image('enabled-sprite', 'src/assets/enabled-sprite-50x50.png'); } create() { + const { cameras } = this; + const mainCamera = cameras.main; + console.log('DemoScene scene', this); this.formationMovement = this.plugins.start('FormationGeneratorPlugin', 'formationMovement'); + this.formationMovement.setup({ + camera: mainCamera, + gridSize: 64, + scene: this, + mouseClickToTrack: MOUSE_BUTTONS.RIGHT, + }); this.fpsText = this.add.text(10, 10, ''); this.fpsText.setScrollFactor(0); this.createCamera(); + this.createSprites(); + this.createInputEvents(); } update(time, delta) { diff --git a/packages/phaser3-formation-generator/src/js/formation-generator-plugin.js b/packages/phaser3-formation-generator/src/js/formation-generator-plugin.js index b77b096..96c4690 100644 --- a/packages/phaser3-formation-generator/src/js/formation-generator-plugin.js +++ b/packages/phaser3-formation-generator/src/js/formation-generator-plugin.js @@ -1,4 +1,8 @@ import Phaser from 'phaser'; +import { createInterfaceScene } from '@pixelburp/phaser3-utils'; + +import PluginConfig from './lib/plugin-config'; +import MouseInterface from './lib/mouse-interface'; /** * @class FormationGeneratorPlugin @@ -7,20 +11,20 @@ import Phaser from 'phaser'; */ export default class FormationGeneratorPlugin extends Phaser.Plugins.BasePlugin { + interfaceScene; + mouseInterface; + /** * @method setup */ - setup(scene, config = {}) { + setup(config = {}) { + this.setConfig(config); + console.log('config', PluginConfig); + this.createInterfaceScene(); } - stop() { - super.stop(); - console.warn('Plugin stopped'); - } - - start() { - super.start(); - console.warn('Plugin started'); + get scenePlugin() { + return PluginConfig.get('scene')?.scene; } /** @@ -36,4 +40,39 @@ export default class FormationGeneratorPlugin extends Phaser.Plugins.BasePlugin */ enable() { } + + /** + * @method setConfig + * @description Updates the plugin's configuration with new values + * @param {Object} config - new configuration object + */ + setConfig(config = {}) { + PluginConfig.setConfig(config); + } + + createInterfaceScene() { + const scenePlugin = this.scenePlugin; + this.interfaceScene = createInterfaceScene(scenePlugin, this); + this.interfaceScene.enable(); + + if (this.mouseInterface) { + this.mouseInterface.destroy(); + } + this.mouseInterface = new MouseInterface(this.interfaceScene, this); + } + + calculate(sprites = [], config) { + console.log('formationMovement', sprites); + this.mouseInterface?.calculate(sprites, config); + } + + stop() { + super.stop(); + console.warn('Plugin stopped'); + } + + start() { + super.start(); + console.warn('Plugin started'); + } } diff --git a/packages/phaser3-formation-generator/src/js/lib/mouse-interface.js b/packages/phaser3-formation-generator/src/js/lib/mouse-interface.js new file mode 100644 index 0000000..f2ba4ce --- /dev/null +++ b/packages/phaser3-formation-generator/src/js/lib/mouse-interface.js @@ -0,0 +1,280 @@ +import Phaser from 'phaser'; +import { forEach, MOUSE_BUTTONS, sortClockwise } from '@pixelburp/phaser3-utils'; +import PluginConfig from './plugin-config'; +import Tile from './tile'; + +const PREVENT_DEFAULT = e => e.preventDefault(); +const DEFAULT_GRID_CONFIG = { + maxCols: 0, + minCols: 0 +}; +const NINETY_DEGREES_AS_RADIANS = Math.PI / 2; +const TEST_RADIUS = 16; +const TEST_COLOR = 0xff00ff; + +const FLOOD_DIRECTIONS = { + NORTH: [0, -1], + // NORTH_WEST: [-1, -1], + // NORTH_EAST: [1, -1], + SOUTH: [0, 1], + // SOUTH_WEST: [-1, 1], + // SOUTH_EAST: [1, 1], + WEST: [-1, 0], + EAST: [1, 0] +}; + +function flood_fill(pos_x, pos_y, target_color, color) { + + // if there is no wall or if i haven't been there, already go back + if(a[pos_x][pos_y] == wall || a[pos_x][pos_y] == color) { + return; + } + + if(a[pos_x][pos_y] != target_color) // if it's not color go back + return; + + a[pos_x][pos_y] = color; // mark the point so that I know if I passed through it. + + flood_fill(pos_x + 1, pos_y, color); // then i can either go south + flood_fill(pos_x - 1, pos_y, color); // or north + flood_fill(pos_x, pos_y + 1, color); // or east + flood_fill(pos_x, pos_y - 1, color); // or west + + return; + +} + +export default class MouseInterface extends Phaser.GameObjects.Graphics { + isDisabled = false; + isDragging = false; + isMouseDown = false; + + start = new Tile(); + end = new Tile(); + gridConfig = DEFAULT_GRID_CONFIG; + targetChildren = []; + previews = []; + + constructor(scene) { + super(scene); + + scene.add.existing(this); + this.enableAllEvents(); + } + + get targetScene() { + return PluginConfig.get('scene'); + } + + getIsValidClickToTrack = button => { + const mouseClickToTrack = PluginConfig.get('mouseClickToTrack'); + return button === mouseClickToTrack; + }; + + disable() { + this.isDisabled = true; + this.isDragging = false; + this.isMouseDown = false; + + this.start.reset(); + this.end.reset(); + this.clear(); + + this.disableAllEvents(); + } + + enable() { + this.isDisabled = false; + this.disableAllEvents(); + this.enableAllEvents(); + } + + disableAllEvents() { + const { scene } = this; + this.enableRightClick(false); + + scene.input.off('pointerup', this.onPointerUp); + scene.input.off('pointermove', this.onPointerMove); + scene.input.off('gameout', this.onGameOut); + } + + enableAllEvents() { + const mouseClickToTrack = PluginConfig.get('mouseClickToTrack'); + this.enableRightClick(mouseClickToTrack === MOUSE_BUTTONS.RIGHT); + this.scene.input.on('gameout', this.onGameOut); + } + + enableRightClick(enable = true) { + const { game } = this.scene; + if (enable) { + game.canvas.oncontextmenu = PREVENT_DEFAULT; + } else { + game.canvas.oncontextmenu = undefined; + } + } + + calculate(sprites, config) { + const { isDisabled, scene } = this; + const { activePointer } = scene.input; + const isClickTypeToTrack = this.getIsValidClickToTrack(activePointer.buttons); + + if (isDisabled || !isClickTypeToTrack) { + return; + } + + this.start.setTo(activePointer.worldX, activePointer.worldY); + this.end.setTo(activePointer.worldX, activePointer.worldY); + this.isMouseDown = true; + this.gridConfig = config; + this.targetChildren = sprites; + + this.generateFormations(); + this.renderThing(); + + scene.input.on('pointermove', this.onPointerMove); + scene.input.on('pointerup', this.onPointerUp); + } + + onPointerUp = () => { + const { scene } = this; + this.isDragging = false; + this.isMouseDown = false; + this.gridConfig = DEFAULT_GRID_CONFIG; + this.targetChildren = []; + + this.start.reset(); + this.end.reset(); + + this.clear(); + + scene.input.off('pointerup', this.onPointerUp); + scene.input.off('pointermove', this.onPointerMove); + }; + + onPointerMove = ({ buttons, worldX, worldY }) => { + const { tileX, tileY } = this.end; + const isClickTypeToTrack = this.getIsValidClickToTrack(buttons); + if (!this.isMouseDown || !isClickTypeToTrack) { + return this; + } + + this.isDragging = true; + this.end.setTo(worldX, worldY); + + const isTileChanged = tileX !== this.end.tileX || tileY !== this.end.tileY; + if (isTileChanged) { + this.generateFormations(); + this.renderThing(); + } + }; + + onGameOut = pointer => { + // If the cursor leaves the viewport ? + this.onPointerUp(); + }; + + /** + * Calculate the desired combo: + * 1. Sort the target children by clockwise order + * 2. Calculate the position from the end[X|Y] for each child, relative to the transposed centroid + * 3. Calculate possible shapes based on the maxCols and minCols value + * 4. Profit! + * Some reading: + * 1. https://www.reddit.com/r/gamedev/comments/5ypqnb/dynamic_unit_array_depth_change_in_total_war/ + * 2. https://www.gamasutra.com/view/feature/131721/implementing_coordinated_movement.php + */ + generateFormations() { + const { end, gridConfig, previews, start, targetChildren } = this; + const { maxCols, minCols } = gridConfig; + const gridSize = PluginConfig.get('gridSize'); + const centroid = Phaser.Geom.Point.GetCentroid(targetChildren); + const sorted = sortClockwise(targetChildren, centroid); + + const line = new Phaser.Geom.Line(start.centerX, start.centerY, end.centerX, end.centerY); + + const midpoint = Phaser.Geom.Line.GetMidPoint(line); + const rotated = Phaser.Geom.Line.RotateAroundPoint(line, midpoint, NINETY_DEGREES_AS_RADIANS); + console.log('midpoint', midpoint); + console.log('line', line); + console.log('rotated', rotated); + this.blah = rotated; + + forEach(targetChildren, child => { + const angle1 = Phaser.Math.Angle.BetweenPoints(child, centroid); + const angle2 = Phaser.Math.Angle.BetweenPoints(centroid, child); + const distance = Phaser.Math.Distance.BetweenPoints(centroid, child); + console.log('angle1', angle1); + console.log('angle2', angle2); + console.log('distance', distance); + }); + + + return; + + // flood fill from midpoint... + +/* + const floodFill = (x, y, currentChildren = []) => { + console.group(`children left: ${currentChildren.length}`); + console.log('x', x, 'y', y); + if (!currentChildren.length) { + return; + } + + currentChildren.pop(); + // child.x = currentPosition.x; + // child.y = currentPosition.y; + this.previews.push({ x, y }); + + Object.keys(FLOOD_DIRECTIONS).forEach((key) => { + currentChildren.pop(); + + console.group(key); + const [offsetX, offsetY] = FLOOD_DIRECTIONS[key]; + // const newPosition = currentPosition.copy().add(x, y); + const newX = x + (offsetX * gridSize); + const newY = y + (offsetY * gridSize); + this.previews.push({ x: newX, y: newY }); + + console.log('newX', newX, 'newY', newY); + console.groupEnd(); + // floodFill(newX, newY, currentChildren); + }); + Object.keys(FLOOD_DIRECTIONS).forEach((key) => { + const [offsetX, offsetY] = FLOOD_DIRECTIONS[key]; + // const newPosition = currentPosition.copy().add(x, y); + const newX = x + (offsetX * gridSize); + const newY = y + (offsetY * gridSize); + floodFill(newX, newY, currentChildren); + }); + // floodFill(x, y - gridSize, currentChildren); + // floodFill(x, y + gridSize, currentChildren); + console.groupEnd(); + }; + + this.previews = []; + // floodFill(midpoint.x, midpoint.y, [ ...targetChildren ]); +*/ + } + + renderThing() { + const { end, previews, start } = this; + const gridSize = PluginConfig.get('gridSize'); + console.log('previews', previews); + + this.clear(); + this.lineStyle(3, 0x00ff00); + + this.strokeLineShape({ x1: start.centerX, y1: start.centerY, x2: end.centerX, y2: end.centerY }); + this.strokeLineShape(this.blah); + + this.fillStyle(TEST_COLOR, 1); + this.fillCircle(end.centerX, end.centerY, TEST_RADIUS); + this.strokeCircle(end.centerX, end.centerY, TEST_RADIUS); + + previews.forEach((preview) => { + this.fillStyle(TEST_COLOR, 1); + this.fillRect(preview.x, preview.y, gridSize, gridSize); + }); + } +} diff --git a/packages/phaser3-formation-generator/src/js/lib/plugin-config.js b/packages/phaser3-formation-generator/src/js/lib/plugin-config.js new file mode 100644 index 0000000..e842c41 --- /dev/null +++ b/packages/phaser3-formation-generator/src/js/lib/plugin-config.js @@ -0,0 +1,28 @@ +import Phaser from 'phaser'; +import { MOUSE_BUTTONS } from '@pixelburp/phaser3-utils'; + +class PluginConfig { + /** + * @name camera + * @type Phaser.Cameras.Scene2D.Camera + */ + camera = null; + + gridSize = 1; + + mouseClickToTrack = MOUSE_BUTTONS.RIGHT; + + scene = null; + + setConfig(props = {}) { + Object.keys(props).forEach(key => { + this[key] = props[key]; + }); + } + + get(key) { + return this[key]; + } +} + +export default new PluginConfig(); diff --git a/packages/phaser3-formation-generator/src/js/lib/tile.js b/packages/phaser3-formation-generator/src/js/lib/tile.js new file mode 100644 index 0000000..30be6c0 --- /dev/null +++ b/packages/phaser3-formation-generator/src/js/lib/tile.js @@ -0,0 +1,46 @@ +import Phaser from 'phaser'; +import PluginConfig from './plugin-config'; + +export default class Tile extends Phaser.Math.Vector2 { + centerX = 0; + centerY = 0; + + tileX = 0; + tileY = 0; + + constructor() { + super(); + } + + get worldX() { + return this.x; + } + + get worldY() { + return this.y; + } + + setTo(x, y) { + super.setTo(x, y); + const gridSize = PluginConfig.get('gridSize'); + const halfGrid = gridSize / 2; + const tileX = Math.floor(x / gridSize); + const tileY = Math.floor(y / gridSize); + + this.centerX = (tileX * gridSize) + halfGrid; + this.centerY = (tileY * gridSize) + halfGrid; + + this.tileX = tileX; + this.tileY = tileY; + } + + reset() { + super.reset(); + + this.centerX = 0; + this.centerY = 0; + + this.tileX = 0; + this.tileY = 0; + } +} diff --git a/packages/phaser3-formation-generator/webpack/base.js b/packages/phaser3-formation-generator/webpack/base.js index 3090f97..d20f5dd 100755 --- a/packages/phaser3-formation-generator/webpack/base.js +++ b/packages/phaser3-formation-generator/webpack/base.js @@ -24,6 +24,7 @@ module.exports = { exclude: /node_modules/, use: { loader: 'babel-loader', + options: config.BABEL, }, }, { diff --git a/packages/phaser3-formation-generator/webpack/config.js b/packages/phaser3-formation-generator/webpack/config.js index 4aaf4c4..6db2608 100644 --- a/packages/phaser3-formation-generator/webpack/config.js +++ b/packages/phaser3-formation-generator/webpack/config.js @@ -1,4 +1,5 @@ const path = require('path'); +const babelRc = require('../../../babel.config'); const SRC = './src'; const ASSETS = `${SRC}/assets`; @@ -9,6 +10,7 @@ const resolve = (p = '') => path.resolve(process.cwd(), p); module.exports = { ASSETS_FOLDER_NAME: ASSETS, ASSETS: resolve(`${ASSETS}`), + BABEL: babelRc, DEMO: resolve(DEMO), DIST: resolve('./dist'), ROOT: resolve('./'), diff --git a/packages/phaser3-formation-generator/webpack/prod.js b/packages/phaser3-formation-generator/webpack/prod.js index 5f90448..38272ef 100755 --- a/packages/phaser3-formation-generator/webpack/prod.js +++ b/packages/phaser3-formation-generator/webpack/prod.js @@ -44,6 +44,7 @@ module.exports = Object.assign(base, { exclude: [/node_modules/], use: { loader: 'babel-loader', + options: config.BABEL, }, }, { diff --git a/packages/phaser3-utils/src/index.exports.js b/packages/phaser3-utils/src/index.exports.js index 5c3f403..890ff21 100644 --- a/packages/phaser3-utils/src/index.exports.js +++ b/packages/phaser3-utils/src/index.exports.js @@ -1,5 +1,7 @@ +export { MOUSE_BUTTONS, MOUSE_BUTTONS_VALUES } from './js/input/constants'; export { default as getFromChild } from './js/util/get-from-child'; export { default as forEach } from './js/util/forEach'; +export { default as sortClockwise } from './js/util/sort-clockwise'; export { default as InterfaceScene, INTERFACE_SCENE_KEY } from './js/scene/interface-scene'; export { createInterfaceScene } from './js/scene/util'; diff --git a/packages/phaser3-utils/src/js/input/constants.js b/packages/phaser3-utils/src/js/input/constants.js new file mode 100644 index 0000000..aae15db --- /dev/null +++ b/packages/phaser3-utils/src/js/input/constants.js @@ -0,0 +1,16 @@ +// 0: No button or un-initialized +// 1: Left button +// 2: Right button +// 4: Wheel button or middle button +// 8: 4th button (typically the "Browser Back" button) +// 16: 5th button (typically the "Browser Forward" button) +export const MOUSE_BUTTONS = { + NO_BUTTON: 0, + LEFT: 1, + RIGHT: 2, + MIDDLE: 4, + FOURTH_BUTTON: 8, + FIFTH_BUTTON: 16, +}; + +export const MOUSE_BUTTONS_VALUES = Object.values(MOUSE_BUTTONS); diff --git a/packages/phaser3-utils/src/js/util/sort-clockwise.js b/packages/phaser3-utils/src/js/util/sort-clockwise.js new file mode 100644 index 0000000..895e24f --- /dev/null +++ b/packages/phaser3-utils/src/js/util/sort-clockwise.js @@ -0,0 +1,57 @@ +/** + * Sorts an array of points in a clockwise direction, relative to a reference point. + * + * The sort is clockwise relative to the display, starting from a 12 o'clock position. + * (In the Cartesian plane, it is anticlockwise, starting from the -y direction.) + * + * Example sequence: (0, -1), (1, 0), (0, 1), (-1, 0) + * + * @method Phaser.Point#sortClockwise + * @static + * @param {array} points - An array of Points or point-like objects (e.g., sprites). + * @param {object|Phaser.Point} [center] - The reference point. If omitted, the {@link #centroid} (midpoint) of the points is used. + * @return {array} The sorted array. + */ +export default function sortClockwise(points, center) { + // Adapted from (ciamej) + let cx = center.x; + let cy = center.y; + + const sort = (a, b) => { + if (a.x - cx >= 0 && b.x - cx < 0) { + return -1; + } + + if (a.x - cx < 0 && b.x - cx >= 0) { + return 1; + } + + if (a.x - cx === 0 && b.x - cx === 0) { + if (a.y - cy >= 0 || b.y - cy >= 0) + { + return (a.y > b.y) ? 1 : -1; + } + + return (b.y > a.y) ? 1 : -1; + } + + // Compute the cross product of vectors (center -> a) * (center -> b) + const det = (a.x - cx) * -(b.y - cy) - (b.x - cx) * -(a.y - cy); + + if (det < 0) { + return -1; + } + + if (det > 0) { + return 1; + } + + // Points a and b are on the same line from the center. Check which point is closer to the center + const d1 = (a.x - cx) * (a.x - cx) + (a.y - cy) * (a.y - cy); + const d2 = (b.x - cx) * (b.x - cx) + (b.y - cy) * (b.y - cy); + + return (d1 > d2) ? -1 : 1; + }; + + return points.sort(sort); +};