pokemon-io-core 0.0.100 → 0.0.101
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/socketTypes.d.ts +9 -0
- package/dist/core/events.d.ts +14 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/randomEvents.d.ts +27 -0
- package/dist/core/randomEvents.js +8 -0
- package/dist/core/randomEventsList.d.ts +6 -0
- package/dist/core/randomEventsList.js +80 -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 +13 -0
- package/dist/engine/events/randomEventTrigger.js +35 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
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;
|
|
@@ -93,4 +93,16 @@ export interface BattleEndEvent extends BaseBattleEvent {
|
|
|
93
93
|
kind: "battle_end";
|
|
94
94
|
winner: "player1" | "player2" | "draw";
|
|
95
95
|
}
|
|
96
|
-
export
|
|
96
|
+
export interface RandomEventTriggeredEvent extends BaseBattleEvent {
|
|
97
|
+
kind: "random_event_triggered";
|
|
98
|
+
eventId: string;
|
|
99
|
+
eventName: string;
|
|
100
|
+
eventDescription: string;
|
|
101
|
+
}
|
|
102
|
+
export interface RandomEventEffectEvent extends BaseBattleEvent {
|
|
103
|
+
kind: "random_event_effect";
|
|
104
|
+
eventId: string;
|
|
105
|
+
targetId: FighterId;
|
|
106
|
+
effectType: string;
|
|
107
|
+
}
|
|
108
|
+
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
package/dist/core/index.js
CHANGED
|
@@ -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,80 @@
|
|
|
1
|
+
// @pokemon-io/combat-core/src/core/randomEventsList.ts
|
|
2
|
+
/**
|
|
3
|
+
* CORE Random Events - Generic events that work with any skin
|
|
4
|
+
* Phase 1: Initial 4 events for MVP
|
|
5
|
+
*/
|
|
6
|
+
export const CORE_RANDOM_EVENTS = [
|
|
7
|
+
// ==================== COMMON ====================
|
|
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
|
+
// ==================== RARE ====================
|
|
36
|
+
{
|
|
37
|
+
id: "police_raid",
|
|
38
|
+
name: "🚨 Redada Policial",
|
|
39
|
+
description: "¡La policía confisca objetos sospechosos!",
|
|
40
|
+
rarity: "rare",
|
|
41
|
+
effects: [
|
|
42
|
+
{
|
|
43
|
+
effectType: "stat_change",
|
|
44
|
+
target: "both",
|
|
45
|
+
statType: "offense",
|
|
46
|
+
statChange: -2,
|
|
47
|
+
condition: {
|
|
48
|
+
type: "has_item",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
// ==================== EPIC ====================
|
|
54
|
+
{
|
|
55
|
+
id: "casino",
|
|
56
|
+
name: "🎰 Casino",
|
|
57
|
+
description: "Una ruleta aparece. La suerte está echada.",
|
|
58
|
+
rarity: "epic",
|
|
59
|
+
effects: [
|
|
60
|
+
{
|
|
61
|
+
effectType: "stat_change",
|
|
62
|
+
target: "random_one",
|
|
63
|
+
randomChoices: [
|
|
64
|
+
{
|
|
65
|
+
effectType: "stat_change",
|
|
66
|
+
target: "self",
|
|
67
|
+
statType: "offense",
|
|
68
|
+
statChange: 3,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
effectType: "stat_change",
|
|
72
|
+
target: "opponent",
|
|
73
|
+
statType: "defense",
|
|
74
|
+
statChange: -2,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
@@ -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,13 @@
|
|
|
1
|
+
import type { RandomEventDefinition } from "../../core/randomEvents.js";
|
|
2
|
+
/**
|
|
3
|
+
* Check if a random event should trigger
|
|
4
|
+
* @param turnNumber Current turn number
|
|
5
|
+
* @returns true if event should trigger (50% chance every 2 turns)
|
|
6
|
+
*/
|
|
7
|
+
export declare const shouldTriggerRandomEvent: (turnNumber: number) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Select a random event from available events based on rarity weights
|
|
10
|
+
* @param availableEvents List of available events
|
|
11
|
+
* @returns Selected event
|
|
12
|
+
*/
|
|
13
|
+
export declare const selectRandomEvent: (availableEvents: RandomEventDefinition[]) => RandomEventDefinition;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @pokemon-io/combat-core/src/engine/events/randomEventTrigger.ts
|
|
2
|
+
import { RARITY_WEIGHTS } from "../../core/randomEvents.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check if a random event should trigger
|
|
5
|
+
* @param turnNumber Current turn number
|
|
6
|
+
* @returns true if event should trigger (50% chance every 2 turns)
|
|
7
|
+
*/
|
|
8
|
+
export const shouldTriggerRandomEvent = (turnNumber) => {
|
|
9
|
+
// Only check on even turns
|
|
10
|
+
if (turnNumber % 2 !== 0) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
// 50% chance
|
|
14
|
+
return Math.random() < 0.5;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Select a random event from available events based on rarity weights
|
|
18
|
+
* @param availableEvents List of available events
|
|
19
|
+
* @returns Selected event
|
|
20
|
+
*/
|
|
21
|
+
export const selectRandomEvent = (availableEvents) => {
|
|
22
|
+
// Calculate total weight
|
|
23
|
+
const totalWeight = availableEvents.reduce((sum, event) => sum + RARITY_WEIGHTS[event.rarity], 0);
|
|
24
|
+
// Roll random number
|
|
25
|
+
let roll = Math.random() * totalWeight;
|
|
26
|
+
// Select event based on weight
|
|
27
|
+
for (const event of availableEvents) {
|
|
28
|
+
roll -= RARITY_WEIGHTS[event.rarity];
|
|
29
|
+
if (roll <= 0) {
|
|
30
|
+
return event;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Fallback (shouldn't happen)
|
|
34
|
+
return availableEvents[0];
|
|
35
|
+
};
|
package/dist/engine/index.d.ts
CHANGED
package/dist/engine/index.js
CHANGED