@umang-boss/claudemon 2.1.2 → 2.2.0
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/award-xp.mjs +129 -58
- package/dist/award-xp.mjs.map +3 -3
- package/dist/server.mjs +492 -99
- package/dist/server.mjs.map +2 -2
- package/package.json +1 -1
- package/src/engine/encounter-pool.ts +51 -6
- package/src/engine/encounters.ts +122 -128
- package/src/gamification/legendary-quests.ts +515 -91
- package/src/hooks/award-xp.ts +3 -5
- package/statusline/buddy-status.sh +15 -11
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pre-built encounter pools — computed once at module load.
|
|
3
|
-
*
|
|
3
|
+
* Provides all base-stage wild-eligible Pokemon grouped by rarity,
|
|
4
|
+
* with helper lookups by type and generation for smart weighting.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { PokemonType, RarityTier } from "./types.js";
|
|
@@ -22,22 +23,64 @@ function isBaseStage(pokemonId: number): boolean {
|
|
|
22
23
|
|
|
23
24
|
const WILD_RARITIES: ReadonlySet<RarityTier> = new Set(["common", "uncommon", "rare"]);
|
|
24
25
|
|
|
26
|
+
// ── Generation ranges ─────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const GEN_RANGES: readonly { gen: number; start: number; end: number }[] = [
|
|
29
|
+
{ gen: 1, start: 1, end: 151 },
|
|
30
|
+
{ gen: 2, start: 152, end: 251 },
|
|
31
|
+
{ gen: 3, start: 252, end: 386 },
|
|
32
|
+
{ gen: 4, start: 387, end: 493 },
|
|
33
|
+
{ gen: 5, start: 494, end: 649 },
|
|
34
|
+
{ gen: 6, start: 650, end: 721 },
|
|
35
|
+
{ gen: 7, start: 722, end: 809 },
|
|
36
|
+
{ gen: 8, start: 810, end: 905 },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Get the generation number for a Pokemon ID. */
|
|
40
|
+
export function getGeneration(pokemonId: number): number {
|
|
41
|
+
for (const { gen, start, end } of GEN_RANGES) {
|
|
42
|
+
if (pokemonId >= start && pokemonId <= end) return gen;
|
|
43
|
+
}
|
|
44
|
+
return 1; // fallback
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
// ── Build pools at module load ────────────────────────────────
|
|
26
48
|
|
|
27
|
-
interface RarityPool {
|
|
49
|
+
export interface RarityPool {
|
|
28
50
|
readonly common: readonly number[];
|
|
29
51
|
readonly uncommon: readonly number[];
|
|
30
52
|
readonly rare: readonly number[];
|
|
31
53
|
}
|
|
32
54
|
|
|
55
|
+
/** All wild-eligible base-stage Pokemon, grouped by rarity. */
|
|
56
|
+
function buildAllPool(): RarityPool {
|
|
57
|
+
const common: number[] = [];
|
|
58
|
+
const uncommon: number[] = [];
|
|
59
|
+
const rare: number[] = [];
|
|
60
|
+
|
|
61
|
+
for (const pokemon of POKEDEX) {
|
|
62
|
+
if (!isBaseStage(pokemon.id)) continue;
|
|
63
|
+
if (!WILD_RARITIES.has(pokemon.rarity)) continue;
|
|
64
|
+
|
|
65
|
+
const rarity = pokemon.rarity as "common" | "uncommon" | "rare";
|
|
66
|
+
if (rarity === "common") common.push(pokemon.id);
|
|
67
|
+
else if (rarity === "uncommon") uncommon.push(pokemon.id);
|
|
68
|
+
else rare.push(pokemon.id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
common: Object.freeze(common),
|
|
73
|
+
uncommon: Object.freeze(uncommon),
|
|
74
|
+
rare: Object.freeze(rare),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Type-indexed pools for legacy compatibility and type lookups. */
|
|
33
79
|
function buildTypePools(): ReadonlyMap<PokemonType, RarityPool> {
|
|
34
80
|
const pools = new Map<PokemonType, { common: number[]; uncommon: number[]; rare: number[] }>();
|
|
35
81
|
|
|
36
82
|
for (const pokemon of POKEDEX) {
|
|
37
|
-
// Only base-stage Pokemon appear in wild encounters
|
|
38
83
|
if (!isBaseStage(pokemon.id)) continue;
|
|
39
|
-
|
|
40
|
-
// Legendary and mythical never appear in wild encounters
|
|
41
84
|
if (!WILD_RARITIES.has(pokemon.rarity)) continue;
|
|
42
85
|
|
|
43
86
|
for (const pokemonType of pokemon.types) {
|
|
@@ -54,7 +97,6 @@ function buildTypePools(): ReadonlyMap<PokemonType, RarityPool> {
|
|
|
54
97
|
}
|
|
55
98
|
}
|
|
56
99
|
|
|
57
|
-
// Freeze all inner arrays for immutability
|
|
58
100
|
const frozen = new Map<PokemonType, RarityPool>();
|
|
59
101
|
for (const [type, pool] of pools) {
|
|
60
102
|
frozen.set(type, {
|
|
@@ -69,3 +111,6 @@ function buildTypePools(): ReadonlyMap<PokemonType, RarityPool> {
|
|
|
69
111
|
|
|
70
112
|
/** Map of PokemonType to Pokemon IDs of that type, grouped by rarity. */
|
|
71
113
|
export const TYPE_POOLS: ReadonlyMap<PokemonType, RarityPool> = buildTypePools();
|
|
114
|
+
|
|
115
|
+
/** All wild-eligible base-stage Pokemon, grouped by rarity (type-agnostic). */
|
|
116
|
+
export const ALL_WILD_POOL: RarityPool = buildAllPool();
|
package/src/engine/encounters.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wild encounter system.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* 100% random encounters with Pokedex-aware smart weighting.
|
|
4
|
+
* No activity-type or time-of-day bias — any Pokemon can appear anytime.
|
|
5
|
+
* Unseen types, gens, and species are boosted to fill the Pokedex faster.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -15,33 +15,11 @@ import type {
|
|
|
15
15
|
CodingStat,
|
|
16
16
|
RarityTier,
|
|
17
17
|
} from "./types.js";
|
|
18
|
+
import { POKEMON_TYPES } from "./types.js";
|
|
18
19
|
import { ENCOUNTER_THRESHOLDS } from "./constants.js";
|
|
19
20
|
import type { EncounterSpeed } from "./constants.js";
|
|
20
21
|
import { POKEMON_BY_ID } from "./pokemon-data.js";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
// ── Activity to Pokemon Type Mapping ──────────────────────────
|
|
24
|
-
|
|
25
|
-
const ENCOUNTER_TYPE_MAP: Readonly<Record<XpEventType, readonly PokemonType[]>> = {
|
|
26
|
-
commit: ["Normal", "Flying"],
|
|
27
|
-
test_pass: ["Fighting", "Normal"],
|
|
28
|
-
test_written: ["Fighting", "Normal"],
|
|
29
|
-
build_success: ["Fire", "Rock"],
|
|
30
|
-
bug_fix: ["Bug", "Poison"],
|
|
31
|
-
lint_fix: ["Bug", "Poison"],
|
|
32
|
-
file_create: ["Normal", "Ground"],
|
|
33
|
-
file_edit: ["Normal", "Ground"],
|
|
34
|
-
search: ["Flying", "Ground"],
|
|
35
|
-
large_refactor: ["Psychic", "Dragon"],
|
|
36
|
-
session_start: ["Grass", "Fairy"],
|
|
37
|
-
daily_streak: ["Water", "Electric"],
|
|
38
|
-
pet: ["Normal", "Fairy"],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/** Maps an XP event type to the Pokemon types that can appear. */
|
|
42
|
-
export function getEncounterTypes(eventType: XpEventType): readonly PokemonType[] {
|
|
43
|
-
return ENCOUNTER_TYPE_MAP[eventType];
|
|
44
|
-
}
|
|
22
|
+
import { ALL_WILD_POOL, getGeneration, GEN_RANGES } from "./encounter-pool.js";
|
|
45
23
|
|
|
46
24
|
// ── Encounter Context ─────────────────────────────────────────
|
|
47
25
|
|
|
@@ -49,8 +27,8 @@ export interface EncounterContext {
|
|
|
49
27
|
xpSinceLastEncounter: number;
|
|
50
28
|
encounterSpeed: EncounterSpeed;
|
|
51
29
|
currentStreak: number;
|
|
52
|
-
recentToolTypes: string[];
|
|
53
|
-
currentHour: number;
|
|
30
|
+
recentToolTypes: string[];
|
|
31
|
+
currentHour: number;
|
|
54
32
|
}
|
|
55
33
|
|
|
56
34
|
// ── Encounter Trigger ─────────────────────────────────────────
|
|
@@ -62,14 +40,10 @@ export interface EncounterContext {
|
|
|
62
40
|
*/
|
|
63
41
|
export function shouldTriggerEncounter(ctx: EncounterContext): boolean {
|
|
64
42
|
const threshold = ENCOUNTER_THRESHOLDS[ctx.encounterSpeed];
|
|
65
|
-
|
|
66
|
-
// Streak bonus: 7+ day streak = halve the threshold
|
|
67
43
|
const streakMultiplier = ctx.currentStreak >= 7 ? 0.5 : 1;
|
|
68
44
|
const effectiveThreshold = Math.floor(threshold * streakMultiplier);
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return true;
|
|
46
|
+
return ctx.xpSinceLastEncounter >= effectiveThreshold;
|
|
73
47
|
}
|
|
74
48
|
|
|
75
49
|
/** Check for bonus encounter (10% chance after a regular encounter). */
|
|
@@ -83,20 +57,8 @@ export function shouldDiversityBonus(recentToolTypes: string[]): boolean {
|
|
|
83
57
|
return uniqueTypes.size >= 3;
|
|
84
58
|
}
|
|
85
59
|
|
|
86
|
-
// ── Time-of-Day Bias ──────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
/** Get time-of-day type biases for encounter generation. */
|
|
89
|
-
export function getTimeOfDayBias(hour: number): PokemonType[] {
|
|
90
|
-
if (hour >= 22 || hour < 5) return ["Ghost", "Dark"]; // Night: Ghost/Dark types
|
|
91
|
-
if (hour >= 5 && hour < 9) return ["Grass", "Fairy"]; // Morning: Grass/Fairy types
|
|
92
|
-
if (hour >= 12 && hour < 14) return ["Fire", "Steel"]; // Midday: Fire/Steel types
|
|
93
|
-
if (hour >= 17 && hour < 20) return ["Water", "Flying"]; // Evening: Water types
|
|
94
|
-
return []; // No bias
|
|
95
|
-
}
|
|
96
|
-
|
|
97
60
|
// ── Rarity Weights ────────────────────────────────────────────
|
|
98
61
|
|
|
99
|
-
/** Relative weights for rarity-based selection. Higher = more likely to appear. */
|
|
100
62
|
const RARITY_WEIGHTS: Readonly<Record<"common" | "uncommon" | "rare", number>> = {
|
|
101
63
|
common: 70,
|
|
102
64
|
uncommon: 25,
|
|
@@ -105,10 +67,6 @@ const RARITY_WEIGHTS: Readonly<Record<"common" | "uncommon" | "rare", number>> =
|
|
|
105
67
|
|
|
106
68
|
// ── Catch Condition Mapping ───────────────────────────────────
|
|
107
69
|
|
|
108
|
-
/**
|
|
109
|
-
* Stat most relevant to each Pokemon type for catch condition evaluation.
|
|
110
|
-
* Used for uncommon+ encounters to determine the required stat.
|
|
111
|
-
*/
|
|
112
70
|
const TYPE_TO_STAT: Readonly<Record<PokemonType, CodingStat>> = {
|
|
113
71
|
Normal: "velocity",
|
|
114
72
|
Fire: "debugging",
|
|
@@ -130,7 +88,6 @@ const TYPE_TO_STAT: Readonly<Record<PokemonType, CodingStat>> = {
|
|
|
130
88
|
Fairy: "wisdom",
|
|
131
89
|
};
|
|
132
90
|
|
|
133
|
-
/** Minimum stat thresholds by rarity tier. */
|
|
134
91
|
const RARITY_STAT_THRESHOLD: Readonly<Record<RarityTier, number>> = {
|
|
135
92
|
common: 0,
|
|
136
93
|
uncommon: 20,
|
|
@@ -139,7 +96,6 @@ const RARITY_STAT_THRESHOLD: Readonly<Record<RarityTier, number>> = {
|
|
|
139
96
|
mythical: 80,
|
|
140
97
|
};
|
|
141
98
|
|
|
142
|
-
/** Minimum Pokemon level required to catch by rarity tier. */
|
|
143
99
|
const RARITY_LEVEL_THRESHOLD: Readonly<Record<RarityTier, number>> = {
|
|
144
100
|
common: 1,
|
|
145
101
|
uncommon: 10,
|
|
@@ -150,7 +106,6 @@ const RARITY_LEVEL_THRESHOLD: Readonly<Record<RarityTier, number>> = {
|
|
|
150
106
|
|
|
151
107
|
// ── Catch Condition ───────────────────────────────────────────
|
|
152
108
|
|
|
153
|
-
/** Get the catch condition for a Pokemon based on its rarity. */
|
|
154
109
|
export function getCatchCondition(pokemonId: number): CatchCondition {
|
|
155
110
|
const pokemon = POKEMON_BY_ID.get(pokemonId);
|
|
156
111
|
if (!pokemon) {
|
|
@@ -161,75 +116,138 @@ export function getCatchCondition(pokemonId: number): CatchCondition {
|
|
|
161
116
|
const requiredLevel = RARITY_LEVEL_THRESHOLD[rarity];
|
|
162
117
|
const minStatValue = RARITY_STAT_THRESHOLD[rarity];
|
|
163
118
|
|
|
164
|
-
// Common Pokemon have no stat requirement
|
|
165
119
|
if (rarity === "common") {
|
|
166
120
|
return { requiredStat: null, minStatValue: 0, requiredLevel };
|
|
167
121
|
}
|
|
168
122
|
|
|
169
|
-
// Use the primary type to determine which stat is required
|
|
170
123
|
const primaryType = pokemon.types[0];
|
|
171
124
|
const requiredStat = TYPE_TO_STAT[primaryType];
|
|
172
125
|
|
|
173
126
|
return { requiredStat, minStatValue, requiredLevel };
|
|
174
127
|
}
|
|
175
128
|
|
|
176
|
-
// ──
|
|
129
|
+
// ── Pokedex Analysis ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/** Count how many caught Pokemon the player has of each type. */
|
|
132
|
+
function getTypeCounts(state: PlayerState): Map<PokemonType, number> {
|
|
133
|
+
const counts = new Map<PokemonType, number>();
|
|
134
|
+
for (const t of POKEMON_TYPES) counts.set(t, 0);
|
|
135
|
+
|
|
136
|
+
for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
|
|
137
|
+
if (!entry.caught) continue;
|
|
138
|
+
const pokemon = POKEMON_BY_ID.get(Number(idStr));
|
|
139
|
+
if (!pokemon) continue;
|
|
140
|
+
for (const t of pokemon.types) {
|
|
141
|
+
if (t !== undefined) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Count how many caught Pokemon the player has from each generation. */
|
|
148
|
+
function getGenCounts(state: PlayerState): Map<number, number> {
|
|
149
|
+
const counts = new Map<number, number>();
|
|
150
|
+
for (const { gen } of GEN_RANGES) counts.set(gen, 0);
|
|
151
|
+
|
|
152
|
+
for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
|
|
153
|
+
if (!entry.caught) continue;
|
|
154
|
+
const gen = getGeneration(Number(idStr));
|
|
155
|
+
counts.set(gen, (counts.get(gen) ?? 0) + 1);
|
|
156
|
+
}
|
|
157
|
+
return counts;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Smart Candidate Pool ──────────────────────────────────────
|
|
177
161
|
|
|
178
162
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
163
|
+
* Build a weighted candidate pool from ALL wild-eligible Pokemon.
|
|
164
|
+
* Weighting considers:
|
|
165
|
+
* - Rarity (common 70, uncommon 25, rare 5)
|
|
166
|
+
* - Unseen species boost (2x if not seen, 4x in early game)
|
|
167
|
+
* - Type diversity (3x if player has 0 caught of this type)
|
|
168
|
+
* - Gen diversity (2x if player has < 3 caught from this gen)
|
|
169
|
+
* - 10% "same again" chance (pure random, no diversity boosts)
|
|
181
170
|
*/
|
|
182
|
-
function
|
|
183
|
-
types: readonly PokemonType[],
|
|
184
|
-
state: PlayerState,
|
|
185
|
-
): { id: number; weight: number }[] {
|
|
171
|
+
function buildSmartPool(state: PlayerState): { id: number; weight: number }[] {
|
|
186
172
|
const candidates: { id: number; weight: number }[] = [];
|
|
187
|
-
const seen = new Set<number>();
|
|
188
173
|
|
|
189
|
-
// Determine the player's starter Pokemon ID
|
|
190
174
|
const starterPokemon = [...state.party, ...state.pcBox].find((p) => p.isStarter);
|
|
191
175
|
const starterPokemonId = starterPokemon?.pokemonId ?? -1;
|
|
192
176
|
|
|
193
|
-
// Set of already-caught Pokemon IDs (for duplicate filtering)
|
|
194
177
|
const caughtIds = new Set<number>();
|
|
178
|
+
const seenIds = new Set<number>();
|
|
195
179
|
for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
180
|
+
const id = Number(idStr);
|
|
181
|
+
if (entry.caught) caughtIds.add(id);
|
|
182
|
+
if (entry.seen) seenIds.add(id);
|
|
199
183
|
}
|
|
200
184
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (!pool) continue;
|
|
204
|
-
|
|
205
|
-
for (const rarity of ["common", "uncommon", "rare"] as const) {
|
|
206
|
-
const ids = pool[rarity];
|
|
207
|
-
const weight = RARITY_WEIGHTS[rarity];
|
|
185
|
+
const totalCaught = state.pokedex.totalCaught ?? 0;
|
|
186
|
+
const isEarlyGame = totalCaught < 50;
|
|
208
187
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
188
|
+
// 10% chance: pure random (no diversity boosting)
|
|
189
|
+
const seed = Math.floor(Date.now() / 1000);
|
|
190
|
+
const sameAgainRoll = seededRandom(seed + 99);
|
|
191
|
+
const applyDiversity = sameAgainRoll >= 0.1;
|
|
192
|
+
|
|
193
|
+
// Pre-compute diversity data only when needed
|
|
194
|
+
let typeCounts: Map<PokemonType, number> | null = null;
|
|
195
|
+
let genCounts: Map<number, number> | null = null;
|
|
196
|
+
if (applyDiversity) {
|
|
197
|
+
typeCounts = getTypeCounts(state);
|
|
198
|
+
genCounts = getGenCounts(state);
|
|
199
|
+
}
|
|
220
200
|
|
|
221
|
-
|
|
201
|
+
for (const rarity of ["common", "uncommon", "rare"] as const) {
|
|
202
|
+
const ids = ALL_WILD_POOL[rarity];
|
|
203
|
+
const baseWeight = RARITY_WEIGHTS[rarity];
|
|
204
|
+
|
|
205
|
+
for (const id of ids) {
|
|
206
|
+
// Exclude starter
|
|
207
|
+
if (id === starterPokemonId) continue;
|
|
208
|
+
|
|
209
|
+
// Uncommon+: skip if already caught (one-time only)
|
|
210
|
+
if (rarity !== "common" && caughtIds.has(id)) continue;
|
|
211
|
+
|
|
212
|
+
let weight = baseWeight;
|
|
213
|
+
|
|
214
|
+
if (applyDiversity) {
|
|
215
|
+
const pokemon = POKEMON_BY_ID.get(id);
|
|
216
|
+
if (pokemon) {
|
|
217
|
+
// Unseen species boost
|
|
218
|
+
if (!seenIds.has(id)) {
|
|
219
|
+
weight *= isEarlyGame ? 4 : 2;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Type diversity: boost types player hasn't caught much
|
|
223
|
+
const primaryType = pokemon.types[0];
|
|
224
|
+
const typeCount = typeCounts!.get(primaryType) ?? 0;
|
|
225
|
+
if (typeCount === 0) {
|
|
226
|
+
weight *= 3; // Never caught this type
|
|
227
|
+
} else if (typeCount < 3) {
|
|
228
|
+
weight *= 1.5; // Few of this type
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Gen diversity: boost underrepresented generations
|
|
232
|
+
const gen = getGeneration(id);
|
|
233
|
+
const genCount = genCounts!.get(gen) ?? 0;
|
|
234
|
+
if (genCount === 0) {
|
|
235
|
+
weight *= 2; // Never caught from this gen
|
|
236
|
+
} else if (genCount < 3) {
|
|
237
|
+
weight *= 1.3; // Few from this gen
|
|
238
|
+
}
|
|
239
|
+
}
|
|
222
240
|
}
|
|
241
|
+
|
|
242
|
+
candidates.push({ id, weight: Math.round(weight) });
|
|
223
243
|
}
|
|
224
244
|
}
|
|
225
245
|
|
|
226
246
|
return candidates;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
* Uses a simple mulberry32 approach for reproducibility within the same second.
|
|
232
|
-
*/
|
|
249
|
+
// ── PRNG ──────────────────────────────────────────────────────
|
|
250
|
+
|
|
233
251
|
function seededRandom(seed: number): number {
|
|
234
252
|
let t = (seed + 0x6d2b79f5) | 0;
|
|
235
253
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
@@ -237,10 +255,6 @@ function seededRandom(seed: number): number {
|
|
|
237
255
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
238
256
|
}
|
|
239
257
|
|
|
240
|
-
/**
|
|
241
|
-
* Select a Pokemon ID from the weighted candidate pool using a deterministic seed.
|
|
242
|
-
* Returns null if the pool is empty.
|
|
243
|
-
*/
|
|
244
258
|
function weightedSelect(
|
|
245
259
|
candidates: readonly { id: number; weight: number }[],
|
|
246
260
|
seed: number,
|
|
@@ -258,19 +272,15 @@ function weightedSelect(
|
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
274
|
|
|
261
|
-
// Fallback to last candidate (floating-point edge case)
|
|
262
275
|
return candidates[candidates.length - 1]!.id;
|
|
263
276
|
}
|
|
264
277
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* Scales with the player's strongest Pokemon level, with some variance.
|
|
268
|
-
*/
|
|
278
|
+
// ── Level Determination ───────────────────────────────────────
|
|
279
|
+
|
|
269
280
|
function determineEncounterLevel(state: PlayerState, seed: number): number {
|
|
270
281
|
const allPokemon = [...state.party, ...state.pcBox];
|
|
271
282
|
const maxLevel = allPokemon.reduce((max, p) => Math.max(max, p.level), 1);
|
|
272
283
|
|
|
273
|
-
// Wild Pokemon appear at 60-100% of the player's max level, minimum 2
|
|
274
284
|
const minLevel = Math.max(2, Math.floor(maxLevel * 0.6));
|
|
275
285
|
const range = Math.max(1, maxLevel - minLevel + 1);
|
|
276
286
|
const level = minLevel + Math.floor(seededRandom(seed + 7) * range);
|
|
@@ -278,31 +288,22 @@ function determineEncounterLevel(state: PlayerState, seed: number): number {
|
|
|
278
288
|
return Math.min(level, 100);
|
|
279
289
|
}
|
|
280
290
|
|
|
291
|
+
// ── Encounter Generation ──────────────────────────────────────
|
|
292
|
+
|
|
281
293
|
/**
|
|
282
|
-
* Generate a wild encounter
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
294
|
+
* Generate a wild encounter — 100% random with Pokedex-aware weighting.
|
|
295
|
+
* No activity-type or time-of-day bias. Any Pokemon can appear anytime.
|
|
296
|
+
* Unseen types, gens, and species are boosted to fill the Pokedex faster.
|
|
297
|
+
*
|
|
298
|
+
* The eventType parameter is kept for API compatibility but is not used
|
|
299
|
+
* for type selection.
|
|
288
300
|
*/
|
|
289
301
|
export function generateEncounter(
|
|
290
|
-
|
|
302
|
+
_eventType: XpEventType,
|
|
291
303
|
state: PlayerState,
|
|
292
|
-
|
|
304
|
+
_timeOfDayTypes?: readonly PokemonType[],
|
|
293
305
|
): WildEncounter | null {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// 40% chance to use time-of-day biased types if available
|
|
297
|
-
if (timeOfDayTypes && timeOfDayTypes.length > 0) {
|
|
298
|
-
const seed = Math.floor(Date.now() / 1000);
|
|
299
|
-
const biasRoll = seededRandom(seed + 42);
|
|
300
|
-
if (biasRoll < 0.4) {
|
|
301
|
-
types = timeOfDayTypes;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const candidates = buildCandidatePool(types, state);
|
|
306
|
+
const candidates = buildSmartPool(state);
|
|
306
307
|
|
|
307
308
|
if (candidates.length === 0) return null;
|
|
308
309
|
|
|
@@ -319,10 +320,6 @@ export function generateEncounter(
|
|
|
319
320
|
|
|
320
321
|
// ── Catch Evaluation ──────────────────────────────────────────
|
|
321
322
|
|
|
322
|
-
/**
|
|
323
|
-
* Check if the active Pokemon can catch the encountered Pokemon.
|
|
324
|
-
* Based on catch rate, required stats, and level.
|
|
325
|
-
*/
|
|
326
323
|
export function canCatch(
|
|
327
324
|
encounter: WildEncounter,
|
|
328
325
|
activePokemon: OwnedPokemon,
|
|
@@ -334,7 +331,6 @@ export function canCatch(
|
|
|
334
331
|
|
|
335
332
|
const { requiredStat, minStatValue, requiredLevel } = encounter.catchCondition;
|
|
336
333
|
|
|
337
|
-
// Check level requirement
|
|
338
334
|
if (activePokemon.level < requiredLevel) {
|
|
339
335
|
return {
|
|
340
336
|
success: false,
|
|
@@ -342,7 +338,6 @@ export function canCatch(
|
|
|
342
338
|
};
|
|
343
339
|
}
|
|
344
340
|
|
|
345
|
-
// Check stat requirement
|
|
346
341
|
if (requiredStat !== null) {
|
|
347
342
|
const currentStat = activePokemon.codingStats[requiredStat];
|
|
348
343
|
if (currentStat < minStatValue) {
|
|
@@ -354,7 +349,6 @@ export function canCatch(
|
|
|
354
349
|
}
|
|
355
350
|
}
|
|
356
351
|
|
|
357
|
-
// Requirements met — roll against catch rate
|
|
358
352
|
const seed = Math.floor(Date.now() / 1000);
|
|
359
353
|
const roll = seededRandom(seed + encounter.pokemonId) * 255;
|
|
360
354
|
const success = pokemon.catchRate > roll;
|