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.
- package/dist/api/battle.d.ts +3 -2
- package/dist/api/socketTypes.d.ts +1 -4
- package/dist/core/battleState.d.ts +16 -1
- package/dist/core/events.d.ts +16 -4
- package/dist/core/moves.d.ts +5 -0
- package/dist/engine/actions/move.js +82 -9
- package/dist/engine/combat/damage.js +12 -1
- package/dist/engine/combat/stages.d.ts +5 -0
- package/dist/engine/combat/stages.js +53 -0
- package/dist/engine/effects/applyEffects.js +73 -1
- package/dist/engine/fighters/fighter.js +30 -3
- package/dist/engine/status/endOfTurn.js +13 -3
- package/dist/engine/turn/resolveTurn.js +49 -7
- package/dist/skins/pokemon/items.js +62 -0
- package/dist/skins/pokemon/moves.js +212 -550
- 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,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
|
-
|
|
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:
|
|
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;
|
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" | "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:
|
|
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:
|
|
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;
|
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 {};
|
|
@@ -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
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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:
|
|
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,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;
|