Add more stats, crit/block/accuracy/dodge/lifesteal

This commit is contained in:
Peter Stockings
2026-01-05 12:39:43 +11:00
parent 171abb681a
commit 86a6afd1df
14 changed files with 815 additions and 406 deletions

View File

@@ -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];