pokemon-io-core 0.0.82 → 0.0.84

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 (76) hide show
  1. package/dist/core/battleState.d.ts +1 -1
  2. package/dist/core/engine.js +43 -7
  3. package/dist/core/index.d.ts +0 -1
  4. package/dist/core/index.js +0 -1
  5. package/dist/engine/actions/forcedSwitch.d.ts +5 -0
  6. package/dist/engine/actions/forcedSwitch.js +44 -0
  7. package/dist/engine/actions/index.d.ts +5 -0
  8. package/dist/engine/actions/index.js +5 -0
  9. package/dist/engine/actions/item.d.ts +6 -0
  10. package/dist/engine/actions/item.js +61 -0
  11. package/dist/engine/actions/move.d.ts +6 -0
  12. package/dist/engine/actions/move.js +120 -0
  13. package/dist/engine/actions/priority.d.ts +6 -0
  14. package/dist/engine/actions/priority.js +30 -0
  15. package/dist/engine/actions/switch.d.ts +6 -0
  16. package/dist/engine/actions/switch.js +55 -0
  17. package/dist/engine/combat/crit.d.ts +2 -0
  18. package/dist/engine/combat/crit.js +4 -0
  19. package/dist/engine/combat/damage.d.ts +20 -0
  20. package/dist/engine/combat/damage.js +66 -0
  21. package/dist/engine/combat/heal.d.ts +6 -0
  22. package/dist/engine/combat/heal.js +25 -0
  23. package/dist/engine/combat/index.d.ts +5 -0
  24. package/dist/engine/combat/index.js +5 -0
  25. package/dist/engine/combat/typeEffectiveness.d.ts +3 -0
  26. package/dist/engine/combat/typeEffectiveness.js +7 -0
  27. package/dist/engine/combat/winner.d.ts +2 -0
  28. package/dist/engine/combat/winner.js +11 -0
  29. package/dist/engine/debug.d.ts +1 -0
  30. package/dist/engine/debug.js +6 -0
  31. package/dist/engine/effects/applyEffects.d.ts +7 -0
  32. package/dist/engine/effects/applyEffects.js +103 -0
  33. package/dist/engine/effects/index.d.ts +2 -0
  34. package/dist/engine/effects/index.js +2 -0
  35. package/dist/engine/effects/target.d.ts +6 -0
  36. package/dist/engine/effects/target.js +26 -0
  37. package/dist/engine/engine.d.ts +53 -0
  38. package/dist/engine/engine.js +1046 -0
  39. package/dist/engine/events.d.ts +4 -0
  40. package/dist/engine/events.js +12 -0
  41. package/dist/engine/fighters/fighter.d.ts +4 -0
  42. package/dist/engine/fighters/fighter.js +53 -0
  43. package/dist/engine/fighters/index.d.ts +3 -0
  44. package/dist/engine/fighters/index.js +3 -0
  45. package/dist/engine/fighters/selectors.d.ts +13 -0
  46. package/dist/engine/fighters/selectors.js +19 -0
  47. package/dist/engine/fighters/update.d.ts +3 -0
  48. package/dist/engine/fighters/update.js +30 -0
  49. package/dist/engine/index.d.ts +11 -1
  50. package/dist/engine/index.js +11 -1
  51. package/dist/engine/rng.d.ts +2 -0
  52. package/dist/engine/rng.js +8 -0
  53. package/dist/engine/rules.d.ts +44 -0
  54. package/dist/engine/rules.js +10 -0
  55. package/dist/engine/runtime.d.ts +7 -0
  56. package/dist/engine/runtime.js +49 -0
  57. package/dist/engine/status/apply.d.ts +6 -0
  58. package/dist/engine/status/apply.js +49 -0
  59. package/dist/engine/status/clear.d.ts +6 -0
  60. package/dist/engine/status/clear.js +47 -0
  61. package/dist/engine/status/endOfTurn.d.ts +6 -0
  62. package/dist/engine/status/endOfTurn.js +80 -0
  63. package/dist/engine/status/hardCc.d.ts +3 -0
  64. package/dist/engine/status/hardCc.js +4 -0
  65. package/dist/engine/status/index.d.ts +4 -0
  66. package/dist/engine/status/index.js +4 -0
  67. package/dist/engine/turn/index.d.ts +1 -0
  68. package/dist/engine/turn/index.js +1 -0
  69. package/dist/engine/turn/resolveTurn.d.ts +5 -0
  70. package/dist/engine/turn/resolveTurn.js +139 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.js +1 -0
  73. package/dist/skins/CombatSkin.d.ts +1 -1
  74. package/dist/skins/cliches/clicheSkin.d.ts +2 -1
  75. package/dist/skins/pokemon/pokemonSkin.d.ts +2 -1
  76. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BattleRuntime } from "../core/engine.js";
1
+ import { BattleRuntime } from "../engine/rules.js";
2
2
  import type { FighterId, TypeId, MoveId, ItemId, AmuletId, StatusId } from "./ids";
3
3
  import type { FighterStats } from "./types.js";
4
4
  export interface EquippedMove {
@@ -209,25 +209,26 @@ export const applyForcedSwitchChoice = (state, targetPlayer, newIndex) => {
209
209
  if (forced.targetPlayer !== targetPlayer)
210
210
  return { state, events: [] };
211
211
  const self = targetPlayer === "player1" ? state.player1 : state.player2;
212
- if (newIndex < 0 || newIndex >= self.fighterTeam.length) {
212
+ if (newIndex < 0 || newIndex >= self.fighterTeam.length)
213
213
  return { state, events: [] };
214
- }
215
214
  const fromIndex = self.activeIndex;
216
215
  if (newIndex === fromIndex)
217
216
  return { state, events: [] };
218
217
  const toFighter = self.fighterTeam[newIndex];
219
- if (!toFighter || !toFighter.isAlive || toFighter.currentHp <= 0) {
218
+ if (!toFighter || !toFighter.isAlive || toFighter.currentHp <= 0)
220
219
  return { state, events: [] };
221
- }
222
220
  const fromFighter = self.fighterTeam[fromIndex];
223
221
  const events = [
224
222
  {
225
- ...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador cambia a ${toFighter.fighterId}`),
223
+ ...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador retira a ${fromFighter.fighterId} y manda a ${toFighter.fighterId}`),
226
224
  fromFighterId: fromFighter.fighterId,
227
225
  toFighterId: toFighter.fighterId,
228
226
  },
227
+ {
228
+ ...createBaseEvent(state.turnNumber, "turn_end", `Termina el turno ${state.turnNumber}`),
229
+ },
229
230
  ];
230
- const nextState = targetPlayer === "player1"
231
+ const switchedState = targetPlayer === "player1"
231
232
  ? {
232
233
  ...state,
233
234
  forcedSwitch: null,
@@ -238,6 +239,10 @@ export const applyForcedSwitchChoice = (state, targetPlayer, newIndex) => {
238
239
  forcedSwitch: null,
239
240
  player2: { ...state.player2, activeIndex: newIndex },
240
241
  };
242
+ const nextState = {
243
+ ...switchedState,
244
+ turnNumber: switchedState.turnNumber + 1,
245
+ };
241
246
  return { state: nextState, events };
242
247
  };
243
248
  let eventCounter = 0;
@@ -697,7 +702,32 @@ const resolveDamageMove = (state, playerKey, action) => {
697
702
  // Todos los efectos (daño, aplicar estado, curas, etc.)
698
703
  const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
699
704
  events.push(...extraEvents);
700
- const newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
705
+ let newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
706
+ // --- FORCED SWITCH (Roar) ---
707
+ const hasForceSwitch = move.effects.some((e) => e.kind === "force_switch");
708
+ if (hasForceSwitch) {
709
+ const targetPlayer = playerKey === "player1" ? "player2" : "player1";
710
+ const targetSide = targetPlayer === "player1" ? newState.player1 : newState.player2;
711
+ const activeIdx = targetSide.activeIndex;
712
+ const hasBenchAlive = targetSide.fighterTeam.some((f, idx) => {
713
+ if (idx === activeIdx)
714
+ return false;
715
+ return f.isAlive && f.currentHp > 0;
716
+ });
717
+ if (hasBenchAlive) {
718
+ newState = {
719
+ ...newState,
720
+ forcedSwitch: {
721
+ targetPlayer,
722
+ reason: "roar",
723
+ },
724
+ };
725
+ events.push({
726
+ ...createBaseEvent(state.turnNumber, "action_declared", `¡El entrenador rival debe cambiar de luchador!`),
727
+ actorId: self.fighterId,
728
+ });
729
+ }
730
+ }
701
731
  return { state: newState, events };
702
732
  };
703
733
  // ------------------------------------------------------
@@ -970,6 +1000,12 @@ export const resolveTurn = (state, actions) => {
970
1000
  // switch_fighter u otros no implementados aún
971
1001
  continue;
972
1002
  }
1003
+ if (currentState.forcedSwitch) {
1004
+ return {
1005
+ newState: currentState,
1006
+ events,
1007
+ };
1008
+ }
973
1009
  const winnerAfterAction = checkWinner(currentState);
974
1010
  if (winnerAfterAction !== "none") {
975
1011
  events.push({
@@ -1,7 +1,6 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
- export * from "./engine.js";
5
4
  export * from "./events.js";
6
5
  export * from "./fighters.js";
7
6
  export * from "./ids.js";
@@ -1,7 +1,6 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
- export * from "./engine.js";
5
4
  export * from "./events.js";
6
5
  export * from "./fighters.js";
7
6
  export * from "./ids.js";
@@ -0,0 +1,5 @@
1
+ import { BattleEvent, BattleState } from "../../core";
2
+ export declare const applyForcedSwitchChoice: (state: BattleState, targetPlayer: "player1" | "player2", newIndex: number) => {
3
+ state: BattleState;
4
+ events: BattleEvent[];
5
+ };
@@ -0,0 +1,44 @@
1
+ import { createBaseEvent } from "../events";
2
+ export const applyForcedSwitchChoice = (state, targetPlayer, newIndex) => {
3
+ const forced = state.forcedSwitch;
4
+ if (!forced)
5
+ return { state, events: [] };
6
+ if (forced.targetPlayer !== targetPlayer)
7
+ return { state, events: [] };
8
+ const self = targetPlayer === "player1" ? state.player1 : state.player2;
9
+ if (newIndex < 0 || newIndex >= self.fighterTeam.length)
10
+ return { state, events: [] };
11
+ const fromIndex = self.activeIndex;
12
+ if (newIndex === fromIndex)
13
+ return { state, events: [] };
14
+ const toFighter = self.fighterTeam[newIndex];
15
+ if (!toFighter || !toFighter.isAlive || toFighter.currentHp <= 0)
16
+ return { state, events: [] };
17
+ const fromFighter = self.fighterTeam[fromIndex];
18
+ const events = [
19
+ {
20
+ ...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador retira a ${fromFighter.fighterId} y manda a ${toFighter.fighterId}`),
21
+ fromFighterId: fromFighter.fighterId,
22
+ toFighterId: toFighter.fighterId,
23
+ },
24
+ {
25
+ ...createBaseEvent(state.turnNumber, "turn_end", `Termina el turno ${state.turnNumber}`),
26
+ },
27
+ ];
28
+ const switchedState = targetPlayer === "player1"
29
+ ? {
30
+ ...state,
31
+ forcedSwitch: null,
32
+ player1: { ...state.player1, activeIndex: newIndex },
33
+ }
34
+ : {
35
+ ...state,
36
+ forcedSwitch: null,
37
+ player2: { ...state.player2, activeIndex: newIndex },
38
+ };
39
+ const nextState = {
40
+ ...switchedState,
41
+ turnNumber: switchedState.turnNumber + 1,
42
+ };
43
+ return { state: nextState, events };
44
+ };
@@ -0,0 +1,5 @@
1
+ export * from './forcedSwitch.js';
2
+ export * from './item.js';
3
+ export * from './move.js';
4
+ export * from './priority.js';
5
+ export * from './switch.js';
@@ -0,0 +1,5 @@
1
+ export * from './forcedSwitch.js';
2
+ export * from './item.js';
3
+ export * from './move.js';
4
+ export * from './priority.js';
5
+ export * from './switch.js';
@@ -0,0 +1,6 @@
1
+ import { BattleEvent, PlayerAction } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const resolveItemUse: (state: RuntimeBattleState, playerKey: "player1" | "player2", action: PlayerAction) => {
4
+ state: RuntimeBattleState;
5
+ events: BattleEvent[];
6
+ };
@@ -0,0 +1,61 @@
1
+ import { dbg } from "../debug";
2
+ import { applyEffectsOnTarget } from "../effects/applyEffects";
3
+ import { createBaseEvent } from "../events";
4
+ import { getPlayersAndActives } from "../fighters/selectors";
5
+ import { updateFightersInState } from "../fighters/update";
6
+ export const resolveItemUse = (state, playerKey, action) => {
7
+ if (action.kind !== "use_item") {
8
+ return { state, events: [] };
9
+ }
10
+ const { selfPlayer, oppPlayer, self, opponent } = getPlayersAndActives(state, playerKey);
11
+ const events = [];
12
+ const itemSlot = selfPlayer.inventory[action.itemIndex];
13
+ if (!itemSlot || itemSlot.usesRemaining <= 0) {
14
+ dbg("use_item: no charges", {
15
+ playerKey,
16
+ itemIndex: action.itemIndex,
17
+ });
18
+ return { state, events };
19
+ }
20
+ const itemDef = state.runtime.itemsById[itemSlot.itemId];
21
+ if (!itemDef) {
22
+ dbg("use_item: item definition not found", {
23
+ playerKey,
24
+ itemId: itemSlot.itemId,
25
+ });
26
+ return { state, events };
27
+ }
28
+ const updatedInventory = selfPlayer.inventory.map((it, idx) => idx === action.itemIndex
29
+ ? { ...it, usesRemaining: Math.max(0, it.usesRemaining - 1) }
30
+ : it);
31
+ let updatedSelf = { ...self };
32
+ let updatedOpponent = { ...opponent };
33
+ events.push({
34
+ ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usó ${itemDef.name}`),
35
+ actorId: self.fighterId,
36
+ });
37
+ const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, null, // los items no usan fórmula de tipo por ahora
38
+ false, itemDef.effects);
39
+ events.push(...extraEvents);
40
+ let newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
41
+ // persistimos el inventario gastado en el jugador que actuó
42
+ if (playerKey === "player1") {
43
+ newState = {
44
+ ...newState,
45
+ player1: {
46
+ ...newState.player1,
47
+ inventory: updatedInventory,
48
+ },
49
+ };
50
+ }
51
+ else {
52
+ newState = {
53
+ ...newState,
54
+ player2: {
55
+ ...newState.player2,
56
+ inventory: updatedInventory,
57
+ },
58
+ };
59
+ }
60
+ return { state: newState, events };
61
+ };
@@ -0,0 +1,6 @@
1
+ import { BattleEvent, PlayerAction } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const resolveDamageMove: (state: RuntimeBattleState, playerKey: "player1" | "player2", action: PlayerAction) => {
4
+ state: RuntimeBattleState;
5
+ events: BattleEvent[];
6
+ };
@@ -0,0 +1,120 @@
1
+ import { computeCritChance } from "../combat/crit";
2
+ import { getTypeEffectiveness } from "../combat/typeEffectiveness";
3
+ import { dbg } from "../debug";
4
+ import { applyEffectsOnTarget } from "../effects/applyEffects";
5
+ import { createBaseEvent } from "../events";
6
+ import { getOpponentAndSelf } from "../fighters/selectors";
7
+ import { updateFightersInState } from "../fighters/update";
8
+ import { chance, randomInRange } from "../rng";
9
+ export const resolveDamageMove = (state, playerKey, action) => {
10
+ if (action.kind !== "use_move") {
11
+ return { state, events: [] };
12
+ }
13
+ const { self, opponent } = getOpponentAndSelf(state, playerKey);
14
+ const events = [];
15
+ const moveSlot = self.moves[action.moveIndex];
16
+ dbg("use_move: slot", moveSlot);
17
+ if (!moveSlot) {
18
+ dbg("use_move: empty slot", { playerKey, moveIndex: action.moveIndex });
19
+ return { state, events };
20
+ }
21
+ const move = state.runtime.movesById[moveSlot.moveId];
22
+ if (!move) {
23
+ dbg("use_move: move definition not found", {
24
+ playerKey,
25
+ moveId: moveSlot.moveId,
26
+ });
27
+ return { state, events };
28
+ }
29
+ if (moveSlot.currentPP <= 0) {
30
+ dbg("use_move: no PP", {
31
+ playerKey,
32
+ fighterId: self.fighterId,
33
+ moveId: move.id,
34
+ });
35
+ return { state, events };
36
+ }
37
+ events.push({
38
+ ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usó ${move.name}`),
39
+ actorId: self.fighterId,
40
+ });
41
+ const accuracy = move.accuracy ?? 100;
42
+ const hitRoll = randomInRange(0, 100);
43
+ const hit = hitRoll < accuracy;
44
+ const updatedMoves = self.moves.map((m, idx) => idx === action.moveIndex
45
+ ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
46
+ : m);
47
+ let updatedSelf = { ...self, moves: updatedMoves };
48
+ let updatedOpponent = { ...opponent };
49
+ if (!hit) {
50
+ dbg("MISS", {
51
+ fighterId: self.fighterId,
52
+ moveId: move.id,
53
+ moveName: move.name,
54
+ hitRoll,
55
+ accuracy,
56
+ });
57
+ events.push({
58
+ ...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
59
+ actorId: self.fighterId,
60
+ moveId: move.id,
61
+ targetId: opponent.fighterId,
62
+ });
63
+ const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
64
+ return { state: newState, events };
65
+ }
66
+ const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
67
+ const isCritical = chance(critChance);
68
+ const effectiveness = getTypeEffectiveness(state, move.typeId, updatedOpponent.classId);
69
+ dbg("HIT", {
70
+ fighterId: self.fighterId,
71
+ moveId: move.id,
72
+ moveName: move.name,
73
+ isCritical,
74
+ effectiveness,
75
+ defenderBeforeHp: opponent.currentHp,
76
+ });
77
+ events.push({
78
+ ...createBaseEvent(state.turnNumber, "move_hit", `${self.fighterId} acierta ${move.name}`),
79
+ actorId: self.fighterId,
80
+ moveId: move.id,
81
+ targetId: opponent.fighterId,
82
+ isCritical,
83
+ effectiveness,
84
+ });
85
+ dbg("MOVE_EFFECTS", {
86
+ moveId: move.id,
87
+ moveName: move.name,
88
+ effects: move.effects,
89
+ });
90
+ // Todos los efectos (daño, aplicar estado, curas, etc.)
91
+ const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
92
+ events.push(...extraEvents);
93
+ let newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
94
+ // --- FORCED SWITCH (Roar) ---
95
+ const hasForceSwitch = move.effects.some((e) => e.kind === "force_switch");
96
+ if (hasForceSwitch) {
97
+ const targetPlayer = playerKey === "player1" ? "player2" : "player1";
98
+ const targetSide = targetPlayer === "player1" ? newState.player1 : newState.player2;
99
+ const activeIdx = targetSide.activeIndex;
100
+ const hasBenchAlive = targetSide.fighterTeam.some((f, idx) => {
101
+ if (idx === activeIdx)
102
+ return false;
103
+ return f.isAlive && f.currentHp > 0;
104
+ });
105
+ if (hasBenchAlive) {
106
+ newState = {
107
+ ...newState,
108
+ forcedSwitch: {
109
+ targetPlayer,
110
+ reason: "roar",
111
+ },
112
+ };
113
+ events.push({
114
+ ...createBaseEvent(state.turnNumber, "action_declared", `¡El entrenador rival debe cambiar de luchador!`),
115
+ actorId: self.fighterId,
116
+ });
117
+ }
118
+ }
119
+ return { state: newState, events };
120
+ };
@@ -0,0 +1,6 @@
1
+ import { PlayerAction } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const getMovePriorityAndSpeed: (state: RuntimeBattleState, playerKey: "player1" | "player2", action: PlayerAction) => {
4
+ priority: number;
5
+ speed: number;
6
+ };
@@ -0,0 +1,30 @@
1
+ import { getOpponentAndSelf } from "../fighters/selectors";
2
+ export const getMovePriorityAndSpeed = (state, playerKey, action) => {
3
+ const { self } = getOpponentAndSelf(state, playerKey);
4
+ if (action.kind === "no_action") {
5
+ return { priority: 0, speed: 0 };
6
+ }
7
+ if (action.kind === "use_move") {
8
+ const moveEntry = self.moves[action.moveIndex];
9
+ if (!moveEntry) {
10
+ return { priority: 0, speed: self.effectiveStats.speed };
11
+ }
12
+ const moveDef = state.runtime.movesById[moveEntry.moveId];
13
+ const priority = moveDef?.priority ?? 0;
14
+ return {
15
+ priority,
16
+ speed: self.effectiveStats.speed,
17
+ };
18
+ }
19
+ if (action.kind === "switch_fighter") {
20
+ return {
21
+ priority: 10, // prioridad especial (ajústalo si quieres)
22
+ speed: self.effectiveStats.speed,
23
+ };
24
+ }
25
+ // Items y switch: prioridad base 0 por ahora
26
+ return {
27
+ priority: 0,
28
+ speed: self.effectiveStats.speed,
29
+ };
30
+ };
@@ -0,0 +1,6 @@
1
+ import { BattleEvent, PlayerAction } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const resolveSwitchFighter: (state: RuntimeBattleState, playerKey: "player1" | "player2", action: PlayerAction) => {
4
+ state: RuntimeBattleState;
5
+ events: BattleEvent[];
6
+ };
@@ -0,0 +1,55 @@
1
+ import { createBaseEvent } from "../events";
2
+ export const resolveSwitchFighter = (state, playerKey, action) => {
3
+ if (action.kind !== "switch_fighter")
4
+ return { state, events: [] };
5
+ const events = [];
6
+ const selfPlayer = playerKey === "player1" ? state.player1 : state.player2;
7
+ const fromIndex = selfPlayer.activeIndex;
8
+ const toIndex = action.newIndex;
9
+ // validaciones
10
+ if (toIndex < 0 || toIndex >= selfPlayer.fighterTeam.length) {
11
+ return { state, events };
12
+ }
13
+ if (toIndex === fromIndex) {
14
+ return { state, events };
15
+ }
16
+ const target = selfPlayer.fighterTeam[toIndex];
17
+ if (!target || !target.isAlive || target.currentHp <= 0) {
18
+ return { state, events };
19
+ }
20
+ const fromFighter = selfPlayer.fighterTeam[fromIndex];
21
+ events.push({
22
+ ...createBaseEvent(state.turnNumber, "fighter_switched", `El entrenador retira a ${fromFighter.fighterId} y manda a ${target.fighterId}`),
23
+ fromFighterId: fromFighter.fighterId,
24
+ toFighterId: target.fighterId,
25
+ toHp: target.currentHp,
26
+ toStatuses: (target.statuses ?? []).map((st) => ({
27
+ statusId: st.statusId,
28
+ stacks: st.stacks,
29
+ remainingTurns: st.remainingTurns,
30
+ })),
31
+ });
32
+ // actualizar activeIndex
33
+ if (playerKey === "player1") {
34
+ return {
35
+ state: {
36
+ ...state,
37
+ player1: {
38
+ ...state.player1,
39
+ activeIndex: toIndex,
40
+ },
41
+ },
42
+ events,
43
+ };
44
+ }
45
+ return {
46
+ state: {
47
+ ...state,
48
+ player2: {
49
+ ...state.player2,
50
+ activeIndex: toIndex,
51
+ },
52
+ },
53
+ events,
54
+ };
55
+ };
@@ -0,0 +1,2 @@
1
+ import { BattleRules } from "../rules";
2
+ export declare const computeCritChance: (rules: BattleRules, critStat: number) => number;
@@ -0,0 +1,4 @@
1
+ export const computeCritChance = (rules, critStat) => {
2
+ const raw = rules.baseCritChance + critStat * rules.critPerStat;
3
+ return Math.max(0, Math.min(1, raw));
4
+ };
@@ -0,0 +1,20 @@
1
+ import { BattleEvent, BattleFighter, FighterId, TypeId } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ interface DamageComputationInput {
4
+ state: RuntimeBattleState;
5
+ attacker: BattleFighter;
6
+ defender: BattleFighter;
7
+ moveTypeId: TypeId;
8
+ basePower: number;
9
+ isCritical: boolean;
10
+ }
11
+ export declare const computeDamage: (input: DamageComputationInput) => {
12
+ damage: number;
13
+ effectiveness: number;
14
+ };
15
+ interface DamageApplyResult {
16
+ updatedDefender: BattleFighter;
17
+ events: BattleEvent[];
18
+ }
19
+ export declare const applyDamageToFighter: (state: RuntimeBattleState, defender: BattleFighter, amount: number, actorId: FighterId, isCritical: boolean) => DamageApplyResult;
20
+ export {};
@@ -0,0 +1,66 @@
1
+ import { dbg } from "../debug";
2
+ import { createBaseEvent } from "../events";
3
+ import { randomInRange } from "../rng";
4
+ import { getTypeEffectiveness } from "./typeEffectiveness";
5
+ export const computeDamage = (input) => {
6
+ dbg("🧨 computeDamage", { input });
7
+ const { state, attacker, defender, moveTypeId, basePower, isCritical } = input;
8
+ const rules = state.runtime.rules;
9
+ const attackerOff = attacker.effectiveStats.offense;
10
+ const defenderDef = defender.effectiveStats.defense;
11
+ const typeEffectiveness = getTypeEffectiveness(state, moveTypeId, defender.classId);
12
+ if (typeEffectiveness === 0) {
13
+ return {
14
+ damage: 0,
15
+ effectiveness: 0,
16
+ };
17
+ }
18
+ const stabMultiplier = attacker.classId === moveTypeId ? rules.stabMultiplier : 1;
19
+ const critMultiplier = isCritical ? rules.critMultiplier : 1;
20
+ const randomFactor = randomInRange(rules.randomMinDamageFactor, rules.randomMaxDamageFactor);
21
+ const rawDamage = basePower *
22
+ (attackerOff / Math.max(1, defenderDef)) *
23
+ typeEffectiveness *
24
+ stabMultiplier *
25
+ critMultiplier *
26
+ randomFactor;
27
+ dbg("🧨 computeDamage", `
28
+ basePower(${basePower}) *
29
+ (attackerOff(${attackerOff}) / Math.max(1, defenderDef(${defenderDef}))) = ${attackerOff / Math.max(1, defenderDef)}*
30
+ typeEffectiveness(${typeEffectiveness}) *
31
+ stabMultiplier(${stabMultiplier}) *
32
+ critMultiplier(${critMultiplier}) *
33
+ randomFactor(${randomFactor}) = ${rawDamage}
34
+ `);
35
+ const finalDamage = Math.max(1, Math.floor(rawDamage));
36
+ dbg("🧨 computeDamage", `
37
+ finalDamage(${finalDamage}) = Math.max(1, Math.floor(rawDamage))
38
+ `);
39
+ return {
40
+ damage: finalDamage,
41
+ effectiveness: typeEffectiveness,
42
+ };
43
+ };
44
+ export const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
45
+ const events = [];
46
+ const newHp = Math.max(0, defender.currentHp - amount);
47
+ const updatedDefender = {
48
+ ...defender,
49
+ currentHp: newHp,
50
+ isAlive: newHp > 0,
51
+ };
52
+ events.push({
53
+ ...createBaseEvent(state.turnNumber, "damage", `${actorId} inflige ${amount} de daño a ${defender.fighterId}`),
54
+ actorId,
55
+ targetId: defender.fighterId,
56
+ amount,
57
+ isCritical,
58
+ });
59
+ if (!updatedDefender.isAlive) {
60
+ events.push({
61
+ ...createBaseEvent(state.turnNumber, "fighter_fainted", `${defender.fighterId} ha caído`),
62
+ fighterId: defender.fighterId,
63
+ });
64
+ }
65
+ return { updatedDefender, events };
66
+ };
@@ -0,0 +1,6 @@
1
+ import { BattleEvent, BattleFighter, FighterId } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const applyHealToFighter: (state: RuntimeBattleState, target: BattleFighter, amount: number, sourceId: FighterId) => {
4
+ updated: BattleFighter;
5
+ events: BattleEvent[];
6
+ };
@@ -0,0 +1,25 @@
1
+ import { dbg } from "../debug";
2
+ import { createBaseEvent } from "../events";
3
+ export const applyHealToFighter = (state, target, amount, sourceId) => {
4
+ const newHp = Math.min(target.maxHp, target.currentHp + amount);
5
+ const healed = newHp - target.currentHp;
6
+ const updated = {
7
+ ...target,
8
+ currentHp: newHp,
9
+ isAlive: newHp > 0,
10
+ };
11
+ const events = [];
12
+ dbg("HEAL", {
13
+ sourceId,
14
+ targetId: target.fighterId,
15
+ healed,
16
+ newHp,
17
+ });
18
+ events.push({
19
+ ...createBaseEvent(state.turnNumber, "heal", `${sourceId} cura ${healed} a ${target.fighterId}`),
20
+ actorId: sourceId,
21
+ targetId: target.fighterId,
22
+ amount: healed,
23
+ });
24
+ return { updated, events };
25
+ };
@@ -0,0 +1,5 @@
1
+ export * from './crit.js';
2
+ export * from './damage.js';
3
+ export * from './heal.js';
4
+ export * from './typeEffectiveness.js';
5
+ export * from './winner.js';
@@ -0,0 +1,5 @@
1
+ export * from './crit.js';
2
+ export * from './damage.js';
3
+ export * from './heal.js';
4
+ export * from './typeEffectiveness.js';
5
+ export * from './winner.js';
@@ -0,0 +1,3 @@
1
+ import { TypeId } from "../../core";
2
+ import { RuntimeBattleState } from "../rules";
3
+ export declare const getTypeEffectiveness: (state: RuntimeBattleState, attackerTypeId: TypeId, defenderTypeId: TypeId) => number;
@@ -0,0 +1,7 @@
1
+ export const getTypeEffectiveness = (state, attackerTypeId, defenderTypeId) => {
2
+ const matrix = state.runtime.typeEffectiveness;
3
+ const row = matrix[attackerTypeId];
4
+ if (!row)
5
+ return 1;
6
+ return row[defenderTypeId] ?? 1;
7
+ };
@@ -0,0 +1,2 @@
1
+ import { RuntimeBattleState } from "../rules";
2
+ export declare const checkWinner: (state: RuntimeBattleState) => "none" | "player1" | "player2" | "draw";
@@ -0,0 +1,11 @@
1
+ export const checkWinner = (state) => {
2
+ const p1Alive = state.player1.fighterTeam.some((f) => f.isAlive && f.currentHp > 0);
3
+ const p2Alive = state.player2.fighterTeam.some((f) => f.isAlive && f.currentHp > 0);
4
+ if (p1Alive && !p2Alive)
5
+ return "player1";
6
+ if (!p1Alive && p2Alive)
7
+ return "player2";
8
+ if (!p1Alive && !p2Alive)
9
+ return "draw";
10
+ return "none";
11
+ };
@@ -0,0 +1 @@
1
+ export declare const dbg: (...args: unknown[]) => void;