@jgengine/core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +5 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +291 -5
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +53 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +219 -0
- package/dist/physics/physicsWorld.js +749 -0
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +66 -2
- package/dist/runtime/gameContext.js +157 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/assetCatalog.d.ts +13 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +7 -1
- package/dist/scene/entityStore.js +15 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +62 -0
- package/dist/time/simClock.js +132 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/buildingIndex.d.ts +16 -0
- package/dist/world/buildingIndex.js +63 -0
- package/dist/world/buildings.d.ts +98 -0
- package/dist/world/buildings.js +320 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/features.d.ts +105 -1
- package/dist/world/features.js +95 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/regions.d.ts +66 -0
- package/dist/world/regions.js +158 -0
- package/dist/world/scatter.d.ts +24 -0
- package/dist/world/scatter.js +126 -0
- package/dist/world/scatterItems.d.ts +38 -0
- package/dist/world/scatterItems.js +85 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +54 -0
- package/dist/world/terrain.js +181 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/water.d.ts +27 -0
- package/dist/world/water.js +125 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/dist/world/wind.d.ts +17 -0
- package/dist/world/wind.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EntityMovement, SceneEntity } from "./entityStore.js";
|
|
2
|
+
export interface FormDef {
|
|
3
|
+
id: string;
|
|
4
|
+
movement?: EntityMovement;
|
|
5
|
+
abilities?: readonly string[];
|
|
6
|
+
model?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FormChangedEvent {
|
|
9
|
+
instanceId: string;
|
|
10
|
+
formId: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface FormEntities {
|
|
13
|
+
get(id: string): SceneEntity | null;
|
|
14
|
+
update(id: string, patch: {
|
|
15
|
+
movement?: EntityMovement;
|
|
16
|
+
name?: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface FormTime {
|
|
20
|
+
after(seconds: number, callback: () => void): () => void;
|
|
21
|
+
}
|
|
22
|
+
export interface FormEvents {
|
|
23
|
+
emit(name: "form.changed", payload: FormChangedEvent): void;
|
|
24
|
+
}
|
|
25
|
+
export interface FormsDeps {
|
|
26
|
+
entities: FormEntities;
|
|
27
|
+
time: FormTime;
|
|
28
|
+
events?: FormEvents;
|
|
29
|
+
}
|
|
30
|
+
export interface Forms {
|
|
31
|
+
register(defs: readonly FormDef[]): void;
|
|
32
|
+
get(formId: string): FormDef | null;
|
|
33
|
+
abilities(instanceId: string): readonly string[] | null;
|
|
34
|
+
active(instanceId: string): string | null;
|
|
35
|
+
shapeshift(instanceId: string, formId: string, durationSeconds?: number): {
|
|
36
|
+
reason: string;
|
|
37
|
+
} | null;
|
|
38
|
+
revert(instanceId: string): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createForms(deps: FormsDeps): Forms;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function createForms(deps) {
|
|
2
|
+
const defs = new Map();
|
|
3
|
+
const runtime = new Map();
|
|
4
|
+
function emitChange(instanceId, formId) {
|
|
5
|
+
deps.events?.emit("form.changed", { instanceId, formId });
|
|
6
|
+
}
|
|
7
|
+
function revertInternal(instanceId) {
|
|
8
|
+
const state = runtime.get(instanceId);
|
|
9
|
+
if (state === undefined)
|
|
10
|
+
return;
|
|
11
|
+
state.cancelTimer?.();
|
|
12
|
+
runtime.delete(instanceId);
|
|
13
|
+
deps.entities.update(instanceId, { movement: state.baseline.movement, name: state.baseline.name });
|
|
14
|
+
emitChange(instanceId, null);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
register(list) {
|
|
18
|
+
for (const def of list)
|
|
19
|
+
defs.set(def.id, def);
|
|
20
|
+
},
|
|
21
|
+
get(formId) {
|
|
22
|
+
return defs.get(formId) ?? null;
|
|
23
|
+
},
|
|
24
|
+
abilities(instanceId) {
|
|
25
|
+
const state = runtime.get(instanceId);
|
|
26
|
+
if (state === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
return defs.get(state.formId)?.abilities ?? null;
|
|
29
|
+
},
|
|
30
|
+
active(instanceId) {
|
|
31
|
+
return runtime.get(instanceId)?.formId ?? null;
|
|
32
|
+
},
|
|
33
|
+
shapeshift(instanceId, formId, durationSeconds) {
|
|
34
|
+
const def = defs.get(formId);
|
|
35
|
+
if (def === undefined)
|
|
36
|
+
return { reason: `unknown form "${formId}"` };
|
|
37
|
+
const entity = deps.entities.get(instanceId);
|
|
38
|
+
if (entity === null)
|
|
39
|
+
return { reason: `entity "${instanceId}" is not spawned` };
|
|
40
|
+
const existing = runtime.get(instanceId);
|
|
41
|
+
existing?.cancelTimer?.();
|
|
42
|
+
const baseline = existing?.baseline ?? { movement: entity.movement, name: entity.name };
|
|
43
|
+
deps.entities.update(instanceId, {
|
|
44
|
+
movement: def.movement ?? baseline.movement,
|
|
45
|
+
name: def.model ?? baseline.name,
|
|
46
|
+
});
|
|
47
|
+
const cancelTimer = durationSeconds === undefined ? null : deps.time.after(durationSeconds, () => revertInternal(instanceId));
|
|
48
|
+
runtime.set(instanceId, { baseline, formId, cancelTimer });
|
|
49
|
+
emitChange(instanceId, formId);
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
revert(instanceId) {
|
|
53
|
+
revertInternal(instanceId);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type MountKind = "ground" | "boat" | "flying" | "swimming";
|
|
2
|
+
export interface MountKit {
|
|
3
|
+
kind: MountKind;
|
|
4
|
+
moveSpeed?: number;
|
|
5
|
+
turnSpeed?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface MountSeat {
|
|
8
|
+
id: string;
|
|
9
|
+
offset: readonly [number, number, number];
|
|
10
|
+
control?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface RideableConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
kit: MountKit;
|
|
15
|
+
seats?: readonly MountSeat[];
|
|
16
|
+
}
|
|
17
|
+
export interface SeatRef {
|
|
18
|
+
mountId: string;
|
|
19
|
+
seatId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Occupant {
|
|
22
|
+
riderId: string;
|
|
23
|
+
seatId: string;
|
|
24
|
+
}
|
|
25
|
+
export type MountResult = {
|
|
26
|
+
ok: true;
|
|
27
|
+
seat: MountSeat;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Mount / rideable control-transfer (issue #83). Registers rideables (each with one or more seats — a
|
|
34
|
+
* control seat drives, the rest ride) and tracks who is on what. It owns no camera or physics: game code
|
|
35
|
+
* reads `cameraTarget(riderId)` to point the follow camera at the mount, and `driveTarget(riderId)` to
|
|
36
|
+
* route that rider's {@link import("../physics/vehicleBody.js").AxisInput}-driven input at the mount's
|
|
37
|
+
* movement kit — the same seam a horse, a truck, or a shared multi-seat ship all plug into.
|
|
38
|
+
*/
|
|
39
|
+
export declare class MountController {
|
|
40
|
+
private readonly mounts;
|
|
41
|
+
private readonly seatOfRider;
|
|
42
|
+
register(config: RideableConfig): void;
|
|
43
|
+
isRegistered(mountId: string): boolean;
|
|
44
|
+
kitOf(mountId: string): MountKit | null;
|
|
45
|
+
seatsOf(mountId: string): readonly MountSeat[];
|
|
46
|
+
private firstFreeSeat;
|
|
47
|
+
mount(riderId: string, mountId: string, seatId?: string): MountResult;
|
|
48
|
+
dismount(riderId: string): string | null;
|
|
49
|
+
seatOf(riderId: string): SeatRef | null;
|
|
50
|
+
isSeated(riderId: string): boolean;
|
|
51
|
+
seatOffset(riderId: string): readonly [number, number, number] | null;
|
|
52
|
+
occupants(mountId: string): readonly Occupant[];
|
|
53
|
+
driver(mountId: string): string | null;
|
|
54
|
+
/** Entity the follow camera should track for this rider: the mount when seated, else the rider. */
|
|
55
|
+
cameraTarget(riderId: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Entity this rider's input drives: the mount when in a control seat, `null` for a passenger seat
|
|
58
|
+
* (they ride but do not steer), and the rider themselves when not mounted at all.
|
|
59
|
+
*/
|
|
60
|
+
driveTarget(riderId: string): string | null;
|
|
61
|
+
}
|
|
62
|
+
export declare function createMountController(): MountController;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const DEFAULT_SEAT = { id: "driver", offset: [0, 0, 0], control: true };
|
|
2
|
+
/**
|
|
3
|
+
* Mount / rideable control-transfer (issue #83). Registers rideables (each with one or more seats — a
|
|
4
|
+
* control seat drives, the rest ride) and tracks who is on what. It owns no camera or physics: game code
|
|
5
|
+
* reads `cameraTarget(riderId)` to point the follow camera at the mount, and `driveTarget(riderId)` to
|
|
6
|
+
* route that rider's {@link import("../physics/vehicleBody.js").AxisInput}-driven input at the mount's
|
|
7
|
+
* movement kit — the same seam a horse, a truck, or a shared multi-seat ship all plug into.
|
|
8
|
+
*/
|
|
9
|
+
export class MountController {
|
|
10
|
+
mounts = new Map();
|
|
11
|
+
seatOfRider = new Map();
|
|
12
|
+
register(config) {
|
|
13
|
+
const seats = config.seats !== undefined && config.seats.length > 0 ? config.seats : [DEFAULT_SEAT];
|
|
14
|
+
this.mounts.set(config.id, { config, seats, bySeat: new Map() });
|
|
15
|
+
}
|
|
16
|
+
isRegistered(mountId) {
|
|
17
|
+
return this.mounts.has(mountId);
|
|
18
|
+
}
|
|
19
|
+
kitOf(mountId) {
|
|
20
|
+
return this.mounts.get(mountId)?.config.kit ?? null;
|
|
21
|
+
}
|
|
22
|
+
seatsOf(mountId) {
|
|
23
|
+
return this.mounts.get(mountId)?.seats ?? [];
|
|
24
|
+
}
|
|
25
|
+
firstFreeSeat(reg) {
|
|
26
|
+
const control = reg.seats.find((s) => s.control === true && !reg.bySeat.has(s.id));
|
|
27
|
+
if (control !== undefined)
|
|
28
|
+
return control;
|
|
29
|
+
return reg.seats.find((s) => !reg.bySeat.has(s.id)) ?? null;
|
|
30
|
+
}
|
|
31
|
+
mount(riderId, mountId, seatId) {
|
|
32
|
+
const reg = this.mounts.get(mountId);
|
|
33
|
+
if (reg === undefined)
|
|
34
|
+
return { ok: false, reason: "unknown_mount" };
|
|
35
|
+
const seat = seatId !== undefined ? reg.seats.find((s) => s.id === seatId) : this.firstFreeSeat(reg);
|
|
36
|
+
if (seat === undefined)
|
|
37
|
+
return { ok: false, reason: "unknown_seat" };
|
|
38
|
+
if (seat === null)
|
|
39
|
+
return { ok: false, reason: "mount_full" };
|
|
40
|
+
if (reg.bySeat.has(seat.id))
|
|
41
|
+
return { ok: false, reason: "seat_taken" };
|
|
42
|
+
this.dismount(riderId);
|
|
43
|
+
reg.bySeat.set(seat.id, riderId);
|
|
44
|
+
this.seatOfRider.set(riderId, { mountId, seatId: seat.id });
|
|
45
|
+
return { ok: true, seat };
|
|
46
|
+
}
|
|
47
|
+
dismount(riderId) {
|
|
48
|
+
const ref = this.seatOfRider.get(riderId);
|
|
49
|
+
if (ref === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
this.mounts.get(ref.mountId)?.bySeat.delete(ref.seatId);
|
|
52
|
+
this.seatOfRider.delete(riderId);
|
|
53
|
+
return ref.mountId;
|
|
54
|
+
}
|
|
55
|
+
seatOf(riderId) {
|
|
56
|
+
return this.seatOfRider.get(riderId) ?? null;
|
|
57
|
+
}
|
|
58
|
+
isSeated(riderId) {
|
|
59
|
+
return this.seatOfRider.has(riderId);
|
|
60
|
+
}
|
|
61
|
+
seatOffset(riderId) {
|
|
62
|
+
const ref = this.seatOfRider.get(riderId);
|
|
63
|
+
if (ref === undefined)
|
|
64
|
+
return null;
|
|
65
|
+
return this.mounts.get(ref.mountId)?.seats.find((s) => s.id === ref.seatId)?.offset ?? null;
|
|
66
|
+
}
|
|
67
|
+
occupants(mountId) {
|
|
68
|
+
const reg = this.mounts.get(mountId);
|
|
69
|
+
if (reg === undefined)
|
|
70
|
+
return [];
|
|
71
|
+
return reg.seats
|
|
72
|
+
.filter((s) => reg.bySeat.has(s.id))
|
|
73
|
+
.map((s) => ({ riderId: reg.bySeat.get(s.id), seatId: s.id }));
|
|
74
|
+
}
|
|
75
|
+
driver(mountId) {
|
|
76
|
+
const reg = this.mounts.get(mountId);
|
|
77
|
+
if (reg === undefined)
|
|
78
|
+
return null;
|
|
79
|
+
for (const seat of reg.seats) {
|
|
80
|
+
if (seat.control === true) {
|
|
81
|
+
const rider = reg.bySeat.get(seat.id);
|
|
82
|
+
if (rider !== undefined)
|
|
83
|
+
return rider;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Entity the follow camera should track for this rider: the mount when seated, else the rider. */
|
|
89
|
+
cameraTarget(riderId) {
|
|
90
|
+
const ref = this.seatOfRider.get(riderId);
|
|
91
|
+
return ref === undefined ? riderId : ref.mountId;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Entity this rider's input drives: the mount when in a control seat, `null` for a passenger seat
|
|
95
|
+
* (they ride but do not steer), and the rider themselves when not mounted at all.
|
|
96
|
+
*/
|
|
97
|
+
driveTarget(riderId) {
|
|
98
|
+
const ref = this.seatOfRider.get(riderId);
|
|
99
|
+
if (ref === undefined)
|
|
100
|
+
return riderId;
|
|
101
|
+
const seat = this.mounts.get(ref.mountId)?.seats.find((s) => s.id === ref.seatId);
|
|
102
|
+
return seat?.control === true ? ref.mountId : null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function createMountController() {
|
|
106
|
+
return new MountController();
|
|
107
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SceneEntity } from "./entityStore.js";
|
|
2
|
+
export interface PossessionSwappedEvent {
|
|
3
|
+
userId: string;
|
|
4
|
+
entityId: string;
|
|
5
|
+
previousEntityId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PossessionEntities {
|
|
8
|
+
get(id: string): SceneEntity | null;
|
|
9
|
+
update(id: string, patch: {
|
|
10
|
+
role?: SceneEntity["role"];
|
|
11
|
+
}): boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface PossessionEvents {
|
|
14
|
+
emit(name: "possession.swapped", payload: PossessionSwappedEvent): void;
|
|
15
|
+
}
|
|
16
|
+
export interface PossessionDeps {
|
|
17
|
+
entities: PossessionEntities;
|
|
18
|
+
events?: PossessionEvents;
|
|
19
|
+
}
|
|
20
|
+
export interface Possession {
|
|
21
|
+
own(userId: string, entityId: string): void;
|
|
22
|
+
disown(userId: string, entityId: string): void;
|
|
23
|
+
owns(userId: string, entityId: string): boolean;
|
|
24
|
+
listOwned(userId: string): readonly string[];
|
|
25
|
+
active(userId: string): string;
|
|
26
|
+
possess(userId: string, entityId: string): {
|
|
27
|
+
reason: string;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
export declare function createPossession(deps: PossessionDeps): Possession;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function createPossession(deps) {
|
|
2
|
+
const owned = new Map();
|
|
3
|
+
const activeByUser = new Map();
|
|
4
|
+
function ownedSet(userId) {
|
|
5
|
+
let set = owned.get(userId);
|
|
6
|
+
if (set === undefined) {
|
|
7
|
+
set = new Set([userId]);
|
|
8
|
+
owned.set(userId, set);
|
|
9
|
+
}
|
|
10
|
+
return set;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
own(userId, entityId) {
|
|
14
|
+
ownedSet(userId).add(entityId);
|
|
15
|
+
},
|
|
16
|
+
disown(userId, entityId) {
|
|
17
|
+
if (entityId === userId)
|
|
18
|
+
return;
|
|
19
|
+
ownedSet(userId).delete(entityId);
|
|
20
|
+
if (activeByUser.get(userId) === entityId)
|
|
21
|
+
activeByUser.delete(userId);
|
|
22
|
+
},
|
|
23
|
+
owns(userId, entityId) {
|
|
24
|
+
return ownedSet(userId).has(entityId);
|
|
25
|
+
},
|
|
26
|
+
listOwned(userId) {
|
|
27
|
+
return Array.from(ownedSet(userId));
|
|
28
|
+
},
|
|
29
|
+
active(userId) {
|
|
30
|
+
return activeByUser.get(userId) ?? userId;
|
|
31
|
+
},
|
|
32
|
+
possess(userId, entityId) {
|
|
33
|
+
if (!ownedSet(userId).has(entityId)) {
|
|
34
|
+
return { reason: `entity "${entityId}" is not owned by "${userId}"` };
|
|
35
|
+
}
|
|
36
|
+
if (deps.entities.get(entityId) === null) {
|
|
37
|
+
return { reason: `entity "${entityId}" is not spawned` };
|
|
38
|
+
}
|
|
39
|
+
const previousEntityId = activeByUser.get(userId) ?? userId;
|
|
40
|
+
if (previousEntityId === entityId)
|
|
41
|
+
return null;
|
|
42
|
+
activeByUser.set(userId, entityId);
|
|
43
|
+
if (deps.entities.get(previousEntityId) !== null) {
|
|
44
|
+
deps.entities.update(previousEntityId, { role: "npc" });
|
|
45
|
+
}
|
|
46
|
+
deps.entities.update(entityId, { role: "player" });
|
|
47
|
+
deps.events?.emit("possession.swapped", { userId, entityId, previousEntityId });
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RosterEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
catalogId: string;
|
|
4
|
+
capturedAt: number;
|
|
5
|
+
equipped: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RosterCaptureOptions {
|
|
8
|
+
id?: string;
|
|
9
|
+
capturedAt?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Roster {
|
|
12
|
+
capture(userId: string, catalogId: string, options?: RosterCaptureOptions): RosterEntry;
|
|
13
|
+
release(userId: string, entryId: string): boolean;
|
|
14
|
+
has(userId: string, entryId: string): boolean;
|
|
15
|
+
get(userId: string, entryId: string): RosterEntry | null;
|
|
16
|
+
list(userId: string): readonly RosterEntry[];
|
|
17
|
+
equippedList(userId: string): readonly RosterEntry[];
|
|
18
|
+
setEquipped(userId: string, entryId: string, equipped: boolean): RosterEntry | null;
|
|
19
|
+
snapshot(userId: string): readonly RosterEntry[];
|
|
20
|
+
hydrate(userId: string, entries: readonly RosterEntry[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface RosterDeps {
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export declare function createRoster(deps?: RosterDeps): Roster;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createRoster(deps = {}) {
|
|
2
|
+
const now = deps.now ?? Date.now;
|
|
3
|
+
const rosters = new Map();
|
|
4
|
+
let counter = 0;
|
|
5
|
+
function requireEntries(userId) {
|
|
6
|
+
let entries = rosters.get(userId);
|
|
7
|
+
if (entries === undefined) {
|
|
8
|
+
entries = new Map();
|
|
9
|
+
rosters.set(userId, entries);
|
|
10
|
+
}
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
capture(userId, catalogId, options = {}) {
|
|
15
|
+
counter += 1;
|
|
16
|
+
const id = options.id ?? `roster_${counter}`;
|
|
17
|
+
const entry = {
|
|
18
|
+
id,
|
|
19
|
+
catalogId,
|
|
20
|
+
capturedAt: options.capturedAt ?? now(),
|
|
21
|
+
equipped: false,
|
|
22
|
+
};
|
|
23
|
+
requireEntries(userId).set(id, entry);
|
|
24
|
+
return entry;
|
|
25
|
+
},
|
|
26
|
+
release(userId, entryId) {
|
|
27
|
+
return rosters.get(userId)?.delete(entryId) ?? false;
|
|
28
|
+
},
|
|
29
|
+
has(userId, entryId) {
|
|
30
|
+
return rosters.get(userId)?.has(entryId) ?? false;
|
|
31
|
+
},
|
|
32
|
+
get(userId, entryId) {
|
|
33
|
+
return rosters.get(userId)?.get(entryId) ?? null;
|
|
34
|
+
},
|
|
35
|
+
list(userId) {
|
|
36
|
+
return Array.from(rosters.get(userId)?.values() ?? []);
|
|
37
|
+
},
|
|
38
|
+
equippedList(userId) {
|
|
39
|
+
return Array.from(rosters.get(userId)?.values() ?? []).filter((entry) => entry.equipped);
|
|
40
|
+
},
|
|
41
|
+
setEquipped(userId, entryId, equipped) {
|
|
42
|
+
const entries = rosters.get(userId);
|
|
43
|
+
const entry = entries?.get(entryId);
|
|
44
|
+
if (entries === undefined || entry === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
const next = { ...entry, equipped };
|
|
47
|
+
entries.set(entryId, next);
|
|
48
|
+
return next;
|
|
49
|
+
},
|
|
50
|
+
snapshot(userId) {
|
|
51
|
+
return Array.from(rosters.get(userId)?.values() ?? []);
|
|
52
|
+
},
|
|
53
|
+
hydrate(userId, entries) {
|
|
54
|
+
rosters.set(userId, new Map(entries.map((entry) => [entry.id, entry])));
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ScreenRect {
|
|
2
|
+
minX: number;
|
|
3
|
+
minY: number;
|
|
4
|
+
maxX: number;
|
|
5
|
+
maxY: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ScreenPoint {
|
|
8
|
+
id: string;
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
}
|
|
12
|
+
/** Normalize two drag corners (in any order) into a rectangle. */
|
|
13
|
+
export declare function screenRect(ax: number, ay: number, bx: number, by: number): ScreenRect;
|
|
14
|
+
export declare function rectContainsPoint(rect: ScreenRect, x: number, y: number): boolean;
|
|
15
|
+
/** Ids of the projected candidates whose screen point falls inside the marquee. */
|
|
16
|
+
export declare function selectWithinRect(candidates: readonly ScreenPoint[], rect: ScreenRect): string[];
|
|
17
|
+
/** True when the drag is large enough to be a marquee rather than a click. */
|
|
18
|
+
export declare function isMarquee(rect: ScreenRect, thresholdPx?: number): boolean;
|
|
19
|
+
export interface SelectionSet {
|
|
20
|
+
add(id: string): void;
|
|
21
|
+
remove(id: string): void;
|
|
22
|
+
toggle(id: string): void;
|
|
23
|
+
has(id: string): boolean;
|
|
24
|
+
replace(ids: Iterable<string>): void;
|
|
25
|
+
clear(): void;
|
|
26
|
+
list(): string[];
|
|
27
|
+
size(): number;
|
|
28
|
+
}
|
|
29
|
+
/** An ordered, deduplicated set of selected instance ids for RTS unit-command routing. */
|
|
30
|
+
export declare function createSelectionSet(initial?: Iterable<string>): SelectionSet;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Normalize two drag corners (in any order) into a rectangle. */
|
|
2
|
+
export function screenRect(ax, ay, bx, by) {
|
|
3
|
+
return {
|
|
4
|
+
minX: Math.min(ax, bx),
|
|
5
|
+
minY: Math.min(ay, by),
|
|
6
|
+
maxX: Math.max(ax, bx),
|
|
7
|
+
maxY: Math.max(ay, by),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function rectContainsPoint(rect, x, y) {
|
|
11
|
+
return x >= rect.minX && x <= rect.maxX && y >= rect.minY && y <= rect.maxY;
|
|
12
|
+
}
|
|
13
|
+
/** Ids of the projected candidates whose screen point falls inside the marquee. */
|
|
14
|
+
export function selectWithinRect(candidates, rect) {
|
|
15
|
+
return candidates.filter((candidate) => rectContainsPoint(rect, candidate.x, candidate.y)).map((c) => c.id);
|
|
16
|
+
}
|
|
17
|
+
/** True when the drag is large enough to be a marquee rather than a click. */
|
|
18
|
+
export function isMarquee(rect, thresholdPx = 4) {
|
|
19
|
+
return rect.maxX - rect.minX >= thresholdPx || rect.maxY - rect.minY >= thresholdPx;
|
|
20
|
+
}
|
|
21
|
+
/** An ordered, deduplicated set of selected instance ids for RTS unit-command routing. */
|
|
22
|
+
export function createSelectionSet(initial) {
|
|
23
|
+
const ids = new Set(initial);
|
|
24
|
+
return {
|
|
25
|
+
add: (id) => void ids.add(id),
|
|
26
|
+
remove: (id) => void ids.delete(id),
|
|
27
|
+
toggle: (id) => (ids.has(id) ? ids.delete(id) : ids.add(id)),
|
|
28
|
+
has: (id) => ids.has(id),
|
|
29
|
+
replace(next) {
|
|
30
|
+
ids.clear();
|
|
31
|
+
for (const id of next)
|
|
32
|
+
ids.add(id);
|
|
33
|
+
},
|
|
34
|
+
clear: () => ids.clear(),
|
|
35
|
+
list: () => Array.from(ids),
|
|
36
|
+
size: () => ids.size,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MountController, type MountKit } from "./mount.js";
|
|
2
|
+
export interface Station {
|
|
3
|
+
id: string;
|
|
4
|
+
facet: string;
|
|
5
|
+
offset: readonly [number, number, number];
|
|
6
|
+
control?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SharedVehicleConfig {
|
|
9
|
+
id: string;
|
|
10
|
+
kit: MountKit;
|
|
11
|
+
stations: readonly Station[];
|
|
12
|
+
}
|
|
13
|
+
export type ClaimResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
station: Station;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
reason: "unknown_vehicle" | "unknown_facet" | "station_taken" | "vehicle_full";
|
|
19
|
+
};
|
|
20
|
+
export declare class StationClaim {
|
|
21
|
+
private readonly controller;
|
|
22
|
+
private readonly vehicles;
|
|
23
|
+
constructor(controller?: MountController);
|
|
24
|
+
register(config: SharedVehicleConfig): void;
|
|
25
|
+
private resolveStation;
|
|
26
|
+
claim(playerId: string, vehicleId: string, facetOrStationId: string): ClaimResult;
|
|
27
|
+
release(playerId: string): string | null;
|
|
28
|
+
facetOf(playerId: string): string | null;
|
|
29
|
+
stationOf(playerId: string): Station | null;
|
|
30
|
+
controllerOf(vehicleId: string, facet: string): string | null;
|
|
31
|
+
crew(vehicleId: string): {
|
|
32
|
+
playerId: string;
|
|
33
|
+
stationId: string;
|
|
34
|
+
facet: string;
|
|
35
|
+
}[];
|
|
36
|
+
stationsOf(vehicleId: string): readonly Station[];
|
|
37
|
+
openFacets(vehicleId: string): string[];
|
|
38
|
+
driver(vehicleId: string): string | null;
|
|
39
|
+
cameraTarget(playerId: string): string;
|
|
40
|
+
driveTarget(playerId: string): string | null;
|
|
41
|
+
get mounts(): MountController;
|
|
42
|
+
}
|
|
43
|
+
export declare function createStationClaim(controller?: MountController): StationClaim;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createMountController, MountController } from "./mount.js";
|
|
2
|
+
export class StationClaim {
|
|
3
|
+
controller;
|
|
4
|
+
vehicles = new Map();
|
|
5
|
+
constructor(controller) {
|
|
6
|
+
this.controller = controller ?? createMountController();
|
|
7
|
+
}
|
|
8
|
+
register(config) {
|
|
9
|
+
const byStationId = new Map();
|
|
10
|
+
const facetToStation = new Map();
|
|
11
|
+
for (const station of config.stations) {
|
|
12
|
+
byStationId.set(station.id, station);
|
|
13
|
+
if (!facetToStation.has(station.facet))
|
|
14
|
+
facetToStation.set(station.facet, station.id);
|
|
15
|
+
}
|
|
16
|
+
this.vehicles.set(config.id, { config, byStationId, facetToStation });
|
|
17
|
+
this.controller.register({
|
|
18
|
+
id: config.id,
|
|
19
|
+
kit: config.kit,
|
|
20
|
+
seats: config.stations.map((station) => ({
|
|
21
|
+
id: station.id,
|
|
22
|
+
offset: station.offset,
|
|
23
|
+
control: station.control,
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
resolveStation(vehicle, facetOrStationId) {
|
|
28
|
+
const direct = vehicle.byStationId.get(facetOrStationId);
|
|
29
|
+
if (direct !== undefined)
|
|
30
|
+
return direct;
|
|
31
|
+
const stationId = vehicle.facetToStation.get(facetOrStationId);
|
|
32
|
+
if (stationId === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
return vehicle.byStationId.get(stationId) ?? null;
|
|
35
|
+
}
|
|
36
|
+
claim(playerId, vehicleId, facetOrStationId) {
|
|
37
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
38
|
+
if (vehicle === undefined)
|
|
39
|
+
return { ok: false, reason: "unknown_vehicle" };
|
|
40
|
+
const station = this.resolveStation(vehicle, facetOrStationId);
|
|
41
|
+
if (station === null)
|
|
42
|
+
return { ok: false, reason: "unknown_facet" };
|
|
43
|
+
const result = this.controller.mount(playerId, vehicleId, station.id);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
return { ok: false, reason: result.reason === "seat_taken" ? "station_taken" : "vehicle_full" };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, station };
|
|
48
|
+
}
|
|
49
|
+
release(playerId) {
|
|
50
|
+
return this.controller.dismount(playerId);
|
|
51
|
+
}
|
|
52
|
+
facetOf(playerId) {
|
|
53
|
+
const ref = this.controller.seatOf(playerId);
|
|
54
|
+
if (ref === null)
|
|
55
|
+
return null;
|
|
56
|
+
return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId)?.facet ?? null;
|
|
57
|
+
}
|
|
58
|
+
stationOf(playerId) {
|
|
59
|
+
const ref = this.controller.seatOf(playerId);
|
|
60
|
+
if (ref === null)
|
|
61
|
+
return null;
|
|
62
|
+
return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
controllerOf(vehicleId, facet) {
|
|
65
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
66
|
+
if (vehicle === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
for (const occupant of this.controller.occupants(vehicleId)) {
|
|
69
|
+
if (vehicle.byStationId.get(occupant.seatId)?.facet === facet)
|
|
70
|
+
return occupant.riderId;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
crew(vehicleId) {
|
|
75
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
76
|
+
if (vehicle === undefined)
|
|
77
|
+
return [];
|
|
78
|
+
return this.controller.occupants(vehicleId).map((occupant) => ({
|
|
79
|
+
playerId: occupant.riderId,
|
|
80
|
+
stationId: occupant.seatId,
|
|
81
|
+
facet: vehicle.byStationId.get(occupant.seatId)?.facet ?? "",
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
stationsOf(vehicleId) {
|
|
85
|
+
return this.vehicles.get(vehicleId)?.config.stations ?? [];
|
|
86
|
+
}
|
|
87
|
+
openFacets(vehicleId) {
|
|
88
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
89
|
+
if (vehicle === undefined)
|
|
90
|
+
return [];
|
|
91
|
+
const taken = new Set(this.controller.occupants(vehicleId).map((o) => o.seatId));
|
|
92
|
+
return vehicle.config.stations.filter((s) => !taken.has(s.id)).map((s) => s.facet);
|
|
93
|
+
}
|
|
94
|
+
driver(vehicleId) {
|
|
95
|
+
return this.controller.driver(vehicleId);
|
|
96
|
+
}
|
|
97
|
+
cameraTarget(playerId) {
|
|
98
|
+
return this.controller.cameraTarget(playerId);
|
|
99
|
+
}
|
|
100
|
+
driveTarget(playerId) {
|
|
101
|
+
return this.controller.driveTarget(playerId);
|
|
102
|
+
}
|
|
103
|
+
get mounts() {
|
|
104
|
+
return this.controller;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createStationClaim(controller) {
|
|
108
|
+
return new StationClaim(controller);
|
|
109
|
+
}
|