@bct-app/game-engine 0.1.1
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/README.md +23 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +409 -0
- package/dist/index.mjs +371 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @bct-app/game-engine
|
|
2
|
+
|
|
3
|
+
Game engine utilities for BCT.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @bct-app/game-engine
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Build
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm --filter @bct-app/game-engine build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Exports
|
|
18
|
+
|
|
19
|
+
- `applyOperationToPlayers`
|
|
20
|
+
- source/payload resolve utilities
|
|
21
|
+
- player copy/map utilities
|
|
22
|
+
- runtime type guards
|
|
23
|
+
- engine-related type definitions
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
type TAlignment = 'GOOD' | 'EVIL';
|
|
2
|
+
type TReminder = {
|
|
3
|
+
mark: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
duration?: number;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
type TPlayerReminder = {
|
|
9
|
+
playerSeat: number;
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
12
|
+
type TPerceivedCharacter = {
|
|
13
|
+
characterId: string;
|
|
14
|
+
abilities: string[];
|
|
15
|
+
asCharacter: boolean;
|
|
16
|
+
};
|
|
17
|
+
type TPopulatedPlayer = {
|
|
18
|
+
seat: number;
|
|
19
|
+
abilities: string[];
|
|
20
|
+
reminders?: TReminder[];
|
|
21
|
+
alignment?: TAlignment;
|
|
22
|
+
characterId?: string;
|
|
23
|
+
perceivedCharacter?: TPerceivedCharacter;
|
|
24
|
+
isDead?: boolean;
|
|
25
|
+
hasUsedDeadVote?: boolean;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
type TAbilityInput = {
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
type?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
type TPayloadSource = {
|
|
34
|
+
from: 'PAYLOAD';
|
|
35
|
+
value: number | number[];
|
|
36
|
+
};
|
|
37
|
+
type TConstantSource = {
|
|
38
|
+
from: 'CONSTANT';
|
|
39
|
+
value: unknown;
|
|
40
|
+
};
|
|
41
|
+
type TEffectorSource = {
|
|
42
|
+
from: 'EFFECTOR';
|
|
43
|
+
value?: unknown;
|
|
44
|
+
};
|
|
45
|
+
type TOtherSource = {
|
|
46
|
+
from: Exclude<string, 'PAYLOAD' | 'CONSTANT' | 'EFFECTOR'>;
|
|
47
|
+
value?: unknown;
|
|
48
|
+
};
|
|
49
|
+
type TSource = TPayloadSource | TConstantSource | TEffectorSource | TOtherSource;
|
|
50
|
+
type TAlignmentSource = TSource;
|
|
51
|
+
type TCharacterSource = TSource;
|
|
52
|
+
type TTarget = {
|
|
53
|
+
from: 'EFFECTOR' | 'PAYLOAD' | 'CUSTOM' | string;
|
|
54
|
+
value: unknown;
|
|
55
|
+
};
|
|
56
|
+
type TEffect = {
|
|
57
|
+
type: string;
|
|
58
|
+
target: TTarget;
|
|
59
|
+
source: TSource;
|
|
60
|
+
followPriority?: boolean;
|
|
61
|
+
};
|
|
62
|
+
type TAbility = {
|
|
63
|
+
id: string;
|
|
64
|
+
inputs: TAbilityInput[];
|
|
65
|
+
effects: TEffect[];
|
|
66
|
+
};
|
|
67
|
+
type TPopulatedCharacter = {
|
|
68
|
+
id: string;
|
|
69
|
+
abilities: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
type TOperation = {
|
|
74
|
+
abilityId: string;
|
|
75
|
+
effector: number;
|
|
76
|
+
payloads?: unknown[];
|
|
77
|
+
};
|
|
78
|
+
type TTimelineNomination = {
|
|
79
|
+
voterSeats?: number[];
|
|
80
|
+
};
|
|
81
|
+
type TTimeline = {
|
|
82
|
+
operations?: TOperation[];
|
|
83
|
+
nominations?: TTimelineNomination[];
|
|
84
|
+
};
|
|
85
|
+
type TApplyOperationToPlayersArgs = {
|
|
86
|
+
players: TPopulatedPlayer[];
|
|
87
|
+
operations: TOperation[];
|
|
88
|
+
allAbilities: TAbility[];
|
|
89
|
+
characters: TPopulatedCharacter[];
|
|
90
|
+
timelines: TTimeline[];
|
|
91
|
+
timelineIndexAtNow?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
declare const applyOperationToPlayers: ({ players, operations, allAbilities, characters, timelines, timelineIndexAtNow, }: TApplyOperationToPlayersArgs) => TPopulatedPlayer[];
|
|
95
|
+
|
|
96
|
+
declare const transformEmptyArray: <T>(arr: T[]) => T[];
|
|
97
|
+
declare const copyPlayers: (players: TPopulatedPlayer[]) => TPopulatedPlayer[];
|
|
98
|
+
declare const buildPlayerSeatMap: (players: TPopulatedPlayer[]) => Map<number, TPopulatedPlayer>;
|
|
99
|
+
declare const adjustValueAsNumberArray: (value: unknown) => number[];
|
|
100
|
+
declare const adjustValueAsNumber: (value: unknown) => number | undefined;
|
|
101
|
+
|
|
102
|
+
declare const getValueFromPayloads: (payloads: unknown[], idx: number | number[]) => unknown;
|
|
103
|
+
declare const getSourceValue: (source: TSource, payloads: unknown[]) => unknown;
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a value from effect source, handling PAYLOAD (with player-seat indirection), CONSTANT, and EFFECTOR.
|
|
106
|
+
*/
|
|
107
|
+
declare const resolveSourceValue: (source: TAlignmentSource | TCharacterSource, writableParts: TAbility["inputs"], payloads: unknown[], effector: TPopulatedPlayer | undefined, snapshotSeatMap: Map<number, TPopulatedPlayer>, resolveFromPlayer: (player: TPopulatedPlayer) => unknown) => unknown;
|
|
108
|
+
|
|
109
|
+
declare const isNumberOrNumberArray: (value: unknown) => value is number | number[];
|
|
110
|
+
declare const isReminder: (value: unknown) => value is TReminder;
|
|
111
|
+
declare const isPlayerReminderArray: (value: unknown) => value is TPlayerReminder[];
|
|
112
|
+
|
|
113
|
+
export { type TAbility, type TAbilityInput, type TAlignment, type TAlignmentSource, type TApplyOperationToPlayersArgs, type TCharacterSource, type TEffect, type TOperation, type TPerceivedCharacter, type TPlayerReminder, type TPopulatedCharacter, type TPopulatedPlayer, type TReminder, type TSource, type TTarget, type TTimeline, type TTimelineNomination, adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
type TAlignment = 'GOOD' | 'EVIL';
|
|
2
|
+
type TReminder = {
|
|
3
|
+
mark: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
duration?: number;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
type TPlayerReminder = {
|
|
9
|
+
playerSeat: number;
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
12
|
+
type TPerceivedCharacter = {
|
|
13
|
+
characterId: string;
|
|
14
|
+
abilities: string[];
|
|
15
|
+
asCharacter: boolean;
|
|
16
|
+
};
|
|
17
|
+
type TPopulatedPlayer = {
|
|
18
|
+
seat: number;
|
|
19
|
+
abilities: string[];
|
|
20
|
+
reminders?: TReminder[];
|
|
21
|
+
alignment?: TAlignment;
|
|
22
|
+
characterId?: string;
|
|
23
|
+
perceivedCharacter?: TPerceivedCharacter;
|
|
24
|
+
isDead?: boolean;
|
|
25
|
+
hasUsedDeadVote?: boolean;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
type TAbilityInput = {
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
type?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
type TPayloadSource = {
|
|
34
|
+
from: 'PAYLOAD';
|
|
35
|
+
value: number | number[];
|
|
36
|
+
};
|
|
37
|
+
type TConstantSource = {
|
|
38
|
+
from: 'CONSTANT';
|
|
39
|
+
value: unknown;
|
|
40
|
+
};
|
|
41
|
+
type TEffectorSource = {
|
|
42
|
+
from: 'EFFECTOR';
|
|
43
|
+
value?: unknown;
|
|
44
|
+
};
|
|
45
|
+
type TOtherSource = {
|
|
46
|
+
from: Exclude<string, 'PAYLOAD' | 'CONSTANT' | 'EFFECTOR'>;
|
|
47
|
+
value?: unknown;
|
|
48
|
+
};
|
|
49
|
+
type TSource = TPayloadSource | TConstantSource | TEffectorSource | TOtherSource;
|
|
50
|
+
type TAlignmentSource = TSource;
|
|
51
|
+
type TCharacterSource = TSource;
|
|
52
|
+
type TTarget = {
|
|
53
|
+
from: 'EFFECTOR' | 'PAYLOAD' | 'CUSTOM' | string;
|
|
54
|
+
value: unknown;
|
|
55
|
+
};
|
|
56
|
+
type TEffect = {
|
|
57
|
+
type: string;
|
|
58
|
+
target: TTarget;
|
|
59
|
+
source: TSource;
|
|
60
|
+
followPriority?: boolean;
|
|
61
|
+
};
|
|
62
|
+
type TAbility = {
|
|
63
|
+
id: string;
|
|
64
|
+
inputs: TAbilityInput[];
|
|
65
|
+
effects: TEffect[];
|
|
66
|
+
};
|
|
67
|
+
type TPopulatedCharacter = {
|
|
68
|
+
id: string;
|
|
69
|
+
abilities: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
type TOperation = {
|
|
74
|
+
abilityId: string;
|
|
75
|
+
effector: number;
|
|
76
|
+
payloads?: unknown[];
|
|
77
|
+
};
|
|
78
|
+
type TTimelineNomination = {
|
|
79
|
+
voterSeats?: number[];
|
|
80
|
+
};
|
|
81
|
+
type TTimeline = {
|
|
82
|
+
operations?: TOperation[];
|
|
83
|
+
nominations?: TTimelineNomination[];
|
|
84
|
+
};
|
|
85
|
+
type TApplyOperationToPlayersArgs = {
|
|
86
|
+
players: TPopulatedPlayer[];
|
|
87
|
+
operations: TOperation[];
|
|
88
|
+
allAbilities: TAbility[];
|
|
89
|
+
characters: TPopulatedCharacter[];
|
|
90
|
+
timelines: TTimeline[];
|
|
91
|
+
timelineIndexAtNow?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
declare const applyOperationToPlayers: ({ players, operations, allAbilities, characters, timelines, timelineIndexAtNow, }: TApplyOperationToPlayersArgs) => TPopulatedPlayer[];
|
|
95
|
+
|
|
96
|
+
declare const transformEmptyArray: <T>(arr: T[]) => T[];
|
|
97
|
+
declare const copyPlayers: (players: TPopulatedPlayer[]) => TPopulatedPlayer[];
|
|
98
|
+
declare const buildPlayerSeatMap: (players: TPopulatedPlayer[]) => Map<number, TPopulatedPlayer>;
|
|
99
|
+
declare const adjustValueAsNumberArray: (value: unknown) => number[];
|
|
100
|
+
declare const adjustValueAsNumber: (value: unknown) => number | undefined;
|
|
101
|
+
|
|
102
|
+
declare const getValueFromPayloads: (payloads: unknown[], idx: number | number[]) => unknown;
|
|
103
|
+
declare const getSourceValue: (source: TSource, payloads: unknown[]) => unknown;
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a value from effect source, handling PAYLOAD (with player-seat indirection), CONSTANT, and EFFECTOR.
|
|
106
|
+
*/
|
|
107
|
+
declare const resolveSourceValue: (source: TAlignmentSource | TCharacterSource, writableParts: TAbility["inputs"], payloads: unknown[], effector: TPopulatedPlayer | undefined, snapshotSeatMap: Map<number, TPopulatedPlayer>, resolveFromPlayer: (player: TPopulatedPlayer) => unknown) => unknown;
|
|
108
|
+
|
|
109
|
+
declare const isNumberOrNumberArray: (value: unknown) => value is number | number[];
|
|
110
|
+
declare const isReminder: (value: unknown) => value is TReminder;
|
|
111
|
+
declare const isPlayerReminderArray: (value: unknown) => value is TPlayerReminder[];
|
|
112
|
+
|
|
113
|
+
export { type TAbility, type TAbilityInput, type TAlignment, type TAlignmentSource, type TApplyOperationToPlayersArgs, type TCharacterSource, type TEffect, type TOperation, type TPerceivedCharacter, type TPlayerReminder, type TPopulatedCharacter, type TPopulatedPlayer, type TReminder, type TSource, type TTarget, type TTimeline, type TTimelineNomination, adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
adjustValueAsNumber: () => adjustValueAsNumber,
|
|
24
|
+
adjustValueAsNumberArray: () => adjustValueAsNumberArray,
|
|
25
|
+
applyOperationToPlayers: () => applyOperationToPlayers,
|
|
26
|
+
buildPlayerSeatMap: () => buildPlayerSeatMap,
|
|
27
|
+
copyPlayers: () => copyPlayers,
|
|
28
|
+
getSourceValue: () => getSourceValue,
|
|
29
|
+
getValueFromPayloads: () => getValueFromPayloads,
|
|
30
|
+
isNumberOrNumberArray: () => isNumberOrNumberArray,
|
|
31
|
+
isPlayerReminderArray: () => isPlayerReminderArray,
|
|
32
|
+
isReminder: () => isReminder,
|
|
33
|
+
resolveSourceValue: () => resolveSourceValue,
|
|
34
|
+
transformEmptyArray: () => transformEmptyArray
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/guards.ts
|
|
39
|
+
var isNumberOrNumberArray = (value) => {
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.every((item) => typeof item === "number");
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
var isReminder = (value) => {
|
|
49
|
+
if (!value || typeof value !== "object") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const candidate = value;
|
|
53
|
+
if (typeof candidate.mark !== "string") {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (candidate.color !== void 0 && typeof candidate.color !== "string") {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (candidate.duration !== void 0 && typeof candidate.duration !== "number") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
var isPlayerReminderArray = (value) => {
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return value.every((item) => {
|
|
69
|
+
if (!item || typeof item !== "object") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const candidate = item;
|
|
73
|
+
return typeof candidate.playerSeat === "number" && typeof candidate.index === "number";
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/player-utils.ts
|
|
78
|
+
var transformEmptyArray = (arr) => {
|
|
79
|
+
return arr.length === 0 ? [] : arr;
|
|
80
|
+
};
|
|
81
|
+
var copyPlayers = (players) => {
|
|
82
|
+
return players.map((player) => ({
|
|
83
|
+
...player,
|
|
84
|
+
abilities: [...player.abilities],
|
|
85
|
+
reminders: [...player.reminders || []]
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
var buildPlayerSeatMap = (players) => {
|
|
89
|
+
const map = /* @__PURE__ */ new Map();
|
|
90
|
+
players.forEach((player) => map.set(player.seat, player));
|
|
91
|
+
return map;
|
|
92
|
+
};
|
|
93
|
+
var adjustValueAsNumberArray = (value) => {
|
|
94
|
+
if (isNumberOrNumberArray(value)) {
|
|
95
|
+
return typeof value === "number" ? [value] : value;
|
|
96
|
+
}
|
|
97
|
+
return [];
|
|
98
|
+
};
|
|
99
|
+
var adjustValueAsNumber = (value) => {
|
|
100
|
+
if (isNumberOrNumberArray(value)) {
|
|
101
|
+
return typeof value === "number" ? value : value[0] || void 0;
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/source-utils.ts
|
|
107
|
+
var getValueFromPayloads = (payloads, idx) => {
|
|
108
|
+
if (Array.isArray(idx)) {
|
|
109
|
+
if (idx.length < 2) {
|
|
110
|
+
return payloads[idx[0]];
|
|
111
|
+
}
|
|
112
|
+
const values = payloads[idx[0]];
|
|
113
|
+
const seatIdx = idx[1];
|
|
114
|
+
if (Array.isArray(values)) {
|
|
115
|
+
return values[seatIdx];
|
|
116
|
+
}
|
|
117
|
+
console.warn("Expected array in payloads for multiple indices, got:", values);
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
return payloads[idx];
|
|
121
|
+
};
|
|
122
|
+
var getSourceValue = (source, payloads) => {
|
|
123
|
+
if (source.from === "PAYLOAD") {
|
|
124
|
+
if (!isNumberOrNumberArray(source.value)) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return getValueFromPayloads(payloads, source.value);
|
|
128
|
+
}
|
|
129
|
+
if (source.from === "CONSTANT") {
|
|
130
|
+
return source.value;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
var resolveSourceValue = (source, writableParts, payloads, effector, snapshotSeatMap, resolveFromPlayer) => {
|
|
135
|
+
if (source.from === "PAYLOAD") {
|
|
136
|
+
const payloadValue = getSourceValue(source, payloads);
|
|
137
|
+
const idx = adjustValueAsNumberArray(source.value);
|
|
138
|
+
const part = writableParts[idx[0]];
|
|
139
|
+
if (part?.type === "PLAYER") {
|
|
140
|
+
const seat = adjustValueAsNumber(payloadValue);
|
|
141
|
+
const player = typeof seat === "number" ? snapshotSeatMap.get(seat) : void 0;
|
|
142
|
+
return player ? resolveFromPlayer(player) : void 0;
|
|
143
|
+
}
|
|
144
|
+
return payloadValue;
|
|
145
|
+
}
|
|
146
|
+
if (source.from === "CONSTANT") {
|
|
147
|
+
return source.value;
|
|
148
|
+
}
|
|
149
|
+
if (source.from === "EFFECTOR") {
|
|
150
|
+
if (!effector) {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
const player = snapshotSeatMap.get(effector.seat);
|
|
154
|
+
return player ? resolveFromPlayer(player) : void 0;
|
|
155
|
+
}
|
|
156
|
+
return void 0;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/apply-operation.ts
|
|
160
|
+
var applyOperationToPlayers = ({
|
|
161
|
+
players,
|
|
162
|
+
operations,
|
|
163
|
+
allAbilities,
|
|
164
|
+
characters,
|
|
165
|
+
timelines,
|
|
166
|
+
timelineIndexAtNow
|
|
167
|
+
}) => {
|
|
168
|
+
if (operations.length === 0) {
|
|
169
|
+
return transformEmptyArray(players);
|
|
170
|
+
}
|
|
171
|
+
const abilityMap = new Map(allAbilities.map((ability) => [ability.id, ability]));
|
|
172
|
+
const characterMap = new Map(characters.map((character) => [character.id, character]));
|
|
173
|
+
const operationTimelineMap = /* @__PURE__ */ new Map();
|
|
174
|
+
timelines.forEach((timeline, idx) => timeline.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
|
|
175
|
+
const playersWithStatus = copyPlayers(players);
|
|
176
|
+
const nowTimelineIndex = typeof timelineIndexAtNow === "number" ? timelineIndexAtNow : timelines.length - 1;
|
|
177
|
+
const operationsByTimeline = /* @__PURE__ */ new Map();
|
|
178
|
+
operations.forEach((operation) => {
|
|
179
|
+
const timelineIndex = operationTimelineMap.get(operation) ?? -1;
|
|
180
|
+
if (!operationsByTimeline.has(timelineIndex)) {
|
|
181
|
+
operationsByTimeline.set(timelineIndex, []);
|
|
182
|
+
}
|
|
183
|
+
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
184
|
+
});
|
|
185
|
+
const applyOps = (ops) => {
|
|
186
|
+
ops.forEach((operation) => {
|
|
187
|
+
const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
|
|
188
|
+
const ability = abilityMap.get(operation.abilityId);
|
|
189
|
+
if (!ability) {
|
|
190
|
+
console.warn("Ability not found for operation:", operation);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
194
|
+
const effector = playerSeatMap.get(operation.effector);
|
|
195
|
+
const payloads = operation.payloads || [];
|
|
196
|
+
const writableParts = ability.inputs.filter((part) => !part.readonly);
|
|
197
|
+
let snapshotSeatMap = null;
|
|
198
|
+
const getSnapshotSeatMap = () => {
|
|
199
|
+
if (!snapshotSeatMap) {
|
|
200
|
+
snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
|
|
201
|
+
}
|
|
202
|
+
return snapshotSeatMap;
|
|
203
|
+
};
|
|
204
|
+
ability.effects.forEach((effect) => {
|
|
205
|
+
const makePlayersEffect = [];
|
|
206
|
+
if (effect.type === "GAME_CHANGE" || effect.type === "CHARACTER_COUNT_CHANGE") {
|
|
207
|
+
console.warn(effect.type + " effect handling not implemented yet.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (effect.target.from === "EFFECTOR") {
|
|
211
|
+
if (!effector) {
|
|
212
|
+
console.warn("Effector player not found for operation:", operation, "effector index:", operation.effector);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
makePlayersEffect.push(effector);
|
|
216
|
+
} else if (effect.target.from === "PAYLOAD") {
|
|
217
|
+
const seatValue = getValueFromPayloads(payloads, effect.target.value);
|
|
218
|
+
if (typeof seatValue !== "number" && !Array.isArray(seatValue)) {
|
|
219
|
+
console.warn("Expected seat number or array of seat numbers from payloads, got:", seatValue);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const seats = Array.isArray(seatValue) ? seatValue : [seatValue];
|
|
223
|
+
seats.forEach((seat) => {
|
|
224
|
+
const player = playerSeatMap.get(seat);
|
|
225
|
+
if (player) {
|
|
226
|
+
makePlayersEffect.push(player);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
} else if (effect.target.from === "CUSTOM") {
|
|
230
|
+
console.warn("Custom target handling not implemented yet.");
|
|
231
|
+
}
|
|
232
|
+
switch (effect.type) {
|
|
233
|
+
case "ABILITY_CHANGE": {
|
|
234
|
+
console.warn("ABILITY_CHANGE effect handling not implemented yet.");
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "ALIGNMENT_CHANGE": {
|
|
238
|
+
const resolved = resolveSourceValue(
|
|
239
|
+
effect.source,
|
|
240
|
+
writableParts,
|
|
241
|
+
payloads,
|
|
242
|
+
effector,
|
|
243
|
+
getSnapshotSeatMap(),
|
|
244
|
+
(player) => player.alignment
|
|
245
|
+
);
|
|
246
|
+
let alignment;
|
|
247
|
+
if (resolved === "GOOD" || resolved === "EVIL") {
|
|
248
|
+
alignment = resolved;
|
|
249
|
+
}
|
|
250
|
+
if (!alignment) {
|
|
251
|
+
console.warn("Alignment not found for ALIGNMENT_CHANGE effect:", effect.source);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
makePlayersEffect.forEach((player) => {
|
|
255
|
+
player.alignment = alignment;
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "CHARACTER_CHANGE":
|
|
260
|
+
case "PERCEIVED_CHARACTER_CHANGE": {
|
|
261
|
+
const resolvedCharacterId = resolveSourceValue(
|
|
262
|
+
effect.source,
|
|
263
|
+
writableParts,
|
|
264
|
+
payloads,
|
|
265
|
+
effector,
|
|
266
|
+
getSnapshotSeatMap(),
|
|
267
|
+
(player) => player.characterId
|
|
268
|
+
);
|
|
269
|
+
let characterId;
|
|
270
|
+
if (typeof resolvedCharacterId === "string") {
|
|
271
|
+
characterId = resolvedCharacterId;
|
|
272
|
+
} else if (Array.isArray(resolvedCharacterId) && typeof resolvedCharacterId[0] === "string") {
|
|
273
|
+
characterId = resolvedCharacterId[0];
|
|
274
|
+
}
|
|
275
|
+
if (!characterId) {
|
|
276
|
+
console.warn("Character ID not found for " + effect.type + " effect:", effect.source);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const newCharacter = characterMap.get(characterId);
|
|
280
|
+
if (!newCharacter) {
|
|
281
|
+
console.warn("Character not found for " + effect.type + " effect:", characterId);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (effect.type === "CHARACTER_CHANGE") {
|
|
285
|
+
const abilityIds = newCharacter.abilities.map((ability2) => ability2.id);
|
|
286
|
+
makePlayersEffect.forEach((player) => {
|
|
287
|
+
player.characterId = newCharacter.id;
|
|
288
|
+
player.abilities = abilityIds;
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
makePlayersEffect.forEach((player) => {
|
|
292
|
+
player.perceivedCharacter = {
|
|
293
|
+
characterId: newCharacter.id,
|
|
294
|
+
abilities: newCharacter.abilities.map((ability2) => ability2.id),
|
|
295
|
+
asCharacter: effect.followPriority || false
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "REMINDER_ADD": {
|
|
302
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
303
|
+
if (!isReminder(maybeReminder)) {
|
|
304
|
+
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
308
|
+
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
makePlayersEffect.forEach((player) => {
|
|
312
|
+
if (!player.reminders) {
|
|
313
|
+
player.reminders = [];
|
|
314
|
+
}
|
|
315
|
+
player.reminders.push(maybeReminder);
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "REMINDER_REMOVE": {
|
|
320
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
321
|
+
if (!isPlayerReminderArray(maybeReminder)) {
|
|
322
|
+
console.warn("Invalid reminder data for REMINDER_REMOVE effect:", maybeReminder);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
maybeReminder.sort((left, right) => right.index - left.index).forEach((reminder) => {
|
|
326
|
+
const player = playerSeatMap.get(reminder.playerSeat);
|
|
327
|
+
if (!player) {
|
|
328
|
+
console.warn("Player not found for REMINDER_REMOVE effect:", reminder.playerSeat);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!player.reminders) {
|
|
332
|
+
player.reminders = [];
|
|
333
|
+
}
|
|
334
|
+
player.reminders = player.reminders.filter((_value, index) => index !== reminder.index);
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "SEAT_CHANGE": {
|
|
339
|
+
console.warn("SEAT_CHANGE effect handling not implemented yet.");
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case "STATUS_CHANGE": {
|
|
343
|
+
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
344
|
+
if (maybeStatus === "DEAD") {
|
|
345
|
+
makePlayersEffect.forEach((player) => {
|
|
346
|
+
player.isDead = true;
|
|
347
|
+
});
|
|
348
|
+
} else if (maybeStatus === "ALIVE") {
|
|
349
|
+
makePlayersEffect.forEach((player) => {
|
|
350
|
+
player.isDead = false;
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
console.warn("Invalid status value for STATUS_CHANGE effect:", maybeStatus);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
default: {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const settingsOps = operationsByTimeline.get(-1);
|
|
364
|
+
if (settingsOps) {
|
|
365
|
+
applyOps(settingsOps);
|
|
366
|
+
}
|
|
367
|
+
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
368
|
+
const timeline = timelines[i];
|
|
369
|
+
if (!timeline) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const nominations = timeline.nominations;
|
|
373
|
+
if (nominations && nominations.length > 0) {
|
|
374
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
375
|
+
nominations.forEach((nomination) => {
|
|
376
|
+
const voterSeats = nomination.voterSeats;
|
|
377
|
+
if (!voterSeats || voterSeats.length === 0) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
voterSeats.forEach((seat) => {
|
|
381
|
+
const player = playerSeatMap.get(seat);
|
|
382
|
+
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
383
|
+
player.hasUsedDeadVote = true;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const timelineOps = operationsByTimeline.get(i);
|
|
389
|
+
if (timelineOps) {
|
|
390
|
+
applyOps(timelineOps);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return transformEmptyArray(playersWithStatus);
|
|
394
|
+
};
|
|
395
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
396
|
+
0 && (module.exports = {
|
|
397
|
+
adjustValueAsNumber,
|
|
398
|
+
adjustValueAsNumberArray,
|
|
399
|
+
applyOperationToPlayers,
|
|
400
|
+
buildPlayerSeatMap,
|
|
401
|
+
copyPlayers,
|
|
402
|
+
getSourceValue,
|
|
403
|
+
getValueFromPayloads,
|
|
404
|
+
isNumberOrNumberArray,
|
|
405
|
+
isPlayerReminderArray,
|
|
406
|
+
isReminder,
|
|
407
|
+
resolveSourceValue,
|
|
408
|
+
transformEmptyArray
|
|
409
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// src/guards.ts
|
|
2
|
+
var isNumberOrNumberArray = (value) => {
|
|
3
|
+
if (typeof value === "number") {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.every((item) => typeof item === "number");
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
};
|
|
11
|
+
var isReminder = (value) => {
|
|
12
|
+
if (!value || typeof value !== "object") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const candidate = value;
|
|
16
|
+
if (typeof candidate.mark !== "string") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (candidate.color !== void 0 && typeof candidate.color !== "string") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (candidate.duration !== void 0 && typeof candidate.duration !== "number") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
var isPlayerReminderArray = (value) => {
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return value.every((item) => {
|
|
32
|
+
if (!item || typeof item !== "object") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const candidate = item;
|
|
36
|
+
return typeof candidate.playerSeat === "number" && typeof candidate.index === "number";
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/player-utils.ts
|
|
41
|
+
var transformEmptyArray = (arr) => {
|
|
42
|
+
return arr.length === 0 ? [] : arr;
|
|
43
|
+
};
|
|
44
|
+
var copyPlayers = (players) => {
|
|
45
|
+
return players.map((player) => ({
|
|
46
|
+
...player,
|
|
47
|
+
abilities: [...player.abilities],
|
|
48
|
+
reminders: [...player.reminders || []]
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
var buildPlayerSeatMap = (players) => {
|
|
52
|
+
const map = /* @__PURE__ */ new Map();
|
|
53
|
+
players.forEach((player) => map.set(player.seat, player));
|
|
54
|
+
return map;
|
|
55
|
+
};
|
|
56
|
+
var adjustValueAsNumberArray = (value) => {
|
|
57
|
+
if (isNumberOrNumberArray(value)) {
|
|
58
|
+
return typeof value === "number" ? [value] : value;
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
};
|
|
62
|
+
var adjustValueAsNumber = (value) => {
|
|
63
|
+
if (isNumberOrNumberArray(value)) {
|
|
64
|
+
return typeof value === "number" ? value : value[0] || void 0;
|
|
65
|
+
}
|
|
66
|
+
return void 0;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/source-utils.ts
|
|
70
|
+
var getValueFromPayloads = (payloads, idx) => {
|
|
71
|
+
if (Array.isArray(idx)) {
|
|
72
|
+
if (idx.length < 2) {
|
|
73
|
+
return payloads[idx[0]];
|
|
74
|
+
}
|
|
75
|
+
const values = payloads[idx[0]];
|
|
76
|
+
const seatIdx = idx[1];
|
|
77
|
+
if (Array.isArray(values)) {
|
|
78
|
+
return values[seatIdx];
|
|
79
|
+
}
|
|
80
|
+
console.warn("Expected array in payloads for multiple indices, got:", values);
|
|
81
|
+
return values;
|
|
82
|
+
}
|
|
83
|
+
return payloads[idx];
|
|
84
|
+
};
|
|
85
|
+
var getSourceValue = (source, payloads) => {
|
|
86
|
+
if (source.from === "PAYLOAD") {
|
|
87
|
+
if (!isNumberOrNumberArray(source.value)) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return getValueFromPayloads(payloads, source.value);
|
|
91
|
+
}
|
|
92
|
+
if (source.from === "CONSTANT") {
|
|
93
|
+
return source.value;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
var resolveSourceValue = (source, writableParts, payloads, effector, snapshotSeatMap, resolveFromPlayer) => {
|
|
98
|
+
if (source.from === "PAYLOAD") {
|
|
99
|
+
const payloadValue = getSourceValue(source, payloads);
|
|
100
|
+
const idx = adjustValueAsNumberArray(source.value);
|
|
101
|
+
const part = writableParts[idx[0]];
|
|
102
|
+
if (part?.type === "PLAYER") {
|
|
103
|
+
const seat = adjustValueAsNumber(payloadValue);
|
|
104
|
+
const player = typeof seat === "number" ? snapshotSeatMap.get(seat) : void 0;
|
|
105
|
+
return player ? resolveFromPlayer(player) : void 0;
|
|
106
|
+
}
|
|
107
|
+
return payloadValue;
|
|
108
|
+
}
|
|
109
|
+
if (source.from === "CONSTANT") {
|
|
110
|
+
return source.value;
|
|
111
|
+
}
|
|
112
|
+
if (source.from === "EFFECTOR") {
|
|
113
|
+
if (!effector) {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
const player = snapshotSeatMap.get(effector.seat);
|
|
117
|
+
return player ? resolveFromPlayer(player) : void 0;
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/apply-operation.ts
|
|
123
|
+
var applyOperationToPlayers = ({
|
|
124
|
+
players,
|
|
125
|
+
operations,
|
|
126
|
+
allAbilities,
|
|
127
|
+
characters,
|
|
128
|
+
timelines,
|
|
129
|
+
timelineIndexAtNow
|
|
130
|
+
}) => {
|
|
131
|
+
if (operations.length === 0) {
|
|
132
|
+
return transformEmptyArray(players);
|
|
133
|
+
}
|
|
134
|
+
const abilityMap = new Map(allAbilities.map((ability) => [ability.id, ability]));
|
|
135
|
+
const characterMap = new Map(characters.map((character) => [character.id, character]));
|
|
136
|
+
const operationTimelineMap = /* @__PURE__ */ new Map();
|
|
137
|
+
timelines.forEach((timeline, idx) => timeline.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
|
|
138
|
+
const playersWithStatus = copyPlayers(players);
|
|
139
|
+
const nowTimelineIndex = typeof timelineIndexAtNow === "number" ? timelineIndexAtNow : timelines.length - 1;
|
|
140
|
+
const operationsByTimeline = /* @__PURE__ */ new Map();
|
|
141
|
+
operations.forEach((operation) => {
|
|
142
|
+
const timelineIndex = operationTimelineMap.get(operation) ?? -1;
|
|
143
|
+
if (!operationsByTimeline.has(timelineIndex)) {
|
|
144
|
+
operationsByTimeline.set(timelineIndex, []);
|
|
145
|
+
}
|
|
146
|
+
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
147
|
+
});
|
|
148
|
+
const applyOps = (ops) => {
|
|
149
|
+
ops.forEach((operation) => {
|
|
150
|
+
const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
|
|
151
|
+
const ability = abilityMap.get(operation.abilityId);
|
|
152
|
+
if (!ability) {
|
|
153
|
+
console.warn("Ability not found for operation:", operation);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
157
|
+
const effector = playerSeatMap.get(operation.effector);
|
|
158
|
+
const payloads = operation.payloads || [];
|
|
159
|
+
const writableParts = ability.inputs.filter((part) => !part.readonly);
|
|
160
|
+
let snapshotSeatMap = null;
|
|
161
|
+
const getSnapshotSeatMap = () => {
|
|
162
|
+
if (!snapshotSeatMap) {
|
|
163
|
+
snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
|
|
164
|
+
}
|
|
165
|
+
return snapshotSeatMap;
|
|
166
|
+
};
|
|
167
|
+
ability.effects.forEach((effect) => {
|
|
168
|
+
const makePlayersEffect = [];
|
|
169
|
+
if (effect.type === "GAME_CHANGE" || effect.type === "CHARACTER_COUNT_CHANGE") {
|
|
170
|
+
console.warn(effect.type + " effect handling not implemented yet.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (effect.target.from === "EFFECTOR") {
|
|
174
|
+
if (!effector) {
|
|
175
|
+
console.warn("Effector player not found for operation:", operation, "effector index:", operation.effector);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
makePlayersEffect.push(effector);
|
|
179
|
+
} else if (effect.target.from === "PAYLOAD") {
|
|
180
|
+
const seatValue = getValueFromPayloads(payloads, effect.target.value);
|
|
181
|
+
if (typeof seatValue !== "number" && !Array.isArray(seatValue)) {
|
|
182
|
+
console.warn("Expected seat number or array of seat numbers from payloads, got:", seatValue);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const seats = Array.isArray(seatValue) ? seatValue : [seatValue];
|
|
186
|
+
seats.forEach((seat) => {
|
|
187
|
+
const player = playerSeatMap.get(seat);
|
|
188
|
+
if (player) {
|
|
189
|
+
makePlayersEffect.push(player);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
} else if (effect.target.from === "CUSTOM") {
|
|
193
|
+
console.warn("Custom target handling not implemented yet.");
|
|
194
|
+
}
|
|
195
|
+
switch (effect.type) {
|
|
196
|
+
case "ABILITY_CHANGE": {
|
|
197
|
+
console.warn("ABILITY_CHANGE effect handling not implemented yet.");
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "ALIGNMENT_CHANGE": {
|
|
201
|
+
const resolved = resolveSourceValue(
|
|
202
|
+
effect.source,
|
|
203
|
+
writableParts,
|
|
204
|
+
payloads,
|
|
205
|
+
effector,
|
|
206
|
+
getSnapshotSeatMap(),
|
|
207
|
+
(player) => player.alignment
|
|
208
|
+
);
|
|
209
|
+
let alignment;
|
|
210
|
+
if (resolved === "GOOD" || resolved === "EVIL") {
|
|
211
|
+
alignment = resolved;
|
|
212
|
+
}
|
|
213
|
+
if (!alignment) {
|
|
214
|
+
console.warn("Alignment not found for ALIGNMENT_CHANGE effect:", effect.source);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
makePlayersEffect.forEach((player) => {
|
|
218
|
+
player.alignment = alignment;
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "CHARACTER_CHANGE":
|
|
223
|
+
case "PERCEIVED_CHARACTER_CHANGE": {
|
|
224
|
+
const resolvedCharacterId = resolveSourceValue(
|
|
225
|
+
effect.source,
|
|
226
|
+
writableParts,
|
|
227
|
+
payloads,
|
|
228
|
+
effector,
|
|
229
|
+
getSnapshotSeatMap(),
|
|
230
|
+
(player) => player.characterId
|
|
231
|
+
);
|
|
232
|
+
let characterId;
|
|
233
|
+
if (typeof resolvedCharacterId === "string") {
|
|
234
|
+
characterId = resolvedCharacterId;
|
|
235
|
+
} else if (Array.isArray(resolvedCharacterId) && typeof resolvedCharacterId[0] === "string") {
|
|
236
|
+
characterId = resolvedCharacterId[0];
|
|
237
|
+
}
|
|
238
|
+
if (!characterId) {
|
|
239
|
+
console.warn("Character ID not found for " + effect.type + " effect:", effect.source);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const newCharacter = characterMap.get(characterId);
|
|
243
|
+
if (!newCharacter) {
|
|
244
|
+
console.warn("Character not found for " + effect.type + " effect:", characterId);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (effect.type === "CHARACTER_CHANGE") {
|
|
248
|
+
const abilityIds = newCharacter.abilities.map((ability2) => ability2.id);
|
|
249
|
+
makePlayersEffect.forEach((player) => {
|
|
250
|
+
player.characterId = newCharacter.id;
|
|
251
|
+
player.abilities = abilityIds;
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
makePlayersEffect.forEach((player) => {
|
|
255
|
+
player.perceivedCharacter = {
|
|
256
|
+
characterId: newCharacter.id,
|
|
257
|
+
abilities: newCharacter.abilities.map((ability2) => ability2.id),
|
|
258
|
+
asCharacter: effect.followPriority || false
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "REMINDER_ADD": {
|
|
265
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
266
|
+
if (!isReminder(maybeReminder)) {
|
|
267
|
+
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
271
|
+
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
makePlayersEffect.forEach((player) => {
|
|
275
|
+
if (!player.reminders) {
|
|
276
|
+
player.reminders = [];
|
|
277
|
+
}
|
|
278
|
+
player.reminders.push(maybeReminder);
|
|
279
|
+
});
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "REMINDER_REMOVE": {
|
|
283
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
284
|
+
if (!isPlayerReminderArray(maybeReminder)) {
|
|
285
|
+
console.warn("Invalid reminder data for REMINDER_REMOVE effect:", maybeReminder);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
maybeReminder.sort((left, right) => right.index - left.index).forEach((reminder) => {
|
|
289
|
+
const player = playerSeatMap.get(reminder.playerSeat);
|
|
290
|
+
if (!player) {
|
|
291
|
+
console.warn("Player not found for REMINDER_REMOVE effect:", reminder.playerSeat);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!player.reminders) {
|
|
295
|
+
player.reminders = [];
|
|
296
|
+
}
|
|
297
|
+
player.reminders = player.reminders.filter((_value, index) => index !== reminder.index);
|
|
298
|
+
});
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "SEAT_CHANGE": {
|
|
302
|
+
console.warn("SEAT_CHANGE effect handling not implemented yet.");
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "STATUS_CHANGE": {
|
|
306
|
+
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
307
|
+
if (maybeStatus === "DEAD") {
|
|
308
|
+
makePlayersEffect.forEach((player) => {
|
|
309
|
+
player.isDead = true;
|
|
310
|
+
});
|
|
311
|
+
} else if (maybeStatus === "ALIVE") {
|
|
312
|
+
makePlayersEffect.forEach((player) => {
|
|
313
|
+
player.isDead = false;
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
console.warn("Invalid status value for STATUS_CHANGE effect:", maybeStatus);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
default: {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const settingsOps = operationsByTimeline.get(-1);
|
|
327
|
+
if (settingsOps) {
|
|
328
|
+
applyOps(settingsOps);
|
|
329
|
+
}
|
|
330
|
+
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
331
|
+
const timeline = timelines[i];
|
|
332
|
+
if (!timeline) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const nominations = timeline.nominations;
|
|
336
|
+
if (nominations && nominations.length > 0) {
|
|
337
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
338
|
+
nominations.forEach((nomination) => {
|
|
339
|
+
const voterSeats = nomination.voterSeats;
|
|
340
|
+
if (!voterSeats || voterSeats.length === 0) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
voterSeats.forEach((seat) => {
|
|
344
|
+
const player = playerSeatMap.get(seat);
|
|
345
|
+
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
346
|
+
player.hasUsedDeadVote = true;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const timelineOps = operationsByTimeline.get(i);
|
|
352
|
+
if (timelineOps) {
|
|
353
|
+
applyOps(timelineOps);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return transformEmptyArray(playersWithStatus);
|
|
357
|
+
};
|
|
358
|
+
export {
|
|
359
|
+
adjustValueAsNumber,
|
|
360
|
+
adjustValueAsNumberArray,
|
|
361
|
+
applyOperationToPlayers,
|
|
362
|
+
buildPlayerSeatMap,
|
|
363
|
+
copyPlayers,
|
|
364
|
+
getSourceValue,
|
|
365
|
+
getValueFromPayloads,
|
|
366
|
+
isNumberOrNumberArray,
|
|
367
|
+
isPlayerReminderArray,
|
|
368
|
+
isReminder,
|
|
369
|
+
resolveSourceValue,
|
|
370
|
+
transformEmptyArray
|
|
371
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bct-app/game-engine",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Game engine utilities for BCT",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"build": "tsup src/index.ts --dts --format cjs,esm",
|
|
21
|
+
"dev": "tsup src/index.ts --dts --format cjs,esm --watch"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"game-engine",
|
|
25
|
+
"bct"
|
|
26
|
+
],
|
|
27
|
+
"author": "BCT-YH",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.0.2",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|