changed visual movement speed to be slower and made diagonal movement with arrow keys work
This commit is contained in:
@@ -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 },
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user