S2: Unit spawn & control milestone

- Port skin system into Unit base class (animations via _createAnimation/playAnimation)
- Wire SystemOrchestrator + all systems into Map_Player create/update
- Connect SelectionSystem to scene input (disable old Interface handlers)
- Switch pathfinding dispatch to new PathfindingSystem
- Auto-spawn player units on game start
This commit is contained in:
2026-05-30 03:45:45 +00:00
parent ec7fdd5f8d
commit 119737442d
8 changed files with 115 additions and 42 deletions

View File

@@ -87,12 +87,16 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite {
_createAnimation(config) {
const key = config.key;
if (!this.scene.anims.exists(key)) {
this.scene.anims.create({
const animConfig = {
key,
frames: config.frames || [],
frameRate: config.frameRate || 10,
repeat: config.repeat ?? -1,
});
};
if (config.repeatDelay !== undefined) {
animConfig.repeatDelay = config.repeatDelay;
}
this.scene.anims.create(animConfig);
}
this._anims.set(key, true);
if (!this._currentAnim) {

View File

@@ -64,7 +64,7 @@ export default class Infantry extends Unit {
setAnimations() {
for (const key in this.STATES) {
const stateConfig = this.STATES[key];
this.scene.anims.create({
this._createAnimation({
key: stateConfig.key,
frames: this.scene.anims.generateFrameNumbers(this.skin, {
start: stateConfig.animationConfig.start || 0,

View File

@@ -8,7 +8,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("IDLING");
ctx._playAnimation("IDLING");
},
onExit: (ctx) => {},
updateFunction: (ctx, time, delta) => {
@@ -26,7 +26,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("MOVING");
ctx._playAnimation("MOVING");
ctx.clearTarget();
},
onExit: (ctx) => {},
@@ -45,7 +45,7 @@ export default {
repeat: 0,
},
onEnter: (ctx) => {
ctx.anims.play("DYING");
ctx._playAnimation("DYING");
ctx.dead = true;
setTimeout(() => {
ctx.destroy();
@@ -63,7 +63,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("SHOOTING");
ctx._playAnimation("SHOOTING");
ctx.orientToTarget();
},
onExit: (ctx) => {

View File

@@ -8,7 +8,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("IDLING");
ctx._playAnimation("IDLING");
},
onExit: (ctx) => {},
updateFunction: (ctx, time, delta) => {
@@ -26,7 +26,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("MOVING");
ctx._playAnimation("MOVING");
ctx.clearTarget();
},
onExit: (ctx) => {},
@@ -45,7 +45,7 @@ export default {
repeat: 0,
},
onEnter: (ctx) => {
ctx.anims.play("DYING");
ctx._playAnimation("DYING");
ctx.dead = true;
setTimeout(() => {
ctx.destroy();
@@ -64,7 +64,7 @@ export default {
repeat: -1,
},
onEnter: (ctx) => {
ctx.anims.play("SHOOTING");
ctx._playAnimation("SHOOTING");
ctx.orientToTarget();
},
onExit: (ctx) => {

View File

@@ -67,7 +67,7 @@ export default class Tank extends Unit {
setAnimations() {
for (const key in this.STATES) {
const stateConfig = this.STATES[key];
this.scene.anims.create({
this._createAnimation({
key: stateConfig.key,
frames: this.scene.anims.generateFrameNumbers(this.skin, {
start: stateConfig.animationConfig.start || 0,
@@ -75,7 +75,7 @@ export default class Tank extends Unit {
}),
frameRate: stateConfig.animationConfig.frameRate || 10,
repeat: stateConfig.animationConfig.repeat || 0,
repeatDelay: stateConfig.animationConfig.repeatDelay || 0,
repeatDelay: stateConfig.animationConfig.repeatDelay,
});
}
}

View File

@@ -170,21 +170,10 @@ export default class Interface {
Phaser.Input.Keyboard.KeyCodes.SHIFT
);
this.scene.input.on(
Phaser.Input.Events.POINTER_DOWN,
this.handlePointerDown,
this
);
this.scene.input.on(
Phaser.Input.Events.POINTER_MOVE,
this.handlePointerMove,
this
);
this.scene.input.on(
Phaser.Input.Events.POINTER_UP,
this.handlePointerUp,
this
);
// NOTE: Pointer DOWN / MOVE / UP are now handled exclusively by
// SelectionSystem. The old Interface handlers (handlePointerDown,
// handlePointerMove, handlePointerUp) are kept on the class so
// existing call-sites compile but are no longer wired.
this.scene.input.on(
Phaser.Input.Events.POINTER_WHEEL,
this.handlePointerWheel,

View File

@@ -5,6 +5,7 @@ import Ukrainian_Rifle from "Entities/skins/ukrainian-infantry";
import CONSTANTS from "PhaserClasses/CustomConstants";
import Interface from "PhaserClasses/interface";
import { NetworkSystemClient } from "Systems/NetworkSystem.js";
import SystemOrchestrator from "Systems/SystemOrchestrator.js";
export default class Map_Player extends Phaser.Scene {
constructor() {
@@ -125,8 +126,43 @@ export default class Map_Player extends Phaser.Scene {
this.createMap();
this.interface = new Interface(this).init();
// ── System Orchestrator: initialize all 9+ systems ────────────────────
const colyseus = this.game?.colyseus;
this.orchestrator = new SystemOrchestrator(this, {
room: colyseus?.room ?? null,
mapKey: 'test1',
tilesetKey: 'floorsPrimary',
tilesetName: 'floorsPrimary',
debug: false,
});
this.orchestrator.init();
// Initialize pathfinding after MapSystem has the tilemap
this.orchestrator.initPathfinding();
// Wire up Colyseus networking if the client was created by Server_Connector
this._initNetworking();
// Wire the orchestrator's NetworkSystem to the scene for update() access
this._syncNetworkSystem();
// ── Auto-spawn player units ──────────────────────────────────────────
this.createInfantry();
// ── Clean up on shutdown ─────────────────────────────────────────────
this.events.on('shutdown', () => {
this.orchestrator?.shutdown();
});
}
/**
* Sync the orchestrator-managed NetworkSystemClient reference to
* `this.networkSystem` so that both old and new code can find it.
*/
_syncNetworkSystem() {
if (this.orchestrator?.systems?.network) {
this.networkSystem = this.orchestrator.systems.network;
}
}
/**
@@ -171,6 +207,11 @@ export default class Map_Player extends Phaser.Scene {
this.networkSystem = new NetworkSystemClient(this, room);
// Also register with the orchestrator so its update loop ticks it
if (this.orchestrator) {
this.orchestrator.systems.network = this.networkSystem;
}
console.log(
"[Map_Player] NetworkSystemClient wired to room:",
room.id
@@ -180,9 +221,10 @@ export default class Map_Player extends Phaser.Scene {
update(time, delta) {
this.interface.controls.update(delta);
// Tick the network system (snapshot interpolation + scene application)
if (this.networkSystem) {
this.networkSystem.update(time, delta);
// Canonical orchestrator tick: selection → economy → CPs → buildings →
// entities → pathfinding → combat → network
if (this.orchestrator) {
this.orchestrator.update(time, delta);
}
}
}

View File

@@ -227,6 +227,12 @@ export default class SelectionSystem {
const { tile } = target;
if (!tile) return;
const pathfinding = this.scene.orchestrator?.systems?.pathfinding;
if (!pathfinding) {
console.warn('[SelectionSystem] PathfindingSystem not available');
return;
}
const positions = this.getFormationPositions(leaderPos, entities.length);
entities.forEach((entity, i) => {
@@ -235,17 +241,22 @@ export default class SelectionSystem {
y: tile.y + (positions[i]?.y ?? 0),
};
if (this.scene.interface?.pathfinder) {
const startTile = this.scene.interface.generateTileXY(
new Phaser.Math.Vector2(entity.x, entity.y),
);
this.scene.interface.pathfinder.findPath(
entity,
startTile,
offsetTile,
false,
);
}
const startTile = pathfinding.worldToTileCoords(entity.x, entity.y);
pathfinding.findPath(startTile, offsetTile, {}, (path) => {
if (path === null) {
console.warn(`[SelectionSystem] No path found for entity`);
return;
}
// Entities expect the path format from EasyStar: [{x, y}, ...]
// Tank.moveToPath(path) and Infantry.moveToPath(path, shiftDown)
if (typeof entity.moveToPath === 'function') {
// Determine if shift is held (waypoint append for infantry)
const shiftDown = this.shiftKey?.isDown ?? false;
entity.moveToPath(path, shiftDown);
}
});
});
}
@@ -663,6 +674,28 @@ export default class SelectionSystem {
this.handlePointerUp,
this,
);
// --- Right-click: issue commands ---
// Phaser fires POINTER_DOWN for both buttons. We multiplex inside the
// handler: left/middle goes through handlePointerDown → handlePointerUp,
// right goes through handleRightClick.
this.scene.input.on(
Phaser.Input.Events.POINTER_DOWN,
this.#onPointerDownMultiplex,
this,
);
}
/**
* Multiplex POINTER_DOWN: right-clicks → handleRightClick,
* everything else → already handled by handlePointerDown.
* This is a separate listener registered after handlePointerDown so both
* fire, but handlePointerDown bails early when rightButtonDown() is true.
*/
#onPointerDownMultiplex(pointer, currentlyOver) {
if (pointer.rightButtonDown()) {
this.handleRightClick(pointer, currentlyOver);
}
}
/**
@@ -685,6 +718,11 @@ export default class SelectionSystem {
this.handlePointerUp,
this,
);
this.scene.input.off(
Phaser.Input.Events.POINTER_DOWN,
this.#onPointerDownMultiplex,
this,
);
if (this.selectionBox) {
this.selectionBox.destroy();