Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LOADOUTS] Use new loadouts with multiple spawn sections, add loadout generation logics placeholders. #111

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 30 additions & 29 deletions src/engine/configs/gameplay/character_descriptions/ArmyLoadout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
loadoutCharacterItemsWithoutDetector,
loadoutF1Grenades,
loadoutFort,
loadoutGroza,
loadoutPm,
loadoutRgd5Grenades,
loadoutSvd,
Expand Down Expand Up @@ -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 },
],
]}
>
<CharacterProfileCriticals />
Expand All @@ -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 },
],
]}
>
<CharacterProfileCriticals />
Expand All @@ -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 },
],
]}
>
<CharacterProfileCriticals />
Expand All @@ -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 }]]}
>
<CharacterProfileCriticals />
<DefaultCharacterDialogs />
Expand Down Expand Up @@ -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: 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 },
{ section: weapons.wpn_pb, ammoType: 2 },
],
[
{ section: weapons.grenade_f1, count: 2 },
{ section: weapons.grenade_rgd5, count: 3 },
],
]}
>
<CharacterProfileCriticals />
Expand Down
24 changes: 20 additions & 4 deletions src/engine/configs/gameplay/components/SpecificCharacter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ICharacterDescriptionProps {
crouchType?: number;
terrainSection?: Optional<string>;
supplies?: Array<ISpawnItemDescriptor>;
loadout?: Array<ILoadoutItemDescriptor>;
loadouts?: Array<Array<ILoadoutItemDescriptor>>;
noRandom?: boolean;
mechanicMode?: boolean;
mapIcon?: JSXNode;
Expand Down Expand Up @@ -63,7 +63,7 @@ export function SpecificCharacter(props: ICharacterDescriptionProps): JSXNode {
visual,
soundConfig,
supplies = [],
loadout = [],
loadouts = [],
noRandom,
moneyInfinite,
terrainSection = "stalker_terrain",
Expand All @@ -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 (
<specific_character
id={id}
Expand All @@ -103,10 +117,12 @@ export function SpecificCharacter(props: ICharacterDescriptionProps): JSXNode {
{team ? <team>{team}</team> : null}
{terrainSection ? <terrain_sect>{terrainSection}</terrain_sect> : null}
{soundConfig ? <snd_config>{soundConfig}</snd_config> : null}
{loadout.length || supplies.length ? (
{loadouts.length || supplies.length ? (
<supplies>
{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}
</supplies>
) : (
<supplies />
Expand Down
81 changes: 62 additions & 19 deletions src/engine/configs/gameplay/utils/create_loadout.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});

Expand Down Expand Up @@ -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", () => {
Expand All @@ -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."
);
});
});
66 changes: 37 additions & 29 deletions src/engine/configs/gameplay/utils/create_loadout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -22,19 +22,11 @@ export interface ISpawnItemDescriptor {
*/
export function createSpawnList(descriptors: Array<ISpawnItemDescriptor>, lineEnding: string = "\n"): string {
return descriptors.reduce((acc, it) => {
let current: string = `${it.section} = ${it.count ?? 1}`;
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 (typeof it.probability === "number" && it.probability !== 1) {
current += `, prob=${it.probability}`;
Expand All @@ -56,11 +48,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;
}

/**
Expand All @@ -73,17 +65,9 @@ export function createSpawnLoadout(descriptors: Array<ILoadoutItemDescriptor>, 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}`;
Expand All @@ -100,3 +84,27 @@ export function createSpawnLoadout(descriptors: Array<ILoadoutItemDescriptor>, l
return acc + current + ` \\n${lineEnding}`;
}, "");
}

/**
* Serialize parameter data into loadout string.
* Format and stringify data to propagate it into c++ engine correctly.
*
* @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) {
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}' set to ${data}.`);
}

return `, ${name}=${data}`;
}
}

return "";
}
Loading
Loading