Add more stats, crit/block/accuracy/dodge/lifesteal
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { generateWorld } from '../world/generator';
|
||||
import { isWall, inBounds } from '../world/world-logic';
|
||||
import { type CombatantActor } from '../../core/types';
|
||||
|
||||
describe('World Generator', () => {
|
||||
describe('generateWorld', () => {
|
||||
@@ -8,7 +9,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -24,7 +27,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -32,17 +37,20 @@ describe('World Generator', () => {
|
||||
const { world, playerId } = generateWorld(1, runState);
|
||||
|
||||
expect(playerId).toBe(1);
|
||||
const player = world.actors.get(playerId);
|
||||
const player = world.actors.get(playerId) as CombatantActor;
|
||||
expect(player).toBeDefined();
|
||||
expect(player?.isPlayer).toBe(true);
|
||||
expect(player?.stats).toEqual(runState.stats);
|
||||
expect(player.category).toBe("combatant");
|
||||
expect(player.isPlayer).toBe(true);
|
||||
expect(player.stats).toEqual(runState.stats);
|
||||
});
|
||||
|
||||
it('should create walkable rooms', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -58,7 +66,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -74,7 +84,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -85,14 +97,14 @@ describe('World Generator', () => {
|
||||
expect(world.actors.size).toBeGreaterThan(1);
|
||||
|
||||
// All non-player actors should be enemies
|
||||
const enemies = Array.from(world.actors.values()).filter(a => !a.isPlayer);
|
||||
const enemies = Array.from(world.actors.values()).filter(a => a.category === "combatant" && !a.isPlayer) as CombatantActor[];
|
||||
expect(enemies.length).toBeGreaterThan(0);
|
||||
|
||||
// Enemies should have stats
|
||||
enemies.forEach(enemy => {
|
||||
expect(enemy.stats).toBeDefined();
|
||||
expect(enemy.stats!.hp).toBeGreaterThan(0);
|
||||
expect(enemy.stats!.attack).toBeGreaterThan(0);
|
||||
expect(enemy.stats.hp).toBeGreaterThan(0);
|
||||
expect(enemy.stats.attack).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +112,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -121,7 +135,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -137,7 +153,9 @@ describe('World Generator', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
},
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
@@ -145,15 +163,15 @@ describe('World Generator', () => {
|
||||
const { world: world1 } = generateWorld(1, runState);
|
||||
const { world: world5 } = generateWorld(5, runState);
|
||||
|
||||
const enemies1 = Array.from(world1.actors.values()).filter(a => !a.isPlayer);
|
||||
const enemies5 = Array.from(world5.actors.values()).filter(a => !a.isPlayer);
|
||||
const enemies1 = Array.from(world1.actors.values()).filter(a => a.category === "combatant" && !a.isPlayer) as CombatantActor[];
|
||||
const enemies5 = Array.from(world5.actors.values()).filter(a => a.category === "combatant" && !a.isPlayer) as CombatantActor[];
|
||||
|
||||
// Higher level should have more enemies
|
||||
expect(enemies5.length).toBeGreaterThan(enemies1.length);
|
||||
|
||||
// Higher level enemies should have higher stats
|
||||
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies1.length;
|
||||
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies5.length;
|
||||
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies1.length;
|
||||
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies5.length;
|
||||
expect(avgHp5).toBeGreaterThan(avgHp1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyAction } from '../simulation/simulation';
|
||||
import { type World, type Actor, type EntityId } from '../../core/types';
|
||||
import { type World, type Actor, type EntityId, type CombatantActor } from '../../core/types';
|
||||
|
||||
describe('Combat Simulation', () => {
|
||||
const createTestWorld = (actors: Map<EntityId, Actor>): World => ({
|
||||
@@ -14,6 +14,14 @@ describe('Combat Simulation', () => {
|
||||
const createTestStats = (overrides: Partial<any> = {}) => ({
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [],
|
||||
// New stats (defaults for tests to keep them deterministic)
|
||||
critChance: 0,
|
||||
critMultiplier: 100,
|
||||
accuracy: 100, // Always hit in tests unless specified
|
||||
lifesteal: 0,
|
||||
evasion: 0,
|
||||
blockChance: 0,
|
||||
luck: 0,
|
||||
...overrides
|
||||
});
|
||||
|
||||
@@ -22,7 +30,9 @@ describe('Combat Simulation', () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: 3, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -30,7 +40,9 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
type: "rat",
|
||||
pos: { x: 4, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -38,20 +50,22 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
const enemy = world.actors.get(2)!;
|
||||
expect(enemy.stats!.hp).toBeLessThan(10);
|
||||
const enemy = world.actors.get(2) as CombatantActor;
|
||||
expect(enemy.stats.hp).toBeLessThan(10);
|
||||
|
||||
// Should have attack event
|
||||
expect(events.some(e => e.type === 'attacked')).toBe(true);
|
||||
expect(events.some(e => e.type === "attacked")).toBe(true);
|
||||
});
|
||||
|
||||
it('should kill enemy when damage exceeds hp', () => {
|
||||
it("should kill enemy when damage exceeds hp", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: 3, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -59,7 +73,9 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
type: "rat",
|
||||
pos: { x: 4, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -67,20 +83,22 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
// Enemy should be removed from world
|
||||
expect(world.actors.has(2)).toBe(false);
|
||||
|
||||
// Should have killed event
|
||||
expect(events.some(e => e.type === 'killed')).toBe(true);
|
||||
expect(events.some(e => e.type === "killed")).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply defense to reduce damage', () => {
|
||||
it("should apply defense to reduce damage", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: 3, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -88,7 +106,9 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
type: "rat",
|
||||
pos: { x: 4, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -96,22 +116,98 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
applyAction(world, 1, { type: 'attack', targetId: 2 });
|
||||
applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
const enemy = world.actors.get(2)!;
|
||||
const damage = 10 - enemy.stats!. hp;
|
||||
const enemy = world.actors.get(2) as CombatantActor;
|
||||
const damage = 10 - enemy.stats.hp;
|
||||
|
||||
// Damage should be reduced by defense (5 attack - 3 defense = 2 damage)
|
||||
expect(damage).toBe(2);
|
||||
});
|
||||
|
||||
it("should emit dodged event when attack misses", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ accuracy: 0 }) // Force miss
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ evasion: 0 })
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
expect(events.some(e => e.type === "dodged")).toBe(true);
|
||||
expect((world.actors.get(2) as CombatantActor).stats.hp).toBe(20);
|
||||
});
|
||||
|
||||
it("should emit crit flag when critical strike occurs", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ critChance: 100, critMultiplier: 200, attack: 10 })
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ defense: 0, hp: 100 })
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
const damagedEvent = events.find(e => e.type === "damaged") as any;
|
||||
expect(damagedEvent.isCrit).toBe(true);
|
||||
expect(damagedEvent.amount).toBe(20); // 10 * 2.0
|
||||
});
|
||||
|
||||
it("should emit block flag and reduce damage", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ attack: 10 })
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ defense: 0, blockChance: 100, hp: 100 }) // Force block
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
const damagedEvent = events.find(e => e.type === "damaged") as any;
|
||||
expect(damagedEvent.isBlock).toBe(true);
|
||||
expect(damagedEvent.amount).toBe(5); // 10 * 0.5
|
||||
});
|
||||
|
||||
it("should heal attacker via lifesteal", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ attack: 10, lifesteal: 50, hp: 5, maxHp: 20 })
|
||||
});
|
||||
actors.set(2, {
|
||||
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
|
||||
stats: createTestStats({ defense: 0 })
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
|
||||
|
||||
expect(events.some(e => e.type === "healed")).toBe(true);
|
||||
expect((world.actors.get(1) as CombatantActor).stats.hp).toBe(10); // 5 + (10 * 0.5)
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyAction - move', () => {
|
||||
it('should move actor to new position', () => {
|
||||
describe("applyAction - move", () => {
|
||||
it("should move actor to new position", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: 3, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0,
|
||||
@@ -119,13 +215,13 @@ describe('Combat Simulation', () => {
|
||||
});
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
const events = applyAction(world, 1, { type: 'move', dx: 1, dy: 0 });
|
||||
const events = applyAction(world, 1, { type: "move", dx: 1, dy: 0 });
|
||||
|
||||
const player = world.actors.get(1)!;
|
||||
const player = world.actors.get(1) as CombatantActor;
|
||||
expect(player.pos).toEqual({ x: 4, y: 3 });
|
||||
|
||||
// Should have moved event
|
||||
expect(events.some(e => e.type === 'moved')).toBe(true);
|
||||
expect(events.some(e => e.type === "moved")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,10 +89,13 @@ describe('World Utilities', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
world.actors.set(1, {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: 3, y: 3 },
|
||||
speed: 100,
|
||||
energy: 0
|
||||
energy: 0,
|
||||
stats: { hp: 10, maxHp: 10, attack: 1, defense: 0 } as any
|
||||
});
|
||||
|
||||
expect(isBlocked(world, 3, 3)).toBe(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { World, EntityId, Action, SimEvent, Actor } from "../../core/types";
|
||||
import type { World, EntityId, Action, SimEvent, Actor, CombatantActor, CollectibleActor } from "../../core/types";
|
||||
|
||||
import { isBlocked } from "../world/world-logic";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
@@ -24,34 +24,40 @@ export function applyAction(w: World, actorId: EntityId, action: Action): SimEve
|
||||
}
|
||||
|
||||
// Spend energy for any action (move/wait/attack)
|
||||
actor.energy -= GAME_CONFIG.gameplay.actionCost;
|
||||
if (actor.category === "combatant") {
|
||||
actor.energy -= GAME_CONFIG.gameplay.actionCost;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function handleExpCollection(w: World, player: Actor, events: SimEvent[]) {
|
||||
const orbs = [...w.actors.values()].filter(a => a.type === "exp_orb" && a.pos.x === player.pos.x && a.pos.y === player.pos.y);
|
||||
if (player.category !== "combatant") return;
|
||||
|
||||
const orbs = [...w.actors.values()].filter(a =>
|
||||
a.category === "collectible" &&
|
||||
a.type === "exp_orb" &&
|
||||
a.pos.x === player.pos.x &&
|
||||
a.pos.y === player.pos.y
|
||||
) as CollectibleActor[];
|
||||
|
||||
for (const orb of orbs) {
|
||||
const amount = (orb as any).expAmount || 0;
|
||||
if (player.stats) {
|
||||
player.stats.exp += amount;
|
||||
events.push({
|
||||
type: "exp-collected",
|
||||
actorId: player.id,
|
||||
amount,
|
||||
x: player.pos.x,
|
||||
y: player.pos.y
|
||||
});
|
||||
|
||||
checkLevelUp(player, events);
|
||||
}
|
||||
const amount = orb.expAmount || 0;
|
||||
player.stats.exp += amount;
|
||||
events.push({
|
||||
type: "exp-collected",
|
||||
actorId: player.id,
|
||||
amount,
|
||||
x: player.pos.x,
|
||||
y: player.pos.y
|
||||
});
|
||||
|
||||
checkLevelUp(player, events);
|
||||
w.actors.delete(orb.id);
|
||||
}
|
||||
}
|
||||
|
||||
function checkLevelUp(player: Actor, events: SimEvent[]) {
|
||||
if (!player.stats) return;
|
||||
function checkLevelUp(player: CombatantActor, events: SimEvent[]) {
|
||||
const s = player.stats;
|
||||
|
||||
while (s.exp >= s.expToNextLevel) {
|
||||
@@ -91,7 +97,7 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }):
|
||||
const to = { ...actor.pos };
|
||||
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
|
||||
|
||||
if (actor.isPlayer) {
|
||||
if (actor.category === "combatant" && actor.isPlayer) {
|
||||
handleExpCollection(w, actor, events);
|
||||
}
|
||||
|
||||
@@ -104,19 +110,68 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }):
|
||||
|
||||
function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): SimEvent[] {
|
||||
const target = w.actors.get(action.targetId);
|
||||
if (target && target.stats && actor.stats) {
|
||||
if (target && target.category === "combatant" && actor.category === "combatant") {
|
||||
const events: SimEvent[] = [{ type: "attacked", attackerId: actor.id, targetId: action.targetId }];
|
||||
|
||||
const dmg = Math.max(1, actor.stats.attack - target.stats.defense);
|
||||
// 1. Accuracy vs Evasion Check
|
||||
const hitChance = actor.stats.accuracy - target.stats.evasion;
|
||||
const hitRoll = Math.random() * 100;
|
||||
|
||||
if (hitRoll > hitChance) {
|
||||
// Miss!
|
||||
events.push({
|
||||
type: "dodged",
|
||||
targetId: action.targetId,
|
||||
x: target.pos.x,
|
||||
y: target.pos.y
|
||||
});
|
||||
return events;
|
||||
}
|
||||
|
||||
// 2. Base Damage Calculation
|
||||
let dmg = Math.max(1, actor.stats.attack - target.stats.defense);
|
||||
|
||||
// 3. Critical Strike Check
|
||||
const critRoll = Math.random() * 100;
|
||||
const isCrit = critRoll < actor.stats.critChance;
|
||||
if (isCrit) {
|
||||
dmg = Math.floor(dmg * (actor.stats.critMultiplier / 100));
|
||||
}
|
||||
|
||||
// 4. Block Chance Check
|
||||
const blockRoll = Math.random() * 100;
|
||||
let isBlock = false;
|
||||
if (blockRoll < target.stats.blockChance) {
|
||||
dmg = Math.floor(dmg * 0.5); // Block reduces damage by 50%
|
||||
isBlock = true;
|
||||
}
|
||||
|
||||
target.stats.hp -= dmg;
|
||||
|
||||
// 5. Lifesteal Logic
|
||||
if (actor.stats.lifesteal > 0 && dmg > 0) {
|
||||
const healAmount = Math.floor(dmg * (actor.stats.lifesteal / 100));
|
||||
if (healAmount > 0) {
|
||||
actor.stats.hp = Math.min(actor.stats.maxHp, actor.stats.hp + healAmount);
|
||||
events.push({
|
||||
type: "healed",
|
||||
actorId: actor.id,
|
||||
amount: healAmount,
|
||||
x: actor.pos.x,
|
||||
y: actor.pos.y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
events.push({
|
||||
type: "damaged",
|
||||
targetId: action.targetId,
|
||||
amount: dmg,
|
||||
hp: target.stats.hp,
|
||||
x: target.pos.x,
|
||||
y: target.pos.y
|
||||
y: target.pos.y,
|
||||
isCrit,
|
||||
isBlock
|
||||
});
|
||||
|
||||
if (target.stats.hp <= 0) {
|
||||
@@ -126,7 +181,7 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): S
|
||||
killerId: actor.id,
|
||||
x: target.pos.x,
|
||||
y: target.pos.y,
|
||||
victimType: target.type
|
||||
victimType: target.type as "player" | "rat" | "bat"
|
||||
});
|
||||
w.actors.delete(target.id);
|
||||
|
||||
@@ -135,15 +190,12 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): S
|
||||
const expAmount = enemyDef?.expValue || 0;
|
||||
const orbId = Math.max(0, ...w.actors.keys(), target.id) + 1;
|
||||
w.actors.set(orbId, {
|
||||
|
||||
id: orbId,
|
||||
isPlayer: false,
|
||||
category: "collectible",
|
||||
type: "exp_orb",
|
||||
pos: { ...target.pos },
|
||||
speed: 0,
|
||||
energy: 0,
|
||||
expAmount // Hidden property for simulation
|
||||
} as any);
|
||||
expAmount // Explicit member in CollectibleActor
|
||||
});
|
||||
|
||||
events.push({ type: "orb-spawned", orbId, x: target.pos.x, y: target.pos.y });
|
||||
}
|
||||
@@ -158,7 +210,7 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): S
|
||||
* - if adjacent to player, attack
|
||||
* - else step toward player using greedy Manhattan
|
||||
*/
|
||||
export function decideEnemyAction(w: World, enemy: Actor, player: Actor): Action {
|
||||
export function decideEnemyAction(w: World, enemy: CombatantActor, player: CombatantActor): Action {
|
||||
const dx = player.pos.x - enemy.pos.x;
|
||||
const dy = player.pos.y - enemy.pos.y;
|
||||
const dist = Math.abs(dx) + Math.abs(dy);
|
||||
@@ -192,17 +244,23 @@ export function decideEnemyAction(w: World, enemy: Actor, player: Actor): Action
|
||||
* Returns enemy events accumulated along the way.
|
||||
*/
|
||||
export function stepUntilPlayerTurn(w: World, playerId: EntityId): { awaitingPlayerId: EntityId; events: SimEvent[] } {
|
||||
const player = w.actors.get(playerId);
|
||||
if (!player) throw new Error("Player missing");
|
||||
const player = w.actors.get(playerId) as CombatantActor;
|
||||
if (!player || player.category !== "combatant") throw new Error("Player missing or invalid");
|
||||
|
||||
const events: SimEvent[] = [];
|
||||
|
||||
while (true) {
|
||||
while (![...w.actors.values()].some(a => a.energy >= GAME_CONFIG.gameplay.energyThreshold)) {
|
||||
for (const a of w.actors.values()) a.energy += a.speed;
|
||||
while (![...w.actors.values()].some(a => a.category === "combatant" && a.energy >= GAME_CONFIG.gameplay.energyThreshold)) {
|
||||
for (const a of w.actors.values()) {
|
||||
if (a.category === "combatant") {
|
||||
a.energy += a.speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ready = [...w.actors.values()].filter(a => a.energy >= GAME_CONFIG.gameplay.energyThreshold);
|
||||
const ready = [...w.actors.values()].filter(a =>
|
||||
a.category === "combatant" && a.energy >= GAME_CONFIG.gameplay.energyThreshold
|
||||
) as CombatantActor[];
|
||||
|
||||
ready.sort((a, b) => (b.energy - a.energy) || (a.id - b.id));
|
||||
const actor = ready[0];
|
||||
|
||||
@@ -35,6 +35,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
|
||||
actors.set(playerId, {
|
||||
id: playerId,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
type: "player",
|
||||
pos: { x: playerX, y: playerY },
|
||||
@@ -229,6 +230,7 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
|
||||
|
||||
actors.set(enemyId, {
|
||||
id: enemyId,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
type,
|
||||
pos: { x: enemyX, y: enemyY },
|
||||
@@ -247,6 +249,13 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
|
||||
strength: 0,
|
||||
dexterity: 0,
|
||||
intelligence: 0,
|
||||
critChance: 0,
|
||||
critMultiplier: 100,
|
||||
accuracy: 80,
|
||||
lifesteal: 0,
|
||||
evasion: 0,
|
||||
blockChance: 0,
|
||||
luck: 0,
|
||||
passiveNodes: []
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user