Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`. - Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`. - Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions. - Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions. - Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
This commit is contained in:
@@ -1,7 +1,64 @@
|
||||
import { encounterLibrary } from './encounter-library';
|
||||
import {
|
||||
getEncounterById,
|
||||
getEncountersByTier,
|
||||
getLibraryVersion,
|
||||
getRandomEncounterByTier,
|
||||
pickFlavor,
|
||||
} from './encounter-library';
|
||||
|
||||
describe('encounterLibrary', () => {
|
||||
it('should work', () => {
|
||||
expect(encounterLibrary()).toEqual('encounter-library');
|
||||
function seqRng(values: number[]): () => number {
|
||||
let i = 0;
|
||||
return () => values[i++ % values.length];
|
||||
}
|
||||
|
||||
describe('encounter-library', () => {
|
||||
it('getLibraryVersion returns a semver string', () => {
|
||||
expect(getLibraryVersion()).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
it('getEncounterById returns the correct encounter', () => {
|
||||
const enc = getEncounterById('generator_repair');
|
||||
expect(enc?.key).toBe('generator_repair');
|
||||
expect(enc?.baseProbability).toBeGreaterThan(0);
|
||||
expect(enc?.tags).toContain('generator');
|
||||
});
|
||||
|
||||
it('getEncounterById returns undefined for unknown key', () => {
|
||||
expect(getEncounterById('does_not_exist')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getEncountersByTier returns only encounters of that tier', () => {
|
||||
const tier1 = getEncountersByTier(1);
|
||||
expect(tier1.every((e) => e.tier === 1)).toBe(true);
|
||||
expect(tier1.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('getRandomEncounterByTier returns an encounter from the tier', () => {
|
||||
const rng = seqRng([0, 0.5, 0.99]);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const enc = getRandomEncounterByTier(1, rng);
|
||||
expect(enc.tier).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('pickFlavor returns a success flavor on success', () => {
|
||||
const enc = getEncounterById('generator_repair')!;
|
||||
const flavor = pickFlavor(enc, { success: true }, seqRng([0]));
|
||||
expect(enc.flavorSuccess).toContain(flavor);
|
||||
});
|
||||
|
||||
it('pickFlavor returns a failure flavor on failure', () => {
|
||||
const enc = getEncounterById('generator_repair')!;
|
||||
const flavor = pickFlavor(enc, { success: false }, seqRng([0]));
|
||||
expect(enc.flavorFailure).toContain(flavor);
|
||||
});
|
||||
|
||||
it('all encounters have non-empty flavor arrays', () => {
|
||||
for (const tier of [1, 2, 3] as const) {
|
||||
for (const enc of getEncountersByTier(tier)) {
|
||||
expect(enc.flavorSuccess.length).toBeGreaterThan(0);
|
||||
expect(enc.flavorFailure.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,70 @@
|
||||
export function encounterLibrary(): string {
|
||||
return 'encounter-library';
|
||||
import type { EncounterDefinition } from '@fog-explorer/api-interfaces';
|
||||
|
||||
interface RawEncounter {
|
||||
key: string;
|
||||
baseProbability: number;
|
||||
tags: string[];
|
||||
tier: number;
|
||||
flavorSuccess: string[];
|
||||
flavorFailure: string[];
|
||||
}
|
||||
interface EncountersFile {
|
||||
version: string;
|
||||
encounters: RawEncounter[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const encountersData = require('./encounters.json') as EncountersFile;
|
||||
|
||||
export interface LibraryEncounter extends EncounterDefinition {
|
||||
tier: 1 | 2 | 3;
|
||||
flavorSuccess: string[];
|
||||
flavorFailure: string[];
|
||||
}
|
||||
|
||||
export interface FlavorContext {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const LIBRARY_VERSION: string = encountersData.version;
|
||||
|
||||
const ALL_ENCOUNTERS: LibraryEncounter[] = encountersData.encounters.map((e) => ({
|
||||
key: e.key,
|
||||
baseProbability: e.baseProbability,
|
||||
tags: e.tags,
|
||||
tier: e.tier as 1 | 2 | 3,
|
||||
flavorSuccess: e.flavorSuccess,
|
||||
flavorFailure: e.flavorFailure,
|
||||
}));
|
||||
|
||||
export function getLibraryVersion(): string {
|
||||
return LIBRARY_VERSION;
|
||||
}
|
||||
|
||||
export function getEncounterById(key: string): LibraryEncounter | undefined {
|
||||
return ALL_ENCOUNTERS.find((e) => e.key === key);
|
||||
}
|
||||
|
||||
export function getEncountersByTier(tier: 1 | 2 | 3): LibraryEncounter[] {
|
||||
return ALL_ENCOUNTERS.filter((e) => e.tier === tier);
|
||||
}
|
||||
|
||||
export function getRandomEncounterByTier(
|
||||
tier: 1 | 2 | 3,
|
||||
rng: () => number
|
||||
): LibraryEncounter {
|
||||
const pool = getEncountersByTier(tier);
|
||||
if (pool.length === 0) {
|
||||
throw new Error(`No encounters for tier ${tier}`);
|
||||
}
|
||||
return pool[Math.floor(rng() * pool.length)];
|
||||
}
|
||||
|
||||
export function pickFlavor(
|
||||
encounter: LibraryEncounter,
|
||||
ctx: FlavorContext,
|
||||
rng: () => number
|
||||
): string {
|
||||
const pool = ctx.success ? encounter.flavorSuccess : encounter.flavorFailure;
|
||||
return pool[Math.floor(rng() * pool.length)];
|
||||
}
|
||||
|
||||
165
libs/encounter-library/src/lib/encounters.json
Normal file
165
libs/encounter-library/src/lib/encounters.json
Normal file
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"encounters": [
|
||||
{
|
||||
"key": "generator_repair",
|
||||
"baseProbability": 0.55,
|
||||
"tags": ["generator", "objectives"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"The generator sputters to life. Light floods the area.",
|
||||
"Sparks fly, then hum — the generator catches.",
|
||||
"Wires connected. The machine breathes again."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The generator kicks back. Too many watchers in the dark.",
|
||||
"The mechanism jams. Footsteps echo nearby.",
|
||||
"A noise betrays the position. The generator goes cold."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "totem_cleanse",
|
||||
"baseProbability": 0.50,
|
||||
"tags": ["totem", "altruistic", "objectives"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"The totem crumbles. Its curse lifts from the fog.",
|
||||
"Bones scatter. The hex dissolves into smoke.",
|
||||
"The ritual unravels. Something distant screams."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The totem resists. Its pull is stronger than expected.",
|
||||
"A presence drives the survivor back before the cleanse completes.",
|
||||
"The hex holds. Dread thickens the air."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "chest_search",
|
||||
"baseProbability": 0.45,
|
||||
"tags": ["chest", "item"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"The chest yields a worn medkit. Small mercies.",
|
||||
"A flashlight, still charged. The fog recedes slightly.",
|
||||
"A useful tool among the debris."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The chest is empty. Only rust and regret.",
|
||||
"The lid splinters — nothing useful inside.",
|
||||
"A noise nearby cuts the search short."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "hook_escape",
|
||||
"baseProbability": 0.40,
|
||||
"tags": ["hook", "survival"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"With grim determination, the survivor slips free.",
|
||||
"Arms aching, the hook releases. Freedom, for now.",
|
||||
"A desperate push — the chains give way."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The struggle exhausts. The hook holds.",
|
||||
"Every movement drives the barb deeper. Stay still.",
|
||||
"The fog presses in. The hook remains."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "exit_gate",
|
||||
"baseProbability": 0.50,
|
||||
"tags": ["exit", "objectives"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"The gate grinds open. Cold air rushes in.",
|
||||
"Generators humming, the lock gives. Almost there.",
|
||||
"The exit yields. Light from outside cuts the fog."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The gate mechanism is jammed. Precious seconds lost.",
|
||||
"A shadow falls across the panel. The attempt abandoned.",
|
||||
"The switch is stuck. The gate stays shut."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "patrol_avoid",
|
||||
"baseProbability": 0.60,
|
||||
"tags": ["stealth", "survival"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"Still as stone. The threat passes without noticing.",
|
||||
"A breath held long — then silence. Safe.",
|
||||
"The fog swallows the survivor whole. Unseen."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"A twig snaps. Eye contact — then the chase begins.",
|
||||
"The survivor misjudges the angle. Spotted.",
|
||||
"Heartbeat too loud. Presence too close."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "medkit_use",
|
||||
"baseProbability": 0.65,
|
||||
"tags": ["healing", "altruistic", "survival"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"Bandages tight, the wound closes. Pain recedes.",
|
||||
"The medkit does its job. The survivor steadies.",
|
||||
"A few tense minutes — injuries tended."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"Supplies exhausted before the job is done.",
|
||||
"Shaking hands fumble the medkit. Time runs out.",
|
||||
"The wound is worse than it looked. Supplies fall short."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pallet_drop",
|
||||
"baseProbability": 0.55,
|
||||
"tags": ["survival", "escape"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"The pallet crashes down. A moment bought.",
|
||||
"Timber splinters between them. Distance gained.",
|
||||
"The drop lands true. The chase falters."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The pallet drops wide. No gap created.",
|
||||
"Too slow — the obstacle proves useless.",
|
||||
"The throw miscalculated. The pursuit continues."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "basement_search",
|
||||
"baseProbability": 0.35,
|
||||
"tags": ["chest", "item", "high-risk"],
|
||||
"tier": 3,
|
||||
"flavorSuccess": [
|
||||
"The basement yields rare supplies. Worth the risk.",
|
||||
"A pristine toolbox — almost worth dying for.",
|
||||
"The gamble paid off. The survivor emerges with something valuable."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The basement was a trap. Retreat costs dearly.",
|
||||
"The stairwell offers no escape. A mistake made clear.",
|
||||
"The risk was not worth the reward found — nothing."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "hatch_find",
|
||||
"baseProbability": 0.30,
|
||||
"tags": ["exit", "survival", "high-risk"],
|
||||
"tier": 3,
|
||||
"flavorSuccess": [
|
||||
"The hatch sighs open. One last mercy from the fog.",
|
||||
"A sound — familiar, haunting. The hatch, just ahead.",
|
||||
"Against all odds, the escape route reveals itself."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The hatch is nowhere. Only fog and silence.",
|
||||
"Close — so close. Then it closes.",
|
||||
"The sound was something else entirely."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user