pokemon-io-core 0.0.100 → 0.0.102
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 +12 -0
- package/dist/api/socketTypes.d.ts +9 -0
- package/dist/core/battleRuntime.d.ts +46 -0
- package/dist/core/battleRuntime.js +1 -0
- package/dist/core/battleState.d.ts +2 -2
- package/dist/core/events.d.ts +26 -2
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.js +7 -2
- package/dist/core/randomEvents.d.ts +27 -0
- package/dist/core/randomEvents.js +8 -0
- package/dist/core/randomEventsList.d.ts +11 -0
- package/dist/core/randomEventsList.js +302 -0
- package/dist/engine/combat/stages.js +32 -27
- package/dist/engine/events/applyRandomEvent.d.ts +11 -0
- package/dist/engine/events/applyRandomEvent.js +229 -0
- package/dist/engine/events/index.d.ts +2 -0
- package/dist/engine/events/index.js +3 -0
- package/dist/engine/events/randomEventTrigger.d.ts +16 -0
- package/dist/engine/events/randomEventTrigger.js +67 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/rules.d.ts +2 -40
- package/dist/engine/turn/resolveTurn.js +43 -0
- package/package.json +1 -1
package/dist/api/battle.d.ts
CHANGED
|
@@ -65,5 +65,17 @@ export interface BattleView {
|
|
|
65
65
|
player1: boolean;
|
|
66
66
|
player2: boolean;
|
|
67
67
|
};
|
|
68
|
+
pendingRandomEvent?: {
|
|
69
|
+
eventName: string;
|
|
70
|
+
eventDescription: string;
|
|
71
|
+
imageUrl?: string;
|
|
72
|
+
effects: Array<{
|
|
73
|
+
effectType: string;
|
|
74
|
+
target: string;
|
|
75
|
+
percentValue?: number;
|
|
76
|
+
statType?: string;
|
|
77
|
+
statChange?: number;
|
|
78
|
+
}>;
|
|
79
|
+
} | null;
|
|
68
80
|
forcedSwitch: ForcedSwitchView | null;
|
|
69
81
|
}
|
|
@@ -64,6 +64,9 @@ export interface ClientToServerEvents {
|
|
|
64
64
|
roomId: string;
|
|
65
65
|
emoteId: EmoteId;
|
|
66
66
|
}) => void;
|
|
67
|
+
"battle:acknowledgeRandomEvent": (payload: {
|
|
68
|
+
roomId: string;
|
|
69
|
+
}) => void;
|
|
67
70
|
ping: () => void;
|
|
68
71
|
}
|
|
69
72
|
export interface ServerToClientEvents {
|
|
@@ -83,5 +86,11 @@ export interface ServerToClientEvents {
|
|
|
83
86
|
emoteId: EmoteId;
|
|
84
87
|
timestamp: number;
|
|
85
88
|
}) => void;
|
|
89
|
+
"battle:randomEventTriggered": (payload: {
|
|
90
|
+
eventId: string;
|
|
91
|
+
eventName: string;
|
|
92
|
+
eventDescription: string;
|
|
93
|
+
imageUrl?: string;
|
|
94
|
+
}) => void;
|
|
86
95
|
pong: (msg: string) => void;
|
|
87
96
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AmuletDefinition } from "./amulets.js";
|
|
2
|
+
import { FighterDefinition } from "./fighters.js";
|
|
3
|
+
import { ItemDefinition } from "./items.js";
|
|
4
|
+
import { MoveDefinition } from "./moves.js";
|
|
5
|
+
import { StatusDefinition } from "./status.js";
|
|
6
|
+
import { TypeDefinition, TypeEffectivenessMatrix } from "./types.js";
|
|
7
|
+
import { ItemId, MoveId, StatusId, TypeId } from "./ids.js";
|
|
8
|
+
export interface BattleRules {
|
|
9
|
+
baseCritChance: number;
|
|
10
|
+
critPerStat: number;
|
|
11
|
+
critMultiplier: number;
|
|
12
|
+
stabMultiplier: number;
|
|
13
|
+
randomMinDamageFactor: number;
|
|
14
|
+
randomMaxDamageFactor: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BattleRuntime {
|
|
17
|
+
rules: BattleRules;
|
|
18
|
+
typesById: Record<TypeId, TypeDefinition>;
|
|
19
|
+
movesById: Record<MoveId, MoveDefinition>;
|
|
20
|
+
itemsById: Record<ItemId, ItemDefinition>;
|
|
21
|
+
amuletsById: Record<string, AmuletDefinition>;
|
|
22
|
+
statusesById: Record<StatusId, StatusDefinition>;
|
|
23
|
+
typeEffectiveness: TypeEffectivenessMatrix;
|
|
24
|
+
}
|
|
25
|
+
export interface PlayerFighterBattleConfig {
|
|
26
|
+
fighter: FighterDefinition;
|
|
27
|
+
maxHp: number;
|
|
28
|
+
moves: MoveDefinition[];
|
|
29
|
+
amulet: AmuletDefinition | null;
|
|
30
|
+
}
|
|
31
|
+
export interface PlayerBattleConfig {
|
|
32
|
+
fighters: PlayerFighterBattleConfig[];
|
|
33
|
+
items: ItemDefinition[];
|
|
34
|
+
amulet: AmuletDefinition | null;
|
|
35
|
+
}
|
|
36
|
+
export interface BattleConfig {
|
|
37
|
+
types: TypeDefinition[];
|
|
38
|
+
typeEffectiveness: TypeEffectivenessMatrix;
|
|
39
|
+
moves: MoveDefinition[];
|
|
40
|
+
items: ItemDefinition[];
|
|
41
|
+
amulets: AmuletDefinition[];
|
|
42
|
+
statuses: StatusDefinition[];
|
|
43
|
+
player1: PlayerBattleConfig;
|
|
44
|
+
player2: PlayerBattleConfig;
|
|
45
|
+
rules?: Partial<BattleRules>;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BattleRuntime } from "
|
|
1
|
+
import { BattleRuntime } from "./battleRuntime.js";
|
|
2
2
|
import type { FighterId, TypeId, MoveId, ItemId, AmuletId, StatusId } from "./ids";
|
|
3
3
|
import type { FighterStats } from "./types.js";
|
|
4
4
|
export interface EquippedMove {
|
|
@@ -62,5 +62,5 @@ export interface BattleState {
|
|
|
62
62
|
player1: PlayerBattleState;
|
|
63
63
|
player2: PlayerBattleState;
|
|
64
64
|
runtime: BattleRuntime;
|
|
65
|
-
forcedSwitch
|
|
65
|
+
forcedSwitch?: ForcedSwitchState;
|
|
66
66
|
}
|
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" | "status_tick" | "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" | "random_event_triggered" | "random_event_effect" | "battle_end";
|
|
3
3
|
export interface BaseBattleEvent {
|
|
4
4
|
id: string;
|
|
5
5
|
turnNumber: number;
|
|
@@ -18,6 +18,7 @@ export interface StatsChangedEvent extends BaseBattleEvent {
|
|
|
18
18
|
accuracy?: number;
|
|
19
19
|
evasion?: number;
|
|
20
20
|
};
|
|
21
|
+
source?: "move" | "status" | "item" | "random_event";
|
|
21
22
|
}
|
|
22
23
|
export interface ActionDeclaredEvent extends BaseBattleEvent {
|
|
23
24
|
kind: "action_declared";
|
|
@@ -49,12 +50,14 @@ export interface DamageEvent extends BaseBattleEvent {
|
|
|
49
50
|
targetId: FighterId;
|
|
50
51
|
amount: number;
|
|
51
52
|
isCritical: boolean;
|
|
53
|
+
source?: "move" | "status" | "item" | "random_event";
|
|
52
54
|
}
|
|
53
55
|
export interface HealEvent extends BaseBattleEvent {
|
|
54
56
|
kind: "heal";
|
|
55
57
|
actorId: FighterId;
|
|
56
58
|
targetId: FighterId;
|
|
57
59
|
amount: number;
|
|
60
|
+
source?: "move" | "status" | "item" | "random_event";
|
|
58
61
|
}
|
|
59
62
|
export interface StatusAppliedEvent extends BaseBattleEvent {
|
|
60
63
|
kind: "status_applied";
|
|
@@ -93,4 +96,25 @@ export interface BattleEndEvent extends BaseBattleEvent {
|
|
|
93
96
|
kind: "battle_end";
|
|
94
97
|
winner: "player1" | "player2" | "draw";
|
|
95
98
|
}
|
|
96
|
-
export
|
|
99
|
+
export interface RandomEventTriggeredEvent extends BaseBattleEvent {
|
|
100
|
+
kind: "random_event_triggered";
|
|
101
|
+
eventId: string;
|
|
102
|
+
eventName: string;
|
|
103
|
+
eventDescription: string;
|
|
104
|
+
eventRarity: "common" | "rare" | "epic" | "legendary";
|
|
105
|
+
imageUrl?: string;
|
|
106
|
+
effects: Array<{
|
|
107
|
+
effectType: string;
|
|
108
|
+
target: string;
|
|
109
|
+
percentValue?: number;
|
|
110
|
+
statType?: string;
|
|
111
|
+
statChange?: number;
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
export interface RandomEventEffectEvent extends BaseBattleEvent {
|
|
115
|
+
kind: "random_event_effect";
|
|
116
|
+
eventId: string;
|
|
117
|
+
targetId: FighterId;
|
|
118
|
+
effectType: string;
|
|
119
|
+
}
|
|
120
|
+
export type BattleEvent = ActionDeclaredEvent | MoveMissEvent | MoveHitEvent | ItemUsedEvent | DamageEvent | HealEvent | StatusAppliedEvent | StatusExpiredEvent | StatusTickEvent | FighterFaintedEvent | FighterSwitchedEvent | BattleEndEvent | StatsChangedEvent | RandomEventTriggeredEvent | RandomEventEffectEvent | BaseBattleEvent;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export * from "./actions.js";
|
|
2
|
-
export * from "./
|
|
3
|
-
export
|
|
2
|
+
export * from "./randomEvents.js";
|
|
3
|
+
export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS } from "./randomEventsList.js";
|
|
4
4
|
export * from "./emotes.js";
|
|
5
5
|
export * from "./events.js";
|
|
6
|
+
export * from "./events.js";
|
|
6
7
|
export * from "./fighters.js";
|
|
7
8
|
export * from "./ids.js";
|
|
8
9
|
export * from "./items.js";
|
|
9
10
|
export * from "./moves.js";
|
|
10
11
|
export * from "./status.js";
|
|
12
|
+
export * from "./randomEventsList.js";
|
|
13
|
+
export * from "./battleRuntime.js";
|
|
14
|
+
export * from "./battleState.js";
|
|
15
|
+
export * from "./amulets.js";
|
|
11
16
|
export * from "./types.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export * from "./actions.js";
|
|
2
|
-
export * from "./
|
|
3
|
-
export
|
|
2
|
+
export * from "./randomEvents.js";
|
|
3
|
+
export { CORE_RANDOM_EVENTS, POKEMON_RANDOM_EVENTS } from "./randomEventsList.js";
|
|
4
4
|
export * from "./emotes.js";
|
|
5
5
|
export * from "./events.js";
|
|
6
|
+
export * from "./events.js";
|
|
6
7
|
export * from "./fighters.js";
|
|
7
8
|
export * from "./ids.js";
|
|
8
9
|
export * from "./items.js";
|
|
9
10
|
export * from "./moves.js";
|
|
10
11
|
export * from "./status.js";
|
|
12
|
+
export * from "./randomEventsList.js";
|
|
13
|
+
export * from "./battleRuntime.js";
|
|
14
|
+
export * from "./battleState.js";
|
|
15
|
+
export * from "./amulets.js";
|
|
11
16
|
export * from "./types.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type EventRarity = "common" | "rare" | "epic" | "legendary";
|
|
2
|
+
export type EventEffectType = "damage_percent" | "heal_percent" | "stat_change" | "apply_status" | "reset_stats" | "swap_hp";
|
|
3
|
+
export type EventTarget = "both" | "self" | "opponent" | "random_one";
|
|
4
|
+
export interface EventCondition {
|
|
5
|
+
type: "has_item" | "hp_below_percent" | "hp_above_percent" | "fighter_type" | "has_status" | "stat_stage_above" | "always";
|
|
6
|
+
value?: string | number;
|
|
7
|
+
}
|
|
8
|
+
export interface RandomEventEffect {
|
|
9
|
+
effectType: EventEffectType;
|
|
10
|
+
target: EventTarget;
|
|
11
|
+
condition?: EventCondition;
|
|
12
|
+
percentValue?: number;
|
|
13
|
+
statType?: "offense" | "defense" | "speed" | "crit" | "accuracy" | "evasion";
|
|
14
|
+
statChange?: number;
|
|
15
|
+
statusId?: string;
|
|
16
|
+
statusDuration?: number;
|
|
17
|
+
randomChoices?: RandomEventEffect[];
|
|
18
|
+
}
|
|
19
|
+
export interface RandomEventDefinition {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
rarity: EventRarity;
|
|
24
|
+
imageUrl?: string;
|
|
25
|
+
effects: RandomEventEffect[];
|
|
26
|
+
}
|
|
27
|
+
export declare const RARITY_WEIGHTS: Record<EventRarity, number>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RandomEventDefinition } from "./randomEvents.js";
|
|
2
|
+
/**
|
|
3
|
+
* CORE Random Events - Generic events that work with any skin
|
|
4
|
+
* Phase 1 + Phase 2: 9 total CORE events
|
|
5
|
+
*/
|
|
6
|
+
export declare const CORE_RANDOM_EVENTS: RandomEventDefinition[];
|
|
7
|
+
/**
|
|
8
|
+
* POKEMON-SPECIFIC Random Events
|
|
9
|
+
* These only appear when using the Pokemon skin
|
|
10
|
+
*/
|
|
11
|
+
export declare const POKEMON_RANDOM_EVENTS: RandomEventDefinition[];
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// @pokemon-io/combat-core/src/core/randomEventsList.ts
|
|
2
|
+
/**
|
|
3
|
+
* CORE Random Events - Generic events that work with any skin
|
|
4
|
+
* Phase 1 + Phase 2: 9 total CORE events
|
|
5
|
+
*/
|
|
6
|
+
export const CORE_RANDOM_EVENTS = [
|
|
7
|
+
// ==================== COMMON (4 events) ====================
|
|
8
|
+
{
|
|
9
|
+
id: "fatigue",
|
|
10
|
+
name: "💤 Cansancio General",
|
|
11
|
+
description: "El cansancio afecta a ambos luchadores.",
|
|
12
|
+
rarity: "common",
|
|
13
|
+
effects: [
|
|
14
|
+
{
|
|
15
|
+
effectType: "stat_change",
|
|
16
|
+
target: "both",
|
|
17
|
+
statType: "speed",
|
|
18
|
+
statChange: -1,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "refreshing_breeze",
|
|
24
|
+
name: "🍃 Brisa Refrescante",
|
|
25
|
+
description: "Una suave brisa restaura la calma.",
|
|
26
|
+
rarity: "common",
|
|
27
|
+
effects: [
|
|
28
|
+
{
|
|
29
|
+
effectType: "heal_percent",
|
|
30
|
+
target: "both",
|
|
31
|
+
percentValue: 5, // 5% HP
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "adrenaline_rush",
|
|
37
|
+
name: "⚡ Adrenalina",
|
|
38
|
+
description: "Una inyección de adrenalina aumenta la velocidad de ambos.",
|
|
39
|
+
rarity: "common",
|
|
40
|
+
effects: [
|
|
41
|
+
{
|
|
42
|
+
effectType: "stat_change",
|
|
43
|
+
target: "both",
|
|
44
|
+
statType: "speed",
|
|
45
|
+
statChange: 1,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "intimidation",
|
|
51
|
+
name: "😨 Intimidación Mutua",
|
|
52
|
+
description: "Ambos luchadores se sienten intimidados.",
|
|
53
|
+
rarity: "common",
|
|
54
|
+
effects: [
|
|
55
|
+
{
|
|
56
|
+
effectType: "stat_change",
|
|
57
|
+
target: "both",
|
|
58
|
+
statType: "offense",
|
|
59
|
+
statChange: -1,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
// ==================== RARE (3 events) ====================
|
|
64
|
+
{
|
|
65
|
+
id: "police_raid",
|
|
66
|
+
name: "🚨 Redada Policial",
|
|
67
|
+
description: "¡La policía confisca objetos sospechosos!",
|
|
68
|
+
rarity: "rare",
|
|
69
|
+
effects: [
|
|
70
|
+
{
|
|
71
|
+
effectType: "stat_change",
|
|
72
|
+
target: "both",
|
|
73
|
+
statType: "offense",
|
|
74
|
+
statChange: -2,
|
|
75
|
+
condition: {
|
|
76
|
+
type: "has_item",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "meteor_shower",
|
|
83
|
+
name: "☄️ Lluvia de Meteoritos",
|
|
84
|
+
description: "¡Meteoritos caen del cielo dañando a ambos!",
|
|
85
|
+
rarity: "rare",
|
|
86
|
+
effects: [
|
|
87
|
+
{
|
|
88
|
+
effectType: "damage_percent",
|
|
89
|
+
target: "both",
|
|
90
|
+
percentValue: 10,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "lucky_charm",
|
|
96
|
+
name: "🍀 Amuleto de la Suerte",
|
|
97
|
+
description: "Un luchador aleatorio se siente afortunado.",
|
|
98
|
+
rarity: "rare",
|
|
99
|
+
effects: [
|
|
100
|
+
{
|
|
101
|
+
effectType: "stat_change",
|
|
102
|
+
target: "random_one",
|
|
103
|
+
statType: "crit",
|
|
104
|
+
statChange: 2,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
// ==================== EPIC (2 events) ====================
|
|
109
|
+
{
|
|
110
|
+
id: "casino",
|
|
111
|
+
name: "🎰 Casino",
|
|
112
|
+
description: "Una ruleta aparece. La suerte está echada.",
|
|
113
|
+
rarity: "epic",
|
|
114
|
+
effects: [
|
|
115
|
+
{
|
|
116
|
+
effectType: "stat_change",
|
|
117
|
+
target: "random_one",
|
|
118
|
+
randomChoices: [
|
|
119
|
+
{
|
|
120
|
+
effectType: "stat_change",
|
|
121
|
+
target: "self",
|
|
122
|
+
statType: "offense",
|
|
123
|
+
statChange: 3,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
effectType: "stat_change",
|
|
127
|
+
target: "opponent",
|
|
128
|
+
statType: "defense",
|
|
129
|
+
statChange: -2,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "reset_button",
|
|
137
|
+
name: "🔄 Botón de Reinicio",
|
|
138
|
+
description: "Todas las estadísticas vuelven a la normalidad.",
|
|
139
|
+
rarity: "epic",
|
|
140
|
+
effects: [
|
|
141
|
+
{
|
|
142
|
+
effectType: "reset_stats",
|
|
143
|
+
target: "both",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* POKEMON-SPECIFIC Random Events
|
|
150
|
+
* These only appear when using the Pokemon skin
|
|
151
|
+
*/
|
|
152
|
+
export const POKEMON_RANDOM_EVENTS = [
|
|
153
|
+
// ==================== COMMON (3 events) ====================
|
|
154
|
+
{
|
|
155
|
+
id: "pokemon_center",
|
|
156
|
+
name: "🏥 Centro Pokémon Móvil",
|
|
157
|
+
description: "Una enfermera Joy aparece y cura a ambos Pokémon.",
|
|
158
|
+
rarity: "common",
|
|
159
|
+
effects: [
|
|
160
|
+
{
|
|
161
|
+
effectType: "heal_percent",
|
|
162
|
+
target: "both",
|
|
163
|
+
percentValue: 15,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "team_rocket",
|
|
169
|
+
name: "🚀 ¡Equipo Rocket!",
|
|
170
|
+
description: "¡Preparaos para los problemas! El ataque de ambos baja.",
|
|
171
|
+
rarity: "common",
|
|
172
|
+
effects: [
|
|
173
|
+
{
|
|
174
|
+
effectType: "stat_change",
|
|
175
|
+
target: "both",
|
|
176
|
+
statType: "offense",
|
|
177
|
+
statChange: -1,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "rare_candy",
|
|
183
|
+
name: "🍬 Caramelo Raro",
|
|
184
|
+
description: "Un Pokémon aleatorio encuentra un caramelo y se fortalece.",
|
|
185
|
+
rarity: "common",
|
|
186
|
+
effects: [
|
|
187
|
+
{
|
|
188
|
+
effectType: "stat_change",
|
|
189
|
+
target: "random_one",
|
|
190
|
+
statType: "offense",
|
|
191
|
+
statChange: 1,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
effectType: "stat_change",
|
|
195
|
+
target: "random_one",
|
|
196
|
+
statType: "defense",
|
|
197
|
+
statChange: 1,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
// ==================== RARE (4 events) ====================
|
|
202
|
+
{
|
|
203
|
+
id: "sandstorm",
|
|
204
|
+
name: "🌪️ Tormenta de Arena",
|
|
205
|
+
description: "Una tormenta de arena daña a todos los Pokémon.",
|
|
206
|
+
rarity: "rare",
|
|
207
|
+
effects: [
|
|
208
|
+
{
|
|
209
|
+
effectType: "damage_percent",
|
|
210
|
+
target: "both",
|
|
211
|
+
percentValue: 8,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "professor_oak",
|
|
217
|
+
name: "👴 Profesor Oak",
|
|
218
|
+
description: "¡El Profesor Oak anima a tu Pokémon!",
|
|
219
|
+
rarity: "rare",
|
|
220
|
+
effects: [
|
|
221
|
+
{
|
|
222
|
+
effectType: "stat_change",
|
|
223
|
+
target: "random_one",
|
|
224
|
+
statType: "offense",
|
|
225
|
+
statChange: 2,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
effectType: "stat_change",
|
|
229
|
+
target: "random_one",
|
|
230
|
+
statType: "speed",
|
|
231
|
+
statChange: 1,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "dawn_stone",
|
|
237
|
+
name: "💎 Piedra Alba",
|
|
238
|
+
description: "Una piedra evolutiva aumenta la defensa de un Pokémon.",
|
|
239
|
+
rarity: "rare",
|
|
240
|
+
effects: [
|
|
241
|
+
{
|
|
242
|
+
effectType: "stat_change",
|
|
243
|
+
target: "random_one",
|
|
244
|
+
statType: "defense",
|
|
245
|
+
statChange: 3,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "potion_spray",
|
|
251
|
+
name: "💊 Lluvia de Pociones",
|
|
252
|
+
description: "Pociones caen del cielo curando a ambos Pokémon.",
|
|
253
|
+
rarity: "rare",
|
|
254
|
+
effects: [
|
|
255
|
+
{
|
|
256
|
+
effectType: "heal_percent",
|
|
257
|
+
target: "both",
|
|
258
|
+
percentValue: 12,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
// ==================== EPIC (2 events) ====================
|
|
263
|
+
{
|
|
264
|
+
id: "legendary_appearance",
|
|
265
|
+
name: "✨ Aparición Legendaria",
|
|
266
|
+
description: "Un Pokémon legendario otorga poder a un luchador aleatorio.",
|
|
267
|
+
rarity: "epic",
|
|
268
|
+
effects: [
|
|
269
|
+
{
|
|
270
|
+
effectType: "stat_change",
|
|
271
|
+
target: "random_one",
|
|
272
|
+
statType: "offense",
|
|
273
|
+
statChange: 3,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
effectType: "stat_change",
|
|
277
|
+
target: "random_one",
|
|
278
|
+
statType: "defense",
|
|
279
|
+
statChange: 2,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
effectType: "stat_change",
|
|
283
|
+
target: "random_one",
|
|
284
|
+
statType: "speed",
|
|
285
|
+
statChange: 2,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "max_revive",
|
|
291
|
+
name: "⚡ Revivir Máximo",
|
|
292
|
+
description: "El Pokémon con menos vida recupera un 30% de HP.",
|
|
293
|
+
rarity: "epic",
|
|
294
|
+
effects: [
|
|
295
|
+
{
|
|
296
|
+
effectType: "heal_percent",
|
|
297
|
+
target: "random_one", // TODO: Cambiar a "lowest_hp" cuando se implemente
|
|
298
|
+
percentValue: 30,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
];
|
|
@@ -1,33 +1,38 @@
|
|
|
1
|
+
// Balanced stat multipliers with gradual progression
|
|
2
|
+
// Formula: Buffs = 1 + (stage × 0.33), Debuffs = 1 / (1 + |stage| × 0.33)
|
|
3
|
+
// Max: +6 = 3.0x, Min: -6 = 0.43x
|
|
1
4
|
export const STAGE_MULTIPLIERS_STATS = {
|
|
2
|
-
"-6":
|
|
3
|
-
"-5":
|
|
4
|
-
"-4":
|
|
5
|
-
"-3":
|
|
6
|
-
"-2":
|
|
7
|
-
"-1":
|
|
8
|
-
0:
|
|
9
|
-
1:
|
|
10
|
-
2:
|
|
11
|
-
3:
|
|
12
|
-
4:
|
|
13
|
-
5:
|
|
14
|
-
6:
|
|
5
|
+
"-6": 1 / (1 + 6 * 0.33), // ≈ 0.43x
|
|
6
|
+
"-5": 1 / (1 + 5 * 0.33), // ≈ 0.48x
|
|
7
|
+
"-4": 1 / (1 + 4 * 0.33), // ≈ 0.54x
|
|
8
|
+
"-3": 1 / (1 + 3 * 0.33), // = 0.60x
|
|
9
|
+
"-2": 1 / (1 + 2 * 0.33), // ≈ 0.67x
|
|
10
|
+
"-1": 1 / (1 + 1 * 0.33), // ≈ 0.75x
|
|
11
|
+
0: 1.0,
|
|
12
|
+
1: 1 + 1 * 0.33, // ≈ 1.33x
|
|
13
|
+
2: 1 + 2 * 0.33, // ≈ 1.67x
|
|
14
|
+
3: 1 + 3 * 0.33, // = 2.00x
|
|
15
|
+
4: 1 + 4 * 0.33, // ≈ 2.33x
|
|
16
|
+
5: 1 + 5 * 0.33, // ≈ 2.67x
|
|
17
|
+
6: 1 + 6 * 0.33, // = 3.00x
|
|
15
18
|
};
|
|
16
|
-
// Accuracy/Evasion multipliers (
|
|
19
|
+
// Accuracy/Evasion multipliers (using same balanced formula for consistency)
|
|
20
|
+
// Formula: same as stats (1 + stage × 0.33 for buffs, 1/(1 + |stage| × 0.33) for debuffs)
|
|
21
|
+
// Max: +6 = 3.0x, Min: -6 = 0.43x
|
|
17
22
|
export const STAGE_MULTIPLIERS_ACCURACY = {
|
|
18
|
-
"-6":
|
|
19
|
-
"-5":
|
|
20
|
-
"-4":
|
|
21
|
-
"-3":
|
|
22
|
-
"-2":
|
|
23
|
-
"-1":
|
|
24
|
-
0:
|
|
25
|
-
1:
|
|
26
|
-
2:
|
|
27
|
-
3:
|
|
28
|
-
4:
|
|
29
|
-
5:
|
|
30
|
-
6:
|
|
23
|
+
"-6": 1 / (1 + 6 * 0.33), // ≈ 0.43x
|
|
24
|
+
"-5": 1 / (1 + 5 * 0.33), // ≈ 0.48x
|
|
25
|
+
"-4": 1 / (1 + 4 * 0.33), // ≈ 0.54x
|
|
26
|
+
"-3": 1 / (1 + 3 * 0.33), // = 0.60x
|
|
27
|
+
"-2": 1 / (1 + 2 * 0.33), // ≈ 0.67x
|
|
28
|
+
"-1": 1 / (1 + 1 * 0.33), // ≈ 0.75x
|
|
29
|
+
0: 1.0,
|
|
30
|
+
1: 1 + 1 * 0.33, // ≈ 1.33x
|
|
31
|
+
2: 1 + 2 * 0.33, // ≈ 1.67x
|
|
32
|
+
3: 1 + 3 * 0.33, // = 2.00x
|
|
33
|
+
4: 1 + 4 * 0.33, // ≈ 2.33x
|
|
34
|
+
5: 1 + 5 * 0.33, // ≈ 2.67x
|
|
35
|
+
6: 1 + 6 * 0.33, // = 3.00x
|
|
31
36
|
};
|
|
32
37
|
// Critical Hit Ratio (Table)
|
|
33
38
|
// Stage 0: ~4.17% (1/24)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RandomEventDefinition } from "../../core/randomEvents.js";
|
|
2
|
+
import type { BattleState } from "../../core/battleState.js";
|
|
3
|
+
import type { BattleEvent } from "../../core/events.js";
|
|
4
|
+
/**
|
|
5
|
+
* Apply a random event to the battle state
|
|
6
|
+
* Follows Single Responsibility - only handles event application
|
|
7
|
+
*/
|
|
8
|
+
export declare const applyRandomEvent: (state: BattleState, event: RandomEventDefinition) => {
|
|
9
|
+
state: BattleState;
|
|
10
|
+
events: BattleEvent[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Simple ID generator (no external dependency)
|
|
2
|
+
const generateId = () => `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3
|
+
/**
|
|
4
|
+
* Apply a random event to the battle state
|
|
5
|
+
* Follows Single Responsibility - only handles event application
|
|
6
|
+
*/
|
|
7
|
+
export const applyRandomEvent = (state, event) => {
|
|
8
|
+
const events = [];
|
|
9
|
+
// Announce event trigger
|
|
10
|
+
events.push({
|
|
11
|
+
id: generateId(),
|
|
12
|
+
turnNumber: state.turnNumber,
|
|
13
|
+
kind: "random_event_triggered",
|
|
14
|
+
eventId: event.id,
|
|
15
|
+
eventName: event.name,
|
|
16
|
+
eventDescription: event.description,
|
|
17
|
+
message: `🎲 ${event.name}: ${event.description}`,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
});
|
|
20
|
+
let updatedState = state;
|
|
21
|
+
// Apply each effect
|
|
22
|
+
for (const effect of event.effects) {
|
|
23
|
+
const result = applyEventEffect(updatedState, effect);
|
|
24
|
+
updatedState = result.state;
|
|
25
|
+
events.push(...result.events);
|
|
26
|
+
}
|
|
27
|
+
return { state: updatedState, events };
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Apply a single event effect
|
|
31
|
+
* Follows Open/Closed - easy to extend with new effect types
|
|
32
|
+
*/
|
|
33
|
+
const applyEventEffect = (state, effect) => {
|
|
34
|
+
const events = [];
|
|
35
|
+
// Handle random choice effects (casino-style)
|
|
36
|
+
if (effect.randomChoices && effect.randomChoices.length > 0) {
|
|
37
|
+
const chosen = effect.randomChoices[Math.floor(Math.random() * effect.randomChoices.length)];
|
|
38
|
+
return applyEventEffect(state, chosen);
|
|
39
|
+
}
|
|
40
|
+
let updatedState = state;
|
|
41
|
+
// Determine targets based on effect.target
|
|
42
|
+
const targets = determineTargets(state, effect);
|
|
43
|
+
// Apply effect to each target
|
|
44
|
+
for (const { fighter, playerKey } of targets) {
|
|
45
|
+
// Check if condition is met
|
|
46
|
+
if (effect.condition && !checkCondition(fighter, effect.condition)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const result = applySingleEffectToFighter(updatedState, playerKey, fighter, effect);
|
|
50
|
+
updatedState = result.state;
|
|
51
|
+
events.push(...result.events);
|
|
52
|
+
}
|
|
53
|
+
return { state: updatedState, events };
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Determine which fighters should be affected by the effect
|
|
57
|
+
*/
|
|
58
|
+
const determineTargets = (state, effect) => {
|
|
59
|
+
const p1Active = state.player1.fighterTeam[state.player1.activeIndex];
|
|
60
|
+
const p2Active = state.player2.fighterTeam[state.player2.activeIndex];
|
|
61
|
+
switch (effect.target) {
|
|
62
|
+
case "both":
|
|
63
|
+
return [
|
|
64
|
+
{ fighter: p1Active, playerKey: "player1" },
|
|
65
|
+
{ fighter: p2Active, playerKey: "player2" },
|
|
66
|
+
];
|
|
67
|
+
case "self":
|
|
68
|
+
return [{ fighter: p1Active, playerKey: "player1" }];
|
|
69
|
+
case "opponent":
|
|
70
|
+
return [{ fighter: p2Active, playerKey: "player2" }];
|
|
71
|
+
case "random_one":
|
|
72
|
+
return Math.random() < 0.5
|
|
73
|
+
? [{ fighter: p1Active, playerKey: "player1" }]
|
|
74
|
+
: [{ fighter: p2Active, playerKey: "player2" }];
|
|
75
|
+
default:
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Check if a condition is met for a fighter
|
|
81
|
+
* Follows Interface Segregation - simple condition checking
|
|
82
|
+
*/
|
|
83
|
+
const checkCondition = (fighter, condition) => {
|
|
84
|
+
switch (condition.type) {
|
|
85
|
+
case "always":
|
|
86
|
+
return true;
|
|
87
|
+
case "has_item":
|
|
88
|
+
// BattleFighter doesn't have inventory, items are in PlayerBattleState
|
|
89
|
+
// For now, ignore this condition (always false for fighters)
|
|
90
|
+
return false;
|
|
91
|
+
case "hp_below_percent":
|
|
92
|
+
if (typeof condition.value !== "number")
|
|
93
|
+
return false;
|
|
94
|
+
const maxHp = fighter.baseStats.offense + fighter.baseStats.defense;
|
|
95
|
+
const hpPercent = (fighter.currentHp / maxHp) * 100;
|
|
96
|
+
return hpPercent < condition.value;
|
|
97
|
+
case "hp_above_percent":
|
|
98
|
+
if (typeof condition.value !== "number")
|
|
99
|
+
return false;
|
|
100
|
+
const maxHp2 = fighter.baseStats.offense + fighter.baseStats.defense;
|
|
101
|
+
const hpPercent2 = (fighter.currentHp / maxHp2) * 100;
|
|
102
|
+
return hpPercent2 > condition.value;
|
|
103
|
+
case "fighter_type":
|
|
104
|
+
// BattleFighter uses classId, not types array
|
|
105
|
+
// Match against classId
|
|
106
|
+
if (typeof condition.value !== "string")
|
|
107
|
+
return false;
|
|
108
|
+
return fighter.classId === condition.value;
|
|
109
|
+
case "has_status":
|
|
110
|
+
return fighter.statuses && fighter.statuses.length > 0;
|
|
111
|
+
case "stat_stage_above":
|
|
112
|
+
if (typeof condition.value !== "number")
|
|
113
|
+
return false;
|
|
114
|
+
const totalStages = Object.values(fighter.statStages).reduce((sum, val) => sum + Math.abs(val), 0);
|
|
115
|
+
return totalStages > condition.value;
|
|
116
|
+
default:
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Apply effect to a single fighter
|
|
122
|
+
* Follows Single Responsibility - one effect type at a time
|
|
123
|
+
*/
|
|
124
|
+
const applySingleEffectToFighter = (state, playerKey, fighter, effect) => {
|
|
125
|
+
const events = [];
|
|
126
|
+
let updatedFighter = fighter;
|
|
127
|
+
switch (effect.effectType) {
|
|
128
|
+
case "damage_percent":
|
|
129
|
+
if (effect.percentValue) {
|
|
130
|
+
const maxHp = fighter.baseStats.offense + fighter.baseStats.defense;
|
|
131
|
+
const damage = Math.floor(maxHp * (effect.percentValue / 100));
|
|
132
|
+
updatedFighter = {
|
|
133
|
+
...fighter,
|
|
134
|
+
currentHp: Math.max(0, fighter.currentHp - damage),
|
|
135
|
+
};
|
|
136
|
+
events.push({
|
|
137
|
+
id: generateId(),
|
|
138
|
+
turnNumber: state.turnNumber,
|
|
139
|
+
kind: "random_event_effect",
|
|
140
|
+
eventId: "",
|
|
141
|
+
targetId: fighter.fighterId,
|
|
142
|
+
effectType: "damage",
|
|
143
|
+
message: `${fighter.fighterId} recibió ${damage} de daño del evento`,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case "heal_percent":
|
|
149
|
+
if (effect.percentValue) {
|
|
150
|
+
const maxHp = fighter.baseStats.offense + fighter.baseStats.defense;
|
|
151
|
+
const heal = Math.floor(maxHp * (effect.percentValue / 100));
|
|
152
|
+
updatedFighter = {
|
|
153
|
+
...fighter,
|
|
154
|
+
currentHp: Math.min(maxHp, fighter.currentHp + heal),
|
|
155
|
+
};
|
|
156
|
+
events.push({
|
|
157
|
+
id: generateId(),
|
|
158
|
+
turnNumber: state.turnNumber,
|
|
159
|
+
kind: "random_event_effect",
|
|
160
|
+
eventId: "",
|
|
161
|
+
targetId: fighter.fighterId,
|
|
162
|
+
effectType: "heal",
|
|
163
|
+
message: `${fighter.fighterId} recuperó ${heal} HP`,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case "stat_change":
|
|
169
|
+
if (effect.statType && effect.statChange !== undefined) {
|
|
170
|
+
const currentStage = fighter.statStages[effect.statType];
|
|
171
|
+
const newStage = Math.max(-6, Math.min(6, currentStage + effect.statChange));
|
|
172
|
+
updatedFighter = {
|
|
173
|
+
...fighter,
|
|
174
|
+
statStages: {
|
|
175
|
+
...fighter.statStages,
|
|
176
|
+
[effect.statType]: newStage,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const change = newStage - currentStage;
|
|
180
|
+
if (change !== 0) {
|
|
181
|
+
events.push({
|
|
182
|
+
id: generateId(),
|
|
183
|
+
turnNumber: state.turnNumber,
|
|
184
|
+
kind: "stats_changed",
|
|
185
|
+
targetId: fighter.fighterId,
|
|
186
|
+
changes: { [effect.statType]: change },
|
|
187
|
+
message: `${fighter.fighterId} ${change > 0 ? "aumentó" : "redujo"} ${effect.statType} en ${Math.abs(change)}`,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "reset_stats":
|
|
194
|
+
updatedFighter = {
|
|
195
|
+
...fighter,
|
|
196
|
+
statStages: {
|
|
197
|
+
offense: 0,
|
|
198
|
+
defense: 0,
|
|
199
|
+
speed: 0,
|
|
200
|
+
crit: 0,
|
|
201
|
+
accuracy: 0,
|
|
202
|
+
evasion: 0,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
events.push({
|
|
206
|
+
id: generateId(),
|
|
207
|
+
turnNumber: state.turnNumber,
|
|
208
|
+
kind: "random_event_effect",
|
|
209
|
+
eventId: "",
|
|
210
|
+
targetId: fighter.fighterId,
|
|
211
|
+
effectType: "reset_stats",
|
|
212
|
+
message: `${fighter.fighterId} tuvo sus stats reseteadas`,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
// Update state with modified fighter
|
|
220
|
+
const activeIndex = state[playerKey].activeIndex;
|
|
221
|
+
const updatedState = {
|
|
222
|
+
...state,
|
|
223
|
+
[playerKey]: {
|
|
224
|
+
...state[playerKey],
|
|
225
|
+
fighterTeam: state[playerKey].fighterTeam.map((f, idx) => idx === activeIndex ? updatedFighter : f),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
return { state: updatedState, events };
|
|
229
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RandomEventDefinition } from "../../core/randomEvents.js";
|
|
2
|
+
/**
|
|
3
|
+
* Determina si debe dispararse un evento aleatorio en este turno
|
|
4
|
+
*
|
|
5
|
+
* @param turnNumber - Número del turno actual
|
|
6
|
+
* @returns true si debe ocurrir un evento
|
|
7
|
+
*/
|
|
8
|
+
export declare function shouldTriggerRandomEvent(turnNumber: number): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Selecciona un evento aleatorio basado en los pesos de rareza
|
|
11
|
+
* Usa "weighted random selection" para dar más probabilidad a eventos comunes
|
|
12
|
+
*
|
|
13
|
+
* @param events - Array de eventos disponibles
|
|
14
|
+
* @returns Evento seleccionado
|
|
15
|
+
*/
|
|
16
|
+
export declare function selectRandomEvent(events: RandomEventDefinition[]): RandomEventDefinition;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @pokemon-io/combat-core/src/engine/events/randomEventTrigger.ts
|
|
2
|
+
import { RARITY_WEIGHTS, } from "../../core/randomEvents.js";
|
|
3
|
+
/**
|
|
4
|
+
* 🎲 LÓGICA DE PROBABILIDAD DE EVENTOS ALEATORIOS
|
|
5
|
+
*
|
|
6
|
+
* 1. TRIGGER (¿Ocurre un evento?):
|
|
7
|
+
* - Se revisa cada 2 turnos (turno 2, 4, 6, etc.)
|
|
8
|
+
* - Probabilidad: 50% (0.5) en producción
|
|
9
|
+
* - DEBUG MODE: 100% para pruebas (línea 27)
|
|
10
|
+
*
|
|
11
|
+
* 2. SELECCIÓN (¿Qué evento ocurre?):
|
|
12
|
+
* - Se usa "weighted random selection" basado en rareza
|
|
13
|
+
* - Pesos definidos en RARITY_WEIGHTS:
|
|
14
|
+
* • Common: 60% de probabilidad
|
|
15
|
+
* • Rare: 25% de probabilidad
|
|
16
|
+
* • Epic: 12% de probabilidad
|
|
17
|
+
* • Legendary: 3% de probabilidad
|
|
18
|
+
*/
|
|
19
|
+
// 🔧 DEBUG: Cambia esto a false para volver a 50% de probabilidad
|
|
20
|
+
const DEBUG_MODE = true;
|
|
21
|
+
const TRIGGER_CHANCE = DEBUG_MODE ? 1.0 : 0.5; // 100% en debug, 50% en producción
|
|
22
|
+
/**
|
|
23
|
+
* Determina si debe dispararse un evento aleatorio en este turno
|
|
24
|
+
*
|
|
25
|
+
* @param turnNumber - Número del turno actual
|
|
26
|
+
* @returns true si debe ocurrir un evento
|
|
27
|
+
*/
|
|
28
|
+
export function shouldTriggerRandomEvent(turnNumber) {
|
|
29
|
+
// Solo en turnos pares (2, 4, 6, ...)
|
|
30
|
+
if (turnNumber % 2 !== 0) {
|
|
31
|
+
console.log(`[Random Events] Turno ${turnNumber}: NO (turno impar)`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Probabilidad de trigger
|
|
35
|
+
const roll = Math.random();
|
|
36
|
+
const triggered = roll < TRIGGER_CHANCE;
|
|
37
|
+
console.log(`[Random Events] Turno ${turnNumber}: ${triggered ? '✅ SÍ' : '❌ NO'} ` +
|
|
38
|
+
`(roll=${roll.toFixed(3)}, threshold=${TRIGGER_CHANCE})`);
|
|
39
|
+
return triggered;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Selecciona un evento aleatorio basado en los pesos de rareza
|
|
43
|
+
* Usa "weighted random selection" para dar más probabilidad a eventos comunes
|
|
44
|
+
*
|
|
45
|
+
* @param events - Array de eventos disponibles
|
|
46
|
+
* @returns Evento seleccionado
|
|
47
|
+
*/
|
|
48
|
+
export function selectRandomEvent(events) {
|
|
49
|
+
// Calcular peso total
|
|
50
|
+
let totalWeight = 0;
|
|
51
|
+
for (const event of events) {
|
|
52
|
+
totalWeight += RARITY_WEIGHTS[event.rarity];
|
|
53
|
+
}
|
|
54
|
+
// Selección ponderada
|
|
55
|
+
let random = Math.random() * totalWeight;
|
|
56
|
+
console.log(`[Random Events] Seleccionando evento (totalWeight=${totalWeight}, roll=${random.toFixed(2)})`);
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
random -= RARITY_WEIGHTS[event.rarity];
|
|
59
|
+
if (random <= 0) {
|
|
60
|
+
console.log(`[Random Events] Evento seleccionado: "${event.name}" (${event.rarity})`);
|
|
61
|
+
return event;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Fallback (no debería ocurrir)
|
|
65
|
+
console.warn("[Random Events] Fallback al primer evento");
|
|
66
|
+
return events[0];
|
|
67
|
+
}
|
package/dist/engine/index.d.ts
CHANGED
package/dist/engine/index.js
CHANGED
package/dist/engine/rules.d.ts
CHANGED
|
@@ -1,43 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
baseCritChance: number;
|
|
4
|
-
critPerStat: number;
|
|
5
|
-
critMultiplier: number;
|
|
6
|
-
stabMultiplier: number;
|
|
7
|
-
randomMinDamageFactor: number;
|
|
8
|
-
randomMaxDamageFactor: number;
|
|
9
|
-
}
|
|
10
|
-
export interface BattleRuntime {
|
|
11
|
-
rules: BattleRules;
|
|
12
|
-
typesById: Record<TypeId, TypeDefinition>;
|
|
13
|
-
movesById: Record<MoveId, MoveDefinition>;
|
|
14
|
-
itemsById: Record<ItemId, ItemDefinition>;
|
|
15
|
-
amuletsById: Record<string, AmuletDefinition>;
|
|
16
|
-
statusesById: Record<StatusId, StatusDefinition>;
|
|
17
|
-
typeEffectiveness: TypeEffectivenessMatrix;
|
|
18
|
-
}
|
|
19
|
-
export interface PlayerFighterBattleConfig {
|
|
20
|
-
fighter: FighterDefinition;
|
|
21
|
-
maxHp: number;
|
|
22
|
-
moves: MoveDefinition[];
|
|
23
|
-
amulet: AmuletDefinition | null;
|
|
24
|
-
}
|
|
25
|
-
export interface PlayerBattleConfig {
|
|
26
|
-
fighters: PlayerFighterBattleConfig[];
|
|
27
|
-
items: ItemDefinition[];
|
|
28
|
-
amulet: AmuletDefinition | null;
|
|
29
|
-
}
|
|
30
|
-
export interface BattleConfig {
|
|
31
|
-
types: TypeDefinition[];
|
|
32
|
-
typeEffectiveness: TypeEffectivenessMatrix;
|
|
33
|
-
moves: MoveDefinition[];
|
|
34
|
-
items: ItemDefinition[];
|
|
35
|
-
amulets: AmuletDefinition[];
|
|
36
|
-
statuses: StatusDefinition[];
|
|
37
|
-
player1: PlayerBattleConfig;
|
|
38
|
-
player2: PlayerBattleConfig;
|
|
39
|
-
rules?: Partial<BattleRules>;
|
|
40
|
-
}
|
|
1
|
+
import { BattleState, BattleRules, BattleRuntime } from "../core/index.js";
|
|
2
|
+
export type { BattleRules, BattleRuntime, BattleConfig, PlayerBattleConfig, PlayerFighterBattleConfig, } from "../core/index.js";
|
|
41
3
|
export interface RuntimeBattleState extends BattleState {
|
|
42
4
|
runtime: BattleRuntime;
|
|
43
5
|
}
|
|
@@ -10,6 +10,9 @@ import { recomputeEffectiveStatsForFighter } from "../fighters/fighter.js";
|
|
|
10
10
|
import { getOpponentAndSelf } from "../fighters/selectors.js";
|
|
11
11
|
import { applyEndOfTurnStatuses } from "../status/endOfTurn.js";
|
|
12
12
|
import { hasHardCc } from "../status/hardCc.js";
|
|
13
|
+
import { shouldTriggerRandomEvent, selectRandomEvent } from "../events/randomEventTrigger.js";
|
|
14
|
+
import { applyRandomEvent } from "../events/applyRandomEvent.js";
|
|
15
|
+
import { CORE_RANDOM_EVENTS } from "../../core/index.js";
|
|
13
16
|
// ------------------------------------------------------
|
|
14
17
|
// Helpers KO -> forced switch (estilo Pokémon real)
|
|
15
18
|
// ------------------------------------------------------
|
|
@@ -267,6 +270,46 @@ export const resolveTurn = (state, actions) => {
|
|
|
267
270
|
winner: winnerAtEnd,
|
|
268
271
|
});
|
|
269
272
|
}
|
|
273
|
+
// ✅ RANDOM EVENTS - Check and apply after forced switch and before turn_end
|
|
274
|
+
const shouldCheckRandomEvent = !currentState.forcedSwitch && // Skip if forced switch pending
|
|
275
|
+
winnerAtEnd === "none"; // Skip if battle ended
|
|
276
|
+
if (shouldCheckRandomEvent && shouldTriggerRandomEvent(currentState.turnNumber)) {
|
|
277
|
+
// Select random event
|
|
278
|
+
const availableEvents = [
|
|
279
|
+
...CORE_RANDOM_EVENTS,
|
|
280
|
+
// TODO: Add skin-specific events (POKEMON_RANDOM_EVENTS)
|
|
281
|
+
];
|
|
282
|
+
const randomEvent = selectRandomEvent(availableEvents);
|
|
283
|
+
// Emit banner event
|
|
284
|
+
events.push({
|
|
285
|
+
...createBaseEvent(currentState.turnNumber, "random_event_triggered", randomEvent.description),
|
|
286
|
+
eventId: randomEvent.id,
|
|
287
|
+
eventName: randomEvent.name,
|
|
288
|
+
eventDescription: randomEvent.description,
|
|
289
|
+
eventRarity: randomEvent.rarity,
|
|
290
|
+
imageUrl: randomEvent.imageUrl,
|
|
291
|
+
effects: randomEvent.effects.map((effect) => ({
|
|
292
|
+
effectType: effect.effectType,
|
|
293
|
+
target: effect.target,
|
|
294
|
+
percentValue: effect.percentValue,
|
|
295
|
+
statType: effect.statType,
|
|
296
|
+
statChange: effect.statChange,
|
|
297
|
+
})),
|
|
298
|
+
});
|
|
299
|
+
// Apply random event effects
|
|
300
|
+
const effectsResult = applyRandomEvent(currentState, randomEvent);
|
|
301
|
+
currentState = effectsResult.state;
|
|
302
|
+
// Add effect events with source="random_event" marker
|
|
303
|
+
const markedEvents = effectsResult.events.map((event) => {
|
|
304
|
+
if (event.kind === "damage" ||
|
|
305
|
+
event.kind === "heal" ||
|
|
306
|
+
event.kind === "stats_changed") {
|
|
307
|
+
return { ...event, source: "random_event" };
|
|
308
|
+
}
|
|
309
|
+
return event;
|
|
310
|
+
});
|
|
311
|
+
events.push(...markedEvents);
|
|
312
|
+
}
|
|
270
313
|
events.push({
|
|
271
314
|
...createBaseEvent(currentState.turnNumber, "turn_end", `Termina el turno ${currentState.turnNumber}`),
|
|
272
315
|
});
|