hrbattle 0.1.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/README.md +103 -0
- package/dist/battle/battleCore.d.ts +83 -0
- package/dist/battle/battleCore.js +779 -0
- package/dist/battle/config/json/designed-buffs.json +608 -0
- package/dist/battle/config/json/designed-hero-skill-books.json +146 -0
- package/dist/battle/config/json/designed-monster-skill-books.json +308 -0
- package/dist/battle/config/json/designed-roster.json +438 -0
- package/dist/battle/config/json/designed-skill-templates.json +81 -0
- package/dist/battle/config/jsonConfigLoader.d.ts +26 -0
- package/dist/battle/config/jsonConfigLoader.js +77 -0
- package/dist/battle/effectSystem.d.ts +26 -0
- package/dist/battle/effectSystem.js +175 -0
- package/dist/battle/eventBus.d.ts +8 -0
- package/dist/battle/eventBus.js +20 -0
- package/dist/battle/formula.d.ts +19 -0
- package/dist/battle/formula.js +92 -0
- package/dist/battle/logger.d.ts +28 -0
- package/dist/battle/logger.js +66 -0
- package/dist/battle/random.d.ts +6 -0
- package/dist/battle/random.js +16 -0
- package/dist/battle/script/designedScripts.d.ts +2 -0
- package/dist/battle/script/designedScripts.js +1013 -0
- package/dist/battle/script/monsterScripts.d.ts +2 -0
- package/dist/battle/script/monsterScripts.js +277 -0
- package/dist/battle/script/monsterScripts18xx.d.ts +2 -0
- package/dist/battle/script/monsterScripts18xx.js +330 -0
- package/dist/battle/script/monsterScripts19xx.d.ts +2 -0
- package/dist/battle/script/monsterScripts19xx.js +271 -0
- package/dist/battle/script/monsterScripts19xxPart2.d.ts +2 -0
- package/dist/battle/script/monsterScripts19xxPart2.js +400 -0
- package/dist/battle/skillEngine.d.ts +36 -0
- package/dist/battle/skillEngine.js +246 -0
- package/dist/battle/targeting.d.ts +3 -0
- package/dist/battle/targeting.js +38 -0
- package/dist/battle/turnbar.d.ts +8 -0
- package/dist/battle/turnbar.js +40 -0
- package/dist/battle/types.d.ts +302 -0
- package/dist/battle/types.js +1 -0
- package/dist/cocos-adapter/BattleFacade.d.ts +8 -0
- package/dist/cocos-adapter/BattleFacade.js +22 -0
- package/dist/cocos-adapter/clientBattleResult.d.ts +127 -0
- package/dist/cocos-adapter/clientBattleResult.js +413 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/package.json +32 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const FOREVER = 999999999;
|
|
2
|
+
const SKIP_ROUND_END = new Set([-999, -99999999]);
|
|
3
|
+
export class EffectSystem {
|
|
4
|
+
eventBus;
|
|
5
|
+
buffs = new Map();
|
|
6
|
+
buffConfigs = new Map();
|
|
7
|
+
constructor(eventBus) {
|
|
8
|
+
this.eventBus = eventBus;
|
|
9
|
+
}
|
|
10
|
+
registerBuffConfigs(configs) {
|
|
11
|
+
for (const config of configs) {
|
|
12
|
+
this.buffConfigs.set(config.id, config);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
getBuffs(unitId) {
|
|
16
|
+
return this.buffs.get(unitId) ?? [];
|
|
17
|
+
}
|
|
18
|
+
getBuffConfigs() {
|
|
19
|
+
return [...this.buffConfigs.values()];
|
|
20
|
+
}
|
|
21
|
+
hasBuff(unitId, buffId) {
|
|
22
|
+
return this.getBuffs(unitId).some((b) => b.id === buffId && b.stacks > 0);
|
|
23
|
+
}
|
|
24
|
+
getEffectiveStats(unit) {
|
|
25
|
+
const buffs = this.getBuffs(unit.id);
|
|
26
|
+
const stat = { ...unit.stats };
|
|
27
|
+
for (const buff of buffs) {
|
|
28
|
+
const stacks = Math.max(1, buff.stacks);
|
|
29
|
+
const cfg = buff.config;
|
|
30
|
+
if (cfg.speedRateDelta) {
|
|
31
|
+
stat.Spd = Math.max(1, Math.floor((stat.Spd * (10000 + cfg.speedRateDelta * stacks)) / 10000));
|
|
32
|
+
}
|
|
33
|
+
if (cfg.effectResDelta) {
|
|
34
|
+
stat.pERes += cfg.effectResDelta * stacks;
|
|
35
|
+
}
|
|
36
|
+
if (cfg.energyRecoveryRateDelta) {
|
|
37
|
+
stat.pER += cfg.energyRecoveryRateDelta * stacks;
|
|
38
|
+
}
|
|
39
|
+
if (cfg.attackRateDelta) {
|
|
40
|
+
stat.Att = Math.max(1, Math.floor((stat.Att * (10000 + cfg.attackRateDelta * stacks)) / 10000));
|
|
41
|
+
}
|
|
42
|
+
if (cfg.defenseRateDelta) {
|
|
43
|
+
stat.Def = Math.max(1, Math.floor((stat.Def * (10000 + cfg.defenseRateDelta * stacks)) / 10000));
|
|
44
|
+
}
|
|
45
|
+
if (cfg.critRateDelta) {
|
|
46
|
+
stat.pCTR += cfg.critRateDelta * stacks;
|
|
47
|
+
}
|
|
48
|
+
if (cfg.critDamageDelta) {
|
|
49
|
+
stat.pCTD += cfg.critDamageDelta * stacks;
|
|
50
|
+
}
|
|
51
|
+
if (cfg.healEffectDelta) {
|
|
52
|
+
stat.pHE += cfg.healEffectDelta * stacks;
|
|
53
|
+
}
|
|
54
|
+
if (cfg.effectHitDelta) {
|
|
55
|
+
stat.pEHR += cfg.effectHitDelta * stacks;
|
|
56
|
+
}
|
|
57
|
+
if (cfg.damageRateDelta) {
|
|
58
|
+
stat.pBSDI += cfg.damageRateDelta * stacks;
|
|
59
|
+
stat.pESDI += cfg.damageRateDelta * stacks;
|
|
60
|
+
stat.pFSDI += cfg.damageRateDelta * stacks;
|
|
61
|
+
}
|
|
62
|
+
if (cfg.vulnerabilityDelta) {
|
|
63
|
+
stat.pVulnerability += cfg.vulnerabilityDelta * stacks;
|
|
64
|
+
}
|
|
65
|
+
if (cfg.imbalanceRateDelta) {
|
|
66
|
+
stat.pIM += cfg.imbalanceRateDelta * stacks;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return stat;
|
|
70
|
+
}
|
|
71
|
+
addBuff(owner, sourceId, buffId) {
|
|
72
|
+
const cfg = this.buffConfigs.get(buffId);
|
|
73
|
+
if (!cfg) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const list = this.buffs.get(owner.id) ?? [];
|
|
77
|
+
const existing = list.find((b) => b.id === buffId);
|
|
78
|
+
if (existing) {
|
|
79
|
+
if (cfg.maxStacks !== FOREVER) {
|
|
80
|
+
existing.stacks = Math.min(cfg.maxStacks, existing.stacks + 1);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
existing.stacks += 1;
|
|
84
|
+
}
|
|
85
|
+
if (cfg.durationPerStack !== FOREVER) {
|
|
86
|
+
existing.remainingRounds = Math.max(existing.remainingRounds, cfg.durationPerStack);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const instance = {
|
|
91
|
+
id: buffId,
|
|
92
|
+
ownerId: owner.id,
|
|
93
|
+
stacks: 1,
|
|
94
|
+
remainingRounds: cfg.durationPerStack,
|
|
95
|
+
config: cfg,
|
|
96
|
+
sourceId
|
|
97
|
+
};
|
|
98
|
+
list.push(instance);
|
|
99
|
+
this.buffs.set(owner.id, list);
|
|
100
|
+
}
|
|
101
|
+
if (cfg.stun) {
|
|
102
|
+
owner.runtime.stunned = true;
|
|
103
|
+
}
|
|
104
|
+
this.eventBus.emit({ name: "OnBuffAdd", payload: { ownerId: owner.id, buffId } });
|
|
105
|
+
}
|
|
106
|
+
removeBuff(owner, buffId) {
|
|
107
|
+
const list = this.getBuffs(owner.id);
|
|
108
|
+
const next = list.filter((b) => b.id !== buffId);
|
|
109
|
+
this.buffs.set(owner.id, next);
|
|
110
|
+
this.eventBus.emit({ name: "OnBuffRemove", payload: { ownerId: owner.id, buffId } });
|
|
111
|
+
this.recomputeStates(owner);
|
|
112
|
+
}
|
|
113
|
+
onTurnStart(owner, allUnits) {
|
|
114
|
+
const dotEvents = [];
|
|
115
|
+
for (const buff of this.getBuffs(owner.id)) {
|
|
116
|
+
if (buff.config.dotRatio && buff.stacks > 0) {
|
|
117
|
+
const dot = Math.floor(owner.stats.Att * buff.config.dotRatio * buff.stacks);
|
|
118
|
+
if (dot > 0) {
|
|
119
|
+
dotEvents.push({
|
|
120
|
+
targetId: owner.id,
|
|
121
|
+
value: dot,
|
|
122
|
+
buffId: buff.id,
|
|
123
|
+
element: buff.config.dotElement
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (buff.config.taunt) {
|
|
128
|
+
this.forceTauntPosition(owner, allUnits);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.recomputeStates(owner);
|
|
132
|
+
return dotEvents;
|
|
133
|
+
}
|
|
134
|
+
onTurnEnd(owner, allUnits) {
|
|
135
|
+
const list = this.getBuffs(owner.id);
|
|
136
|
+
for (const buff of list) {
|
|
137
|
+
if (SKIP_ROUND_END.has(buff.config.onRoundEndDelta)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (buff.config.onRoundEndDelta === -1) {
|
|
141
|
+
buff.stacks = Math.max(0, buff.stacks - 1);
|
|
142
|
+
}
|
|
143
|
+
if (buff.remainingRounds !== FOREVER) {
|
|
144
|
+
buff.remainingRounds = Math.max(0, buff.remainingRounds - 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.cleanup(owner);
|
|
148
|
+
this.recomputeStates(owner);
|
|
149
|
+
this.tryReposition(owner, allUnits);
|
|
150
|
+
}
|
|
151
|
+
cleanup(owner) {
|
|
152
|
+
const list = this.getBuffs(owner.id).filter((b) => b.stacks > 0 && (b.remainingRounds > 0 || b.remainingRounds === FOREVER));
|
|
153
|
+
this.buffs.set(owner.id, list);
|
|
154
|
+
}
|
|
155
|
+
recomputeStates(owner) {
|
|
156
|
+
owner.runtime.stunned = this.getBuffs(owner.id).some((b) => b.config.stun);
|
|
157
|
+
}
|
|
158
|
+
forceTauntPosition(owner, allUnits) {
|
|
159
|
+
const allies = allUnits.filter((u) => u.teamId === owner.teamId && u.runtime.alive).sort((a, b) => a.position - b.position);
|
|
160
|
+
allies.forEach((unit, index) => {
|
|
161
|
+
unit.position = (index + 1);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
tryReposition(owner, allUnits) {
|
|
165
|
+
if (!owner.runtime.stunned) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const allies = allUnits.filter((u) => u.teamId === owner.teamId && u.runtime.alive).sort((a, b) => a.position - b.position);
|
|
169
|
+
const withoutOwner = allies.filter((u) => u.id !== owner.id);
|
|
170
|
+
const merged = [...withoutOwner, owner];
|
|
171
|
+
merged.forEach((unit, index) => {
|
|
172
|
+
unit.position = (index + 1);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BattleEvent, BattleEventName } from "./types";
|
|
2
|
+
type EventHandler<T> = (payload: T) => void;
|
|
3
|
+
export declare class BattleEventBus {
|
|
4
|
+
private handlers;
|
|
5
|
+
on<T>(name: BattleEventName, handler: EventHandler<T>, priority?: number): () => void;
|
|
6
|
+
emit<T>(event: BattleEvent<T>): void;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class BattleEventBus {
|
|
2
|
+
handlers = new Map();
|
|
3
|
+
on(name, handler, priority = 0) {
|
|
4
|
+
const list = this.handlers.get(name) ?? [];
|
|
5
|
+
const record = { priority, handler: handler };
|
|
6
|
+
list.push(record);
|
|
7
|
+
list.sort((a, b) => b.priority - a.priority);
|
|
8
|
+
this.handlers.set(name, list);
|
|
9
|
+
return () => {
|
|
10
|
+
const current = this.handlers.get(name) ?? [];
|
|
11
|
+
this.handlers.set(name, current.filter((item) => item !== record));
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
emit(event) {
|
|
15
|
+
const list = this.handlers.get(event.name) ?? [];
|
|
16
|
+
for (const item of list) {
|
|
17
|
+
item.handler(event.payload);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ElementType, SkillType, UnitModel } from "./types";
|
|
2
|
+
import { SeedRng } from "./random";
|
|
3
|
+
export interface DamageResult {
|
|
4
|
+
finalDam: number;
|
|
5
|
+
baseDam: number;
|
|
6
|
+
critDam: number;
|
|
7
|
+
isCritical: boolean;
|
|
8
|
+
isRepression: -1 | 0 | 1;
|
|
9
|
+
}
|
|
10
|
+
export declare function repression(attackerElement: ElementType, defenderElement: ElementType): -1 | 0 | 1;
|
|
11
|
+
export declare function calcDamage(attacker: UnitModel, defender: UnitModel, dam: number, type: SkillType, elem: ElementType, rng: SeedRng): DamageResult;
|
|
12
|
+
export interface HealShieldResult {
|
|
13
|
+
finalValue: number;
|
|
14
|
+
baseValue: number;
|
|
15
|
+
critValue: number;
|
|
16
|
+
isCritical: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function calcHealOrShield(caster: UnitModel, target: UnitModel, value: number, rng: SeedRng): HealShieldResult;
|
|
19
|
+
export declare function calcEffectProbability(baseProb: number, isFixedProb: boolean, attacker: UnitModel, defender: UnitModel, rng: SeedRng): boolean;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const DEF_PARTICLE = 300;
|
|
2
|
+
const elementFieldMap = {
|
|
3
|
+
fire: { inc: "pFDI", red: "pFDI_Def" },
|
|
4
|
+
thunder: { inc: "pTDI", red: "pTDI_Def" },
|
|
5
|
+
water: { inc: "pWDI", red: "pWDI_Def" },
|
|
6
|
+
rock: { inc: "pRDI", red: "pRDI_Def" },
|
|
7
|
+
wind: { inc: "pADI", red: "pADI_Def" },
|
|
8
|
+
light: { inc: "pLDI", red: "pLDI_Def" },
|
|
9
|
+
dark: { inc: "pDDI", red: "pDDI_Def" }
|
|
10
|
+
};
|
|
11
|
+
const repressMap = {
|
|
12
|
+
water: "fire",
|
|
13
|
+
fire: "wind",
|
|
14
|
+
wind: "rock",
|
|
15
|
+
rock: "thunder",
|
|
16
|
+
thunder: "water"
|
|
17
|
+
};
|
|
18
|
+
function floorDiv(a, b) {
|
|
19
|
+
return Math.floor(a / b);
|
|
20
|
+
}
|
|
21
|
+
function clampPercent(v) {
|
|
22
|
+
return Math.min(10000, Math.max(0, Math.floor(v)));
|
|
23
|
+
}
|
|
24
|
+
export function repression(attackerElement, defenderElement) {
|
|
25
|
+
if (repressMap[attackerElement] === defenderElement) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
if (repressMap[defenderElement] === attackerElement) {
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
function skillDamageRatio(attacker, type) {
|
|
34
|
+
if (type === "basic") {
|
|
35
|
+
return 10000 + attacker.stats.pBSDI;
|
|
36
|
+
}
|
|
37
|
+
if (type === "core") {
|
|
38
|
+
return 10000 + attacker.stats.pESDI;
|
|
39
|
+
}
|
|
40
|
+
return 10000 + attacker.stats.pFSDI;
|
|
41
|
+
}
|
|
42
|
+
export function calcDamage(attacker, defender, dam, type, elem, rng) {
|
|
43
|
+
let bDam = floorDiv(dam * DEF_PARTICLE, DEF_PARTICLE + defender.stats.Def);
|
|
44
|
+
bDam = floorDiv(bDam * skillDamageRatio(attacker, type), 10000);
|
|
45
|
+
const elementFields = elementFieldMap[elem];
|
|
46
|
+
const inc = 10000 + attacker.stats[elementFields.inc];
|
|
47
|
+
const red = 10000 + defender.stats[elementFields.red];
|
|
48
|
+
bDam = floorDiv(bDam * inc, red);
|
|
49
|
+
const rep = repression(attacker.element, defender.element);
|
|
50
|
+
if (rep === 1) {
|
|
51
|
+
bDam = floorDiv(bDam * 11500, 10000);
|
|
52
|
+
}
|
|
53
|
+
if (rep === -1) {
|
|
54
|
+
bDam = floorDiv(bDam * 8500, 10000);
|
|
55
|
+
}
|
|
56
|
+
const critProb = clampPercent(attacker.stats.pCTR - defender.stats.pCTR_Def);
|
|
57
|
+
const isCritical = Math.floor(rng.next() * 10000) < critProb;
|
|
58
|
+
const crtRatio = clampPercent(attacker.stats.pCTD - defender.stats.pCTD_Def - 10000);
|
|
59
|
+
const critDam = isCritical ? Math.max(0, floorDiv(bDam * crtRatio, 10000)) : 0;
|
|
60
|
+
const rawFinal = bDam + critDam;
|
|
61
|
+
const vulnRatio = 10000 + defender.stats.pVulnerability;
|
|
62
|
+
const finalDam = floorDiv(rawFinal * vulnRatio, 10000);
|
|
63
|
+
return {
|
|
64
|
+
finalDam,
|
|
65
|
+
baseDam: bDam,
|
|
66
|
+
critDam,
|
|
67
|
+
isCritical,
|
|
68
|
+
isRepression: rep
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function calcHealOrShield(caster, target, value, rng) {
|
|
72
|
+
const baseValue = floorDiv(value * (10000 + caster.stats.pHE), 10000);
|
|
73
|
+
const critProb = clampPercent(caster.stats.pCTR - target.stats.pCTR_Def);
|
|
74
|
+
const isCritical = Math.floor(rng.next() * 10000) < critProb;
|
|
75
|
+
const crtRatio = clampPercent(caster.stats.pCTD - target.stats.pCTD_Def - 10000);
|
|
76
|
+
const critValue = isCritical ? Math.max(0, floorDiv(baseValue * crtRatio, 10000)) : 0;
|
|
77
|
+
return {
|
|
78
|
+
finalValue: baseValue + critValue,
|
|
79
|
+
baseValue,
|
|
80
|
+
critValue,
|
|
81
|
+
isCritical
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function calcEffectProbability(baseProb, isFixedProb, attacker, defender, rng) {
|
|
85
|
+
let prob = baseProb;
|
|
86
|
+
if (!isFixedProb) {
|
|
87
|
+
prob = floorDiv(prob * (10000 + attacker.stats.pEHR), 10000 + defender.stats.pERes);
|
|
88
|
+
}
|
|
89
|
+
prob = clampPercent(prob);
|
|
90
|
+
const rand = Math.floor(rng.next() * 10000);
|
|
91
|
+
return rand < prob;
|
|
92
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BattleActionLog, BattleSnapshot, ElementType, UnitId, UnitModel, UnitStats } from "./types";
|
|
2
|
+
export declare class BattleLogger {
|
|
3
|
+
readonly actionLogs: BattleActionLog[];
|
|
4
|
+
readonly snapshots: BattleSnapshot[];
|
|
5
|
+
readonly errors: Array<{
|
|
6
|
+
turn: number;
|
|
7
|
+
message: string;
|
|
8
|
+
detail?: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
private pendingDotDamages;
|
|
11
|
+
private pendingImbalanceBreaks;
|
|
12
|
+
private pendingImbalanceChanges;
|
|
13
|
+
logAction(log: BattleActionLog): void;
|
|
14
|
+
/** 记录DOT伤害,用于在下一个快照中展示 */
|
|
15
|
+
logDotDamage(targetId: UnitId, value: number, buffId: string, element?: ElementType): void;
|
|
16
|
+
/** 记录失衡破击事件 */
|
|
17
|
+
logImbalanceBreak(targetId: UnitId, attackerId: UnitId, element: ElementType): void;
|
|
18
|
+
/** 记录失衡值变化来源 */
|
|
19
|
+
logImbalanceChange(targetId: UnitId, delta: number, reason: "turn_start_clear" | "skill_effect", sourceActorId?: UnitId, sourceSkillId?: string): void;
|
|
20
|
+
logSnapshot(turn: number, phase: BattleSnapshot["phase"], units: UnitModel[], resolveEffectiveStats: (unit: UnitModel) => UnitStats, resolveBuffs: (unit: UnitModel) => Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
stacks: number;
|
|
24
|
+
remainingRounds: number;
|
|
25
|
+
sourceId: string;
|
|
26
|
+
}>, resolveImbalanceMax: (unit: UnitModel) => number): void;
|
|
27
|
+
logError(turn: number, message: string, detail?: unknown): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class BattleLogger {
|
|
2
|
+
actionLogs = [];
|
|
3
|
+
snapshots = [];
|
|
4
|
+
errors = [];
|
|
5
|
+
pendingDotDamages = [];
|
|
6
|
+
pendingImbalanceBreaks = [];
|
|
7
|
+
pendingImbalanceChanges = [];
|
|
8
|
+
logAction(log) {
|
|
9
|
+
this.actionLogs.push(log);
|
|
10
|
+
}
|
|
11
|
+
/** 记录DOT伤害,用于在下一个快照中展示 */
|
|
12
|
+
logDotDamage(targetId, value, buffId, element) {
|
|
13
|
+
this.pendingDotDamages.push({ targetId, value, buffId, element });
|
|
14
|
+
}
|
|
15
|
+
/** 记录失衡破击事件 */
|
|
16
|
+
logImbalanceBreak(targetId, attackerId, element) {
|
|
17
|
+
this.pendingImbalanceBreaks.push({ targetId, attackerId, element });
|
|
18
|
+
}
|
|
19
|
+
/** 记录失衡值变化来源 */
|
|
20
|
+
logImbalanceChange(targetId, delta, reason, sourceActorId, sourceSkillId) {
|
|
21
|
+
if (delta === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.pendingImbalanceChanges.push({ targetId, delta, reason, sourceActorId, sourceSkillId });
|
|
25
|
+
}
|
|
26
|
+
logSnapshot(turn, phase, units, resolveEffectiveStats, resolveBuffs, resolveImbalanceMax) {
|
|
27
|
+
const snapshot = {
|
|
28
|
+
turn,
|
|
29
|
+
phase,
|
|
30
|
+
units: units.map((u) => {
|
|
31
|
+
const effective = resolveEffectiveStats(u);
|
|
32
|
+
return {
|
|
33
|
+
id: u.id,
|
|
34
|
+
hp: u.runtime.Hp,
|
|
35
|
+
shield: u.runtime.Shield,
|
|
36
|
+
ap: u.runtime.AP,
|
|
37
|
+
energy: u.runtime.Energy,
|
|
38
|
+
alive: u.runtime.alive,
|
|
39
|
+
position: u.position,
|
|
40
|
+
effectiveAllStats: { ...effective },
|
|
41
|
+
buffs: resolveBuffs(u),
|
|
42
|
+
imbalance: u.runtime.Imbalance,
|
|
43
|
+
imbalanceMax: resolveImbalanceMax(u)
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
// 如果有待处理的DOT伤害,记录到快照中并清空
|
|
48
|
+
if (this.pendingDotDamages.length > 0) {
|
|
49
|
+
snapshot.dotDamages = [...this.pendingDotDamages];
|
|
50
|
+
this.pendingDotDamages = [];
|
|
51
|
+
}
|
|
52
|
+
// 如果有待处理的失衡破击事件,记录到快照中并清空
|
|
53
|
+
if (this.pendingImbalanceBreaks.length > 0) {
|
|
54
|
+
snapshot.imbalanceBreaks = [...this.pendingImbalanceBreaks];
|
|
55
|
+
this.pendingImbalanceBreaks = [];
|
|
56
|
+
}
|
|
57
|
+
if (this.pendingImbalanceChanges.length > 0) {
|
|
58
|
+
snapshot.imbalanceChanges = [...this.pendingImbalanceChanges];
|
|
59
|
+
this.pendingImbalanceChanges = [];
|
|
60
|
+
}
|
|
61
|
+
this.snapshots.push(snapshot);
|
|
62
|
+
}
|
|
63
|
+
logError(turn, message, detail) {
|
|
64
|
+
this.errors.push({ turn, message, detail });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class SeedRng {
|
|
2
|
+
state;
|
|
3
|
+
constructor(seed) {
|
|
4
|
+
this.state = seed >>> 0;
|
|
5
|
+
}
|
|
6
|
+
next() {
|
|
7
|
+
this.state = (1664525 * this.state + 1013904223) >>> 0;
|
|
8
|
+
return this.state / 0x100000000;
|
|
9
|
+
}
|
|
10
|
+
nextInt(maxExclusive) {
|
|
11
|
+
if (maxExclusive <= 0) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
return Math.floor(this.next() * maxExclusive);
|
|
15
|
+
}
|
|
16
|
+
}
|