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.
Files changed (45) hide show
  1. package/README.md +103 -0
  2. package/dist/battle/battleCore.d.ts +83 -0
  3. package/dist/battle/battleCore.js +779 -0
  4. package/dist/battle/config/json/designed-buffs.json +608 -0
  5. package/dist/battle/config/json/designed-hero-skill-books.json +146 -0
  6. package/dist/battle/config/json/designed-monster-skill-books.json +308 -0
  7. package/dist/battle/config/json/designed-roster.json +438 -0
  8. package/dist/battle/config/json/designed-skill-templates.json +81 -0
  9. package/dist/battle/config/jsonConfigLoader.d.ts +26 -0
  10. package/dist/battle/config/jsonConfigLoader.js +77 -0
  11. package/dist/battle/effectSystem.d.ts +26 -0
  12. package/dist/battle/effectSystem.js +175 -0
  13. package/dist/battle/eventBus.d.ts +8 -0
  14. package/dist/battle/eventBus.js +20 -0
  15. package/dist/battle/formula.d.ts +19 -0
  16. package/dist/battle/formula.js +92 -0
  17. package/dist/battle/logger.d.ts +28 -0
  18. package/dist/battle/logger.js +66 -0
  19. package/dist/battle/random.d.ts +6 -0
  20. package/dist/battle/random.js +16 -0
  21. package/dist/battle/script/designedScripts.d.ts +2 -0
  22. package/dist/battle/script/designedScripts.js +1013 -0
  23. package/dist/battle/script/monsterScripts.d.ts +2 -0
  24. package/dist/battle/script/monsterScripts.js +277 -0
  25. package/dist/battle/script/monsterScripts18xx.d.ts +2 -0
  26. package/dist/battle/script/monsterScripts18xx.js +330 -0
  27. package/dist/battle/script/monsterScripts19xx.d.ts +2 -0
  28. package/dist/battle/script/monsterScripts19xx.js +271 -0
  29. package/dist/battle/script/monsterScripts19xxPart2.d.ts +2 -0
  30. package/dist/battle/script/monsterScripts19xxPart2.js +400 -0
  31. package/dist/battle/skillEngine.d.ts +36 -0
  32. package/dist/battle/skillEngine.js +246 -0
  33. package/dist/battle/targeting.d.ts +3 -0
  34. package/dist/battle/targeting.js +38 -0
  35. package/dist/battle/turnbar.d.ts +8 -0
  36. package/dist/battle/turnbar.js +40 -0
  37. package/dist/battle/types.d.ts +302 -0
  38. package/dist/battle/types.js +1 -0
  39. package/dist/cocos-adapter/BattleFacade.d.ts +8 -0
  40. package/dist/cocos-adapter/BattleFacade.js +22 -0
  41. package/dist/cocos-adapter/clientBattleResult.d.ts +127 -0
  42. package/dist/cocos-adapter/clientBattleResult.js +413 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +8 -0
  45. package/package.json +32 -0
@@ -0,0 +1,779 @@
1
+ import { BattleEventBus } from "./eventBus";
2
+ import { EffectSystem } from "./effectSystem";
3
+ import { BattleLogger } from "./logger";
4
+ import { SeedRng } from "./random";
5
+ import { repression } from "./formula";
6
+ import { ConfigSkillEngine, ScriptSkillEngine, SkillExecutor } from "./skillEngine";
7
+ import { TurnBarSystem } from "./turnbar";
8
+ export class BattleCore {
9
+ initUnits;
10
+ config;
11
+ units;
12
+ rng;
13
+ eventBus = new BattleEventBus();
14
+ turnBar = new TurnBarSystem();
15
+ logger = new BattleLogger();
16
+ effectSystem = new EffectSystem(this.eventBus);
17
+ configSkillEngine = new ConfigSkillEngine();
18
+ scriptSkillEngine = new ScriptSkillEngine();
19
+ skillExecutor = new SkillExecutor(this.configSkillEngine, this.scriptSkillEngine);
20
+ skillSlots = new Map();
21
+ chasePredicates = [];
22
+ chaseSkillResolvers = [];
23
+ pendingImbalanceClear = new Set();
24
+ pendingReactiveCounters = [];
25
+ scriptPassivesInitialized = false;
26
+ activeSkillContext = null;
27
+ turn = 0;
28
+ actionCount = 0;
29
+ constructor(initUnits, config) {
30
+ this.initUnits = initUnits;
31
+ this.config = config;
32
+ this.rng = new SeedRng(config.seed);
33
+ this.units = initUnits.map((u) => ({
34
+ id: u.id,
35
+ name: u.name,
36
+ teamId: u.teamId,
37
+ position: u.position,
38
+ element: u.element,
39
+ stats: { ...u.stats },
40
+ runtime: {
41
+ Hp: u.stats.Mhp,
42
+ AP: 0,
43
+ Energy: 0,
44
+ EnergyMax: this.maxUltimateCost(u.skills),
45
+ Shield: 0,
46
+ Imbalance: 0,
47
+ alive: true,
48
+ stunned: false
49
+ },
50
+ skills: u.skills.map((s) => ({
51
+ id: s.id,
52
+ type: s.type,
53
+ cooldown: s.cooldown,
54
+ energyCost: s.energyCost,
55
+ energyGain: s.energyGain
56
+ }))
57
+ }));
58
+ for (const unit of this.units) {
59
+ this.skillSlots.set(unit.id, initUnits
60
+ .find((u) => u.id === unit.id)
61
+ .skills.map((d) => ({
62
+ definition: d,
63
+ cooldownRemain: 0
64
+ })));
65
+ }
66
+ this.registerConfiguredChaseRules();
67
+ this.registerReactiveCounterRules();
68
+ }
69
+ getEventBus() {
70
+ return this.eventBus;
71
+ }
72
+ getLogger() {
73
+ return this.logger;
74
+ }
75
+ getEffectSystem() {
76
+ return this.effectSystem;
77
+ }
78
+ getConfigSkillEngine() {
79
+ return this.configSkillEngine;
80
+ }
81
+ getScriptSkillEngine() {
82
+ return this.scriptSkillEngine;
83
+ }
84
+ getUnitById(unitId) {
85
+ return this.units.find((u) => u.id === unitId);
86
+ }
87
+ addBuffToUnit(sourceId, targetId, buffId) {
88
+ this.applyBuff(sourceId, targetId, buffId);
89
+ }
90
+ /** 动态召唤一个新单位加入战斗 */
91
+ spawnUnit(initUnit) {
92
+ // 检查同队存活单位数量,最多4个
93
+ const teamAlive = this.units.filter((u) => u.teamId === initUnit.teamId && u.runtime.alive);
94
+ if (teamAlive.length >= 4) {
95
+ return null;
96
+ }
97
+ const position = (teamAlive.length + 1);
98
+ const unit = {
99
+ id: initUnit.id,
100
+ name: initUnit.name,
101
+ teamId: initUnit.teamId,
102
+ position,
103
+ element: initUnit.element,
104
+ stats: { ...initUnit.stats },
105
+ runtime: {
106
+ Hp: initUnit.stats.Mhp,
107
+ AP: 0,
108
+ Energy: 0,
109
+ EnergyMax: this.maxUltimateCost(initUnit.skills),
110
+ Shield: 0,
111
+ Imbalance: 0,
112
+ alive: true,
113
+ stunned: false
114
+ },
115
+ skills: initUnit.skills.map((s) => ({
116
+ id: s.id,
117
+ type: s.type,
118
+ cooldown: s.cooldown,
119
+ energyCost: s.energyCost,
120
+ energyGain: s.energyGain
121
+ }))
122
+ };
123
+ this.units.push(unit);
124
+ this.skillSlots.set(unit.id, initUnit.skills.map((d) => ({
125
+ definition: d,
126
+ cooldownRemain: 0
127
+ })));
128
+ // 初始化被动技能
129
+ const passiveSlots = this.getSlots(unit.id).filter((slot) => slot.definition.type === "passive" && slot.definition.mode === "script" && slot.definition.activateOnBattleStart);
130
+ for (const passiveSlot of passiveSlots) {
131
+ this.executeSkill(unit, passiveSlot.definition, "assist");
132
+ }
133
+ this.rearrangePositions();
134
+ return unit;
135
+ }
136
+ /** 延长目标单位所有主动技能的冷却回合数 */
137
+ extendCooldown(unitId, delta) {
138
+ const slots = this.skillSlots.get(unitId);
139
+ if (!slots) {
140
+ return;
141
+ }
142
+ for (const slot of slots) {
143
+ if (slot.definition.type === "basic" || slot.definition.type === "core" || slot.definition.type === "ultimate") {
144
+ slot.cooldownRemain = Math.max(0, slot.cooldownRemain + delta);
145
+ }
146
+ }
147
+ }
148
+ /** 击杀指定单位 */
149
+ killUnit(unitId) {
150
+ const unit = this.getUnitById(unitId);
151
+ if (!unit || !unit.runtime.alive) {
152
+ return;
153
+ }
154
+ unit.runtime.Hp = 0;
155
+ unit.runtime.alive = false;
156
+ this.eventBus.emit({ name: "OnDeath", payload: { targetId: unit.id, sourceId: unit.id } });
157
+ this.rearrangePositions();
158
+ }
159
+ grantActionProgress(unitId, ratio) {
160
+ const unit = this.getUnitById(unitId);
161
+ if (!unit || !unit.runtime.alive) {
162
+ return;
163
+ }
164
+ this.turnBar.grantActionProgress(unit, ratio);
165
+ }
166
+ registerSkillScript(scriptId, script) {
167
+ this.scriptSkillEngine.registerScript(scriptId, script);
168
+ }
169
+ registerChase(predicate, resolver) {
170
+ this.chasePredicates.push(predicate);
171
+ this.chaseSkillResolvers.push(resolver);
172
+ }
173
+ registerReactiveCounterRules() {
174
+ this.eventBus.on("OnAfterDamage", ({ sourceId, targetId, value }) => {
175
+ if (value <= 0 || sourceId === targetId || this.activeSkillContext?.trigger === "counter") {
176
+ return;
177
+ }
178
+ const source = this.units.find((u) => u.id === sourceId);
179
+ const target = this.units.find((u) => u.id === targetId);
180
+ if (!source || !target || !source.runtime.alive || !target.runtime.alive) {
181
+ return;
182
+ }
183
+ const reactiveCounterBuff = this.effectSystem
184
+ .getBuffs(target.id)
185
+ .filter((buff) => buff.stacks > 0 && buff.config.reactiveCounter)
186
+ .sort((a, b) => (b.config.reactiveCounterPriority ?? 0) - (a.config.reactiveCounterPriority ?? 0))[0];
187
+ if (!reactiveCounterBuff) {
188
+ return;
189
+ }
190
+ const basic = this.getSlots(target.id).find((slot) => slot.definition.type === "basic")?.definition;
191
+ if (!basic) {
192
+ return;
193
+ }
194
+ this.consumeBuffStack(target, reactiveCounterBuff.id);
195
+ this.pendingReactiveCounters.push({
196
+ sourceId,
197
+ targetId,
198
+ turn: this.turn,
199
+ sourceSkillId: this.activeSkillContext?.skillId ?? basic.id,
200
+ sourceTriggeredBy: this.activeSkillContext?.trigger
201
+ });
202
+ }, 0);
203
+ }
204
+ processPendingReactiveCounters() {
205
+ while (this.pendingReactiveCounters.length > 0) {
206
+ const pending = this.pendingReactiveCounters.shift();
207
+ const source = this.units.find((u) => u.id === pending.sourceId);
208
+ const target = this.units.find((u) => u.id === pending.targetId);
209
+ if (!source || !target || !source.runtime.alive || !target.runtime.alive) {
210
+ continue;
211
+ }
212
+ const basic = this.getSlots(target.id).find((slot) => slot.definition.type === "basic")?.definition;
213
+ if (!basic) {
214
+ continue;
215
+ }
216
+ const counterCtx = {
217
+ turn: pending.turn,
218
+ sourceActorId: source.id,
219
+ sourceSkillId: pending.sourceSkillId || basic.id,
220
+ sourceTargets: [target.id],
221
+ sourceTriggeredBy: pending.sourceTriggeredBy
222
+ };
223
+ const counterResult = this.executeSkill(target, basic, "counter", counterCtx, [source]);
224
+ this.logger.logAction({
225
+ turn: pending.turn,
226
+ actorId: target.id,
227
+ skillId: basic.id,
228
+ triggeredBy: "counter",
229
+ result: counterResult
230
+ });
231
+ this.logger.logSnapshot(pending.turn, "afterSkill", this.units, (unit) => this.effectSystem.getEffectiveStats(unit), (unit) => this.snapshotBuffs(unit), (unit) => this.computeImbalanceMax(unit));
232
+ }
233
+ }
234
+ consumeBuffStack(owner, buffId) {
235
+ const buff = this.effectSystem.getBuffs(owner.id).find((item) => item.id === buffId);
236
+ if (!buff) {
237
+ return;
238
+ }
239
+ if (buff.stacks <= 1) {
240
+ this.effectSystem.removeBuff(owner, buffId);
241
+ return;
242
+ }
243
+ buff.stacks -= 1;
244
+ }
245
+ registerConfiguredChaseRules() {
246
+ for (const unit of this.units) {
247
+ const slots = this.getSlots(unit.id);
248
+ for (const slot of slots) {
249
+ const rule = slot.definition.chaseRule;
250
+ if (!rule || rule.enabled === false) {
251
+ continue;
252
+ }
253
+ this.registerChase((ctx, actor) => {
254
+ if (actor.id !== unit.id || !actor.runtime.alive) {
255
+ return false;
256
+ }
257
+ if (!(rule.allowSelfTrigger ?? false) && ctx.sourceActorId === actor.id) {
258
+ return false;
259
+ }
260
+ if (rule.triggerSourceSkillIds && rule.triggerSourceSkillIds.length > 0 && !rule.triggerSourceSkillIds.includes(ctx.sourceSkillId)) {
261
+ return false;
262
+ }
263
+ if (rule.triggerSourceTriggers && rule.triggerSourceTriggers.length > 0) {
264
+ if (!ctx.sourceTriggeredBy || !rule.triggerSourceTriggers.includes(ctx.sourceTriggeredBy)) {
265
+ return false;
266
+ }
267
+ }
268
+ if (rule.requireSourceTargets ?? true) {
269
+ if ((ctx.sourceTargets?.length ?? 0) === 0) {
270
+ return false;
271
+ }
272
+ }
273
+ if (rule.sameTeamOnly ?? true) {
274
+ const sourceActor = this.units.find((u) => u.id === ctx.sourceActorId);
275
+ if (!sourceActor || sourceActor.teamId !== actor.teamId) {
276
+ return false;
277
+ }
278
+ }
279
+ const chance = Math.min(10000, Math.max(0, Math.floor(rule.chance ?? 10000)));
280
+ return Math.floor(this.rng.next() * 10000) < chance;
281
+ }, (actor) => {
282
+ const useSkillId = rule.useSkillId ?? slot.definition.id;
283
+ return this.getSlots(actor.id).find((s) => s.definition.id === useSkillId)?.definition;
284
+ });
285
+ }
286
+ }
287
+ }
288
+ run() {
289
+ this.initializeScriptPassives();
290
+ while (!this.winner() && this.actionCount < this.config.maxTurns) {
291
+ const actor = this.turnBar.fillUntilAction(this.units);
292
+ if (!actor) {
293
+ break;
294
+ }
295
+ this.turn += 1;
296
+ this.actionCount += 1;
297
+ this.logger.logSnapshot(this.turn, "beforeAction", this.units, (unit) => this.effectSystem.getEffectiveStats(unit), (unit) => this.snapshotBuffs(unit), (unit) => this.computeImbalanceMax(unit));
298
+ this.eventBus.emit({ name: "OnTurnStart", payload: { actorId: actor.id, turn: this.turn } });
299
+ this.applyTurnStart(actor);
300
+ this.processPendingReactiveCounters();
301
+ if (actor.runtime.alive && !actor.runtime.stunned) {
302
+ this.performAction(actor);
303
+ }
304
+ this.eventBus.emit({ name: "OnTurnEnd", payload: { actorId: actor.id, turn: this.turn } });
305
+ this.applyTurnEnd(actor);
306
+ this.decrementAllCooldowns(actor.id);
307
+ this.turnBar.consumeAction(actor);
308
+ this.logger.logSnapshot(this.turn, "turnEnd", this.units, (unit) => this.effectSystem.getEffectiveStats(unit), (unit) => this.snapshotBuffs(unit), (unit) => this.computeImbalanceMax(unit));
309
+ }
310
+ return {
311
+ winner: this.resolveWinnerByRule(),
312
+ totalTurns: this.actionCount,
313
+ logs: this.logger
314
+ };
315
+ }
316
+ initializeScriptPassives() {
317
+ if (this.scriptPassivesInitialized) {
318
+ return;
319
+ }
320
+ this.scriptPassivesInitialized = true;
321
+ for (const unit of this.units) {
322
+ const passiveSlots = this.getSlots(unit.id).filter((slot) => slot.definition.type === "passive" && slot.definition.mode === "script" && slot.definition.activateOnBattleStart);
323
+ for (const passiveSlot of passiveSlots) {
324
+ this.executeSkill(unit, passiveSlot.definition, "assist");
325
+ }
326
+ }
327
+ }
328
+ performAction(actor) {
329
+ const slots = this.getSlots(actor.id);
330
+ const skillOrder = ["ultimate", "core", "basic"];
331
+ const chaseUsed = new Set();
332
+ for (const type of skillOrder) {
333
+ if (!actor.runtime.alive) {
334
+ break;
335
+ }
336
+ const slot = slots.find((s) => s.definition.type === type);
337
+ if (!slot) {
338
+ continue;
339
+ }
340
+ const canCast = this.canCast(actor, slot);
341
+ if (!canCast) {
342
+ continue;
343
+ }
344
+ slot.cooldownRemain = slot.definition.cooldown;
345
+ if (slot.definition.type === "ultimate") {
346
+ actor.runtime.Energy = Math.max(0, actor.runtime.Energy - slot.definition.energyCost);
347
+ }
348
+ const result = this.executeSkill(actor, slot.definition, "normal");
349
+ this.logger.logAction({
350
+ turn: this.turn,
351
+ actorId: actor.id,
352
+ skillId: slot.definition.id,
353
+ triggeredBy: "normal",
354
+ result
355
+ });
356
+ if (slot.definition.type !== "ultimate") {
357
+ this.gainEnergy(actor, slot.definition.energyGain);
358
+ }
359
+ this.logger.logSnapshot(this.turn, "afterSkill", this.units, (unit) => this.effectSystem.getEffectiveStats(unit), (unit) => this.snapshotBuffs(unit), (unit) => this.computeImbalanceMax(unit));
360
+ this.processPendingReactiveCounters();
361
+ this.rearrangePositions();
362
+ if (!actor.runtime.alive) {
363
+ break;
364
+ }
365
+ this.tryChaseAndAssist(actor, result, chaseUsed);
366
+ if (this.winner()) {
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ canCast(actor, slot) {
372
+ if (!actor.runtime.alive || actor.runtime.stunned) {
373
+ return false;
374
+ }
375
+ if (slot.cooldownRemain > 0) {
376
+ return false;
377
+ }
378
+ if (slot.definition.type === "ultimate") {
379
+ return actor.runtime.Energy >= slot.definition.energyCost;
380
+ }
381
+ return true;
382
+ }
383
+ executeSkill(actor, skill, trigger, triggerCtx, selectedTargetsOverride) {
384
+ this.eventBus.emit({ name: "OnBeforeCast", payload: { actorId: actor.id, skillId: skill.id, trigger } });
385
+ const previousSkillContext = this.activeSkillContext;
386
+ this.activeSkillContext = { actorId: actor.id, skillId: skill.id, trigger };
387
+ const result = (() => {
388
+ try {
389
+ return this.skillExecutor.execute(skill, actor, {
390
+ rng: this.rng,
391
+ getUnits: () => this.units,
392
+ applyDamage: (sourceId, targetId, value, staggerElement) => this.applyDamage(sourceId, targetId, value, staggerElement),
393
+ applyHeal: (targetId, value) => this.applyHeal(targetId, value),
394
+ applyShield: (targetId, value) => this.applyShield(targetId, value),
395
+ applyBuff: (sourceId, targetId, buffId) => this.applyBuff(sourceId, targetId, buffId),
396
+ getEffectiveStats: (unit) => this.effectSystem.getEffectiveStats(unit),
397
+ createScriptApi: (skillElement) => ({
398
+ damage: (sourceId, targetId, value) => this.applyDamage(sourceId, targetId, value, skillElement),
399
+ heal: (targetId, value) => this.applyHeal(targetId, value),
400
+ shield: (targetId, value) => this.applyShield(targetId, value),
401
+ adjustImbalance: (targetId, delta) => {
402
+ const sourceActorId = this.activeSkillContext?.actorId ?? actor.id;
403
+ const sourceSkillId = this.activeSkillContext?.skillId ?? skill.id;
404
+ return this.applyImbalanceDelta(sourceActorId, sourceSkillId, targetId, delta, "skill_effect");
405
+ },
406
+ addBuff: (sourceId, targetId, buffId) => this.applyBuff(sourceId, targetId, buffId),
407
+ removeBuff: (targetId, buffId) => {
408
+ const target = this.getUnitById(targetId);
409
+ if (target) {
410
+ this.effectSystem.removeBuff(target, buffId);
411
+ }
412
+ },
413
+ grantExtraTurn: (unitId) => this.turnBar.grantExtraTurn(unitId),
414
+ adjustActionProgress: (unitId, ratio) => this.grantActionProgress(unitId, ratio),
415
+ gainEnergy: (unitId, value) => this.gainEnergyToUnit(unitId, value),
416
+ getUnit: (unitId) => this.units.find((u) => u.id === unitId),
417
+ getAliveUnits: () => this.units.filter((u) => u.runtime.alive),
418
+ hasBuff: (unitId, buffId) => this.effectSystem.hasBuff(unitId, buffId),
419
+ getBuffs: (unitId) => this.effectSystem.getBuffs(unitId),
420
+ randomInt: (maxExclusive) => (maxExclusive <= 0 ? 0 : this.rng.nextInt(maxExclusive)),
421
+ pushPosition: (unitId, delta) => {
422
+ const unit = this.getUnitById(unitId);
423
+ if (unit) {
424
+ unit.position = Math.max(1, Math.min(4, unit.position + delta));
425
+ }
426
+ },
427
+ preventDeath: (unitId) => {
428
+ const unit = this.getUnitById(unitId);
429
+ if (unit && unit.runtime.Hp <= 0) {
430
+ unit.runtime.Hp = 1;
431
+ unit.runtime.alive = true;
432
+ }
433
+ },
434
+ isUnitTurn: (unitId) => {
435
+ return this.activeSkillContext?.actorId === unitId;
436
+ },
437
+ castSkill: (actorId, skillId, trigger, targetIds) => {
438
+ const castActor = this.getUnitById(actorId);
439
+ if (!castActor || !castActor.runtime.alive) {
440
+ return null;
441
+ }
442
+ const castSkill = this.getSlots(actorId).find((slot) => slot.definition.id === skillId)?.definition;
443
+ if (!castSkill) {
444
+ return null;
445
+ }
446
+ const selectedTargets = (targetIds ?? [])
447
+ .map((id) => this.getUnitById(id))
448
+ .filter((u) => Boolean(u && u.runtime.alive));
449
+ const triggerCtx = {
450
+ turn: this.turn,
451
+ sourceActorId: this.activeSkillContext?.actorId ?? actorId,
452
+ sourceSkillId: this.activeSkillContext?.skillId ?? skillId,
453
+ sourceTargets: selectedTargets.map((u) => u.id),
454
+ sourceTriggeredBy: this.activeSkillContext?.trigger
455
+ };
456
+ const castResult = this.executeSkill(castActor, castSkill, trigger, triggerCtx, selectedTargets.length > 0 ? selectedTargets : undefined);
457
+ this.logger.logAction({
458
+ turn: this.turn,
459
+ actorId,
460
+ skillId,
461
+ triggeredBy: trigger,
462
+ result: castResult
463
+ });
464
+ this.logger.logSnapshot(this.turn, "afterSkill", this.units, (u) => this.effectSystem.getEffectiveStats(u), (u) => this.snapshotBuffs(u), (u) => this.computeImbalanceMax(u));
465
+ this.processPendingReactiveCounters();
466
+ return castResult;
467
+ },
468
+ onAnyEvent: (listenerUnitId, eventName, handler) => {
469
+ this.eventBus.on(eventName, handler);
470
+ },
471
+ spawnUnit: (initUnit) => this.spawnUnit(initUnit),
472
+ extendCooldown: (unitId, delta) => this.extendCooldown(unitId, delta),
473
+ killUnit: (unitId) => this.killUnit(unitId)
474
+ })
475
+ }, trigger, triggerCtx, selectedTargetsOverride);
476
+ }
477
+ finally {
478
+ this.activeSkillContext = previousSkillContext;
479
+ }
480
+ })();
481
+ this.eventBus.emit({ name: "OnAfterCast", payload: { actorId: actor.id, skillId: skill.id, trigger, result } });
482
+ return result;
483
+ }
484
+ tryChaseAndAssist(actor, result, chaseUsed) {
485
+ const triggerCtx = {
486
+ turn: this.turn,
487
+ sourceActorId: actor.id,
488
+ sourceSkillId: result.skillId,
489
+ sourceTargets: result.targets,
490
+ sourceTriggeredBy: result.triggeredBy
491
+ };
492
+ for (const unit of this.units.filter((u) => u.runtime.alive && !chaseUsed.has(u.id))) {
493
+ for (let i = 0; i < this.chasePredicates.length; i++) {
494
+ if (!this.chasePredicates[i](triggerCtx, unit)) {
495
+ continue;
496
+ }
497
+ const skill = this.chaseSkillResolvers[i](unit);
498
+ if (!skill) {
499
+ continue;
500
+ }
501
+ const chaseResult = this.executeSkill(unit, skill, "chase", triggerCtx);
502
+ this.logger.logAction({
503
+ turn: this.turn,
504
+ actorId: unit.id,
505
+ skillId: skill.id,
506
+ triggeredBy: "chase",
507
+ result: chaseResult
508
+ });
509
+ this.logger.logSnapshot(this.turn, "afterSkill", this.units, (u) => this.effectSystem.getEffectiveStats(u), (u) => this.snapshotBuffs(u), (u) => this.computeImbalanceMax(u));
510
+ this.processPendingReactiveCounters();
511
+ chaseUsed.add(unit.id);
512
+ }
513
+ }
514
+ if (result.targets.length === 0) {
515
+ return;
516
+ }
517
+ const firstTarget = this.units.find((u) => u.id === result.targets[0] && u.runtime.alive);
518
+ if (!firstTarget) {
519
+ return;
520
+ }
521
+ const allies = this.units.filter((u) => u.teamId === actor.teamId && u.id !== actor.id && u.runtime.alive);
522
+ for (const ally of allies) {
523
+ const chance = Math.min(10000, Math.max(0, Math.floor(ally.stats.pAR)));
524
+ if (Math.floor(this.rng.next() * 10000) >= chance) {
525
+ continue;
526
+ }
527
+ const basic = this.getSlots(ally.id).find((s) => s.definition.type === "basic")?.definition;
528
+ if (!basic) {
529
+ continue;
530
+ }
531
+ const assistRes = this.executeSkill(ally, basic, "assist", {
532
+ ...triggerCtx,
533
+ sourceTargets: [firstTarget.id]
534
+ });
535
+ this.logger.logAction({
536
+ turn: this.turn,
537
+ actorId: ally.id,
538
+ skillId: basic.id,
539
+ triggeredBy: "assist",
540
+ result: assistRes
541
+ });
542
+ this.logger.logSnapshot(this.turn, "afterSkill", this.units, (u) => this.effectSystem.getEffectiveStats(u), (u) => this.snapshotBuffs(u), (u) => this.computeImbalanceMax(u));
543
+ this.processPendingReactiveCounters();
544
+ }
545
+ }
546
+ applyDamage(sourceId, targetId, value, staggerElement, skipShare = false) {
547
+ const target = this.mustUnit(targetId);
548
+ const source = this.mustUnit(sourceId);
549
+ this.eventBus.emit({ name: "OnBeforeDamage", payload: { sourceId, targetId, value } });
550
+ this.eventBus.emit({ name: "OnDamageDealt", payload: { sourceId, targetId, value } });
551
+ let damage = Math.max(0, Math.floor(value));
552
+ if (!skipShare && sourceId !== targetId) {
553
+ const shareBuff = this.effectSystem.getBuffs(target.id).find((buff) => buff.stacks > 0 && (buff.config.damageShareToSourceRatio ?? 0) > 0);
554
+ if (shareBuff && shareBuff.sourceId !== target.id) {
555
+ const shareTo = this.units.find((unit) => unit.id === shareBuff.sourceId && unit.runtime.alive);
556
+ if (shareTo) {
557
+ const ratio = Math.max(0, Math.min(1, shareBuff.config.damageShareToSourceRatio ?? 0));
558
+ const sharedDamage = Math.floor(damage * ratio);
559
+ if (sharedDamage > 0) {
560
+ this.applyDamage(sourceId, shareTo.id, sharedDamage, undefined, true);
561
+ damage -= sharedDamage;
562
+ }
563
+ }
564
+ }
565
+ }
566
+ if (target.runtime.Shield > 0) {
567
+ const absorbed = Math.min(target.runtime.Shield, damage);
568
+ target.runtime.Shield -= absorbed;
569
+ damage -= absorbed;
570
+ }
571
+ target.runtime.Hp = Math.max(0, target.runtime.Hp - damage);
572
+ if (target.runtime.Hp === 0) {
573
+ target.runtime.alive = false;
574
+ this.eventBus.emit({ name: "OnDeath", payload: { targetId: target.id, sourceId: source.id } });
575
+ this.eventBus.emit({ name: "OnKill", payload: { killerId: source.id, targetId: target.id } });
576
+ }
577
+ this.eventBus.emit({ name: "OnAfterDamage", payload: { sourceId, targetId, value: damage } });
578
+ if (staggerElement && sourceId !== targetId && target.runtime.alive) {
579
+ this.accumulateStagger(source, target, staggerElement);
580
+ }
581
+ return damage;
582
+ }
583
+ applyHeal(targetId, value) {
584
+ const target = this.mustUnit(targetId);
585
+ if (!target.runtime.alive) {
586
+ return 0;
587
+ }
588
+ const heal = Math.max(0, Math.floor(value));
589
+ const old = target.runtime.Hp;
590
+ target.runtime.Hp = Math.min(target.stats.Mhp, target.runtime.Hp + heal);
591
+ return target.runtime.Hp - old;
592
+ }
593
+ applyShield(targetId, value) {
594
+ const target = this.mustUnit(targetId);
595
+ if (!target.runtime.alive) {
596
+ return 0;
597
+ }
598
+ const shield = Math.max(0, Math.floor(value));
599
+ target.runtime.Shield += shield;
600
+ return shield;
601
+ }
602
+ applyImbalanceDelta(sourceActorId, sourceSkillId, targetId, delta, reason) {
603
+ const target = this.mustUnit(targetId);
604
+ if (!target.runtime.alive || delta === 0) {
605
+ return 0;
606
+ }
607
+ const max = this.computeImbalanceMax(target);
608
+ const from = target.runtime.Imbalance;
609
+ const to = Math.max(0, Math.min(max, from + Math.floor(delta)));
610
+ target.runtime.Imbalance = to;
611
+ const applied = to - from;
612
+ if (applied !== 0) {
613
+ this.logger.logImbalanceChange(target.id, applied, reason, sourceActorId, sourceSkillId);
614
+ }
615
+ if (to < max && this.pendingImbalanceClear.has(target.id)) {
616
+ this.pendingImbalanceClear.delete(target.id);
617
+ }
618
+ return applied;
619
+ }
620
+ computeImbalanceMax(unit) {
621
+ const effectiveStats = this.effectSystem.getEffectiveStats(unit);
622
+ return Math.floor(Math.floor(unit.stats.Mhp * 6000 / 10000) * (10000 + effectiveStats.pITM) / 10000);
623
+ }
624
+ accumulateStagger(source, target, skillElement) {
625
+ if (this.pendingImbalanceClear.has(target.id)) {
626
+ return;
627
+ }
628
+ const effectiveSource = this.effectSystem.getEffectiveStats(source);
629
+ const baseStagger = effectiveSource.Att;
630
+ let stagger = Math.floor(baseStagger * (10000 + effectiveSource.pIM) / 10000);
631
+ const rep = repression(source.element, target.element);
632
+ if (rep === 1) {
633
+ stagger = Math.floor(stagger * 13000 / 10000);
634
+ }
635
+ else if (rep === -1) {
636
+ stagger = Math.floor(stagger * 7000 / 10000);
637
+ }
638
+ const max = this.computeImbalanceMax(target);
639
+ const from = target.runtime.Imbalance;
640
+ target.runtime.Imbalance = Math.min(max, from + stagger);
641
+ const applied = target.runtime.Imbalance - from;
642
+ if (applied !== 0) {
643
+ this.logger.logImbalanceChange(target.id, applied, "skill_effect", this.activeSkillContext?.actorId ?? source.id, this.activeSkillContext?.skillId);
644
+ }
645
+ if (target.runtime.Imbalance >= max) {
646
+ this.onImbalanceBreak(source, target, skillElement);
647
+ }
648
+ }
649
+ onImbalanceBreak(source, target, element) {
650
+ const max = this.computeImbalanceMax(target);
651
+ target.runtime.Imbalance = max;
652
+ this.pendingImbalanceClear.add(target.id);
653
+ const breakBuffs = this.effectSystem
654
+ .getBuffConfigs()
655
+ .filter((buff) => buff.applyOnImbalanceBreak || buff.applyOnElementBreakElements?.includes(element));
656
+ const appliedBuffIds = new Set();
657
+ for (const breakBuff of breakBuffs) {
658
+ if (appliedBuffIds.has(breakBuff.id)) {
659
+ continue;
660
+ }
661
+ appliedBuffIds.add(breakBuff.id);
662
+ this.applyBuff(source.id, target.id, breakBuff.id);
663
+ }
664
+ this.eventBus.emit({ name: "OnImbalanceBreak", payload: { sourceId: source.id, targetId: target.id, element } });
665
+ this.logger.logImbalanceBreak(target.id, source.id, element);
666
+ }
667
+ applyBuff(sourceId, targetId, buffId) {
668
+ const target = this.mustUnit(targetId);
669
+ this.effectSystem.addBuff(target, sourceId, buffId);
670
+ }
671
+ applyTurnStart(actor) {
672
+ if (this.pendingImbalanceClear.has(actor.id)) {
673
+ const from = actor.runtime.Imbalance;
674
+ actor.runtime.Imbalance = 0;
675
+ this.pendingImbalanceClear.delete(actor.id);
676
+ this.logger.logImbalanceChange(actor.id, -from, "turn_start_clear", actor.id);
677
+ }
678
+ const dotEvents = this.effectSystem.onTurnStart(actor, this.units);
679
+ for (const dot of dotEvents) {
680
+ this.applyDamage(actor.id, dot.targetId, dot.value);
681
+ // 记录DOT伤害到logger,用于飘字展示
682
+ this.logger.logDotDamage(dot.targetId, dot.value, dot.buffId, dot.element);
683
+ }
684
+ this.rearrangePositions();
685
+ }
686
+ applyTurnEnd(actor) {
687
+ this.effectSystem.onTurnEnd(actor, this.units);
688
+ this.rearrangePositions();
689
+ }
690
+ gainEnergy(actor, value) {
691
+ this.gainEnergyToUnit(actor.id, value);
692
+ }
693
+ gainEnergyToUnit(unitId, value) {
694
+ const actor = this.mustUnit(unitId);
695
+ const ratio = 10000 + actor.stats.pER;
696
+ const gain = Math.floor((Math.max(0, value) * ratio) / 10000);
697
+ actor.runtime.Energy = Math.min(actor.runtime.EnergyMax, actor.runtime.Energy + gain);
698
+ }
699
+ decrementAllCooldowns(actorId) {
700
+ for (const [id, slots] of this.skillSlots.entries()) {
701
+ if (id !== actorId) {
702
+ continue;
703
+ }
704
+ for (const slot of slots) {
705
+ slot.cooldownRemain = Math.max(0, slot.cooldownRemain - 1);
706
+ }
707
+ }
708
+ }
709
+ getSlots(unitId) {
710
+ return this.skillSlots.get(unitId) ?? [];
711
+ }
712
+ rearrangePositions() {
713
+ for (const teamId of [1, 2]) {
714
+ const alive = this.units.filter((u) => u.teamId === teamId && u.runtime.alive).sort((a, b) => a.position - b.position);
715
+ alive.forEach((u, idx) => {
716
+ u.position = (idx + 1);
717
+ });
718
+ }
719
+ }
720
+ winner() {
721
+ const t1Alive = this.units.some((u) => u.teamId === 1 && u.runtime.alive);
722
+ const t2Alive = this.units.some((u) => u.teamId === 2 && u.runtime.alive);
723
+ if (!t1Alive && !t2Alive) {
724
+ return 0;
725
+ }
726
+ if (!t1Alive) {
727
+ return 2;
728
+ }
729
+ if (!t2Alive) {
730
+ return 1;
731
+ }
732
+ return 0;
733
+ }
734
+ resolveWinnerByRule() {
735
+ const w = this.winner();
736
+ if (w) {
737
+ return w;
738
+ }
739
+ const t1 = this.hpRatio(1);
740
+ const t2 = this.hpRatio(2);
741
+ if (t1 > t2) {
742
+ return 1;
743
+ }
744
+ if (t2 > t1) {
745
+ return 2;
746
+ }
747
+ return 2;
748
+ }
749
+ hpRatio(teamId) {
750
+ const team = this.units.filter((u) => u.teamId === teamId);
751
+ const hp = team.reduce((sum, u) => sum + Math.max(0, u.runtime.Hp), 0);
752
+ const mhp = team.reduce((sum, u) => sum + u.stats.Mhp, 0);
753
+ return mhp > 0 ? hp / mhp : 0;
754
+ }
755
+ mustUnit(id) {
756
+ const unit = this.units.find((u) => u.id === id);
757
+ if (!unit) {
758
+ throw new Error(`unit-not-found:${id}`);
759
+ }
760
+ return unit;
761
+ }
762
+ maxUltimateCost(skills) {
763
+ const ult = skills.find((s) => s.type === "ultimate");
764
+ return ult ? ult.energyCost : 100;
765
+ }
766
+ snapshotBuffs(unit) {
767
+ return this.effectSystem
768
+ .getBuffs(unit.id)
769
+ .filter((b) => b.stacks > 0 && b.remainingRounds > 0)
770
+ .sort((a, b) => a.id.localeCompare(b.id))
771
+ .map((b) => ({
772
+ id: b.id,
773
+ name: b.config.name,
774
+ stacks: b.stacks,
775
+ remainingRounds: b.remainingRounds,
776
+ sourceId: b.sourceId
777
+ }));
778
+ }
779
+ }