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:
Maurycy
2026-05-07 14:25:46 +00:00
parent 65af268b86
commit e8523d270e
66 changed files with 4074 additions and 72 deletions

View File

@@ -11,6 +11,7 @@ export default [
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
'{projectRoot}/vite.config.{js,ts,mjs,mts}',
'{projectRoot}/vitest.config.{js,ts,mjs,mts}',
],
},
],

View File

@@ -7,6 +7,9 @@
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0",
"@fog-explorer/api-interfaces": "*"
},
"devDependencies": {
"vitest": "^4.0.8",
"@nx/vite": "^22.7.1"
}

View File

@@ -12,7 +12,14 @@
"outputPath": "dist/libs/encounter-library",
"main": "libs/encounter-library/src/index.ts",
"tsConfig": "libs/encounter-library/tsconfig.lib.json",
"assets": ["libs/encounter-library/*.md"]
"assets": [
"libs/encounter-library/*.md",
{
"input": "libs/encounter-library/src/lib",
"output": "src/lib",
"glob": "*.json"
}
]
}
}
}

View File

@@ -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);
}
}
});
});

View File

@@ -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)];
}

View 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."
]
}
]
}

View File

@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
"types": ["node"],
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": [

View File

@@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"resolveJsonModule": true,
"types": [
"vitest/globals",
"vitest/importMeta",