From 1faba0c76525bc845fdf55f298d753780dc88d70 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Fri, 3 Jan 2025 22:18:37 +0200 Subject: [PATCH 1/5] Use new loadouts with multiple spawn sections. Signed-off-by: Neloreck --- .../character_descriptions/ArmyLoadout.tsx | 59 ++++++++++--------- .../gameplay/components/SpecificCharacter.tsx | 24 ++++++-- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx index 7dd17be7a..aaf0e0181 100644 --- a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx +++ b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx @@ -9,7 +9,6 @@ import { loadoutCharacterItemsWithoutDetector, loadoutF1Grenades, loadoutFort, - loadoutGroza, loadoutPm, loadoutRgd5Grenades, loadoutSvd, @@ -41,10 +40,11 @@ export function ArmyLoadout(): JSXNode { ...loadoutCharacterDrugsBase(), ...loadoutCharacterFood(), ]} - loadout={[ - { section: weapons.wpn_ak74u, ammoType: 1 }, - { section: weapons.wpn_ak74, ammoType: 1 }, - { section: weapons.wpn_ak74, ammoType: 2 }, + loadouts={[ + [ + { section: weapons.wpn_ak74u, ammoType: 1 }, + { section: weapons.wpn_ak74, ammoType: 1 }, + ], ]} > @@ -66,11 +66,11 @@ export function ArmyLoadout(): JSXNode { ...loadoutCharacterDrugsBase(), ...loadoutCharacterFood(), ]} - loadout={[ - { section: weapons.wpn_ak74u, ammoType: 2 }, - { section: weapons.wpn_ak74, ammoType: 1 }, - { section: weapons.wpn_ak74, ammoType: 2 }, - { section: weapons.wpn_ak74, scope: true, ammoType: 2 }, + loadouts={[ + [ + { section: weapons.wpn_ak74u, ammoType: 2 }, + { section: weapons.wpn_ak74, ammoType: 2 }, + ], ]} > @@ -92,10 +92,11 @@ export function ArmyLoadout(): JSXNode { ...loadoutCharacterDrugsBase(), ...loadoutCharacterFood(), ]} - loadout={[ - { section: weapons.wpn_abakan, ammoType: 1 }, - { section: weapons.wpn_abakan, ammoType: 2 }, - { section: weapons.wpn_ak74, ammoType: 1 }, + loadouts={[ + [ + { section: weapons.wpn_abakan, ammoType: 1 }, + { section: weapons.wpn_ak74, ammoType: 1 }, + ], ]} > @@ -117,11 +118,7 @@ export function ArmyLoadout(): JSXNode { ...loadoutCharacterDrugsBase(), ...loadoutCharacterFood(), ]} - loadout={[ - { section: weapons.wpn_abakan, ammoType: 1 }, - { section: weapons.wpn_abakan, ammoType: 2 }, - { section: weapons.wpn_abakan, scope: true, ammoType: 2 }, - ]} + loadouts={[[{ section: weapons.wpn_abakan, ammoType: 2 }]]} > @@ -157,20 +154,24 @@ export function ArmyLoadout(): JSXNode { soundConfig={"characters_voice\\human_03\\military\\"} rank={60} supplies={[ - ...loadoutGroza({ ap: true }), - ...loadoutFort(), - ...loadoutF1Grenades(4), ...loadoutCharacterItemsWithoutDetector(), ...loadoutCharacterDrugsBase(), ...loadoutCharacterFood(), ]} - loadout={[ - { section: weapons.wpn_groza, ammoType: 2 }, - { section: weapons.wpn_groza, scope: true, ammoType: 2 }, - { section: weapons.wpn_abakan, ammoType: 2 }, - { section: weapons.wpn_abakan, scope: true, ammoType: 2 }, - { section: weapons.wpn_val, ammoType: 2 }, - { section: weapons.wpn_val, scope: true, ammoType: 2 }, + loadouts={[ + [ + { section: weapons.wpn_groza, scope: true, ammoType: 2 }, + { section: weapons.wpn_abakan, scope: true, ammoType: 2 }, + { section: weapons.wpn_val, scope: true, ammoType: 2 }, + ], + [ + { section: weapons.wpn_fort, ammoType: 2 }, + { section: weapons.wpn_pb, ammoType: 2 }, + ], + [ + { section: weapons.grenade_f1, count: 1 }, + { section: weapons.grenade_rgd5, count: 1 }, + ], ]} > diff --git a/src/engine/configs/gameplay/components/SpecificCharacter.tsx b/src/engine/configs/gameplay/components/SpecificCharacter.tsx index 7845290eb..ed43c0107 100644 --- a/src/engine/configs/gameplay/components/SpecificCharacter.tsx +++ b/src/engine/configs/gameplay/components/SpecificCharacter.tsx @@ -32,7 +32,7 @@ export interface ICharacterDescriptionProps { crouchType?: number; terrainSection?: Optional; supplies?: Array; - loadout?: Array; + loadouts?: Array>; noRandom?: boolean; mechanicMode?: boolean; mapIcon?: JSXNode; @@ -63,7 +63,7 @@ export function SpecificCharacter(props: ICharacterDescriptionProps): JSXNode { visual, soundConfig, supplies = [], - loadout = [], + loadouts = [], noRandom, moneyInfinite, terrainSection = "stalker_terrain", @@ -78,6 +78,20 @@ export function SpecificCharacter(props: ICharacterDescriptionProps): JSXNode { throw new Error(`Expected visual to be present for character profile with icon '${icon}'.`); } + if (loadouts) { + for (const loadout of loadouts) { + loadout.reduce((acc, it) => { + if (acc.has(it.section)) { + throw new Error(`Duplicate loadout section for specific character description:" [${it.section}] in '${id}'.`); + } else { + acc.add(it.section); + } + + return acc; + }, new Set()); + } + } + return ( {team} : null} {terrainSection ? {terrainSection} : null} {soundConfig ? {soundConfig} : null} - {loadout.length || supplies.length ? ( + {loadouts.length || supplies.length ? ( {supplies.length ? `\n[spawn]\\n\n${createSpawnList(supplies, "\n")}` : null} - {loadout.length ? `\n[spawn_loadout]\\n\n${createSpawnLoadout(loadout)}` : null} + {loadouts.length + ? loadouts.map((loadout, index) => `\n[spawn_loadout${index || ""}]\\n\n${createSpawnLoadout(loadout)}`) + : null} ) : ( From e6d73e6114bb4b2e875a6146821104c6152895fb Mon Sep 17 00:00:00 2001 From: Neloreck Date: Fri, 3 Jan 2025 22:56:01 +0200 Subject: [PATCH 2/5] Add loadout generation callback handling / loadout manager placeholder. Signed-off-by: Neloreck --- .../managers/loadout/LoadoutManager.test.ts | 20 +++++++++++++++++++ .../core/managers/loadout/LoadoutManager.ts | 17 ++++++++++++++++ src/engine/core/managers/loadout/index.ts | 1 + .../declarations/callbacks/game.test.ts | 10 ++++++++++ .../scripts/declarations/callbacks/game.ts | 6 +++++- .../register/managers_registrator.test.ts | 10 ++++++---- .../scripts/register/managers_registrator.ts | 8 +++++--- 7 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 src/engine/core/managers/loadout/LoadoutManager.test.ts create mode 100644 src/engine/core/managers/loadout/LoadoutManager.ts create mode 100644 src/engine/core/managers/loadout/index.ts diff --git a/src/engine/core/managers/loadout/LoadoutManager.test.ts b/src/engine/core/managers/loadout/LoadoutManager.test.ts new file mode 100644 index 000000000..c2b307985 --- /dev/null +++ b/src/engine/core/managers/loadout/LoadoutManager.test.ts @@ -0,0 +1,20 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; + +import { getManager } from "@/engine/core/database"; +import { LoadoutManager } from "@/engine/core/managers/loadout/LoadoutManager"; +import { ServerObject } from "@/engine/lib/types"; +import { resetRegistry } from "@/fixtures/engine"; +import { MockAlifeObject } from "@/fixtures/xray"; + +describe("LoadoutManager", () => { + beforeEach(() => { + resetRegistry(); + }); + + it("should correctly handle loadout generation event", () => { + const manager: LoadoutManager = getManager(LoadoutManager); + const object: ServerObject = MockAlifeObject.mock(); + + expect(manager.onGenerateServerObjectLoadout(object, object.id, "[test]")).toBe(false); + }); +}); diff --git a/src/engine/core/managers/loadout/LoadoutManager.ts b/src/engine/core/managers/loadout/LoadoutManager.ts new file mode 100644 index 000000000..238fe9c6c --- /dev/null +++ b/src/engine/core/managers/loadout/LoadoutManager.ts @@ -0,0 +1,17 @@ +import { AbstractManager } from "@/engine/core/managers/abstract"; +import { ServerObject, TNumberId } from "@/engine/lib/types"; + +/** + * Manager handling stalkers loadouts, + */ +export class LoadoutManager extends AbstractManager { + /** + * @param object - game object loadout is generated for + * @param id - id of game object for loadout generation + * @param iniData - object spawn ini data as string + * @returns whether loadout was generated or engine should follow default loadout generation logics + */ + public onGenerateServerObjectLoadout(object: ServerObject, id: TNumberId, iniData: string): boolean { + return false; + } +} diff --git a/src/engine/core/managers/loadout/index.ts b/src/engine/core/managers/loadout/index.ts new file mode 100644 index 000000000..2d032c610 --- /dev/null +++ b/src/engine/core/managers/loadout/index.ts @@ -0,0 +1 @@ +export * from "@/engine/core/managers/loadout/LoadoutManager"; diff --git a/src/engine/scripts/declarations/callbacks/game.test.ts b/src/engine/scripts/declarations/callbacks/game.test.ts index b51795e05..39edbe32f 100644 --- a/src/engine/scripts/declarations/callbacks/game.test.ts +++ b/src/engine/scripts/declarations/callbacks/game.test.ts @@ -5,6 +5,7 @@ import { smartCoversList } from "@/engine/core/animation/smart_covers"; import { getManager } from "@/engine/core/database"; import { ActorInputManager } from "@/engine/core/managers/actor"; import { EGameEvent, EventsManager } from "@/engine/core/managers/events"; +import { LoadoutManager } from "@/engine/core/managers/loadout"; import { gameOutroConfig, GameOutroManager } from "@/engine/core/managers/outro"; import { SaveManager } from "@/engine/core/managers/save"; import { TradeManager } from "@/engine/core/managers/trade"; @@ -172,6 +173,10 @@ describe("game external callbacks", () => { }); it("ai_stalker callbacks should be defined", () => { + const manager: LoadoutManager = getManager(LoadoutManager); + + jest.spyOn(manager, "onGenerateServerObjectLoadout").mockImplementation(jest.fn(() => false)); + const object: GameObject = MockGameObject.mock(); const weapon: GameObject = MockGameObject.mock(); @@ -179,5 +184,10 @@ describe("game external callbacks", () => { expect(selectBestStalkerWeapon).toHaveBeenCalledTimes(1); expect(selectBestStalkerWeapon).toHaveBeenCalledWith(object, weapon); + + callNestedBinding("ai_stalker", "CSE_ALifeObject_spawn_supplies", [object, object.id(), "test"]); + + expect(manager.onGenerateServerObjectLoadout).toHaveBeenCalledTimes(1); + expect(manager.onGenerateServerObjectLoadout).toHaveBeenCalledWith(object, object.id(), "test"); }); }); diff --git a/src/engine/scripts/declarations/callbacks/game.ts b/src/engine/scripts/declarations/callbacks/game.ts index 924ea972b..f0348b05a 100644 --- a/src/engine/scripts/declarations/callbacks/game.ts +++ b/src/engine/scripts/declarations/callbacks/game.ts @@ -3,13 +3,14 @@ import { smartCoversList } from "@/engine/core/animation/smart_covers"; import { getManager } from "@/engine/core/database"; import { ActorInputManager } from "@/engine/core/managers/actor"; import { EGameEvent, EventsManager } from "@/engine/core/managers/events"; +import { LoadoutManager } from "@/engine/core/managers/loadout"; import { GameOutroManager } from "@/engine/core/managers/outro"; import { gameOutroConfig } from "@/engine/core/managers/outro/GameOutroConfig"; import { SaveManager } from "@/engine/core/managers/save"; import { TradeManager } from "@/engine/core/managers/trade"; import { extern } from "@/engine/core/utils/binding"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { NetPacket, TName, TNumberId } from "@/engine/lib/types"; +import { NetPacket, ServerObject, TName, TNumberId } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); @@ -113,4 +114,7 @@ extern("visual_memory_manager", { */ extern("ai_stalker", { update_best_weapon: selectBestStalkerWeapon, + CSE_ALifeObject_spawn_supplies: (object: ServerObject, id: TNumberId, iniData: string) => { + return getManager(LoadoutManager).onGenerateServerObjectLoadout(object, id, iniData); + }, }); diff --git a/src/engine/scripts/register/managers_registrator.test.ts b/src/engine/scripts/register/managers_registrator.test.ts index 1df5037ac..e290d519c 100644 --- a/src/engine/scripts/register/managers_registrator.test.ts +++ b/src/engine/scripts/register/managers_registrator.test.ts @@ -9,6 +9,7 @@ import { ProfilingManager } from "@/engine/core/managers/debug/profiling"; import { DialogManager } from "@/engine/core/managers/dialogs"; import { EventsManager } from "@/engine/core/managers/events"; import { LoadScreenManager } from "@/engine/core/managers/interface"; +import { LoadoutManager } from "@/engine/core/managers/loadout"; import { MapDisplayManager } from "@/engine/core/managers/map"; import { MusicManager } from "@/engine/core/managers/music"; import { NotificationManager } from "@/engine/core/managers/notifications"; @@ -38,7 +39,7 @@ describe("managers_registrator entry point", () => { it("registerSchemeModules should correctly re-register required managers", () => { registerManagers(); - expect((registry.managers as AnyObject).size).toBe(26); + expect((registry.managers as AnyObject).size).toBe(27); [ ActorInputManager, @@ -46,13 +47,12 @@ describe("managers_registrator entry point", () => { DatabaseManager, DebugManager, DialogManager, - MusicManager, EventsManager, GameSettingsManager, - SoundManager, - UpgradesManager, LoadScreenManager, + LoadoutManager, MapDisplayManager, + MusicManager, NotificationManager, PdaManager, PhantomManager, @@ -61,11 +61,13 @@ describe("managers_registrator entry point", () => { SaveManager, SimulationManager, SleepManager, + SoundManager, StatisticsManager, TaskManager, TradeManager, TravelManager, TreasureManager, + UpgradesManager, WeatherManager, ].forEach((it) => expect(registry.managers.has(it)).toBe(true)); }); diff --git a/src/engine/scripts/register/managers_registrator.ts b/src/engine/scripts/register/managers_registrator.ts index e64840bae..0d0f70457 100644 --- a/src/engine/scripts/register/managers_registrator.ts +++ b/src/engine/scripts/register/managers_registrator.ts @@ -8,6 +8,7 @@ import { ProfilingManager } from "@/engine/core/managers/debug/profiling"; import { DialogManager } from "@/engine/core/managers/dialogs"; import { EventsManager } from "@/engine/core/managers/events"; import { LoadScreenManager } from "@/engine/core/managers/interface"; +import { LoadoutManager } from "@/engine/core/managers/loadout"; import { MapDisplayManager } from "@/engine/core/managers/map"; import { MusicManager } from "@/engine/core/managers/music"; import { NotificationManager } from "@/engine/core/managers/notifications"; @@ -39,13 +40,12 @@ export function registerManagers(): void { DatabaseManager, DebugManager, DialogManager, - MusicManager, EventsManager, GameSettingsManager, - SoundManager, - UpgradesManager, LoadScreenManager, + LoadoutManager, MapDisplayManager, + MusicManager, NotificationManager, PdaManager, PhantomManager, @@ -54,11 +54,13 @@ export function registerManagers(): void { SaveManager, SimulationManager, SleepManager, + SoundManager, StatisticsManager, TaskManager, TradeManager, TravelManager, TreasureManager, + UpgradesManager, WeatherManager, ]; From 0e866c14c9b0941be2ac012028caf45311184b09 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Sat, 4 Jan 2025 01:54:01 +0200 Subject: [PATCH 3/5] Dynamic scopes chances for army4 stalkers. `createSpawnLoadoutFlag` util added. Signed-off-by: Neloreck --- .../character_descriptions/ArmyLoadout.tsx | 10 ++--- .../configs/gameplay/utils/create_loadout.ts | 42 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx index aaf0e0181..20074f588 100644 --- a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx +++ b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx @@ -160,17 +160,17 @@ export function ArmyLoadout(): JSXNode { ]} loadouts={[ [ - { section: weapons.wpn_groza, scope: true, ammoType: 2 }, - { section: weapons.wpn_abakan, scope: true, ammoType: 2 }, - { section: weapons.wpn_val, scope: true, ammoType: 2 }, + { section: weapons.wpn_groza, scope: 0.2, silencer: 0.2, ammoType: 2 }, + { section: weapons.wpn_abakan, scope: 0.2, ammoType: 2 }, + { section: weapons.wpn_val, scope: 0.2, ammoType: 2 }, ], [ { section: weapons.wpn_fort, ammoType: 2 }, { section: weapons.wpn_pb, ammoType: 2 }, ], [ - { section: weapons.grenade_f1, count: 1 }, - { section: weapons.grenade_rgd5, count: 1 }, + { section: weapons.grenade_f1, count: 2 }, + { section: weapons.grenade_rgd5, count: 3 }, ], ]} > diff --git a/src/engine/configs/gameplay/utils/create_loadout.ts b/src/engine/configs/gameplay/utils/create_loadout.ts index 47bcce3fc..54b8eb525 100644 --- a/src/engine/configs/gameplay/utils/create_loadout.ts +++ b/src/engine/configs/gameplay/utils/create_loadout.ts @@ -56,11 +56,11 @@ export interface ILoadoutItemDescriptor { ammoType?: number; cond?: TRate; count?: TCount; - launcher?: boolean; + launcher?: TRate | boolean; level?: TName; - scope?: boolean; + scope?: TRate | boolean; section: TSection; - silencer?: boolean; + silencer?: TRate | boolean; } /** @@ -73,17 +73,9 @@ export function createSpawnLoadout(descriptors: Array, l return descriptors.reduce((acc, it) => { let current: string = `${it.section}=${it.count ?? 1}`; - if (it.scope) { - current += ", scope"; - } - - if (it.silencer) { - current += ", silencer"; - } - - if (it.launcher) { - current += ", launcher"; - } + current += createSpawnLoadoutFlag("scope", it.scope); + current += createSpawnLoadoutFlag("silencer", it.silencer); + current += createSpawnLoadoutFlag("launcher", it.launcher); if (it.ammoType) { current += `, ammo_type=${it.ammoType}`; @@ -100,3 +92,25 @@ export function createSpawnLoadout(descriptors: Array, l return acc + current + ` \\n${lineEnding}`; }, ""); } + +/** + * todo; + * + * @param name + * @param data + */ +export function createSpawnLoadoutFlag(name: TName, data?: TRate | boolean): string { + if (data) { + if (typeof data === "number") { + if (data < 0 || data > 1) { + throw new Error(`Invalid range for scope probability value: ${name} as ${data}`); + } + + return `, ${name}=${data}`; + } else { + return `, ${name}`; + } + } + + return ""; +} From d63f255ad65f06b15be689b42090b9a5930773ea Mon Sep 17 00:00:00 2001 From: Neloreck Date: Sat, 4 Jan 2025 01:55:15 +0200 Subject: [PATCH 4/5] Doc added. Signed-off-by: Neloreck --- src/engine/configs/gameplay/utils/create_loadout.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/engine/configs/gameplay/utils/create_loadout.ts b/src/engine/configs/gameplay/utils/create_loadout.ts index 54b8eb525..f5e414c40 100644 --- a/src/engine/configs/gameplay/utils/create_loadout.ts +++ b/src/engine/configs/gameplay/utils/create_loadout.ts @@ -94,10 +94,12 @@ export function createSpawnLoadout(descriptors: Array, l } /** - * todo; + * Serialize parameter data into loadout string. + * Format and stringify data to propagate it into c++ engine correctly. * - * @param name - * @param data + * @param name - name of the parameter to serialize + * @param data - value to inject into serialized string + * @returns raw string part for item field loadout configuration */ export function createSpawnLoadoutFlag(name: TName, data?: TRate | boolean): string { if (data) { From 1f37e4cb2994a47ed040c1e663c2138c0ca5d230 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Sat, 4 Jan 2025 02:16:46 +0200 Subject: [PATCH 5/5] Loadout generation tests. Signed-off-by: Neloreck --- .../character_descriptions/ArmyLoadout.tsx | 6 +- .../gameplay/utils/create_loadout.test.ts | 81 ++++++++++++++----- .../configs/gameplay/utils/create_loadout.ts | 30 +++---- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx index 20074f588..24be6c5bd 100644 --- a/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx +++ b/src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx @@ -160,9 +160,9 @@ export function ArmyLoadout(): JSXNode { ]} loadouts={[ [ - { section: weapons.wpn_groza, scope: 0.2, silencer: 0.2, ammoType: 2 }, - { section: weapons.wpn_abakan, scope: 0.2, ammoType: 2 }, - { section: weapons.wpn_val, scope: 0.2, ammoType: 2 }, + { section: weapons.wpn_groza, scope: 0.25, silencer: 0.2, ammoType: 2 }, + { section: weapons.wpn_abakan, scope: 0.25, ammoType: 2 }, + { section: weapons.wpn_val, scope: 0.25, ammoType: 2 }, ], [ { section: weapons.wpn_fort, ammoType: 2 }, diff --git a/src/engine/configs/gameplay/utils/create_loadout.test.ts b/src/engine/configs/gameplay/utils/create_loadout.test.ts index 95ad1b292..2a7ee58eb 100644 --- a/src/engine/configs/gameplay/utils/create_loadout.test.ts +++ b/src/engine/configs/gameplay/utils/create_loadout.test.ts @@ -1,49 +1,57 @@ import { describe, expect, it } from "@jest/globals"; -import { createSpawnList, createSpawnLoadout } from "@/engine/configs/gameplay/utils/create_loadout"; +import { + createSpawnList, + createSpawnLoadout, + createSpawnLoadoutFlag, +} from "@/engine/configs/gameplay/utils/create_loadout"; describe("createSpawnList util", () => { it("should correctly generate resulting strings", () => { - expect(createSpawnList([{ section: "test_sect" }])).toBe("test_sect = 1 \\n\n"); - expect(createSpawnList([{ section: "test_sect", probability: 1 }])).toBe("test_sect = 1 \\n\n"); - expect(createSpawnList([{ section: "test_sect", count: 4 }])).toBe("test_sect = 4 \\n\n"); - expect(createSpawnList([{ section: "test_sect", probability: 0.25 }])).toBe("test_sect = 1, prob=0.25 \\n\n"); - expect(createSpawnList([{ section: "test_sect", cond: 0.25 }])).toBe("test_sect = 1, cond=0.25 \\n\n"); - expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5 }])).toBe( - "test_sect = 4, prob=0.5 \\n\n" - ); + expect(createSpawnList([{ section: "test_sect" }])).toBe("test_sect=1 \\n\n"); + expect(createSpawnList([{ section: "test_sect", probability: 1 }])).toBe("test_sect=1 \\n\n"); + expect(createSpawnList([{ section: "test_sect", count: 4 }])).toBe("test_sect=4 \\n\n"); + expect(createSpawnList([{ section: "test_sect", probability: 0.25 }])).toBe("test_sect=1, prob=0.25 \\n\n"); + expect(createSpawnList([{ section: "test_sect", cond: 0.25 }])).toBe("test_sect=1, cond=0.25 \\n\n"); + expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5 }])).toBe("test_sect=4, prob=0.5 \\n\n"); expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5, cond: 0.45 }])).toBe( - "test_sect = 4, prob=0.5, cond=0.45 \\n\n" + "test_sect=4, prob=0.5, cond=0.45 \\n\n" ); expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5, scope: true }])).toBe( - "test_sect = 4, scope, prob=0.5 \\n\n" + "test_sect=4, scope, prob=0.5 \\n\n" ); expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5, launcher: true }])).toBe( - "test_sect = 4, launcher, prob=0.5 \\n\n" + "test_sect=4, launcher, prob=0.5 \\n\n" ); expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5, silencer: true }])).toBe( - "test_sect = 4, silencer, prob=0.5 \\n\n" + "test_sect=4, silencer, prob=0.5 \\n\n" ); expect(createSpawnList([{ section: "test_sect", count: 4, probability: 0.5, scope: true, silencer: true }])).toBe( - "test_sect = 4, scope, silencer, prob=0.5 \\n\n" + "test_sect=4, scope, silencer, prob=0.5 \\n\n" ); expect( createSpawnList([ { section: "test_sect", count: 4, probability: 0.5, scope: true, silencer: true, launcher: true }, ]) - ).toBe("test_sect = 4, scope, silencer, launcher, prob=0.5 \\n\n"); + ).toBe("test_sect=4, scope, silencer, launcher, prob=0.5 \\n\n"); expect( createSpawnList([ { section: "test_sect", count: 4, probability: 0.5, scope: true }, { section: "another_sect", count: 1, probability: 0.25, silencer: true }, ]) - ).toBe("test_sect = 4, scope, prob=0.5 \\n\nanother_sect = 1, silencer, prob=0.25 \\n\n"); + ).toBe("test_sect=4, scope, prob=0.5 \\n\nanother_sect=1, silencer, prob=0.25 \\n\n"); + expect( + createSpawnList([ + { section: "test_sect", count: 4, scope: 0.2, launcher: 1, silencer: true }, + { section: "another_sect", count: 1, silencer: true }, + ]) + ).toBe("test_sect=4, scope=0.2, silencer, launcher \\n\n" + "another_sect=1, silencer \\n\n"); }); it("should respect line endings", () => { - expect(createSpawnList([{ section: "test_sect" }], "\r\n")).toBe("test_sect = 1 \\n\r\n"); - expect(createSpawnList([{ section: "test_sect" }], "\n")).toBe("test_sect = 1 \\n\n"); - expect(createSpawnList([{ section: "test_sect" }])).toBe("test_sect = 1 \\n\n"); + expect(createSpawnList([{ section: "test_sect" }], "\r\n")).toBe("test_sect=1 \\n\r\n"); + expect(createSpawnList([{ section: "test_sect" }], "\n")).toBe("test_sect=1 \\n\n"); + expect(createSpawnList([{ section: "test_sect" }])).toBe("test_sect=1 \\n\n"); }); }); @@ -76,6 +84,12 @@ describe("createSpawnLoadout util", () => { { section: "another_sect", count: 1, silencer: true, ammoType: 2 }, ]) ).toBe("test_sect=4, scope \\n\nanother_sect=1, silencer, ammo_type=2 \\n\n"); + expect( + createSpawnLoadout([ + { section: "test_sect", count: 4, scope: 0.2, launcher: 1, silencer: true }, + { section: "another_sect", count: 1, silencer: true, ammoType: 2 }, + ]) + ).toBe("test_sect=4, scope=0.2, silencer, launcher \\n\n" + "another_sect=1, silencer, ammo_type=2 \\n\n"); }); it("should respect line endings", () => { @@ -84,3 +98,32 @@ describe("createSpawnLoadout util", () => { expect(createSpawnLoadout([{ section: "test_sect" }])).toBe("test_sect=1 \\n\n"); }); }); + +describe("createSpawnLoadoutFlag util", () => { + it("should correctly serialize values", () => { + expect(createSpawnLoadoutFlag("test", false)).toBe(""); + expect(createSpawnLoadoutFlag("test", undefined)).toBe(""); + expect(createSpawnLoadoutFlag("test", 0)).toBe(""); + expect(createSpawnLoadoutFlag("test", true)).toBe(", test"); + expect(createSpawnLoadoutFlag("test", 1)).toBe(", test"); + expect(createSpawnLoadoutFlag("test", 0.5)).toBe(", test=0.5"); + expect(createSpawnLoadoutFlag("test", 0.25)).toBe(", test=0.25"); + expect(createSpawnLoadoutFlag("test", 0.75)).toBe(", test=0.75"); + expect(createSpawnLoadoutFlag("test", 0.99)).toBe(", test=0.99"); + }); + + it("should correctly throw exception on validation error", () => { + expect(() => createSpawnLoadoutFlag("test", -100)).toThrow( + "Invalid range for scope probability value: 'test' set to -100." + ); + expect(() => createSpawnLoadoutFlag("test", -1)).toThrow( + "Invalid range for scope probability value: 'test' set to -1." + ); + expect(() => createSpawnLoadoutFlag("test", 1.1)).toThrow( + "Invalid range for scope probability value: 'test' set to 1.1." + ); + expect(() => createSpawnLoadoutFlag("test", 100)).toThrow( + "Invalid range for scope probability value: 'test' set to 100." + ); + }); +}); diff --git a/src/engine/configs/gameplay/utils/create_loadout.ts b/src/engine/configs/gameplay/utils/create_loadout.ts index f5e414c40..de5832018 100644 --- a/src/engine/configs/gameplay/utils/create_loadout.ts +++ b/src/engine/configs/gameplay/utils/create_loadout.ts @@ -7,11 +7,11 @@ import { TCount, TName, TRate, TSection } from "@/engine/lib/types"; export interface ISpawnItemDescriptor { cond?: TRate; count?: TCount; - launcher?: boolean; + launcher?: TRate | boolean; probability?: TRate; - scope?: boolean; + scope?: TRate | boolean; section: TSection; - silencer?: boolean; + silencer?: TRate | boolean; } /** @@ -22,19 +22,11 @@ export interface ISpawnItemDescriptor { */ export function createSpawnList(descriptors: Array, lineEnding: string = "\n"): string { return descriptors.reduce((acc, it) => { - let current: string = `${it.section} = ${it.count ?? 1}`; - - if (it.scope) { - current += ", scope"; - } - - if (it.silencer) { - current += ", silencer"; - } + let current: string = `${it.section}=${it.count ?? 1}`; - if (it.launcher) { - current += ", launcher"; - } + current += createSpawnLoadoutFlag("scope", it.scope); + current += createSpawnLoadoutFlag("silencer", it.silencer); + current += createSpawnLoadoutFlag("launcher", it.launcher); if (typeof it.probability === "number" && it.probability !== 1) { current += `, prob=${it.probability}`; @@ -103,14 +95,14 @@ export function createSpawnLoadout(descriptors: Array, l */ export function createSpawnLoadoutFlag(name: TName, data?: TRate | boolean): string { if (data) { - if (typeof data === "number") { + if (data === true || data === 1) { + return `, ${name}`; + } else if (typeof data === "number") { if (data < 0 || data > 1) { - throw new Error(`Invalid range for scope probability value: ${name} as ${data}`); + throw new Error(`Invalid range for scope probability value: '${name}' set to ${data}.`); } return `, ${name}=${data}`; - } else { - return `, ${name}`; } }