pokemon-io-core 0.0.52 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +6 -6
  2. package/dist/api/battle.js +0 -2
  3. package/dist/api/index.d.ts +0 -3
  4. package/dist/api/index.js +0 -3
  5. package/dist/api/room.js +0 -2
  6. package/dist/api/socketTypes.js +0 -1
  7. package/dist/core/actions.js +0 -1
  8. package/dist/core/amulets.js +0 -1
  9. package/dist/core/battleState.d.ts +0 -39
  10. package/dist/core/battleState.js +0 -1
  11. package/dist/core/engine.d.ts +0 -45
  12. package/dist/core/engine.js +0 -869
  13. package/dist/core/events.js +0 -1
  14. package/dist/core/fighters.js +0 -1
  15. package/dist/core/ids.js +0 -1
  16. package/dist/core/index.d.ts +0 -11
  17. package/dist/core/index.js +0 -11
  18. package/dist/core/items.js +0 -1
  19. package/dist/core/moves.js +0 -2
  20. package/dist/core/status.js +0 -1
  21. package/dist/core/types.js +0 -1
  22. package/dist/engine/engine.d.ts +0 -45
  23. package/dist/engine/engine.js +0 -869
  24. package/dist/engine/index.d.ts +0 -1
  25. package/dist/engine/index.js +0 -1
  26. package/dist/engine/pokemonBattleService.d.ts +0 -6
  27. package/dist/engine/pokemonBattleService.js +0 -32
  28. package/dist/index.d.ts +0 -6
  29. package/dist/index.js +0 -6
  30. package/dist/skins/CombatSkin.d.ts +0 -18
  31. package/dist/skins/CombatSkin.js +0 -1
  32. package/dist/skins/index.d.ts +0 -2
  33. package/dist/skins/index.js +0 -2
  34. package/dist/skins/pokemon/fighters.js +0 -174
  35. package/dist/skins/pokemon/index.d.ts +0 -6
  36. package/dist/skins/pokemon/index.js +0 -6
  37. package/dist/skins/pokemon/items.js +0 -143
  38. package/dist/skins/pokemon/moves.js +0 -459
  39. package/dist/skins/pokemon/pokemonSkin.js +0 -91
  40. package/dist/skins/pokemon/statuses.js +0 -85
  41. package/dist/skins/pokemon/types.js +0 -97
@@ -1,869 +0,0 @@
1
- // src/domain/combat/engine/battleEngine.ts
2
- // ------------------------------------------------------
3
- // Debug helper
4
- // ------------------------------------------------------
5
- const DEBUG_BATTLE = true;
6
- const dbg = (...args) => {
7
- if (!DEBUG_BATTLE)
8
- return;
9
- // eslint-disable-next-line no-console
10
- console.log("[BATTLE]", ...args);
11
- };
12
- const DEFAULT_RULES = {
13
- baseCritChance: 0.05,
14
- critPerStat: 0.002,
15
- critMultiplier: 1.3,
16
- stabMultiplier: 1.2,
17
- randomMinDamageFactor: 0.85,
18
- randomMaxDamageFactor: 1.0,
19
- };
20
- const buildMap = (list) => list.reduce((acc, item) => {
21
- acc[item.id] = item;
22
- return acc;
23
- }, {});
24
- const cloneStats = (stats) => ({
25
- offense: stats.offense,
26
- defense: stats.defense,
27
- speed: stats.speed,
28
- crit: stats.crit,
29
- });
30
- // ------------------------------------------------------
31
- // Creación de estado inicial
32
- // ------------------------------------------------------
33
- const createBattleFighter = (cfg) => {
34
- if (cfg.moves.length !== 4) {
35
- throw new Error("Each fighter must have exactly 4 moves in MVP");
36
- }
37
- if (cfg.items.length > 4) {
38
- throw new Error("A fighter cannot have more than 4 items");
39
- }
40
- return {
41
- fighterId: cfg.fighter.id,
42
- classId: cfg.fighter.classId,
43
- maxHp: cfg.maxHp,
44
- currentHp: cfg.maxHp,
45
- baseStats: cloneStats(cfg.fighter.baseStats),
46
- effectiveStats: cloneStats(cfg.fighter.baseStats),
47
- moves: cfg.moves.map((move) => ({
48
- moveId: move.id,
49
- currentPP: move.maxPP,
50
- })),
51
- items: cfg.items.map((item) => ({
52
- itemId: item.id,
53
- usesRemaining: item.maxUses,
54
- })),
55
- amuletId: cfg.amulet ? cfg.amulet.id : null,
56
- statuses: [],
57
- isAlive: true,
58
- };
59
- };
60
- const recomputeEffectiveStatsForFighter = (state, fighter) => {
61
- // Partimos de base
62
- let eff = { ...fighter.baseStats };
63
- // 1) Aplicar estados
64
- for (const st of fighter.statuses) {
65
- const def = state.runtime.statusesById[st.statusId];
66
- if (!def)
67
- continue;
68
- const stacks = st.stacks === 2 ? 2 : 1;
69
- for (const effDef of def.effectsPerStack) {
70
- const totalStacks = stacks; // podrías multiplicar efectos por stacks
71
- switch (effDef.kind) {
72
- case "modify_stats":
73
- eff.offense += (effDef.offenseDelta ?? 0) * totalStacks;
74
- eff.defense += (effDef.defenseDelta ?? 0) * totalStacks;
75
- eff.speed += (effDef.speedDelta ?? 0) * totalStacks;
76
- eff.crit += (effDef.critDelta ?? 0) * totalStacks;
77
- break;
78
- case "modify_effective_speed":
79
- eff.speed = Math.floor(eff.speed * Math.pow(effDef.multiplier, totalStacks));
80
- break;
81
- default:
82
- break;
83
- }
84
- }
85
- }
86
- // 2) Aquí podrías aplicar amuletos, buffs temporales o lo que quieras
87
- return {
88
- ...fighter,
89
- effectiveStats: eff,
90
- };
91
- };
92
- const createPlayerBattleState = (cfg) => {
93
- const fighter = createBattleFighter(cfg);
94
- return {
95
- fighterTeam: [fighter],
96
- activeIndex: 0,
97
- };
98
- };
99
- export const createInitialBattleState = (config) => {
100
- const rules = {
101
- ...DEFAULT_RULES,
102
- ...config.rules,
103
- };
104
- const runtime = {
105
- rules,
106
- typesById: buildMap(config.types),
107
- movesById: buildMap(config.moves),
108
- itemsById: buildMap(config.items),
109
- amuletsById: buildMap(config.amulets),
110
- statusesById: buildMap(config.statuses),
111
- typeEffectiveness: config.typeEffectiveness,
112
- };
113
- const player1 = createPlayerBattleState(config.player1);
114
- const player2 = createPlayerBattleState(config.player2);
115
- const state = {
116
- turnNumber: 1,
117
- player1,
118
- player2,
119
- runtime,
120
- };
121
- return state;
122
- };
123
- // ------------------------------------------------------
124
- // Helpers
125
- // ------------------------------------------------------
126
- const getActiveFighter = (player) => player.fighterTeam[player.activeIndex];
127
- const getOpponentAndSelf = (state, playerKey) => {
128
- const selfPlayer = playerKey === "player1" ? state.player1 : state.player2;
129
- const oppPlayer = playerKey === "player1" ? state.player2 : state.player1;
130
- return {
131
- self: getActiveFighter(selfPlayer),
132
- opponent: getActiveFighter(oppPlayer),
133
- };
134
- };
135
- const getTypeEffectiveness = (state, attackerTypeId, defenderTypeId) => {
136
- const matrix = state.runtime.typeEffectiveness;
137
- const row = matrix[attackerTypeId];
138
- if (!row)
139
- return 1;
140
- return row[defenderTypeId] ?? 1;
141
- };
142
- const randomInRange = (min, max) => Math.random() * (max - min) + min;
143
- const chance = (probability) => {
144
- if (probability <= 0)
145
- return false;
146
- if (probability >= 1)
147
- return true;
148
- return Math.random() < probability;
149
- };
150
- // ------------------------------------------------------
151
- // Cálculo de críticos y daño
152
- // ------------------------------------------------------
153
- const computeCritChance = (rules, critStat) => {
154
- const raw = rules.baseCritChance + critStat * rules.critPerStat;
155
- return Math.max(0, Math.min(1, raw));
156
- };
157
- const computeDamage = (input) => {
158
- const { state, attacker, defender, moveTypeId, basePower, isCritical } = input;
159
- const rules = state.runtime.rules;
160
- const attackerOff = attacker.effectiveStats.offense;
161
- const defenderDef = defender.effectiveStats.defense;
162
- const typeEffectiveness = getTypeEffectiveness(state, moveTypeId, defender.classId);
163
- if (typeEffectiveness === 0) {
164
- return {
165
- damage: 0,
166
- effectiveness: 0,
167
- };
168
- }
169
- const stabMultiplier = attacker.classId === moveTypeId ? rules.stabMultiplier : 1;
170
- const critMultiplier = isCritical ? rules.critMultiplier : 1;
171
- const randomFactor = randomInRange(rules.randomMinDamageFactor, rules.randomMaxDamageFactor);
172
- const rawDamage = basePower *
173
- (attackerOff / Math.max(1, defenderDef)) *
174
- typeEffectiveness *
175
- stabMultiplier *
176
- critMultiplier *
177
- randomFactor;
178
- const finalDamage = Math.max(1, Math.floor(rawDamage));
179
- return {
180
- damage: finalDamage,
181
- effectiveness: typeEffectiveness,
182
- };
183
- };
184
- let eventCounter = 0;
185
- const nextEventId = () => {
186
- eventCounter += 1;
187
- return `evt_${eventCounter}`;
188
- };
189
- const createBaseEvent = (turnNumber, kind, message) => ({
190
- id: nextEventId(),
191
- kind,
192
- turnNumber,
193
- message,
194
- timestamp: Date.now(),
195
- });
196
- const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
197
- const events = [];
198
- const newHp = Math.max(0, defender.currentHp - amount);
199
- const updatedDefender = {
200
- ...defender,
201
- currentHp: newHp,
202
- isAlive: newHp > 0,
203
- };
204
- events.push({
205
- ...createBaseEvent(state.turnNumber, "damage", `${actorId} inflige ${amount} de daño a ${defender.fighterId}`),
206
- actorId,
207
- targetId: defender.fighterId,
208
- amount,
209
- isCritical,
210
- });
211
- if (!updatedDefender.isAlive) {
212
- events.push({
213
- ...createBaseEvent(state.turnNumber, "fighter_fainted", `${defender.fighterId} ha caído`),
214
- fighterId: defender.fighterId,
215
- });
216
- }
217
- return { updatedDefender, events };
218
- };
219
- // ------------------------------------------------------
220
- // apply_status
221
- // ------------------------------------------------------
222
- const applyStatusToFighter = (state, target, statusId) => {
223
- const def = state.runtime.statusesById[statusId];
224
- dbg("APPLY_STATUS_FN", {
225
- targetId: target.fighterId,
226
- statusId,
227
- hasDef: !!def,
228
- currentStatuses: target.statuses,
229
- });
230
- if (!def)
231
- return { updated: target, events: [] };
232
- const events = [];
233
- const existing = target.statuses.find((s) => s.statusId === statusId);
234
- const duration = Math.floor(randomInRange(def.minDurationTurns, def.maxDurationTurns + 1)) ||
235
- def.minDurationTurns;
236
- let newStatuses = [...target.statuses];
237
- if (!existing) {
238
- newStatuses.push({
239
- statusId,
240
- stacks: 1,
241
- remainingTurns: duration,
242
- });
243
- }
244
- else if (existing.stacks === 1) {
245
- newStatuses = newStatuses.map((s) => s.statusId === statusId
246
- ? { ...s, stacks: 2, remainingTurns: duration }
247
- : s);
248
- }
249
- else {
250
- newStatuses = newStatuses.map((s) => s.statusId === statusId ? { ...s, remainingTurns: duration } : s);
251
- }
252
- dbg("STATUS_APPLY", {
253
- fighterId: target.fighterId,
254
- statusId,
255
- duration,
256
- prevStacks: existing?.stacks ?? 0,
257
- });
258
- events.push({
259
- ...createBaseEvent(state.turnNumber, "status_applied", `Se aplica ${statusId} a ${target.fighterId}`),
260
- targetId: target.fighterId,
261
- statusId,
262
- });
263
- return {
264
- updated: { ...target, statuses: newStatuses },
265
- events,
266
- };
267
- };
268
- // ------------------------------------------------------
269
- // clear_status
270
- // ------------------------------------------------------
271
- const clearStatusFromFighter = (state, target, effect) => {
272
- const { statusIds, kinds, clearAll } = effect;
273
- const events = [];
274
- const before = target.statuses;
275
- let after = before;
276
- if (clearAll) {
277
- after = [];
278
- }
279
- else {
280
- after = before.filter((st) => {
281
- const def = state.runtime.statusesById[st.statusId];
282
- if (statusIds && statusIds.includes(st.statusId)) {
283
- return false;
284
- }
285
- if (kinds && def && kinds.includes(def.kind)) {
286
- return false;
287
- }
288
- return true;
289
- });
290
- }
291
- if (after.length === before.length) {
292
- // nada que limpiar
293
- return { updated: target, events };
294
- }
295
- const removed = before
296
- .filter((old) => !after.some((st) => st.statusId === old.statusId))
297
- .map((st) => st.statusId);
298
- dbg("STATUS_CLEAR", {
299
- fighterId: target.fighterId,
300
- cleared: removed,
301
- clearAll: !!clearAll,
302
- });
303
- events.push({
304
- ...createBaseEvent(state.turnNumber, "status_cleared", `Se limpian estados (${removed.join(", ")}) de ${target.fighterId}`),
305
- targetId: target.fighterId,
306
- statusIds: removed,
307
- });
308
- return {
309
- updated: {
310
- ...target,
311
- statuses: after,
312
- },
313
- events,
314
- };
315
- };
316
- // ------------------------------------------------------
317
- // heal
318
- // ------------------------------------------------------
319
- const applyHealToFighter = (state, target, amount, sourceId) => {
320
- const newHp = Math.min(target.maxHp, target.currentHp + amount);
321
- const healed = newHp - target.currentHp;
322
- const updated = {
323
- ...target,
324
- currentHp: newHp,
325
- isAlive: newHp > 0,
326
- };
327
- const events = [];
328
- dbg("HEAL", {
329
- sourceId,
330
- targetId: target.fighterId,
331
- healed,
332
- newHp,
333
- });
334
- events.push({
335
- ...createBaseEvent(state.turnNumber, "heal", `${sourceId} cura ${healed} a ${target.fighterId}`),
336
- actorId: sourceId,
337
- targetId: target.fighterId,
338
- amount: healed,
339
- });
340
- return { updated, events };
341
- };
342
- // ------------------------------------------------------
343
- // Target por defecto y resolución de target
344
- // ------------------------------------------------------
345
- const defaultTargetForKind = (kind) => {
346
- switch (kind) {
347
- case "damage":
348
- case "apply_status":
349
- return "enemy";
350
- case "heal":
351
- case "clear_status":
352
- case "modify_stats":
353
- case "modify_hit_chance":
354
- case "modify_crit_chance":
355
- case "modify_priority":
356
- case "modify_effective_speed":
357
- case "shield":
358
- return "self";
359
- default:
360
- return "self";
361
- }
362
- };
363
- const resolveEffectTarget = (eff, actor, target) => {
364
- const t = eff.target ?? defaultTargetForKind(eff.kind);
365
- const isSelf = t === "self";
366
- return {
367
- primary: isSelf ? actor : target,
368
- isSelf,
369
- };
370
- };
371
- // ------------------------------------------------------
372
- // Motor de efectos genérico (move + item)
373
- // ------------------------------------------------------
374
- const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritical, effects) => {
375
- dbg("applyEffectsOnTarget", {
376
- actorId: actor.fighterId,
377
- targetId: target.fighterId,
378
- moveTypeId,
379
- isCritical,
380
- effects,
381
- });
382
- let currentActor = actor;
383
- let currentTarget = target;
384
- const events = [];
385
- dbg("APPLY_EFFECTS_START", {
386
- actorId: actor.fighterId,
387
- targetId: target.fighterId,
388
- moveTypeId,
389
- isCritical,
390
- effectsCount: effects?.length ?? 0,
391
- });
392
- for (const eff of effects) {
393
- dbg("EFFECT_LOOP", eff);
394
- switch (eff.kind) {
395
- case "heal": {
396
- dbg("EFFECT_HEAL_START", {
397
- amount: eff.amount,
398
- targetDefault: eff.target,
399
- });
400
- const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
401
- const res = applyHealToFighter(state, primary, eff.amount, actor.fighterId);
402
- if (isSelf)
403
- currentActor = res.updated;
404
- else
405
- currentTarget = res.updated;
406
- events.push(...res.events);
407
- break;
408
- }
409
- case "apply_status": {
410
- dbg("EFFECT_APPLY_STATUS_START", {
411
- statusId: eff.statusId,
412
- targetDefault: eff.target,
413
- });
414
- const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
415
- const res = applyStatusToFighter(state, primary, eff.statusId);
416
- if (isSelf)
417
- currentActor = res.updated;
418
- else
419
- currentTarget = res.updated;
420
- events.push(...res.events);
421
- break;
422
- }
423
- case "clear_status": {
424
- dbg("debemos eliminar status");
425
- const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
426
- const res = clearStatusFromFighter(state, primary, eff);
427
- if (isSelf)
428
- currentActor = res.updated;
429
- else
430
- currentTarget = res.updated;
431
- events.push(...res.events);
432
- break;
433
- }
434
- case "damage": {
435
- dbg("debemos hacer daño");
436
- const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
437
- let totalDamage = 0;
438
- // Daño de fórmula (stats + tipo + crit…)
439
- if (eff.basePower && moveTypeId) {
440
- const { damage } = computeDamage({
441
- state,
442
- attacker: currentActor,
443
- defender: primary,
444
- moveTypeId,
445
- basePower: eff.basePower,
446
- isCritical,
447
- });
448
- totalDamage += damage;
449
- }
450
- // Daño plano adicional
451
- if (eff.flatAmount && eff.flatAmount > 0) {
452
- totalDamage += eff.flatAmount;
453
- }
454
- if (totalDamage > 0) {
455
- const res = applyDamageToFighter(state, primary, totalDamage, currentActor.fighterId, isCritical);
456
- if (isSelf)
457
- currentActor = res.updatedDefender;
458
- else
459
- currentTarget = res.updatedDefender;
460
- events.push(...res.events);
461
- }
462
- break;
463
- }
464
- // TODO: conectar shield, modify_stats, etc.
465
- default:
466
- break;
467
- }
468
- }
469
- return { actor: currentActor, target: currentTarget, events };
470
- };
471
- const getMovePriorityAndSpeed = (state, playerKey, action) => {
472
- const { self } = getOpponentAndSelf(state, playerKey);
473
- if (action.kind === "no_action") {
474
- return { priority: 0, speed: 0 };
475
- }
476
- if (action.kind === "use_move") {
477
- const moveEntry = self.moves[action.moveIndex];
478
- if (!moveEntry) {
479
- return { priority: 0, speed: self.effectiveStats.speed };
480
- }
481
- const moveDef = state.runtime.movesById[moveEntry.moveId];
482
- const priority = moveDef?.priority ?? 0;
483
- return {
484
- priority,
485
- speed: self.effectiveStats.speed,
486
- };
487
- }
488
- // Items y switch: prioridad base 0 por ahora
489
- return {
490
- priority: 0,
491
- speed: self.effectiveStats.speed,
492
- };
493
- };
494
- // ------------------------------------------------------
495
- // use_move
496
- // ------------------------------------------------------
497
- const resolveDamageMove = (state, playerKey, action) => {
498
- if (action.kind !== "use_move") {
499
- return { state, events: [] };
500
- }
501
- const { self, opponent } = getOpponentAndSelf(state, playerKey);
502
- const events = [];
503
- const moveSlot = self.moves[action.moveIndex];
504
- dbg("use_move: slot", moveSlot);
505
- if (!moveSlot) {
506
- dbg("use_move: empty slot", { playerKey, moveIndex: action.moveIndex });
507
- return { state, events };
508
- }
509
- const move = state.runtime.movesById[moveSlot.moveId];
510
- if (!move) {
511
- dbg("use_move: move definition not found", {
512
- playerKey,
513
- moveId: moveSlot.moveId,
514
- });
515
- return { state, events };
516
- }
517
- if (moveSlot.currentPP <= 0) {
518
- dbg("use_move: no PP", {
519
- playerKey,
520
- fighterId: self.fighterId,
521
- moveId: move.id,
522
- });
523
- return { state, events };
524
- }
525
- events.push({
526
- ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa ${move.name}`),
527
- actorId: self.fighterId,
528
- });
529
- const accuracy = move.accuracy ?? 100;
530
- const hitRoll = randomInRange(0, 100);
531
- const hit = hitRoll < accuracy;
532
- const updatedMoves = self.moves.map((m, idx) => idx === action.moveIndex
533
- ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
534
- : m);
535
- let updatedSelf = { ...self, moves: updatedMoves };
536
- let updatedOpponent = { ...opponent };
537
- if (!hit) {
538
- dbg("MISS", {
539
- fighterId: self.fighterId,
540
- moveId: move.id,
541
- moveName: move.name,
542
- hitRoll,
543
- accuracy,
544
- });
545
- events.push({
546
- ...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
547
- actorId: self.fighterId,
548
- moveId: move.id,
549
- targetId: opponent.fighterId,
550
- });
551
- const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
552
- return { state: newState, events };
553
- }
554
- const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
555
- const isCritical = chance(critChance);
556
- const effectiveness = getTypeEffectiveness(state, move.typeId, updatedOpponent.classId);
557
- dbg("HIT", {
558
- fighterId: self.fighterId,
559
- moveId: move.id,
560
- moveName: move.name,
561
- isCritical,
562
- effectiveness,
563
- defenderBeforeHp: opponent.currentHp,
564
- });
565
- events.push({
566
- ...createBaseEvent(state.turnNumber, "move_hit", `${self.fighterId} acierta ${move.name}`),
567
- actorId: self.fighterId,
568
- moveId: move.id,
569
- targetId: opponent.fighterId,
570
- isCritical,
571
- effectiveness,
572
- });
573
- dbg("MOVE_EFFECTS", {
574
- moveId: move.id,
575
- moveName: move.name,
576
- effects: move.effects,
577
- });
578
- // Todos los efectos (daño, aplicar estado, curas, etc.)
579
- const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
580
- events.push(...extraEvents);
581
- const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
582
- return { state: newState, events };
583
- };
584
- // ------------------------------------------------------
585
- // use_item (ahora el target real está en cada efecto)
586
- // ------------------------------------------------------
587
- const resolveItemUse = (state, playerKey, action) => {
588
- if (action.kind !== "use_item") {
589
- return { state, events: [] };
590
- }
591
- const { self, opponent } = getOpponentAndSelf(state, playerKey);
592
- const events = [];
593
- const itemSlot = self.items[action.itemIndex];
594
- if (!itemSlot || itemSlot.usesRemaining <= 0) {
595
- dbg("use_item: no charges", {
596
- playerKey,
597
- itemIndex: action.itemIndex,
598
- });
599
- return { state, events };
600
- }
601
- const itemDef = state.runtime.itemsById[itemSlot.itemId];
602
- if (!itemDef) {
603
- dbg("use_item: item definition not found", {
604
- playerKey,
605
- itemId: itemSlot.itemId,
606
- });
607
- return { state, events };
608
- }
609
- const updatedItems = self.items.map((it, idx) => idx === action.itemIndex
610
- ? { ...it, usesRemaining: Math.max(0, it.usesRemaining - 1) }
611
- : it);
612
- let updatedSelf = { ...self, items: updatedItems };
613
- let updatedOpponent = { ...opponent };
614
- events.push({
615
- ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usa objeto ${itemDef.name}`),
616
- actorId: self.fighterId,
617
- });
618
- const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, null, // los items no usan fórmula de tipo por ahora
619
- false, itemDef.effects);
620
- events.push(...extraEvents);
621
- const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
622
- return { state: newState, events };
623
- };
624
- // ------------------------------------------------------
625
- // Actualizar fighters en el estado
626
- // ------------------------------------------------------
627
- const updateFightersInState = (state, actingPlayerKey, updatedSelf, updatedOpponent) => {
628
- const player1 = { ...state.player1 };
629
- const player2 = { ...state.player2 };
630
- const updateInPlayer = (player, updated) => {
631
- const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
632
- return {
633
- ...player,
634
- fighterTeam: newTeam,
635
- };
636
- };
637
- if (actingPlayerKey === "player1") {
638
- const selfPlayer = updateInPlayer(player1, updatedSelf);
639
- const oppPlayer = updateInPlayer(player2, updatedOpponent);
640
- return {
641
- ...state,
642
- player1: selfPlayer,
643
- player2: oppPlayer,
644
- };
645
- }
646
- const selfPlayer = updateInPlayer(player2, updatedSelf);
647
- const oppPlayer = updateInPlayer(player1, updatedOpponent);
648
- return {
649
- ...state,
650
- player1: oppPlayer,
651
- player2: selfPlayer,
652
- };
653
- };
654
- // ------------------------------------------------------
655
- // CC duro + check winner
656
- // ------------------------------------------------------
657
- const hasHardCc = (state, fighter) => fighter.statuses.some((st) => {
658
- const def = state.runtime.statusesById[st.statusId];
659
- return def?.kind === "hard_cc";
660
- });
661
- const checkWinner = (state) => {
662
- const f1 = getActiveFighter(state.player1);
663
- const f2 = getActiveFighter(state.player2);
664
- const p1Alive = f1.isAlive && f1.currentHp > 0;
665
- const p2Alive = f2.isAlive && f2.currentHp > 0;
666
- if (p1Alive && !p2Alive)
667
- return "player1";
668
- if (!p1Alive && p2Alive)
669
- return "player2";
670
- if (!p1Alive && !p2Alive)
671
- return "draw";
672
- return "none";
673
- };
674
- // ------------------------------------------------------
675
- // Fin de turno: aplicar DoT de estados
676
- // ------------------------------------------------------
677
- const applyEndOfTurnStatuses = (state) => {
678
- const events = [];
679
- const applyForPlayer = (player) => {
680
- const active = getActiveFighter(player);
681
- let updated = { ...active };
682
- const updatedStatuses = [];
683
- for (const st of active.statuses) {
684
- const def = state.runtime.statusesById[st.statusId];
685
- if (!def) {
686
- dbg("STATUS_MISSING_DEF", {
687
- fighterId: active.fighterId,
688
- statusId: st.statusId,
689
- });
690
- continue;
691
- }
692
- let damageFromStatus = 0;
693
- def.effectsPerStack.forEach((eff) => {
694
- if (eff.kind === "damage") {
695
- const stacksMultiplier = st.stacks === 2 ? 2 : 1;
696
- const base = typeof eff.flatAmount === "number"
697
- ? eff.flatAmount
698
- : typeof eff.basePower === "number"
699
- ? eff.basePower
700
- : 10; // fallback
701
- damageFromStatus += base * stacksMultiplier;
702
- }
703
- });
704
- if (damageFromStatus > 0 && updated.isAlive) {
705
- dbg("STATUS_DOT", {
706
- fighterId: updated.fighterId,
707
- statusId: st.statusId,
708
- damageFromStatus,
709
- stacks: st.stacks,
710
- });
711
- const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
712
- updated = damageRes.updatedDefender;
713
- events.push(...damageRes.events);
714
- }
715
- const remaining = st.remainingTurns - 1;
716
- if (remaining > 0) {
717
- updatedStatuses.push({
718
- ...st,
719
- remainingTurns: remaining,
720
- });
721
- }
722
- else {
723
- dbg("STATUS_EXPIRE", {
724
- fighterId: updated.fighterId,
725
- statusId: st.statusId,
726
- });
727
- events.push({
728
- ...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
729
- targetId: updated.fighterId,
730
- statusId: st.statusId,
731
- });
732
- }
733
- }
734
- updated = {
735
- ...updated,
736
- statuses: updatedStatuses,
737
- };
738
- const newTeam = player.fighterTeam.map((f, idx) => idx === player.activeIndex ? updated : f);
739
- return {
740
- ...player,
741
- fighterTeam: newTeam,
742
- };
743
- };
744
- const p1 = applyForPlayer(state.player1);
745
- const p2 = applyForPlayer(state.player2);
746
- const newState = {
747
- ...state,
748
- player1: p1,
749
- player2: p2,
750
- };
751
- return { state: newState, events };
752
- };
753
- // ------------------------------------------------------
754
- // Bucle principal de turno
755
- // ------------------------------------------------------
756
- export const resolveTurn = (state, actions) => {
757
- const runtimeState = state;
758
- let currentState = runtimeState;
759
- const recalcForPlayer = (player) => {
760
- const team = player.fighterTeam.map((f) => recomputeEffectiveStatsForFighter(runtimeState, f));
761
- return { ...player, fighterTeam: team };
762
- };
763
- currentState = {
764
- ...currentState,
765
- player1: recalcForPlayer(currentState.player1),
766
- player2: recalcForPlayer(currentState.player2),
767
- };
768
- const events = [];
769
- dbg(`TURN ${runtimeState.turnNumber} start`, {
770
- p1Action: actions.player1.kind,
771
- p2Action: actions.player2.kind,
772
- });
773
- events.push({
774
- ...createBaseEvent(runtimeState.turnNumber, "turn_start", `Comienza el turno ${runtimeState.turnNumber}`),
775
- });
776
- const entries = [
777
- {
778
- playerKey: "player1",
779
- action: actions.player1,
780
- ...getMovePriorityAndSpeed(runtimeState, "player1", actions.player1),
781
- },
782
- {
783
- playerKey: "player2",
784
- action: actions.player2,
785
- ...getMovePriorityAndSpeed(runtimeState, "player2", actions.player2),
786
- },
787
- ];
788
- entries.sort((a, b) => {
789
- if (b.priority !== a.priority) {
790
- return b.priority - a.priority;
791
- }
792
- if (b.speed !== a.speed) {
793
- return b.speed - a.speed;
794
- }
795
- // empate total → coin flip
796
- return Math.random() < 0.5 ? -1 : 1;
797
- });
798
- dbg(`TURN ${runtimeState.turnNumber} order`, entries.map((e) => ({
799
- player: e.playerKey,
800
- action: e.action.kind,
801
- priority: e.priority,
802
- speed: e.speed,
803
- })));
804
- for (const entry of entries) {
805
- const { playerKey, action } = entry;
806
- if (action.kind === "no_action")
807
- continue;
808
- const { self } = getOpponentAndSelf(currentState, playerKey);
809
- if (!self.isAlive || self.currentHp <= 0)
810
- continue;
811
- if (hasHardCc(currentState, self)) {
812
- events.push({
813
- ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
814
- actorId: self.fighterId,
815
- });
816
- continue;
817
- }
818
- if (action.kind === "use_move") {
819
- const result = resolveDamageMove(currentState, playerKey, action);
820
- currentState = result.state;
821
- events.push(...result.events);
822
- }
823
- else if (action.kind === "use_item") {
824
- const result = resolveItemUse(currentState, playerKey, action);
825
- currentState = result.state;
826
- events.push(...result.events);
827
- }
828
- else {
829
- // switch_fighter u otros no implementados aún
830
- continue;
831
- }
832
- const winnerAfterAction = checkWinner(currentState);
833
- if (winnerAfterAction !== "none") {
834
- events.push({
835
- ...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAfterAction}`),
836
- winner: winnerAfterAction,
837
- });
838
- const finalState = {
839
- ...currentState,
840
- turnNumber: currentState.turnNumber + 1,
841
- };
842
- return {
843
- newState: finalState,
844
- events,
845
- };
846
- }
847
- }
848
- const statusResult = applyEndOfTurnStatuses(currentState);
849
- currentState = statusResult.state;
850
- events.push(...statusResult.events);
851
- const winnerAtEnd = checkWinner(currentState);
852
- if (winnerAtEnd !== "none") {
853
- events.push({
854
- ...createBaseEvent(currentState.turnNumber, "battle_end", `La batalla termina: ${winnerAtEnd}`),
855
- winner: winnerAtEnd,
856
- });
857
- }
858
- events.push({
859
- ...createBaseEvent(currentState.turnNumber, "turn_end", `Termina el turno ${currentState.turnNumber}`),
860
- });
861
- const finalState = {
862
- ...currentState,
863
- turnNumber: currentState.turnNumber + 1,
864
- };
865
- return {
866
- newState: finalState,
867
- events,
868
- };
869
- };