pokemon-io-core 0.0.96 → 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[];
@@ -1,9 +1,6 @@
1
1
  import { BattleView } from "./battle.js";
2
2
  import { RoomView } from "./room.js";
3
3
  export interface ClientToServerEvents {
4
- "player:hello": (payload: {
5
- playerKey: string;
6
- }) => void;
7
4
  "room:create": (payload: {
8
5
  nickname: string;
9
6
  roomName: string;
@@ -19,7 +16,7 @@ export interface ClientToServerEvents {
19
16
  }) => void;
20
17
  "room:reconnect": (payload: {
21
18
  roomId: string;
22
- playerKey: string;
19
+ oldPlayerId: string;
23
20
  }) => void;
24
21
  "room:setLoadout": (payload: {
25
22
  roomId: string;
@@ -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;
@@ -21,11 +26,21 @@ export interface BattleFighter {
21
26
  currentHp: number;
22
27
  baseStats: FighterStats;
23
28
  effectiveStats: FighterStats;
29
+ statStages: StatStages;
30
+ volatileStatus: VolatileStatus;
24
31
  moves: EquippedMove[];
25
32
  amuletId: AmuletId | null;
26
33
  statuses: ActiveStatus[];
27
34
  isAlive: boolean;
28
35
  }
36
+ export interface StatStages {
37
+ offense: number;
38
+ defense: number;
39
+ speed: number;
40
+ crit: number;
41
+ accuracy: number;
42
+ evasion: number;
43
+ }
29
44
  export interface PlayerBattleState {
30
45
  fighterTeam: BattleFighter[];
31
46
  activeIndex: number;
@@ -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" | "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" | "fighter_fainted" | "turn_end" | "stats_changed" | "battle_end";
3
3
  export interface BaseBattleEvent {
4
4
  id: string;
5
5
  turnNumber: number;
@@ -7,6 +7,18 @@ export interface BaseBattleEvent {
7
7
  message: string;
8
8
  timestamp: number;
9
9
  }
10
+ export interface StatsChangedEvent extends BaseBattleEvent {
11
+ kind: "stats_changed";
12
+ targetId: FighterId;
13
+ changes: {
14
+ offense?: number;
15
+ defense?: number;
16
+ speed?: number;
17
+ crit?: number;
18
+ accuracy?: number;
19
+ evasion?: number;
20
+ };
21
+ }
10
22
  export interface ActionDeclaredEvent extends BaseBattleEvent {
11
23
  kind: "action_declared";
12
24
  actorId: FighterId;
@@ -48,7 +60,7 @@ export interface StatusAppliedEvent extends BaseBattleEvent {
48
60
  kind: "status_applied";
49
61
  targetId: FighterId;
50
62
  statusId: StatusId;
51
- stacks: 1 | 2;
63
+ stacks: number;
52
64
  durationTurns: number;
53
65
  }
54
66
  export interface FighterSwitchedEvent extends BaseBattleEvent {
@@ -58,7 +70,7 @@ export interface FighterSwitchedEvent extends BaseBattleEvent {
58
70
  toHp: number;
59
71
  toStatuses: {
60
72
  statusId: string;
61
- stacks: 1 | 2;
73
+ stacks: number;
62
74
  remainingTurns: number;
63
75
  }[];
64
76
  }
@@ -75,4 +87,4 @@ export interface BattleEndEvent extends BaseBattleEvent {
75
87
  kind: "battle_end";
76
88
  winner: "player1" | "player2" | "draw";
77
89
  }
78
- export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | BaseBattleEvent;
90
+ export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | BaseBattleEvent;
@@ -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 {};
@@ -1,4 +1,5 @@
1
1
  import { computeCritChance } from "../combat/crit.js";
2
+ import { getAccuracyMultiplier } from "../combat/stages.js";
2
3
  import { getTypeEffectiveness } from "../combat/typeEffectiveness.js";
3
4
  import { dbg } from "../debug.js";
4
5
  import { applyEffectsOnTarget } from "../effects/applyEffects.js";
@@ -38,21 +39,90 @@ export const resolveDamageMove = (state, playerKey, action) => {
38
39
  ...createBaseEvent(state.turnNumber, "action_declared", `${self.fighterId} usó ${move.name}`),
39
40
  actorId: self.fighterId,
40
41
  });
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 };
42
+ // Prepare mutable copies for updates through the function
43
+ let updatedSelf = { ...self };
48
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;
114
+ const accMult = getAccuracyMultiplier(accStage);
115
+ const baseAcc = move.accuracy ?? 100;
116
+ const finalAccuracy = baseAcc * accMult;
117
+ const hitRoll = randomInRange(0, 100);
118
+ const hit = hitRoll < finalAccuracy;
49
119
  if (!hit) {
50
120
  dbg("MISS", {
51
121
  fighterId: self.fighterId,
52
122
  moveId: move.id,
53
123
  moveName: move.name,
54
124
  hitRoll,
55
- accuracy,
125
+ accuracy: finalAccuracy,
56
126
  });
57
127
  events.push({
58
128
  ...createBaseEvent(state.turnNumber, "move_miss", `${self.fighterId} falla ${move.name}`),
@@ -63,6 +133,9 @@ export const resolveDamageMove = (state, playerKey, action) => {
63
133
  const newState = updateFightersInState(state, playerKey, updatedSelf, updatedOpponent);
64
134
  return { state: newState, events };
65
135
  }
136
+ // -------------------------------------------------------------
137
+ // 4. Hit / Crit / Damage / Effects
138
+ // -------------------------------------------------------------
66
139
  const critChance = computeCritChance(state.runtime.rules, updatedSelf.effectiveStats.crit);
67
140
  const isCritical = chance(critChance);
68
141
  const effectiveness = getTypeEffectiveness(state, move.typeId, updatedOpponent.classId);
@@ -87,7 +160,7 @@ export const resolveDamageMove = (state, playerKey, action) => {
87
160
  moveName: move.name,
88
161
  effects: move.effects,
89
162
  });
90
- // Todos los efectos (daño, aplicar estado, curas, etc.)
163
+ // Apply Effects
91
164
  const { actor: finalSelf, target: finalOpp, events: extraEvents, } = applyEffectsOnTarget(state, updatedSelf, updatedOpponent, move.typeId, isCritical, move.effects);
92
165
  events.push(...extraEvents);
93
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,
@@ -0,0 +1,5 @@
1
+ export declare const STAGE_MULTIPLIERS_STATS: Record<number, number>;
2
+ export declare const STAGE_MULTIPLIERS_ACCURACY: Record<number, number>;
3
+ export declare const getCritChanceFromStage: (stage: number) => number;
4
+ export declare const getStatMultiplier: (stage: number) => number;
5
+ export declare const getAccuracyMultiplier: (stage: number) => number;
@@ -0,0 +1,53 @@
1
+ export const STAGE_MULTIPLIERS_STATS = {
2
+ "-6": 2 / 8,
3
+ "-5": 2 / 7,
4
+ "-4": 2 / 6,
5
+ "-3": 2 / 5,
6
+ "-2": 2 / 4,
7
+ "-1": 2 / 3,
8
+ 0: 2 / 2,
9
+ 1: 3 / 2,
10
+ 2: 4 / 2,
11
+ 3: 5 / 2,
12
+ 4: 6 / 2,
13
+ 5: 7 / 2,
14
+ 6: 8 / 2,
15
+ };
16
+ // Accuracy/Evasion multipliers (3/3 base)
17
+ export const STAGE_MULTIPLIERS_ACCURACY = {
18
+ "-6": 3 / 9,
19
+ "-5": 3 / 8,
20
+ "-4": 3 / 7,
21
+ "-3": 3 / 6,
22
+ "-2": 3 / 5,
23
+ "-1": 3 / 4,
24
+ 0: 3 / 3,
25
+ 1: 4 / 3,
26
+ 2: 5 / 3,
27
+ 3: 6 / 3,
28
+ 4: 7 / 3,
29
+ 5: 8 / 3,
30
+ 6: 9 / 3,
31
+ };
32
+ // Critical Hit Ratio (Table)
33
+ // Stage 0: ~4.17% (1/24)
34
+ // Stage 1: 12.5% (1/8)
35
+ // Stage 2: 50% (1/2)
36
+ // Stage 3+: 100% (1/1)
37
+ export const getCritChanceFromStage = (stage) => {
38
+ if (stage <= 0)
39
+ return 1 / 24;
40
+ if (stage === 1)
41
+ return 1 / 8;
42
+ if (stage === 2)
43
+ return 1 / 2;
44
+ return 1;
45
+ };
46
+ export const getStatMultiplier = (stage) => {
47
+ const clamped = Math.max(-6, Math.min(6, stage));
48
+ return STAGE_MULTIPLIERS_STATS[clamped] ?? 1;
49
+ };
50
+ export const getAccuracyMultiplier = (stage) => {
51
+ const clamped = Math.max(-6, Math.min(6, stage));
52
+ return STAGE_MULTIPLIERS_ACCURACY[clamped] ?? 1;
53
+ };
@@ -4,6 +4,8 @@ import { dbg } from "../debug.js";
4
4
  import { applyStatusToFighter } from "../status/apply.js";
5
5
  import { clearStatusFromFighter } from "../status/clear.js";
6
6
  import { resolveEffectTarget } from "./target.js";
7
+ import { createBaseEvent } from "../events.js";
8
+ import { recomputeEffectiveStatsForFighter } from "../fighters/fighter.js";
7
9
  export const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritical, effects) => {
8
10
  dbg("applyEffectsOnTarget", {
9
11
  actorId: actor.fighterId,
@@ -23,6 +25,9 @@ export const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritica
23
25
  effectsCount: effects?.length ?? 0,
24
26
  });
25
27
  for (const eff of effects) {
28
+ if (eff.chance !== undefined && Math.random() > eff.chance) {
29
+ continue;
30
+ }
26
31
  dbg("EFFECT_LOOP", eff);
27
32
  switch (eff.kind) {
28
33
  case "heal": {
@@ -94,7 +99,74 @@ export const applyEffectsOnTarget = (state, actor, target, moveTypeId, isCritica
94
99
  }
95
100
  break;
96
101
  }
97
- // TODO: conectar shield, modify_stats, etc.
102
+ case "modify_stats": {
103
+ const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
104
+ const newStages = { ...primary.statStages };
105
+ const changes = {};
106
+ if (eff.offenseDelta) {
107
+ newStages.offense = Math.max(-6, Math.min(6, newStages.offense + eff.offenseDelta));
108
+ changes.offense = eff.offenseDelta;
109
+ }
110
+ if (eff.defenseDelta) {
111
+ newStages.defense = Math.max(-6, Math.min(6, newStages.defense + eff.defenseDelta));
112
+ changes.defense = eff.defenseDelta;
113
+ }
114
+ if (eff.speedDelta) {
115
+ newStages.speed = Math.max(-6, Math.min(6, newStages.speed + eff.speedDelta));
116
+ changes.speed = eff.speedDelta;
117
+ }
118
+ if (eff.critDelta) {
119
+ newStages.crit = Math.max(0, Math.min(6, newStages.crit + eff.critDelta));
120
+ changes.crit = eff.critDelta;
121
+ }
122
+ let updated = { ...primary, statStages: newStages };
123
+ updated = recomputeEffectiveStatsForFighter(state, updated);
124
+ events.push({
125
+ ...createBaseEvent(state.turnNumber, "stats_changed", `Estadísticas cambiadas`),
126
+ targetId: primary.fighterId,
127
+ changes
128
+ });
129
+ if (isSelf)
130
+ currentActor = updated;
131
+ else
132
+ currentTarget = updated;
133
+ break;
134
+ }
135
+ case "modify_crit_chance": {
136
+ const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
137
+ const newStages = { ...primary.statStages };
138
+ newStages.crit = Math.max(0, Math.min(6, newStages.crit + eff.delta));
139
+ let updated = { ...primary, statStages: newStages };
140
+ updated = recomputeEffectiveStatsForFighter(state, updated);
141
+ events.push({
142
+ ...createBaseEvent(state.turnNumber, "stats_changed", `Ratio de crítico cambiado`),
143
+ targetId: primary.fighterId,
144
+ changes: { crit: eff.delta }
145
+ });
146
+ if (isSelf)
147
+ currentActor = updated;
148
+ else
149
+ currentTarget = updated;
150
+ break;
151
+ }
152
+ case "modify_hit_chance": {
153
+ // Interpreted as Accuracy Stage
154
+ const { primary, isSelf } = resolveEffectTarget(eff, currentActor, currentTarget);
155
+ const newStages = { ...primary.statStages };
156
+ newStages.accuracy = Math.max(-6, Math.min(6, newStages.accuracy + eff.delta));
157
+ let updated = { ...primary, statStages: newStages };
158
+ updated = recomputeEffectiveStatsForFighter(state, updated);
159
+ events.push({
160
+ ...createBaseEvent(state.turnNumber, "stats_changed", `Precisión cambiada`),
161
+ targetId: primary.fighterId,
162
+ changes: { accuracy: eff.delta }
163
+ });
164
+ if (isSelf)
165
+ currentActor = updated;
166
+ else
167
+ currentTarget = updated;
168
+ break;
169
+ }
98
170
  default:
99
171
  break;
100
172
  }
@@ -1,4 +1,5 @@
1
1
  import { cloneStats } from "../runtime.js";
2
+ import { getCritChanceFromStage, getStatMultiplier, } from "../combat/stages.js";
2
3
  export const createBattleFighter = (cfg) => {
3
4
  if (cfg.moves.length !== 4) {
4
5
  throw new Error("Each fighter must have exactly 4 moves in MVP");
@@ -10,19 +11,35 @@ export const createBattleFighter = (cfg) => {
10
11
  currentHp: cfg.maxHp,
11
12
  baseStats: cloneStats(cfg.fighter.baseStats),
12
13
  effectiveStats: cloneStats(cfg.fighter.baseStats),
14
+ statStages: {
15
+ offense: 0,
16
+ defense: 0,
17
+ speed: 0,
18
+ crit: 0,
19
+ accuracy: 0,
20
+ evasion: 0,
21
+ },
13
22
  moves: cfg.moves.map((move) => ({
14
23
  moveId: move.id,
15
24
  currentPP: move.maxPP,
16
25
  })),
17
26
  amuletId: cfg.amulet ? cfg.amulet.id : null,
18
27
  statuses: [],
28
+ volatileStatus: null,
19
29
  isAlive: true,
20
30
  };
21
31
  };
22
32
  export const recomputeEffectiveStatsForFighter = (state, fighter) => {
23
33
  // Partimos de base
24
34
  let eff = { ...fighter.baseStats };
25
- // 1) Aplicar estados
35
+ // 1) Aplicar Etapas (Stages)
36
+ eff.offense = Math.floor(eff.offense * getStatMultiplier(fighter.statStages.offense));
37
+ eff.defense = Math.floor(eff.defense * getStatMultiplier(fighter.statStages.defense));
38
+ eff.speed = Math.floor(eff.speed * getStatMultiplier(fighter.statStages.speed));
39
+ // Crit se reemplaza por el valor de la etapa (o se suma si baseStats tuviera un % base)
40
+ // Asumimos que baseStats.crit es 0 o un bono base pequeño.
41
+ eff.crit = getCritChanceFromStage(fighter.statStages.crit) + eff.crit;
42
+ // 2) Aplicar estados (Bonificadores temporales/external)
26
43
  for (const st of fighter.statuses) {
27
44
  const def = state.runtime.statusesById[st.statusId];
28
45
  if (!def)
@@ -32,10 +49,11 @@ export const recomputeEffectiveStatsForFighter = (state, fighter) => {
32
49
  const totalStacks = stacks; // podrías multiplicar efectos por stacks
33
50
  switch (effDef.kind) {
34
51
  case "modify_stats":
52
+ // Estos serían bonos flat POST-etapas
35
53
  eff.offense += (effDef.offenseDelta ?? 0) * totalStacks;
36
54
  eff.defense += (effDef.defenseDelta ?? 0) * totalStacks;
37
55
  eff.speed += (effDef.speedDelta ?? 0) * totalStacks;
38
- eff.crit += (effDef.critDelta ?? 0) * totalStacks;
56
+ // eff.crit += (effDef.critDelta ?? 0) * totalStacks; // Crit ya va por stages
39
57
  break;
40
58
  case "modify_effective_speed":
41
59
  eff.speed = Math.floor(eff.speed * Math.pow(effDef.multiplier, totalStacks));
@@ -45,7 +63,16 @@ export const recomputeEffectiveStatsForFighter = (state, fighter) => {
45
63
  }
46
64
  }
47
65
  }
48
- // 2) Aquí podrías aplicar amuletos, buffs temporales o lo que quieras
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
+ }
49
76
  return {
50
77
  ...fighter,
51
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;