pokemon-io-core 0.0.97 → 0.0.98

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.
@@ -1,7 +1,7 @@
1
- import { BattleEvent, StatusId } from "../core/index.js";
1
+ import { BattleEvent, StatusId, VolatileStatus } from "../core/index.js";
2
2
  export interface BattlePlayerStatusView {
3
3
  statusId: StatusId;
4
- stacks: 1 | 2;
4
+ stacks: number;
5
5
  remainingTurns: number;
6
6
  }
7
7
  export interface BattlePlayerMoveView {
@@ -25,6 +25,7 @@ export interface BattlePlayerView {
25
25
  nickname: string;
26
26
  hp: number;
27
27
  statuses: BattlePlayerStatusView[];
28
+ volatileStatus: VolatileStatus;
28
29
  activeFighterId: string | null;
29
30
  activeIndex: number;
30
31
  activeMoves: BattlePlayerMoveView[];
@@ -11,9 +11,14 @@ export interface EquippedItem {
11
11
  }
12
12
  export interface ActiveStatus {
13
13
  statusId: StatusId;
14
- stacks: 1 | 2;
14
+ stacks: number;
15
15
  remainingTurns: number;
16
16
  }
17
+ export type VolatileStatus = {
18
+ kind: "charging";
19
+ moveId: string;
20
+ invulnerability?: string;
21
+ } | null;
17
22
  export interface BattleFighter {
18
23
  fighterId: FighterId;
19
24
  classId: TypeId;
@@ -22,6 +27,7 @@ export interface BattleFighter {
22
27
  baseStats: FighterStats;
23
28
  effectiveStats: FighterStats;
24
29
  statStages: StatStages;
30
+ volatileStatus: VolatileStatus;
25
31
  moves: EquippedMove[];
26
32
  amuletId: AmuletId | null;
27
33
  statuses: ActiveStatus[];
@@ -60,7 +60,7 @@ export interface StatusAppliedEvent extends BaseBattleEvent {
60
60
  kind: "status_applied";
61
61
  targetId: FighterId;
62
62
  statusId: StatusId;
63
- stacks: 1 | 2;
63
+ stacks: number;
64
64
  durationTurns: number;
65
65
  }
66
66
  export interface FighterSwitchedEvent extends BaseBattleEvent {
@@ -70,7 +70,7 @@ export interface FighterSwitchedEvent extends BaseBattleEvent {
70
70
  toHp: number;
71
71
  toStatuses: {
72
72
  statusId: string;
73
- stacks: 1 | 2;
73
+ stacks: number;
74
74
  remainingTurns: number;
75
75
  }[];
76
76
  }
@@ -4,6 +4,7 @@ export type EffectKind = "damage" | "heal" | "shield" | "modify_stats" | "apply_
4
4
  export type TargetKind = EffectTarget;
5
5
  interface BaseEffect {
6
6
  target?: EffectTarget;
7
+ chance?: number;
7
8
  }
8
9
  export interface DamageEffect extends BaseEffect {
9
10
  kind: "damage";
@@ -76,6 +77,10 @@ export interface MoveDefinition {
76
77
  accuracy?: number;
77
78
  maxPP: number;
78
79
  priority?: number;
80
+ charge?: {
81
+ invulnerability?: string;
82
+ };
83
+ bypassInvulnerability?: string[];
79
84
  effects: EffectDefinition[];
80
85
  }
81
86
  export {};
@@ -39,18 +39,83 @@ export const resolveDamageMove = (state, playerKey, action) => {
39
39
  ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usó ${move.name}`),
40
40
  actorId: self.fighterId,
41
41
  });
42
- // Cálculo de puntería con Stages
43
- const accStage = self.statStages.accuracy - opponent.statStages.evasion;
42
+ // Prepare mutable copies for updates through the function
43
+ let updatedSelf = { ...self };
44
+ let updatedOpponent = { ...opponent };
45
+ // -------------------------------------------------------------
46
+ // 1. Invulnerability Check (Target is Charging/Flying/Digging)
47
+ // -------------------------------------------------------------
48
+ if (updatedOpponent.volatileStatus?.kind === "charging" &&
49
+ updatedOpponent.volatileStatus.invulnerability) {
50
+ const isBypassed = move.bypassInvulnerability?.includes(updatedOpponent.volatileStatus.invulnerability);
51
+ if (!isBypassed) {
52
+ dbg("MISS_INVULNERABLE", {
53
+ attacker: self.fighterId,
54
+ defender: opponent.fighterId,
55
+ invulnerability: updatedOpponent.volatileStatus.invulnerability,
56
+ });
57
+ events.push({
58
+ ...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} no alcanza a ${opponent.fighterId}`),
59
+ actorId: self.fighterId,
60
+ moveId: move.id,
61
+ targetId: opponent.fighterId,
62
+ });
63
+ // Still consume PP? Normally yes, you missed.
64
+ const updatedMoves = updatedSelf.moves.map((m, idx) => idx === action.moveIndex
65
+ ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
66
+ : m);
67
+ updatedSelf.moves = updatedMoves;
68
+ const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
69
+ return { state: newState, events };
70
+ }
71
+ }
72
+ // -------------------------------------------------------------
73
+ // 2. Charging Logic (Self)
74
+ // -------------------------------------------------------------
75
+ const isCharging = updatedSelf.volatileStatus?.kind === "charging";
76
+ // A) Start Charging (Turn 1)
77
+ if (!isCharging && move.charge) {
78
+ dbg("START_CHARGING", { moveId: move.id });
79
+ // Set status
80
+ updatedSelf.volatileStatus = {
81
+ kind: "charging",
82
+ moveId: move.id,
83
+ invulnerability: move.charge.invulnerability,
84
+ };
85
+ // Updates message
86
+ events[events.length - 1].message = `${self.fighterId} prepara ${move.name}...`;
87
+ // Deduct PP on charge turn
88
+ const updatedMoves = updatedSelf.moves.map((m, idx) => idx === action.moveIndex
89
+ ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
90
+ : m);
91
+ updatedSelf.moves = updatedMoves;
92
+ const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
93
+ return { state: newState, events };
94
+ }
95
+ // B) Execute Charge (Turn 2)
96
+ if (isCharging) {
97
+ // Clear status
98
+ updatedSelf.volatileStatus = null;
99
+ // Do NOT deduct PP (paid in turn 1)
100
+ // We do nothing to moves here.
101
+ }
102
+ else {
103
+ // Normal Move (not charging move, and not currently charging)
104
+ // Deduct PP
105
+ const updatedMoves = updatedSelf.moves.map((m, idx) => idx === action.moveIndex
106
+ ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
107
+ : m);
108
+ updatedSelf.moves = updatedMoves;
109
+ }
110
+ // -------------------------------------------------------------
111
+ // 3. Accuracy Calculation
112
+ // -------------------------------------------------------------
113
+ const accStage = updatedSelf.statStages.accuracy - updatedOpponent.statStages.evasion;
44
114
  const accMult = getAccuracyMultiplier(accStage);
45
115
  const baseAcc = move.accuracy ?? 100;
46
116
  const finalAccuracy = baseAcc * accMult;
47
117
  const hitRoll = randomInRange(0, 100);
48
118
  const hit = hitRoll < finalAccuracy;
49
- const updatedMoves = self.moves.map((m, idx) => idx === action.moveIndex
50
- ? { ...m, currentPP: Math.max(0, m.currentPP - 1) }
51
- : m);
52
- let updatedSelf = { ...self, moves: updatedMoves };
53
- let updatedOpponent = { ...opponent };
54
119
  if (!hit) {
55
120
  dbg("MISS", {
56
121
  fighterId: self.fighterId,
@@ -68,6 +133,9 @@ export const resolveDamageMove = (state, playerKey, action) => {
68
133
  const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
69
134
  return { state: newState, events };
70
135
  }
136
+ // -------------------------------------------------------------
137
+ // 4. Hit / Crit / Damage / Effects
138
+ // -------------------------------------------------------------
71
139
  const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
72
140
  const isCritical = chance(critChance);
73
141
  const effectiveness = getTypeEffectiveness(state, move.typeId, updatedOpponent.classId);
@@ -92,7 +160,7 @@ export const resolveDamageMove = (state, playerKey, action) => {
92
160
  moveName: move.name,
93
161
  effects: move.effects,
94
162
  });
95
- // Todos los efectos (daño, aplicar estado, curas, etc.)
163
+ // Apply Effects
96
164
  const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
97
165
  events.push(...extraEvents);
98
166
  let newState = updateFightersInState(state, playerKey, finalSelf, finalOpp);
@@ -44,11 +44,22 @@ export const computeDamage = (input) => {
44
44
  export const applyDamageToFighter = (state, defender, amount, actorId, isCritical) => {
45
45
  const events = [];
46
46
  const newHp = Math.max(0, defender.currentHp - amount);
47
- const updatedDefender = {
47
+ let updatedDefender = {
48
48
  ...defender,
49
49
  currentHp: newHp,
50
50
  isAlive: newHp > 0,
51
51
  };
52
+ // ✅ Sleep Wake check (30%)
53
+ if (updatedDefender.isAlive &&
54
+ updatedDefender.statuses.some((s) => s.statusId === "sleep") &&
55
+ Math.random() < 0.3) {
56
+ updatedDefender.statuses = updatedDefender.statuses.filter((s) => s.statusId !== "sleep");
57
+ events.push({
58
+ ...createBaseEvent(state.turnNumber, "status_cleared", `${defender.fighterId} se despierta por el golpe`),
59
+ targetId: defender.fighterId,
60
+ statusId: "sleep",
61
+ });
62
+ }
52
63
  events.push({
53
64
  ...createBaseEvent(state.turnNumber, "damage", `${actorId} inflige ${amount} de daño a ${defender.fighterId}`),
54
65
  actorId,
@@ -25,6 +25,9 @@ export const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritica
25
25
  effectsCount: effects?.length ?? 0,
26
26
  });
27
27
  for (const eff of effects) {
28
+ if (eff.chance !== undefined && Math.random() > eff.chance) {
29
+ continue;
30
+ }
28
31
  dbg("EFFECT_LOOP", eff);
29
32
  switch (eff.kind) {
30
33
  case "heal": {
@@ -25,6 +25,7 @@ export const createBattleFighter = (cfg) => {
25
25
  })),
26
26
  amuletId: cfg.amulet ? cfg.amulet.id : null,
27
27
  statuses: [],
28
+ volatileStatus: null,
28
29
  isAlive: true,
29
30
  };
30
31
  };
@@ -63,6 +64,15 @@ export const recomputeEffectiveStatsForFighter = (state, fighter) => {
63
64
  }
64
65
  }
65
66
  // 3) Aquí podrías aplicar amuletos, buffs temporales o lo que quieras
67
+ // 3) Hardcoded Status Multipliers (Balance Overhaul)
68
+ const hasBurn = fighter.statuses.some((s) => s.statusId === "burn");
69
+ const hasParalysis = fighter.statuses.some((s) => s.statusId === "paralysis");
70
+ if (hasBurn) {
71
+ eff.offense = Math.floor(eff.offense * 0.5);
72
+ }
73
+ if (hasParalysis) {
74
+ eff.speed = Math.floor(eff.speed * 0.5);
75
+ }
66
76
  return {
67
77
  ...fighter,
68
78
  effectiveStats: eff,
@@ -24,7 +24,7 @@ export const applyEndOfTurnStatuses = (state) => {
24
24
  let damageFromStatus = 0;
25
25
  def.effectsPerStack.forEach((eff) => {
26
26
  if (eff.kind === "damage") {
27
- const stacksMultiplier = st.stacks === 2 ? 2 : 1;
27
+ const stacksMultiplier = st.stacks;
28
28
  const base = typeof eff.flatAmount === "number"
29
29
  ? eff.flatAmount
30
30
  : typeof eff.basePower === "number"
@@ -45,8 +45,18 @@ export const applyEndOfTurnStatuses = (state) => {
45
45
  }
46
46
  // solo decrementamos si sigue vivo
47
47
  const remaining = st.remainingTurns - 1;
48
- if (remaining > 0)
49
- updatedStatuses.push({ ...st, remainingTurns: remaining });
48
+ if (remaining > 0) {
49
+ let nextStacks = st.stacks;
50
+ // ✅ Toxic Scaling
51
+ if (st.statusId === "poison") {
52
+ nextStacks += 1;
53
+ }
54
+ updatedStatuses.push({
55
+ ...st,
56
+ remainingTurns: remaining,
57
+ stacks: nextStacks,
58
+ });
59
+ }
50
60
  else {
51
61
  events.push({
52
62
  ...createBaseEvent(state.turnNumber, "status_expired", `El estado ${st.statusId} expira en ${updated.fighterId}`),
@@ -61,9 +61,29 @@ export const resolveTurn = (state, actions) => {
61
61
  player2: recalcForPlayer(currentState.player2),
62
62
  };
63
63
  const events = [];
64
+ // -----------------------------------------------------------------------
65
+ // 1. Force Actions for Charging Fighters (Fly, Dig, etc.)
66
+ // -----------------------------------------------------------------------
67
+ const overrideAction = (player, originalAction) => {
68
+ const active = player.fighterTeam[player.activeIndex];
69
+ if (active && active.isAlive && active.volatileStatus?.kind === "charging") {
70
+ const moveId = active.volatileStatus.moveId;
71
+ const moveIndex = active.moves.findIndex((m) => m.moveId === moveId);
72
+ if (moveIndex >= 0) {
73
+ return { kind: "use_move", moveIndex };
74
+ }
75
+ }
76
+ return originalAction;
77
+ };
78
+ const finalActions = {
79
+ player1: overrideAction(runtimeState.player1, actions.player1),
80
+ player2: overrideAction(runtimeState.player2, actions.player2),
81
+ };
64
82
  dbg(`TURN ${runtimeState.turnNumber} start`, {
65
- p1Action: actions.player1.kind,
66
- p2Action: actions.player2.kind,
83
+ p1Action: finalActions.player1.kind,
84
+ p2Action: finalActions.player2.kind,
85
+ originalP1: actions.player1.kind,
86
+ originalP2: actions.player2.kind,
67
87
  });
68
88
  events.push({
69
89
  ...createBaseEvent(runtimeState.turnNumber, "turn_start", `Comienza el turno ${runtimeState.turnNumber}`),
@@ -71,13 +91,13 @@ export const resolveTurn = (state, actions) => {
71
91
  const entries = [
72
92
  {
73
93
  playerKey: "player1",
74
- action: actions.player1,
75
- ...getMovePriorityAndSpeed(runtimeState, "player1", actions.player1),
94
+ action: finalActions.player1,
95
+ ...getMovePriorityAndSpeed(runtimeState, "player1", finalActions.player1),
76
96
  },
77
97
  {
78
98
  playerKey: "player2",
79
- action: actions.player2,
80
- ...getMovePriorityAndSpeed(runtimeState, "player2", actions.player2),
99
+ action: finalActions.player2,
100
+ ...getMovePriorityAndSpeed(runtimeState, "player2", finalActions.player2),
81
101
  },
82
102
  ];
83
103
  entries.sort((a, b) => {
@@ -124,9 +144,31 @@ export const resolveTurn = (state, actions) => {
124
144
  const { self } = getOpponentAndSelf(currentState, playerKey);
125
145
  if (!self.isAlive || self.currentHp <= 0)
126
146
  continue;
147
+ // ✅ Paralysis check (25% chance to skip)
148
+ if (self.statuses.some((s) => s.statusId === "paralysis") &&
149
+ Math.random() < 0.25) {
150
+ events.push({
151
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} está paralizado y no puede moverse`),
152
+ actorId: self.fighterId,
153
+ });
154
+ continue;
155
+ }
127
156
  if (hasHardCc(currentState, self)) {
157
+ let interruptionSuffix = "";
158
+ // Si estaba cargando y es interrumpido, pierde la carga (Fly/Dig fallan)
159
+ if (self.volatileStatus?.kind === "charging") {
160
+ const newFighter = { ...self, volatileStatus: null };
161
+ currentState = {
162
+ ...currentState,
163
+ [playerKey]: {
164
+ ...currentState[playerKey],
165
+ fighterTeam: currentState[playerKey].fighterTeam.map((f) => f.fighterId === self.fighterId ? newFighter : f),
166
+ },
167
+ };
168
+ interruptionSuffix = " (Concentración perdida)";
169
+ }
128
170
  events.push({
129
- ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
171
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total${interruptionSuffix}`),
130
172
  actorId: self.fighterId,
131
173
  });
132
174
  continue;
@@ -140,4 +140,66 @@ export const POKEMON_ITEMS = [
140
140
  ],
141
141
  image: "green-berry",
142
142
  },
143
+ {
144
+ id: "attack_potion",
145
+ name: "Poción Ofensiva",
146
+ category: "status_buff",
147
+ maxUses: 2,
148
+ target: "self",
149
+ effects: [
150
+ { kind: "heal", amount: 30 },
151
+ { kind: "modify_stats", target: "self", offenseDelta: 1 },
152
+ ],
153
+ image: "potion_red", // Placeholder
154
+ },
155
+ {
156
+ id: "defense_potion",
157
+ name: "Poción Defensiva",
158
+ category: "status_buff",
159
+ maxUses: 2,
160
+ target: "self",
161
+ effects: [
162
+ { kind: "heal", amount: 30 },
163
+ { kind: "modify_stats", target: "self", defenseDelta: 1 },
164
+ ],
165
+ image: "potion_blue", // Placeholder
166
+ },
167
+ {
168
+ id: "speed_elixir",
169
+ name: "Elixir de Velocidad",
170
+ category: "status_buff",
171
+ maxUses: 2,
172
+ target: "self",
173
+ effects: [
174
+ { kind: "heal", amount: 30 },
175
+ { kind: "modify_stats", target: "self", speedDelta: 1 },
176
+ ],
177
+ image: "potion_yellow", // Placeholder
178
+ },
179
+ {
180
+ id: "crit_injection",
181
+ name: "Inyección Crítica",
182
+ category: "status_buff",
183
+ maxUses: 2,
184
+ target: "self",
185
+ effects: [
186
+ { kind: "heal", amount: 30 },
187
+ { kind: "modify_crit_chance", target: "self", delta: 1 },
188
+ ],
189
+ image: "potion_purple", // Placeholder
190
+ },
191
+ {
192
+ id: "chesto_berry",
193
+ name: "Baya Atania",
194
+ category: "status_buff",
195
+ maxUses: 1,
196
+ target: "self",
197
+ effects: [
198
+ {
199
+ kind: "clear_status",
200
+ statusIds: ["sleep"],
201
+ },
202
+ ],
203
+ image: "chesto-berry", // Placeholder
204
+ },
143
205
  ];