Add levelling up mechanics through experience gained via killing enemies

This commit is contained in:
Peter Stockings
2026-01-04 18:36:31 +11:00
parent 42cd77998d
commit 29e46093f5
11 changed files with 373 additions and 84 deletions

View File

@@ -1,6 +1,8 @@
import { ACTION_COST, ENERGY_THRESHOLD } from "../../core/constants";
import type { World, EntityId, Action, SimEvent, Actor } from "../../core/types";
import { isBlocked } from "../world/world-logic";
import { GAME_CONFIG } from "../../core/config/GameConfig";
export function applyAction(w: World, actorId: EntityId, action: Action): SimEvent[] {
const actor = w.actors.get(actorId);
@@ -22,11 +24,59 @@ export function applyAction(w: World, actorId: EntityId, action: Action): SimEve
}
// Spend energy for any action (move/wait/attack)
actor.energy -= ACTION_COST;
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);
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);
}
w.actors.delete(orb.id);
}
}
function checkLevelUp(player: Actor, events: SimEvent[]) {
if (!player.stats) return;
const s = player.stats;
while (s.exp >= s.expToNextLevel) {
s.level++;
s.exp -= s.expToNextLevel;
// Growth
s.maxHp += GAME_CONFIG.leveling.hpGainPerLevel;
s.hp = s.maxHp; // Heal on level up
s.attack += GAME_CONFIG.leveling.attackGainPerLevel;
// Scale requirement
s.expToNextLevel = Math.floor(s.expToNextLevel * GAME_CONFIG.leveling.expMultiplier);
events.push({
type: "leveled-up",
actorId: player.id,
level: s.level,
x: player.pos.x,
y: player.pos.y
});
}
}
function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }): SimEvent[] {
const from = { ...actor.pos };
const nx = actor.pos.x + action.dx;
@@ -36,12 +86,19 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }):
actor.pos.x = nx;
actor.pos.y = ny;
const to = { ...actor.pos };
return [{ type: "moved", actorId: actor.id, from, to }];
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
if (actor.isPlayer) {
handleExpCollection(w, actor, events);
}
return events;
} else {
return [{ type: "waited", actorId: actor.id }];
}
}
function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): SimEvent[] {
const target = w.actors.get(action.targetId);
if (target && target.stats && actor.stats) {
@@ -69,12 +126,29 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): S
victimType: target.type
});
w.actors.delete(target.id);
// Spawn EXP Orb
const expAmount = target.type === "rat" ? GAME_CONFIG.enemy.ratExp : GAME_CONFIG.enemy.batExp;
const orbId = Math.max(0, ...w.actors.keys(), target.id) + 1;
w.actors.set(orbId, {
id: orbId,
isPlayer: false,
type: "exp_orb",
pos: { ...target.pos },
speed: 0,
energy: 0,
expAmount // Hidden property for simulation
} as any);
events.push({ type: "orb-spawned", orbId, x: target.pos.x, y: target.pos.y });
}
return events;
}
return [{ type: "waited", actorId: actor.id }];
}
/**
* Very basic enemy AI:
* - if adjacent to player, attack
@@ -120,11 +194,12 @@ export function stepUntilPlayerTurn(w: World, playerId: EntityId): { awaitingPla
const events: SimEvent[] = [];
while (true) {
while (![...w.actors.values()].some(a => a.energy >= ENERGY_THRESHOLD)) {
while (![...w.actors.values()].some(a => a.energy >= GAME_CONFIG.gameplay.energyThreshold)) {
for (const a of w.actors.values()) a.energy += a.speed;
}
const ready = [...w.actors.values()].filter(a => a.energy >= ENERGY_THRESHOLD);
const ready = [...w.actors.values()].filter(a => a.energy >= GAME_CONFIG.gameplay.energyThreshold);
ready.sort((a, b) => (b.energy - a.energy) || (a.id - b.id));
const actor = ready[0];