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.
- package/dist/api/battle.d.ts +3 -2
- package/dist/api/socketTypes.d.ts +10 -0
- package/dist/core/battleState.d.ts +7 -1
- package/dist/core/emotes.d.ts +13 -0
- package/dist/core/emotes.js +127 -0
- package/dist/core/events.d.ts +10 -4
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/moves.d.ts +5 -0
- package/dist/engine/actions/move.js +76 -8
- package/dist/engine/combat/damage.js +12 -1
- package/dist/engine/effects/applyEffects.js +3 -0
- package/dist/engine/fighters/fighter.js +10 -0
- package/dist/engine/status/apply.js +3 -0
- package/dist/engine/status/endOfTurn.js +20 -3
- package/dist/engine/turn/resolveTurn.js +73 -12
- package/dist/skins/pokemon/items.js +62 -4
- package/dist/skins/pokemon/moves.js +212 -580
- package/dist/skins/pokemon/statuses.js +22 -53
- package/package.json +1 -1
package/dist/api/battle.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
+
};
|
package/dist/core/events.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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;
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/core/moves.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
43
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
66
|
-
p2Action:
|
|
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:
|
|
75
|
-
...getMovePriorityAndSpeed(runtimeState, "player1",
|
|
94
|
+
action: finalActions.player1,
|
|
95
|
+
...getMovePriorityAndSpeed(runtimeState, "player1", finalActions.player1),
|
|
76
96
|
},
|
|
77
97
|
{
|
|
78
98
|
playerKey: "player2",
|
|
79
|
-
action:
|
|
80
|
-
...getMovePriorityAndSpeed(runtimeState, "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
|
-
|
|
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}
|
|
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
|
-
|
|
135
|
-
|
|
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 === "
|
|
140
|
-
|
|
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
|
}
|