pokemon-io-core 0.0.97 → 0.0.99

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[];
@@ -1,5 +1,6 @@
1
1
  import { BattleView } from "./battle.js";
2
2
  import { RoomView } from "./room.js";
3
+ import { EmoteId } from "../core/emotes.js";
3
4
  export interface ClientToServerEvents {
4
5
  "room:create": (payload: {
5
6
  nickname: string;
@@ -59,6 +60,10 @@ export interface ClientToServerEvents {
59
60
  "battle:rematchChangeLoadout": (payload: {
60
61
  roomId: string;
61
62
  }) => void;
63
+ "battle:sendEmote": (payload: {
64
+ roomId: string;
65
+ emoteId: EmoteId;
66
+ }) => void;
62
67
  ping: () => void;
63
68
  }
64
69
  export interface ServerToClientEvents {
@@ -73,5 +78,10 @@ export interface ServerToClientEvents {
73
78
  message: string;
74
79
  }) => void;
75
80
  "battle:state": (payload: BattleView) => void;
81
+ "battle:emote": (payload: {
82
+ playerId: string;
83
+ emoteId: EmoteId;
84
+ timestamp: number;
85
+ }) => void;
76
86
  pong: (msg: string) => void;
77
87
  }
@@ -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[];
@@ -0,0 +1,13 @@
1
+ export type EmoteCategory = "emotional" | "strategic" | "social";
2
+ export type EmoteId = string;
3
+ export interface EmoteDefinition {
4
+ id: EmoteId;
5
+ category: EmoteCategory;
6
+ emoji: string;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+ export declare const EMOTES: EmoteDefinition[];
11
+ export declare const getEmoteById: (id: EmoteId) => EmoteDefinition | undefined;
12
+ export declare const getEmotesByCategory: (category: EmoteCategory) => EmoteDefinition[];
13
+ export declare const isValidEmoteId: (id: string) => id is EmoteId;
@@ -0,0 +1,127 @@
1
+ // src/core/emotes.ts
2
+ export const EMOTES = [
3
+ // Emotional (6)
4
+ {
5
+ id: "laugh",
6
+ category: "emotional",
7
+ emoji: "😂",
8
+ label: "Risa",
9
+ description: "¡Jaja!",
10
+ },
11
+ {
12
+ id: "scared",
13
+ category: "emotional",
14
+ emoji: "😱",
15
+ label: "Susto",
16
+ description: "¡Oh no!",
17
+ },
18
+ {
19
+ id: "cool",
20
+ category: "emotional",
21
+ emoji: "😎",
22
+ label: "Cool",
23
+ description: "Genial",
24
+ },
25
+ {
26
+ id: "angry",
27
+ category: "emotional",
28
+ emoji: "😡",
29
+ label: "Enfadado",
30
+ description: "¡Grr!",
31
+ },
32
+ {
33
+ id: "sad",
34
+ category: "emotional",
35
+ emoji: "😢",
36
+ label: "Triste",
37
+ description: "Auch...",
38
+ },
39
+ {
40
+ id: "thinking",
41
+ category: "emotional",
42
+ emoji: "🤔",
43
+ label: "Pensando",
44
+ description: "Hmm...",
45
+ },
46
+ // Strategic (6)
47
+ {
48
+ id: "attack",
49
+ category: "strategic",
50
+ emoji: "💥",
51
+ label: "Voy a atacar",
52
+ description: "¡Prepárate!",
53
+ },
54
+ {
55
+ id: "heal",
56
+ category: "strategic",
57
+ emoji: "💊",
58
+ label: "Voy a curarme",
59
+ description: "Necesito curarme",
60
+ },
61
+ {
62
+ id: "switch",
63
+ category: "strategic",
64
+ emoji: "🔄",
65
+ label: "Voy a cambiar",
66
+ description: "¡Cambio!",
67
+ },
68
+ {
69
+ id: "special",
70
+ category: "strategic",
71
+ emoji: "⚡",
72
+ label: "Movimiento especial",
73
+ description: "¡Algo grande viene!",
74
+ },
75
+ {
76
+ id: "defend",
77
+ category: "strategic",
78
+ emoji: "🛡️",
79
+ label: "Voy a defenderme",
80
+ description: "Me protejo",
81
+ },
82
+ {
83
+ id: "plan",
84
+ category: "strategic",
85
+ emoji: "🎯",
86
+ label: "Tengo un plan",
87
+ description: "Sé lo que hago",
88
+ },
89
+ // Social (4)
90
+ {
91
+ id: "well_played",
92
+ category: "social",
93
+ emoji: "👍",
94
+ label: "Bien jugado",
95
+ description: "¡Buen movimiento!",
96
+ },
97
+ {
98
+ id: "thanks",
99
+ category: "social",
100
+ emoji: "🙏",
101
+ label: "Gracias",
102
+ description: "Gracias por la partida",
103
+ },
104
+ {
105
+ id: "hello",
106
+ category: "social",
107
+ emoji: "👋",
108
+ label: "Hola",
109
+ description: "¡Hola!",
110
+ },
111
+ {
112
+ id: "good_luck",
113
+ category: "social",
114
+ emoji: "🤝",
115
+ label: "Buena suerte",
116
+ description: "¡Suerte!",
117
+ },
118
+ ];
119
+ export const getEmoteById = (id) => {
120
+ return EMOTES.find((emote) => emote.id === id);
121
+ };
122
+ export const getEmotesByCategory = (category) => {
123
+ return EMOTES.filter((emote) => emote.category === category);
124
+ };
125
+ export const isValidEmoteId = (id) => {
126
+ return EMOTES.some((emote) => emote.id === id);
127
+ };
@@ -1,5 +1,5 @@
1
1
  import type { FighterId, MoveId, ItemId, StatusId } from "./ids.js";
2
- export type BattleEventKind = "turn_start" | "action_declared" | "action_skipped_hard_cc" | "move_miss" | "move_hit" | "item_used" | "status_cleared" | "damage" | "heal" | "fighter_switched" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "fighter_fainted" | "turn_end" | "stats_changed" | "battle_end";
2
+ export type BattleEventKind = "turn_start" | "action_declared" | "action_skipped_hard_cc" | "move_miss" | "move_hit" | "item_used" | "status_cleared" | "damage" | "heal" | "fighter_switched" | "shield_applied" | "status_applied" | "status_refreshed" | "status_expired" | "status_tick" | "fighter_fainted" | "turn_end" | "stats_changed" | "battle_end";
3
3
  export interface BaseBattleEvent {
4
4
  id: string;
5
5
  turnNumber: number;
@@ -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
  }
@@ -79,6 +79,12 @@ export interface StatusExpiredEvent extends BaseBattleEvent {
79
79
  targetId: FighterId;
80
80
  statusId: StatusId;
81
81
  }
82
+ export interface StatusTickEvent extends BaseBattleEvent {
83
+ kind: "status_tick";
84
+ targetId: FighterId;
85
+ statusId: StatusId;
86
+ stacks: number;
87
+ }
82
88
  export interface FighterFaintedEvent extends BaseBattleEvent {
83
89
  kind: "fighter_fainted";
84
90
  fighterId: FighterId;
@@ -87,4 +93,4 @@ export interface BattleEndEvent extends BaseBattleEvent {
87
93
  kind: "battle_end";
88
94
  winner: "player1" | "player2" | "draw";
89
95
  }
90
- export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | BaseBattleEvent;
96
+ export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | StatusTickEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | BaseBattleEvent;
@@ -1,6 +1,7 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
+ export * from "./emotes.js";
4
5
  export * from "./events.js";
5
6
  export * from "./fighters.js";
6
7
  export * from "./ids.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./actions.js";
2
2
  export * from "./amulets.js";
3
3
  export * from "./battleState.js";
4
+ export * from "./emotes.js";
4
5
  export * from "./events.js";
5
6
  export * from "./fighters.js";
6
7
  export * from "./ids.js";
@@ -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,
@@ -37,10 +37,13 @@ export const applyStatusToFighter = (state, target, statusId) => {
37
37
  duration,
38
38
  prevStacks: existing?.stacks ?? 0,
39
39
  });
40
+ const finalStacks = !existing ? 1 : existing.stacks === 1 ? 2 : existing.stacks;
40
41
  events.push({
41
42
  ...createBaseEvent(state.turnNumber, "status_applied", `Se aplica ${statusId} a ${target.fighterId}`),
42
43
  targetId: target.fighterId,
43
44
  statusId,
45
+ stacks: finalStacks,
46
+ durationTurns: duration,
44
47
  });
45
48
  return {
46
49
  updated: { ...target, statuses: newStatuses },
@@ -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"
@@ -34,6 +34,13 @@ export const applyEndOfTurnStatuses = (state) => {
34
34
  }
35
35
  });
36
36
  if (damageFromStatus > 0) {
37
+ // Emit status tick event for narration
38
+ events.push({
39
+ ...createBaseEvent(state.turnNumber, "status_tick", `${updated.fighterId} sufre por ${st.statusId}`),
40
+ targetId: updated.fighterId,
41
+ statusId: st.statusId,
42
+ stacks: st.stacks,
43
+ });
37
44
  const damageRes = applyDamageToFighter(state, updated, damageFromStatus, updated.fighterId, false);
38
45
  updated = damageRes.updatedDefender;
39
46
  events.push(...damageRes.events);
@@ -45,8 +52,18 @@ export const applyEndOfTurnStatuses = (state) => {
45
52
  }
46
53
  // solo decrementamos si sigue vivo
47
54
  const remaining = st.remainingTurns - 1;
48
- if (remaining > 0)
49
- updatedStatuses.push({ ...st, remainingTurns: remaining });
55
+ if (remaining > 0) {
56
+ let nextStacks = st.stacks;
57
+ // ✅ Toxic Scaling
58
+ if (st.statusId === "poison") {
59
+ nextStacks += 1;
60
+ }
61
+ updatedStatuses.push({
62
+ ...st,
63
+ remainingTurns: remaining,
64
+ stacks: nextStacks,
65
+ });
66
+ }
50
67
  else {
51
68
  events.push({
52
69
  ...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,20 +144,61 @@ export const resolveTurn = (state, actions) => {
124
144
  const { self } = getOpponentAndSelf(currentState, playerKey);
125
145
  if (!self.isAlive || self.currentHp <= 0)
126
146
  continue;
127
- if (hasHardCc(currentState, self)) {
147
+ // ✅ Paralysis check (25% chance to skip ALL actions, including items)
148
+ if (self.statuses.some((s) => s.statusId === "paralysis") &&
149
+ Math.random() < 0.25) {
128
150
  events.push({
129
- ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} no puede actuar por control total`),
151
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${self.fighterId} está paralizado y no puede moverse`),
130
152
  actorId: self.fighterId,
153
+ statusId: "paralysis",
131
154
  });
132
155
  continue;
133
156
  }
134
- if (action.kind === "use_move") {
135
- const result = resolveDamageMove(currentState, playerKey, action);
157
+ // ✅ Items can be used regardless of hard CC (trainer action, not Pokémon action)
158
+ if (action.kind === "use_item") {
159
+ const result = resolveItemUse(currentState, playerKey, action);
136
160
  currentState = result.state;
137
161
  events.push(...result.events);
138
162
  }
139
- else if (action.kind === "use_item") {
140
- const result = resolveItemUse(currentState, playerKey, action);
163
+ else if (action.kind === "use_move") {
164
+ // Moves are blocked by hard CC (sleep, freeze, flinch)
165
+ if (hasHardCc(currentState, self)) {
166
+ // Find the hard CC status
167
+ const ccStatus = self.statuses.find((s) => {
168
+ const def = currentState.runtime.statusesById[s.statusId];
169
+ return def?.kind === "hard_cc";
170
+ });
171
+ let interruptionSuffix = "";
172
+ // Si estaba cargando y es interrumpido, pierde la carga (Fly/Dig fallan)
173
+ if (self.volatileStatus?.kind === "charging") {
174
+ const newFighter = { ...self, volatileStatus: null };
175
+ currentState = {
176
+ ...currentState,
177
+ [playerKey]: {
178
+ ...currentState[playerKey],
179
+ fighterTeam: currentState[playerKey].fighterTeam.map((f) => f.fighterId === self.fighterId ? newFighter : f),
180
+ },
181
+ };
182
+ interruptionSuffix = " (Concentración perdida)";
183
+ }
184
+ // Status-specific messages
185
+ const statusMessages = {
186
+ sleep: `${self.fighterId} está dormido y no puede moverse`,
187
+ freeze: `${self.fighterId} está congelado y no puede actuar`,
188
+ flinch: `${self.fighterId} se acobarda y no puede atacar`,
189
+ };
190
+ const statusId = ccStatus?.statusId || "unknown";
191
+ const message = statusMessages[statusId] ||
192
+ `${self.fighterId} no puede actuar por control total`;
193
+ events.push({
194
+ ...createBaseEvent(currentState.turnNumber, "action_skipped_hard_cc", `${message}${interruptionSuffix}`),
195
+ actorId: self.fighterId,
196
+ statusId: statusId,
197
+ });
198
+ continue;
199
+ }
200
+ // Execute the move
201
+ const result = resolveDamageMove(currentState, playerKey, action);
141
202
  currentState = result.state;
142
203
  events.push(...result.events);
143
204
  }