changed visual movement speed to be slower and made diagonal movement with arrow keys work

This commit is contained in:
2026-01-28 18:59:35 +11:00
parent f01d8de15c
commit 80e82f68a0
6 changed files with 185 additions and 142 deletions

View File

@@ -10,12 +10,12 @@ export interface AnimationConfig {
export const GAME_CONFIG = { export const GAME_CONFIG = {
player: { player: {
initialStats: { initialStats: {
maxHp: 20, maxHp: 20,
hp: 20, hp: 20,
maxMana: 20, maxMana: 20,
mana: 20, mana: 20,
attack: 5, attack: 5,
defense: 2, defense: 2,
level: 1, level: 1,
exp: 0, exp: 0,
@@ -52,7 +52,7 @@ export const GAME_CONFIG = {
roomMinHeight: 4, roomMinHeight: 4,
roomMaxHeight: 10 roomMaxHeight: 10
}, },
enemies: { enemies: {
rat: { rat: {
baseHp: 8, baseHp: 8,
@@ -78,7 +78,7 @@ export const GAME_CONFIG = {
hpPerFloor: 5, hpPerFloor: 5,
attackPerTwoFloors: 1, attackPerTwoFloors: 1,
}, },
leveling: { leveling: {
baseExpToNextLevel: 10, baseExpToNextLevel: 10,
expMultiplier: 1.5, expMultiplier: 1.5,
@@ -130,11 +130,12 @@ export const GAME_CONFIG = {
horizontal: 54, horizontal: 54,
vertical: 55, vertical: 55,
turning: 56 turning: 56
} },
moveDuration: 62 // Visual duration for movement in ms
}, },
ui: { ui: {
// ... rest of content ... // ... rest of content ...
minimapPanelWidth: 340, minimapPanelWidth: 340,
minimapPanelHeight: 220, minimapPanelHeight: 220,
minimapPadding: 20, minimapPadding: 20,
@@ -148,7 +149,7 @@ export const GAME_CONFIG = {
targetingLineGap: 4, targetingLineGap: 4,
targetingLineShorten: 8 targetingLineShorten: 8
}, },
gameplay: { gameplay: {
energyThreshold: 100, energyThreshold: 100,
actionCost: 100 actionCost: 100
@@ -175,7 +176,7 @@ export const GAME_CONFIG = {
{ key: "warrior-idle", textureKey: "warrior", frames: [0, 0, 0, 1, 0, 0, 1, 1], frameRate: 2, repeat: -1 }, { key: "warrior-idle", textureKey: "warrior", frames: [0, 0, 0, 1, 0, 0, 1, 1], frameRate: 2, repeat: -1 },
{ key: "warrior-run", textureKey: "warrior", frames: [2, 3, 4, 5, 6, 7], frameRate: 15, repeat: -1 }, { key: "warrior-run", textureKey: "warrior", frames: [2, 3, 4, 5, 6, 7], frameRate: 15, repeat: -1 },
{ key: "warrior-die", textureKey: "warrior", frames: [8, 9, 10, 11, 12], frameRate: 10, repeat: 0 }, { key: "warrior-die", textureKey: "warrior", frames: [8, 9, 10, 11, 12], frameRate: 10, repeat: 0 },
// Rat // Rat
{ key: "rat-idle", textureKey: "rat", frames: [0, 0, 0, 1], frameRate: 4, repeat: -1 }, { key: "rat-idle", textureKey: "rat", frames: [0, 0, 0, 1], frameRate: 4, repeat: -1 },
{ key: "rat-run", textureKey: "rat", frames: [6, 7, 8, 9, 10], frameRate: 10, repeat: -1 }, { key: "rat-run", textureKey: "rat", frames: [6, 7, 8, 9, 10], frameRate: 10, repeat: -1 },

View File

@@ -20,12 +20,12 @@ export interface GameInputEvents {
export class GameInput extends Phaser.Events.EventEmitter { export class GameInput extends Phaser.Events.EventEmitter {
private scene: Phaser.Scene; private scene: Phaser.Scene;
private cursors: Phaser.Types.Input.Keyboard.CursorKeys; private cursors: Phaser.Types.Input.Keyboard.CursorKeys;
constructor(scene: Phaser.Scene) { constructor(scene: Phaser.Scene) {
super(); super();
this.scene = scene; this.scene = scene;
this.cursors = this.scene.input.keyboard!.createCursorKeys(); this.cursors = this.scene.input.keyboard!.createCursorKeys();
this.setupKeyboard(); this.setupKeyboard();
this.setupMouse(); this.setupMouse();
} }
@@ -57,14 +57,14 @@ export class GameInput extends Phaser.Events.EventEmitter {
// Logic for "confirm-target" vs "move" happens in Scene for now, // Logic for "confirm-target" vs "move" happens in Scene for now,
// or we can distinguish based on internal state if we moved targeting here. // or we can distinguish based on internal state if we moved targeting here.
// For now, let's just emit generic events or specific if clear. // For now, let's just emit generic events or specific if clear.
// Actually, GameScene has specific logic: // Actually, GameScene has specific logic:
// "If targeting active -> Left Click = throw" // "If targeting active -> Left Click = throw"
// "Else -> Left Click = move/attack" // "Else -> Left Click = move/attack"
// To keep GameInput "dumb", we just emit the click details. // To keep GameInput "dumb", we just emit the click details.
// EXCEPT: Panning logic is computed from pointer movement. // EXCEPT: Panning logic is computed from pointer movement.
const tx = Math.floor(p.worldX / TILE_SIZE); const tx = Math.floor(p.worldX / TILE_SIZE);
const ty = Math.floor(p.worldY / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE);
this.emit("tile-click", tx, ty, p.button); this.emit("tile-click", tx, ty, p.button);
@@ -80,12 +80,12 @@ export class GameInput extends Phaser.Events.EventEmitter {
const isShiftDrag = p.isDown && p.event.shiftKey; const isShiftDrag = p.isDown && p.event.shiftKey;
if (isRightDrag || isMiddleDrag || isShiftDrag) { if (isRightDrag || isMiddleDrag || isShiftDrag) {
const { x, y } = p.position; const { x, y } = p.position;
const { x: prevX, y: prevY } = p.prevPosition; const { x: prevX, y: prevY } = p.prevPosition;
const dx = (x - prevX); // Zoom factor needs to be handled by receiver or passed here const dx = (x - prevX); // Zoom factor needs to be handled by receiver or passed here
const dy = (y - prevY); const dy = (y - prevY);
this.emit("pan", dx, dy); this.emit("pan", dx, dy);
} }
} }
}); });
@@ -95,7 +95,7 @@ export class GameInput extends Phaser.Events.EventEmitter {
// Return simplified cursor state for movement // Return simplified cursor state for movement
let dx = 0; let dx = 0;
let dy = 0; let dy = 0;
const left = this.cursors.left?.isDown; const left = this.cursors.left?.isDown;
const right = this.cursors.right?.isDown; const right = this.cursors.right?.isDown;
const up = this.cursors.up?.isDown; const up = this.cursors.up?.isDown;
@@ -107,13 +107,19 @@ export class GameInput extends Phaser.Events.EventEmitter {
if (down) dy += 1; if (down) dy += 1;
const anyJustDown = Phaser.Input.Keyboard.JustDown(this.cursors.left!) || const anyJustDown = Phaser.Input.Keyboard.JustDown(this.cursors.left!) ||
Phaser.Input.Keyboard.JustDown(this.cursors.right!) || Phaser.Input.Keyboard.JustDown(this.cursors.right!) ||
Phaser.Input.Keyboard.JustDown(this.cursors.up!) || Phaser.Input.Keyboard.JustDown(this.cursors.up!) ||
Phaser.Input.Keyboard.JustDown(this.cursors.down!); Phaser.Input.Keyboard.JustDown(this.cursors.down!);
return { dx, dy, anyJustDown }; return {
dx, dy, anyJustDown,
isLeft: !!left,
isRight: !!right,
isUp: !!up,
isDown: !!down
};
} }
public cleanup() { public cleanup() {
this.removeAllListeners(); this.removeAllListeners();
// Determine is scene specific cleanup is needed for inputs // Determine is scene specific cleanup is needed for inputs

View File

@@ -12,19 +12,19 @@ import * as ROT from "rot-js";
* - You cannot path TO an unseen target tile. * - You cannot path TO an unseen target tile.
*/ */
export function findPathAStar( export function findPathAStar(
w: World, w: World,
seen: Uint8Array, seen: Uint8Array,
start: Vec2, start: Vec2,
end: Vec2, end: Vec2,
options: { ignoreBlockedTarget?: boolean; ignoreSeen?: boolean; accessor?: EntityAccessor } = {} options: { ignoreBlockedTarget?: boolean; ignoreSeen?: boolean; accessor?: EntityAccessor } = {}
): Vec2[] { ): Vec2[] {
// Validate target // Validate target
if (!inBounds(w, end.x, end.y)) return []; if (!inBounds(w, end.x, end.y)) return [];
if (isWall(w, end.x, end.y)) return []; if (isWall(w, end.x, end.y)) return [];
// Check if target is blocked (unless ignoring) // Check if target is blocked (unless ignoring)
if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y, options.accessor)) return []; if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y, options.accessor)) return [];
// Check if target is unseen (unless ignoring) // Check if target is unseen (unless ignoring)
if (!options.ignoreSeen && seen[idx(w, end.x, end.y)] !== 1) return []; if (!options.ignoreSeen && seen[idx(w, end.x, end.y)] !== 1) return [];
@@ -36,7 +36,7 @@ export function findPathAStar(
// Start position is always passable // Start position is always passable
if (x === start.x && y === start.y) return true; if (x === start.x && y === start.y) return true;
// Target position is passable (we already validated it above) // Target position is passable (we already validated it above)
if (x === end.x && y === end.y) return true; if (x === end.x && y === end.y) return true;
@@ -49,11 +49,11 @@ export function findPathAStar(
return true; return true;
}; };
// Use rot-js A* pathfinding with 4-directional topology // Use rot-js A* pathfinding with 8-directional topology
const astar = new ROT.Path.AStar(end.x, end.y, passableCallback, { topology: 4 }); const astar = new ROT.Path.AStar(end.x, end.y, passableCallback, { topology: 8 });
const path: Vec2[] = []; const path: Vec2[] = [];
// Compute path from start to end // Compute path from start to end
astar.compute(start.x, start.y, (x: number, y: number) => { astar.compute(start.x, start.y, (x: number, y: number) => {
path.push({ x, y }); path.push({ x, y });

View File

@@ -15,7 +15,7 @@ export class DungeonRenderer {
private scene: Phaser.Scene; private scene: Phaser.Scene;
private map?: Phaser.Tilemaps.Tilemap; private map?: Phaser.Tilemaps.Tilemap;
private layer?: Phaser.Tilemaps.TilemapLayer; private layer?: Phaser.Tilemaps.TilemapLayer;
private playerSprite?: Phaser.GameObjects.Sprite; private playerSprite?: Phaser.GameObjects.Sprite;
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map(); private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
private orbSprites: Map<EntityId, Phaser.GameObjects.Arc> = new Map(); private orbSprites: Map<EntityId, Phaser.GameObjects.Arc> = new Map();
@@ -24,7 +24,7 @@ export class DungeonRenderer {
private fovManager: FovManager; private fovManager: FovManager;
private minimapRenderer: MinimapRenderer; private minimapRenderer: MinimapRenderer;
private fxRenderer: FxRenderer; private fxRenderer: FxRenderer;
private world!: World; private world!: World;
private entityAccessor!: EntityAccessor; private entityAccessor!: EntityAccessor;
private ecsWorld!: ECSWorld; private ecsWorld!: ECSWorld;
@@ -67,7 +67,7 @@ export class DungeonRenderer {
// Setup Tilemap // Setup Tilemap
if (this.map) this.map.destroy(); if (this.map) this.map.destroy();
this.map = this.scene.make.tilemap({ this.map = this.scene.make.tilemap({
data: Array.from({ length: world.height }, (_, y) => data: Array.from({ length: world.height }, (_, y) =>
Array.from({ length: world.width }, (_, x) => this.world.tiles[idx(this.world, x, y)]) Array.from({ length: world.width }, (_, x) => this.world.tiles[idx(this.world, x, y)])
), ),
tileWidth: 16, tileWidth: 16,
@@ -84,7 +84,7 @@ export class DungeonRenderer {
}); });
this.fxRenderer.clearCorpses(); this.fxRenderer.clearCorpses();
// Ensure player sprite exists // Ensure player sprite exists
if (!this.playerSprite) { if (!this.playerSprite) {
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0); this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
@@ -93,13 +93,13 @@ export class DungeonRenderer {
} }
this.minimapRenderer.positionMinimap(); this.minimapRenderer.positionMinimap();
// Reset player sprite position to prevent tween animation from old floor // Reset player sprite position to prevent tween animation from old floor
if (this.playerSprite) { if (this.playerSprite) {
// Kill any active tweens on the player sprite // Kill any active tweens on the player sprite
this.scene.tweens.killTweensOf(this.playerSprite); this.scene.tweens.killTweensOf(this.playerSprite);
const player = this.entityAccessor.getPlayer(); const player = this.entityAccessor.getPlayer();
if (player && player.category === "combatant") { if (player && player.category === "combatant") {
this.playerSprite.setPosition( this.playerSprite.setPosition(
@@ -143,7 +143,7 @@ export class DungeonRenderer {
computeFov() { computeFov() {
const player = this.entityAccessor.getPlayer(); const player = this.entityAccessor.getPlayer();
if (player && player.category === "combatant") { if (player && player.category === "combatant") {
this.fovManager.compute(this.world, player.pos); this.fovManager.compute(this.world, player.pos);
} }
} }
@@ -152,9 +152,9 @@ export class DungeonRenderer {
} }
updateTile(x: number, y: number) { updateTile(x: number, y: number) {
if (!this.map || !this.world) return; if (!this.map || !this.world) return;
const t = this.world.tiles[idx(this.world, x, y)]; const t = this.world.tiles[idx(this.world, x, y)];
this.map.putTileAt(t, x, y); this.map.putTileAt(t, x, y);
} }
get seenArray() { get seenArray() {
@@ -171,7 +171,7 @@ export class DungeonRenderer {
this.layer.forEachTile(tile => { this.layer.forEachTile(tile => {
const i = idx(this.world, tile.x, tile.y); const i = idx(this.world, tile.x, tile.y);
const worldTile = this.world.tiles[i]; const worldTile = this.world.tiles[i];
// Sync visual tile with logical tile (e.g. if grass was destroyed) // Sync visual tile with logical tile (e.g. if grass was destroyed)
if (tile.index !== worldTile) { if (tile.index !== worldTile) {
// We can safely update the index property for basic tile switching // We can safely update the index property for basic tile switching
@@ -201,19 +201,19 @@ export class DungeonRenderer {
for (const [trapId, sprite] of this.trapSprites) { for (const [trapId, sprite] of this.trapSprites) {
const pos = this.ecsWorld.getComponent(trapId, "position"); const pos = this.ecsWorld.getComponent(trapId, "position");
const spriteData = this.ecsWorld.getComponent(trapId, "sprite"); const spriteData = this.ecsWorld.getComponent(trapId, "sprite");
if (pos && spriteData) { if (pos && spriteData) {
const i = idx(this.world, pos.x, pos.y); const i = idx(this.world, pos.x, pos.y);
const isSeen = seen[i] === 1; const isSeen = seen[i] === 1;
const isVis = visible[i] === 1; const isVis = visible[i] === 1;
sprite.setVisible(isSeen); sprite.setVisible(isSeen);
// Update sprite frame in case trap was triggered // Update sprite frame in case trap was triggered
if (sprite.frame.name !== String(spriteData.index)) { if (sprite.frame.name !== String(spriteData.index)) {
sprite.setFrame(spriteData.index); sprite.setFrame(spriteData.index);
} }
// Dim if not currently visible // Dim if not currently visible
if (isSeen && !isVis) { if (isSeen && !isVis) {
sprite.setAlpha(0.4); sprite.setAlpha(0.4);
@@ -241,16 +241,16 @@ export class DungeonRenderer {
if (this.playerSprite) { if (this.playerSprite) {
const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2; const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2;
const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2; const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2;
if (this.playerSprite.x !== tx || this.playerSprite.y !== ty) { if (this.playerSprite.x !== tx || this.playerSprite.y !== ty) {
this.scene.tweens.add({ this.scene.tweens.add({
targets: this.playerSprite, targets: this.playerSprite,
x: tx, x: tx,
y: ty, y: ty,
duration: 120, duration: GAME_CONFIG.rendering.moveDuration,
ease: 'Quad.easeOut', ease: 'Quad.easeOut',
overwrite: true overwrite: true
}); });
} }
this.playerSprite.setVisible(true); this.playerSprite.setVisible(true);
} }
@@ -263,10 +263,10 @@ export class DungeonRenderer {
activeEnemyIds.add(a.id); activeEnemyIds.add(a.id);
let sprite = this.enemySprites.get(a.id); let sprite = this.enemySprites.get(a.id);
const textureKey = a.type; const textureKey = a.type;
const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2; const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2;
const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2; const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2;
if (!sprite) { if (!sprite) {
sprite = this.scene.add.sprite(tx, ty, textureKey, 0); sprite = this.scene.add.sprite(tx, ty, textureKey, 0);
sprite.setDepth(99); sprite.setDepth(99);
@@ -274,22 +274,22 @@ export class DungeonRenderer {
this.enemySprites.set(a.id, sprite); this.enemySprites.set(a.id, sprite);
sprite.setVisible(true); sprite.setVisible(true);
} else { } else {
if (!sprite.visible) { if (!sprite.visible) {
// If it was hidden, snap to new position immediately // If it was hidden, snap to new position immediately
this.scene.tweens.killTweensOf(sprite); this.scene.tweens.killTweensOf(sprite);
sprite.setPosition(tx, ty); sprite.setPosition(tx, ty);
sprite.setVisible(true); sprite.setVisible(true);
} else if (sprite.x !== tx || sprite.y !== ty) { } else if (sprite.x !== tx || sprite.y !== ty) {
// Only tween if it was already visible and moved // Only tween if it was already visible and moved
this.scene.tweens.add({ this.scene.tweens.add({
targets: sprite, targets: sprite,
x: tx, x: tx,
y: ty, y: ty,
duration: 120, duration: GAME_CONFIG.rendering.moveDuration,
ease: 'Quad.easeOut', ease: 'Quad.easeOut',
overwrite: true overwrite: true
}); });
} }
} }
} else if (a.category === "collectible") { } else if (a.category === "collectible") {
@@ -308,23 +308,23 @@ export class DungeonRenderer {
orb.setVisible(true); orb.setVisible(true);
} }
} else if (a.category === "item_drop") { } else if (a.category === "item_drop") {
if (!isVis) continue; if (!isVis) continue;
activeItemIds.add(a.id); activeItemIds.add(a.id);
let itemContainer = this.itemSprites.get(a.id); let itemContainer = this.itemSprites.get(a.id);
if (!itemContainer) { if (!itemContainer) {
// Use ItemSpriteFactory to create sprite with optional glow // Use ItemSpriteFactory to create sprite with optional glow
itemContainer = ItemSpriteFactory.createItemSprite(this.scene, a.item, 0, 0, 1); itemContainer = ItemSpriteFactory.createItemSprite(this.scene, a.item, 0, 0, 1);
itemContainer.setDepth(40); itemContainer.setDepth(40);
this.itemSprites.set(a.id, itemContainer); this.itemSprites.set(a.id, itemContainer);
} }
const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2; const tx = a.pos.x * TILE_SIZE + TILE_SIZE / 2;
const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2; const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2;
itemContainer.setPosition(tx, ty); itemContainer.setPosition(tx, ty);
itemContainer.setVisible(true); itemContainer.setVisible(true);
// bobbing effect on the container // bobbing effect on the container
itemContainer.y += Math.sin(this.scene.time.now / 300) * 2; itemContainer.y += Math.sin(this.scene.time.now / 300) * 2;
} }
} }
@@ -350,13 +350,13 @@ export class DungeonRenderer {
} }
for (const [id, item] of this.itemSprites.entries()) { for (const [id, item] of this.itemSprites.entries()) {
if (!activeItemIds.has(id)) { if (!activeItemIds.has(id)) {
item.setVisible(false); item.setVisible(false);
if (!this.entityAccessor.hasActor(id)) { if (!this.entityAccessor.hasActor(id)) {
item.destroy(); item.destroy();
this.itemSprites.delete(id); this.itemSprites.delete(id);
}
} }
}
} }
this.minimapRenderer.render(this.world, seen, visible, this.entityAccessor); this.minimapRenderer.render(this.world, seen, visible, this.entityAccessor);
@@ -405,44 +405,44 @@ export class DungeonRenderer {
} }
showProjectile(from: Vec2, to: Vec2, itemId: string, onComplete: () => void) { showProjectile(from: Vec2, to: Vec2, itemId: string, onComplete: () => void) {
// World coords // World coords
const startX = from.x * TILE_SIZE + TILE_SIZE / 2; const startX = from.x * TILE_SIZE + TILE_SIZE / 2;
const startY = from.y * TILE_SIZE + TILE_SIZE / 2; const startY = from.y * TILE_SIZE + TILE_SIZE / 2;
const endX = to.x * TILE_SIZE + TILE_SIZE / 2; const endX = to.x * TILE_SIZE + TILE_SIZE / 2;
const endY = to.y * TILE_SIZE + TILE_SIZE / 2; const endY = to.y * TILE_SIZE + TILE_SIZE / 2;
// Create sprite // Create sprite
// Look up sprite index from config // Look up sprite index from config
const itemConfig = ALL_TEMPLATES[itemId as keyof typeof ALL_TEMPLATES]; const itemConfig = ALL_TEMPLATES[itemId as keyof typeof ALL_TEMPLATES];
const texture = itemConfig?.textureKey ?? "items"; const texture = itemConfig?.textureKey ?? "items";
const frame = itemConfig?.spriteIndex ?? 0; const frame = itemConfig?.spriteIndex ?? 0;
// Use 'items' spritesheet // Use 'items' spritesheet
const sprite = this.scene.add.sprite(startX, startY, texture, frame); const sprite = this.scene.add.sprite(startX, startY, texture, frame);
sprite.setDepth(2000); sprite.setDepth(2000);
// Rotate?
const angle = Phaser.Math.Angle.Between(startX, startY, endX, endY);
sprite.setRotation(angle + Math.PI / 4); // Adjust for sprite orientation (diagonal usually)
const dist = Phaser.Math.Distance.Between(startX, startY, endX, endY); // Rotate?
const duration = dist * 2; // speed const angle = Phaser.Math.Angle.Between(startX, startY, endX, endY);
sprite.setRotation(angle + Math.PI / 4); // Adjust for sprite orientation (diagonal usually)
this.scene.tweens.add({ const dist = Phaser.Math.Distance.Between(startX, startY, endX, endY);
targets: sprite, const duration = dist * 2; // speed
x: endX,
y: endY, this.scene.tweens.add({
rotation: sprite.rotation + 4 * Math.PI, // Spin effect targets: sprite,
duration: duration, x: endX,
ease: 'Linear', y: endY,
onComplete: () => { rotation: sprite.rotation + 4 * Math.PI, // Spin effect
sprite.destroy(); duration: duration,
onComplete(); ease: 'Linear',
} onComplete: () => {
}); sprite.destroy();
onComplete();
}
});
} }
shakeCamera() { shakeCamera() {
this.scene.cameras.main.shake(100, 0.01); this.scene.cameras.main.shake(100, 0.01);
} }
} }

View File

@@ -51,6 +51,7 @@ export class GameScene extends Phaser.Scene {
public playerPath: Vec2[] = []; public playerPath: Vec2[] = [];
public awaitingPlayer = false; public awaitingPlayer = false;
private lastMoveTime = 0;
// Sub-systems // Sub-systems
public dungeonRenderer!: DungeonRenderer; public dungeonRenderer!: DungeonRenderer;
@@ -138,7 +139,7 @@ export class GameScene extends Phaser.Scene {
const dx = next.x - player.pos.x; const dx = next.x - player.pos.x;
const dy = next.y - player.pos.y; const dy = next.y - player.pos.y;
if (Math.abs(dx) + Math.abs(dy) !== 1) { if (Math.max(Math.abs(dx), Math.abs(dy)) !== 1) {
this.playerPath = []; this.playerPath = [];
return; return;
} }
@@ -156,7 +157,12 @@ export class GameScene extends Phaser.Scene {
} }
} }
if (this.time.now - this.lastMoveTime < GAME_CONFIG.rendering.moveDuration) {
return;
}
this.commitPlayerAction({ type: "move", dx, dy }); this.commitPlayerAction({ type: "move", dx, dy });
this.lastMoveTime = this.time.now;
this.playerPath.shift(); this.playerPath.shift();
return; return;
} }
@@ -164,8 +170,16 @@ export class GameScene extends Phaser.Scene {
let action: Action | null = this.playerInputHandler.handleCursorMovement(); let action: Action | null = this.playerInputHandler.handleCursorMovement();
if (action) { if (action) {
if (action.type === "move" && this.time.now - this.lastMoveTime < GAME_CONFIG.rendering.moveDuration) {
return;
}
this.playerPath = []; this.playerPath = [];
this.commitPlayerAction(action); this.commitPlayerAction(action);
if (action.type === "move") {
this.lastMoveTime = this.time.now;
}
} }
} }

View File

@@ -7,6 +7,12 @@ import type { Action, RangedWeaponItem } from "../../core/types";
export class PlayerInputHandler { export class PlayerInputHandler {
private scene: GameScene; private scene: GameScene;
// Input Chording state
private pendingDx: number = 0;
private pendingDy: number = 0;
private moveChordTime: number = 0;
private readonly CHORD_WINDOW = 40; // ms to wait for diagonal chord
constructor(scene: GameScene) { constructor(scene: GameScene) {
this.scene = scene; this.scene = scene;
} }
@@ -170,10 +176,28 @@ export class PlayerInputHandler {
} }
public handleCursorMovement(): Action | null { public handleCursorMovement(): Action | null {
const { dx, dy, anyJustDown } = this.scene.gameInput.getCursorState(); const { dx, dy, anyJustDown } = this.scene.gameInput.getCursorState() as any;
const now = this.scene.time.now;
if (anyJustDown) { if (anyJustDown) {
if (dx !== 0 || dy !== 0) { // Start or update chord
if (this.moveChordTime === 0) {
this.moveChordTime = now + this.CHORD_WINDOW;
}
if (dx !== 0) this.pendingDx = dx;
if (dy !== 0) this.pendingDy = dy;
}
if (this.moveChordTime > 0 && now >= this.moveChordTime) {
const finalDx = this.pendingDx;
const finalDy = this.pendingDy;
// Reset state
this.moveChordTime = 0;
this.pendingDx = 0;
this.pendingDy = 0;
if (finalDx !== 0 || finalDy !== 0) {
if (this.scene.targetingSystem.isActive) { if (this.scene.targetingSystem.isActive) {
this.scene.targetingSystem.cancel(); this.scene.targetingSystem.cancel();
this.scene.emitUIUpdate(); this.scene.emitUIUpdate();
@@ -181,17 +205,15 @@ export class PlayerInputHandler {
const player = this.scene.entityAccessor.getPlayer(); const player = this.scene.entityAccessor.getPlayer();
if (!player) return null; if (!player) return null;
const targetX = player.pos.x + dx; const targetX = player.pos.x + finalDx;
const targetY = player.pos.y + dy; const targetY = player.pos.y + finalDy;
const enemy = this.scene.entityAccessor.findEnemyAt(targetX, targetY); const enemy = this.scene.entityAccessor.findEnemyAt(targetX, targetY);
if (enemy) { if (enemy) {
return { type: "attack", targetId: enemy.id }; return { type: "attack", targetId: enemy.id };
} else { } else {
if (Math.abs(dx) + Math.abs(dy) === 1) { return { type: "move", dx: finalDx, dy: finalDy };
return { type: "move", dx, dy };
}
} }
} }
} }