Skip to content

Commit

Permalink
Updated crow spawner scheme. Added docs and tests for sr_crow_spawner…
Browse files Browse the repository at this point in the history
…. Mock updates.
  • Loading branch information
Neloreck committed Sep 17, 2023
1 parent 5a95879 commit db22519
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 65 deletions.
9 changes: 4 additions & 5 deletions src/engine/core/objects/binders/creature/CrowBinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const logger: LuaLogger = new LuaLogger($filename);
export class CrowBinder extends object_binder {
public diedAt: TTimestamp = 0;

public override net_save_relevant(): boolean {
return true;
}
public override reinit(): void {
super.reinit();

Expand Down Expand Up @@ -72,16 +75,12 @@ export class CrowBinder extends object_binder {
super.net_destroy();
}

public override net_save_relevant(): boolean {
return true;
}

public override update(delta: TDuration): void {
super.update(delta);

if (
!this.object.alive() &&
this.diedAt !== 0 &&
!this.object.alive() &&
time_global() - logicsConfig.CROW_CORPSE_RELEASE_TIMEOUT >= this.diedAt
) {
const simulator: AlifeSimulator = alife();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
jest.mock("@/engine/core/utils/scheme/scheme_switch", () => ({ trySwitchToAnotherSection: jest.fn() }));

import { describe, expect, it, jest } from "@jest/globals";
import { alife, patrol } from "xray16";

import { registerActor } from "@/engine/core/database";
import { CrowSpawnerManager } from "@/engine/core/schemes/restrictor/sr_crow_spawner/CrowSpawnerManager";
import { ISchemeCrowSpawnerState } from "@/engine/core/schemes/restrictor/sr_crow_spawner/ISchemeCrowSpawnerState";
import { range } from "@/engine/core/utils/number";
import { trySwitchToAnotherSection } from "@/engine/core/utils/scheme";
import { AlifeSimulator, ClientObject, EScheme, Patrol } from "@/engine/lib/types";
import { mockSchemeState } from "@/fixtures/engine";
import { mockActorClientGameObject, mockClientGameObject } from "@/fixtures/xray";
import { MockVector } from "@/fixtures/xray/mocks/vector.mock";

describe("CrowSpawnerManager class", () => {
it("should correctly initialize", () => {
const object: ClientObject = mockClientGameObject();
const state: ISchemeCrowSpawnerState = mockSchemeState(EScheme.SR_CROW_SPAWNER);
const manager: CrowSpawnerManager = new CrowSpawnerManager(object, state);

expect(manager.nextUpdateAt).toBe(0);
expect(manager.spawnPointsUpdateAt).toEqualLuaTables({});
});

it("should correctly handle updates", () => {
const object: ClientObject = mockClientGameObject();
const state: ISchemeCrowSpawnerState = mockSchemeState(EScheme.SR_CROW_SPAWNER);
const manager: CrowSpawnerManager = new CrowSpawnerManager(object, state);

jest.spyOn(Date, "now").mockImplementation(() => 5500);
jest.spyOn(manager, "spawnCrows").mockImplementation(jest.fn());

state.maxCrowsOnLevel = 10;
manager.nextUpdateAt = Infinity;
manager.update();
expect(manager.spawnCrows).not.toHaveBeenCalled();
expect(trySwitchToAnotherSection).toHaveBeenCalledTimes(1);

state.maxCrowsOnLevel = 0;
manager.nextUpdateAt = 0;
manager.update();
expect(manager.nextUpdateAt).toBe(125_500);
expect(manager.spawnCrows).not.toHaveBeenCalled();
expect(trySwitchToAnotherSection).toHaveBeenCalledTimes(2);

state.maxCrowsOnLevel = 10;
manager.nextUpdateAt = 0;
manager.update();
expect(manager.spawnCrows).toHaveBeenCalled();
expect(manager.nextUpdateAt).toBe(0);
expect(trySwitchToAnotherSection).toHaveBeenCalledTimes(3);
});

it("should correctly handle crow spawn", () => {
const simulator: AlifeSimulator = alife();
const object: ClientObject = mockClientGameObject();
const state: ISchemeCrowSpawnerState = mockSchemeState(EScheme.SR_CROW_SPAWNER);
const manager: CrowSpawnerManager = new CrowSpawnerManager(object, state);

registerActor(mockActorClientGameObject());

jest.spyOn(Date, "now").mockImplementation(() => 5500);

state.maxCrowsOnLevel = 10;
state.pathsList = $fromArray(["test_smart_guard_1_walk", "test_smart_patrol_1_walk"]);

for (const [, name] of state.pathsList) {
const crowPatrol: Patrol = new patrol(name);

jest.spyOn(crowPatrol.point(0), "distance_to_sqr").mockImplementation(() => Infinity);
}

manager.spawnCrows();
manager.spawnCrows();

// After two iterations time is set.
expect(manager.spawnPointsUpdateAt).toEqualLuaTables({
test_smart_guard_1_walk: 15_500,
test_smart_patrol_1_walk: 15_500,
});
expect(simulator.create).toHaveBeenCalledTimes(2);

jest.spyOn(Date, "now").mockImplementation(() => 10_000);

manager.spawnCrows();
manager.spawnCrows();

// No updates.
expect(manager.spawnPointsUpdateAt).toEqualLuaTables({
test_smart_guard_1_walk: 15_500,
test_smart_patrol_1_walk: 15_500,
});
expect(simulator.create).toHaveBeenCalledTimes(2);

jest.spyOn(Date, "now").mockImplementation(() => 50_000);

manager.spawnCrows();
manager.spawnCrows();

// Updated on timeout.
expect(manager.spawnPointsUpdateAt).toEqualLuaTables({
test_smart_guard_1_walk: 60_000,
test_smart_patrol_1_walk: 60_000,
});
expect(simulator.create).toHaveBeenCalledTimes(4);

range(4).forEach((it) => {
expect(simulator.create).toHaveBeenNthCalledWith(
it + 1,
"m_crow",
expect.any(MockVector),
expect.any(Number),
expect.any(Number)
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,77 +6,65 @@ import { ISchemeCrowSpawnerState } from "@/engine/core/schemes/restrictor/sr_cro
import { LuaLogger } from "@/engine/core/utils/logging";
import { trySwitchToAnotherSection } from "@/engine/core/utils/scheme/scheme_switch";
import { copyTable } from "@/engine/core/utils/table";
import { Optional, Patrol, ServerObject, TCount, TDuration, TIndex, TName } from "@/engine/lib/types";
import { AlifeSimulator, LuaArray, Patrol, TDuration, TIndex, TName, TTimestamp, Vector } from "@/engine/lib/types";

const logger: LuaLogger = new LuaLogger($filename);

/**
* todo;
* Manager of crow spawning.
* If scheme is active, spawning crows at random paths defined in scheme config.
*/
export class CrowSpawnerManager extends AbstractSchemeManager<ISchemeCrowSpawnerState> {
public spawnTimeConstant: TDuration = 120_000;
public timeForSpawn: TDuration = time_global();
public spawnPointsIdle: LuaTable = new LuaTable();
public spawnedCount: Optional<TCount> = null;
public static CROW_UPDATE_THROTTLE: TDuration = 120_000;

public nextUpdateAt: TTimestamp = 0;
public spawnPointsUpdateAt: LuaTable<TName, TTimestamp> = new LuaTable();

/**
* todo: Description.
*/
public override activate(): void {
for (const [k, v] of this.state.pathsList!) {
this.spawnPointsIdle.set(v, time_global());
for (const [, pathName] of this.state.pathsList) {
this.spawnPointsUpdateAt.set(pathName, 0);
}
}

/**
* todo: Description.
*/
public update(): void {
// -- check for spawn crows on level
if (this.timeForSpawn < time_global()) {
this.spawnedCount = registry.crows.count;
if (this.spawnedCount < this.state.maxCrowsOnLevel!) {
// -- need to spawn
this.checkForSpawnNewCrow();
const now: TTimestamp = time_global();

if (this.nextUpdateAt < now) {
if (registry.crows.count < this.state.maxCrowsOnLevel) {
this.spawnCrows();
} else {
// -- now look for spawn later
this.timeForSpawn = time_global() + this.spawnTimeConstant;
this.nextUpdateAt = now + CrowSpawnerManager.CROW_UPDATE_THROTTLE;
}
}

if (trySwitchToAnotherSection(this.object, this.state)) {
return;
}
trySwitchToAnotherSection(this.object, this.state);
}

/**
* todo: Description.
* Spawn random crows for current level.
* Check where crows were not spawned recently and add crow objects.
*/
public checkForSpawnNewCrow(): void {
const pathList: LuaTable<number, string> = new LuaTable();
public spawnCrows(): void {
const now: TTimestamp = time_global();
const simulator: AlifeSimulator = alife();
const pathList: LuaArray<TName> = copyTable(new LuaTable(), this.state.pathsList);
const actorPosition: Vector = registry.actor.position();

copyTable(pathList, this.state.pathsList!);
for (const it of $range(1, this.state.pathsList.length())) {
const index: TIndex = math.random(pathList.length());
const selectedPath: TName = pathList.get(index);

for (const it of $range(1, this.state.pathsList!.length())) {
const idx: TIndex = math.random(pathList.length());
const selectedPath: TName = pathList.get(idx);
table.remove(pathList, index);

table.remove(pathList, idx);
if (this.spawnPointsIdle.get(selectedPath) <= time_global()) {
// -- if we have not spawned already in this point
if (this.spawnPointsUpdateAt.get(selectedPath) <= now) {
const crowPatrol: Patrol = new patrol(selectedPath);
const spawnPosition: Vector = crowPatrol.point(0);

if (crowPatrol.point(0).distance_to(registry.actor.position()) > 100) {
const object: ServerObject = alife().create(
"m_crow",
crowPatrol.point(0),
crowPatrol.level_vertex_id(0),
crowPatrol.game_vertex_id(0)
);

// logger.info("Spawn new crow:", object.id, selectedPath);
// Check distance 100*100.
if (spawnPosition.distance_to_sqr(actorPosition) > 10_000) {
simulator.create("m_crow", spawnPosition, crowPatrol.level_vertex_id(0), crowPatrol.game_vertex_id(0));

this.spawnPointsIdle.set(selectedPath, time_global() + 10000);
this.spawnPointsUpdateAt.set(selectedPath, now + 10_000);

return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { IBaseSchemeState } from "@/engine/core/database/types";
import type { LuaArray, TCount, TName } from "@/engine/lib/types";

/**
* Crow spawner scheme state.
* Crow spawner scheme state configured from ini files.
*/
export interface ISchemeCrowSpawnerState extends IBaseSchemeState {
maxCrowsOnLevel: TCount;
Expand Down
7 changes: 0 additions & 7 deletions src/engine/core/schemes/restrictor/sr_crow_spawner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
Scheme to describe logics of crows spawner. <br/>
From time to time spawns crows for declared paths based on maximal possible crows count.

## ini parameters

```
max_crows_on_level - ?
spawn_path - ?
```

## Documentation

[Book: sr_crow_spawner scheme.](https://xray-forge.github.io/stalker-xrf-book/script_engine/schemes/sr_crow_spawner.html)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it } from "@jest/globals";

import { IRegistryObjectState, registerObject } from "@/engine/core/database";
import { ISchemeCrowSpawnerState } from "@/engine/core/schemes/restrictor/sr_crow_spawner/ISchemeCrowSpawnerState";
import { SchemeCrowSpawner } from "@/engine/core/schemes/restrictor/sr_crow_spawner/SchemeCrowSpawner";
import { parseConditionsList } from "@/engine/core/utils/ini";
import { loadSchemeImplementation } from "@/engine/core/utils/scheme";
import { ClientObject, EScheme, ESchemeType, IniFile } from "@/engine/lib/types";
import { mockBaseSchemeLogic } from "@/fixtures/engine";
import { mockClientGameObject, mockIniFile } from "@/fixtures/xray";

describe("SchemeCrowSpawner", () => {
beforeEach(() => {
loadSchemeImplementation(SchemeCrowSpawner);
});

it("should be correctly defined", () => {
expect(SchemeCrowSpawner.SCHEME_SECTION).toBe("sr_crow_spawner");
expect(SchemeCrowSpawner.SCHEME_TYPE).toBe(ESchemeType.RESTRICTOR);
});

it("should correctly read ini configuration", () => {
const object: ClientObject = mockClientGameObject();
const state: IRegistryObjectState = registerObject(object);
const ini: IniFile = mockIniFile("example.ltx", {
"sr_crow_spawner@test": {
on_info: "{+test_info} another@section",
max_crows_on_level: 48,
spawn_path: "a, b, c, d",
},
});

SchemeCrowSpawner.activate(object, ini, EScheme.SR_CROW_SPAWNER, "sr_crow_spawner@test");

const schemeState: ISchemeCrowSpawnerState = state[EScheme.SR_CROW_SPAWNER] as ISchemeCrowSpawnerState;

expect(schemeState.maxCrowsOnLevel).toBe(48);
expect(schemeState.pathsList).toEqualLuaArrays(["a", "b", "c", "d"]);
expect(schemeState.logic).toEqualLuaArrays([
mockBaseSchemeLogic({ name: "on_info", condlist: parseConditionsList("{+test_info} another@section") }),
]);
});

it("should correctly read empty configuration", () => {
const object: ClientObject = mockClientGameObject();
const state: IRegistryObjectState = registerObject(object);
const ini: IniFile = mockIniFile("another.ltx", {});

SchemeCrowSpawner.activate(object, ini, EScheme.SR_CROW_SPAWNER, "sr_crow_spawner@another");

const schemeState: ISchemeCrowSpawnerState = state[EScheme.SR_CROW_SPAWNER] as ISchemeCrowSpawnerState;

expect(schemeState.maxCrowsOnLevel).toBe(16);
expect(schemeState.pathsList).toEqualLuaArrays([]);
expect(schemeState.logic).toBeNull();
});
});
12 changes: 11 additions & 1 deletion src/fixtures/engine/mocks/table.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ export const mockTableUtils = {
return Object.keys(target).length === 0;
}
},
copyTable: (target: AnyObject, source: AnyObject) => Object.assign(target, source),
copyTable: (target: AnyObject, source: AnyObject) => {
if (target instanceof MockLuaTable && source instanceof MockLuaTable) {
for (const [k, v] of source) {
target.set(k, v);
}

return target;
}

return Object.assign(target, source);
},
resetTable: (table: Map<unknown, unknown>) => {
for (const [k] of table) {
table.delete(k);
Expand Down
11 changes: 6 additions & 5 deletions src/fixtures/lua/mocks/LuaTable.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ export class MockLuaTable<K, V> extends Map<K, V> {
/**
* Create new map from JS array.
*/
public static fromArray<T>(from: Array<T>): MockLuaTable<number, T> {
public static fromArray<T>(
from: Array<T>,
into: MockLuaTable<number, T> = new MockLuaTable()
): MockLuaTable<number, T> {
if (from instanceof MockLuaTable) {
return from;
} else if (from === null) {
return from;
}

const mock: MockLuaTable<number, T> = new MockLuaTable();
from.forEach((it, index) => into.set(index + 1, it));

from.forEach((it, index) => mock.set(index + 1, it));

return mock;
return into;
}

/**
Expand Down
Loading

0 comments on commit db22519

Please sign in to comment.