hrbattle 1.0.2 → 1.0.3

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.
@@ -395,7 +395,10 @@ export class BattleCore {
395
395
  applyBuff: (sourceId, targetId, buffId) => this.applyBuff(sourceId, targetId, buffId),
396
396
  getEffectiveStats: (unit) => this.effectSystem.getEffectiveStats(unit),
397
397
  createScriptApi: (skillElement) => ({
398
- damage: (sourceId, targetId, value) => this.applyDamage(sourceId, targetId, value, skillElement),
398
+ damage: (sourceId, targetId, value) => {
399
+ const r = this.applyDamage(sourceId, targetId, value, skillElement);
400
+ return { damage: r.damage, imbalanceDelta: r.imbalanceDelta, triggeredImbalanceBreak: r.triggeredImbalanceBreak };
401
+ },
399
402
  heal: (targetId, value) => this.applyHeal(targetId, value),
400
403
  shield: (targetId, value) => this.applyShield(targetId, value),
401
404
  adjustImbalance: (targetId, delta) => {
@@ -575,10 +578,14 @@ export class BattleCore {
575
578
  this.eventBus.emit({ name: "OnKill", payload: { killerId: source.id, targetId: target.id } });
576
579
  }
577
580
  this.eventBus.emit({ name: "OnAfterDamage", payload: { sourceId, targetId, value: damage } });
581
+ let imbalanceDelta = 0;
582
+ let triggeredImbalanceBreak = false;
578
583
  if (staggerElement && sourceId !== targetId && target.runtime.alive) {
579
- this.accumulateStagger(source, target, staggerElement);
584
+ const staggerResult = this.accumulateStagger(source, target, staggerElement);
585
+ imbalanceDelta = staggerResult.applied;
586
+ triggeredImbalanceBreak = staggerResult.broke;
580
587
  }
581
- return damage;
588
+ return { damage, imbalanceDelta, triggeredImbalanceBreak };
582
589
  }
583
590
  applyHeal(targetId, value) {
584
591
  const target = this.mustUnit(targetId);
@@ -623,7 +630,7 @@ export class BattleCore {
623
630
  }
624
631
  accumulateStagger(source, target, skillElement) {
625
632
  if (this.pendingImbalanceClear.has(target.id)) {
626
- return;
633
+ return { applied: 0, broke: false };
627
634
  }
628
635
  const effectiveSource = this.effectSystem.getEffectiveStats(source);
629
636
  const baseStagger = effectiveSource.Att;
@@ -642,9 +649,12 @@ export class BattleCore {
642
649
  if (applied !== 0) {
643
650
  this.logger.logImbalanceChange(target.id, applied, "skill_effect", this.activeSkillContext?.actorId ?? source.id, this.activeSkillContext?.skillId);
644
651
  }
652
+ let broke = false;
645
653
  if (target.runtime.Imbalance >= max) {
654
+ broke = true;
646
655
  this.onImbalanceBreak(source, target, skillElement);
647
656
  }
657
+ return { applied, broke };
648
658
  }
649
659
  onImbalanceBreak(source, target, element) {
650
660
  const max = this.computeImbalanceMax(target);
@@ -0,0 +1,3 @@
1
+ import type { BuffConfig, SkillTemplate } from "../types";
2
+ export declare const sampleBuffs: BuffConfig[];
3
+ export declare const sampleTemplates: SkillTemplate[];
@@ -0,0 +1,82 @@
1
+ export const sampleBuffs = [
2
+ {
3
+ id: "slow",
4
+ name: "迟缓",
5
+ maxStacks: 5,
6
+ durationPerStack: 1,
7
+ expireRounds: 1,
8
+ onRoundEndDelta: -1,
9
+ speedRateDelta: -1000
10
+ },
11
+ {
12
+ id: "stun",
13
+ name: "眩晕",
14
+ maxStacks: 1,
15
+ durationPerStack: 1,
16
+ onRoundEndDelta: -999,
17
+ stun: true
18
+ },
19
+ {
20
+ id: "taunt",
21
+ name: "嘲讽",
22
+ maxStacks: 1,
23
+ durationPerStack: 2,
24
+ expireRounds: 2,
25
+ onRoundEndDelta: -1,
26
+ taunt: true
27
+ },
28
+ {
29
+ id: "burn",
30
+ name: "点燃",
31
+ maxStacks: 99,
32
+ durationPerStack: 1,
33
+ expireRounds: 1,
34
+ onRoundEndDelta: -1,
35
+ dotRatio: 0.18
36
+ },
37
+ {
38
+ id: "share-damage",
39
+ name: "分摊伤害",
40
+ maxStacks: 1,
41
+ durationPerStack: 3,
42
+ expireRounds: 3,
43
+ onRoundEndDelta: -1
44
+ },
45
+ {
46
+ id: "imbalance-vulnerability",
47
+ name: "失衡易伤",
48
+ maxStacks: 1,
49
+ durationPerStack: 1,
50
+ expireRounds: 1,
51
+ onRoundEndDelta: -1,
52
+ vulnerabilityDelta: 2000
53
+ }
54
+ ];
55
+ export const sampleTemplates = [
56
+ {
57
+ id: "basic-single",
58
+ steps: [
59
+ { kind: "damage", ratio: 0.5 },
60
+ { kind: "buff", ratio: 0, buffId: "burn", fixedProb: 2500 }
61
+ ]
62
+ },
63
+ {
64
+ id: "core-stun",
65
+ steps: [
66
+ { kind: "damage", ratio: 0.8 },
67
+ { kind: "buff", ratio: 0, buffId: "stun", fixedProb: 6500 }
68
+ ]
69
+ },
70
+ {
71
+ id: "ultimate-aoe",
72
+ steps: [{ kind: "damage", ratio: 1.8 }]
73
+ },
74
+ {
75
+ id: "taunt-skill",
76
+ steps: [{ kind: "buff", ratio: 0, buffId: "taunt", fixedProb: 8000 }]
77
+ },
78
+ {
79
+ id: "shield-team",
80
+ steps: [{ kind: "shield", ratio: 0.6 }]
81
+ }
82
+ ];
@@ -14,17 +14,19 @@ function buildEmptyResult(ctx, targets = ctx.targets) {
14
14
  triggeredBy: ctx.trigger
15
15
  };
16
16
  }
17
- function buildDamageHit(ctx, targetId, finalValue) {
17
+ function buildDamageHit(ctx, targetId, finalValue, imbalanceDelta, triggeredImbalanceBreak) {
18
18
  return {
19
19
  targetId,
20
20
  effectType: "damage",
21
21
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
22
- skillType: "basic",
22
+ skillType: ctx.skillType,
23
23
  isCritical: false,
24
24
  baseValue: finalValue,
25
25
  criticalValue: finalValue,
26
26
  finalValue,
27
27
  isRepression: 0,
28
+ imbalanceDelta,
29
+ triggeredImbalanceBreak,
28
30
  damageBreakdown: {
29
31
  preMitigation: finalValue,
30
32
  postMitigation: finalValue,
@@ -38,7 +40,7 @@ function buildBuffHit(ctx, targetId, buffId, resisted = false) {
38
40
  targetId,
39
41
  effectType: "buff",
40
42
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
41
- skillType: "basic",
43
+ skillType: ctx.skillType,
42
44
  isCritical: false,
43
45
  baseValue: 0,
44
46
  criticalValue: 0,
@@ -53,7 +55,7 @@ function buildShieldHit(ctx, targetId, finalValue) {
53
55
  targetId,
54
56
  effectType: "shield",
55
57
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
56
- skillType: "basic",
58
+ skillType: ctx.skillType,
57
59
  isCritical: false,
58
60
  baseValue: finalValue,
59
61
  criticalValue: finalValue,
@@ -66,7 +68,7 @@ function buildHealHit(ctx, targetId, finalValue) {
66
68
  targetId,
67
69
  effectType: "heal",
68
70
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
69
- skillType: "basic",
71
+ skillType: ctx.skillType,
70
72
  isCritical: false,
71
73
  baseValue: finalValue,
72
74
  criticalValue: finalValue,
@@ -86,9 +88,9 @@ const bladeUltimateScript = {
86
88
  const hits = [];
87
89
  if (target && target.runtime.Hp * 100 < target.stats.Mhp * 50) {
88
90
  // 策划:对生命值低于50%的敌人额外造成20%伤害
89
- const damage = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.2));
90
- totalDamage += damage;
91
- hits.push(buildDamageHit(ctx, targetId, damage));
91
+ const r = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.2));
92
+ totalDamage += r.damage;
93
+ hits.push(buildDamageHit(ctx, targetId, r.damage, r.imbalanceDelta));
92
94
  }
93
95
  return {
94
96
  ...buildEmptyResult(ctx, [targetId]),
@@ -110,9 +112,9 @@ const bladeBasicScript = {
110
112
  const hits = [];
111
113
  if (target && target.runtime.Hp * 100 > target.stats.Mhp * 70) {
112
114
  // 策划:对生命值高于70%的敌人造成额外20%伤害
113
- const damage = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.2));
114
- totalDamage += damage;
115
- hits.push(buildDamageHit(ctx, targetId, damage));
115
+ const r = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.2));
116
+ totalDamage += r.damage;
117
+ hits.push(buildDamageHit(ctx, targetId, r.damage, r.imbalanceDelta));
116
118
  }
117
119
  return {
118
120
  ...buildEmptyResult(ctx, [targetId]),
@@ -137,9 +139,9 @@ const bladeCoreScript = {
137
139
  for (let i = 0; i < 3; i++) {
138
140
  const target = enemies[ctx.api.randomInt(enemies.length)];
139
141
  hitTargets.push(target.id);
140
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.3));
141
- totalDamage += damage;
142
- hits.push(buildDamageHit(ctx, target.id, damage));
142
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.3));
143
+ totalDamage += r.damage;
144
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
143
145
  if (ctx.api.randomInt(10000) < 2000) {
144
146
  ctx.api.gainEnergy(ctx.caster, 10);
145
147
  }
@@ -168,24 +170,8 @@ const luoluoBasicScript = {
168
170
  // 洛洛核心技:获得10%生命值的护盾,每层【惩戒/意志】使护盾强度增加10%
169
171
  const luoluoCoreScript = {
170
172
  onCast(ctx) {
171
- const self = ctx.api.getUnit(ctx.caster);
172
- let totalShield = 0;
173
- const hits = [];
174
- if (self) {
175
- // 策划:10%Mhp护盾,每层意志增加10%
176
- const willBuffs = ctx.api.getBuffs(ctx.caster).filter(b => b.id === "will");
177
- const willStacks = willBuffs.reduce((sum, b) => sum + b.stacks, 0);
178
- const shieldMultiplier = 1 + willStacks * 0.1;
179
- const shield = Math.floor(self.stats.Mhp * 0.1 * shieldMultiplier);
180
- ctx.api.shield(ctx.caster, shield);
181
- totalShield += shield;
182
- hits.push(buildShieldHit(ctx, ctx.caster, shield));
183
- }
184
- return {
185
- ...buildEmptyResult(ctx, ctx.targets),
186
- totalShield: totalShield,
187
- hits
188
- };
173
+ // 护盾交由config模板结算,script层不再重复加盾
174
+ return buildEmptyResult(ctx, ctx.targets);
189
175
  }
190
176
  };
191
177
  const miaoluoUltimateScript = {
@@ -206,14 +192,14 @@ const miaoluoUltimateScript = {
206
192
  ctx.api.addBuff(ctx.caster, ctx.caster, "miaoluo-ultimate-focus");
207
193
  hits.push(buildBuffHit(ctx, ctx.caster, "miaoluo-ultimate-focus"));
208
194
  for (let i = 0; i < 3; i += 1) {
209
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.6));
210
- totalDamage += damage;
211
- hits.push(buildDamageHit(ctx, target.id, damage));
195
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.6));
196
+ totalDamage += r.damage;
197
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
212
198
  }
213
199
  if (ctx.api.hasBuff(target.id, "imbalance-vulnerability")) {
214
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.3));
215
- totalDamage += damage;
216
- hits.push(buildDamageHit(ctx, target.id, damage));
200
+ const r2 = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.3));
201
+ totalDamage += r2.damage;
202
+ hits.push(buildDamageHit(ctx, target.id, r2.damage, r2.imbalanceDelta));
217
203
  }
218
204
  }
219
205
  return {
@@ -238,9 +224,9 @@ const miaoluoBasicScript = {
238
224
  let totalDamage = 0;
239
225
  const hits = [];
240
226
  if (self && target) {
241
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
242
- totalDamage += damage;
243
- hits.push(buildDamageHit(ctx, target.id, damage));
227
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
228
+ totalDamage += r.damage;
229
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
244
230
  }
245
231
  return {
246
232
  ...buildEmptyResult(ctx, target ? [target.id] : []),
@@ -264,9 +250,9 @@ const miaoluoCoreScript = {
264
250
  let totalDamage = 0;
265
251
  const hits = [];
266
252
  if (self && target) {
267
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.8));
268
- totalDamage += damage;
269
- hits.push(buildDamageHit(ctx, target.id, damage));
253
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.8));
254
+ totalDamage += r.damage;
255
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
270
256
  ctx.api.addBuff(ctx.caster, target.id, "static");
271
257
  hits.push(buildBuffHit(ctx, target.id, "static"));
272
258
  ctx.api.addBuff(ctx.caster, ctx.caster, "flash");
@@ -288,13 +274,13 @@ const cookBasicScript = {
288
274
  if (self) {
289
275
  const selfCost = Math.floor(self.runtime.Hp * 0.1);
290
276
  if (selfCost > 0) {
291
- const damage = ctx.api.damage(ctx.caster, ctx.caster, selfCost);
292
- totalDamage += damage;
293
- hits.push(buildDamageHit(ctx, ctx.caster, damage));
277
+ const r1 = ctx.api.damage(ctx.caster, ctx.caster, selfCost);
278
+ totalDamage += r1.damage;
279
+ hits.push(buildDamageHit(ctx, ctx.caster, r1.damage, r1.imbalanceDelta));
294
280
  if (targetId) {
295
- const damage2 = ctx.api.damage(ctx.caster, targetId, Math.floor(selfCost * 0.5));
296
- totalDamage += damage2;
297
- hits.push(buildDamageHit(ctx, targetId, damage2));
281
+ const r2 = ctx.api.damage(ctx.caster, targetId, Math.floor(selfCost * 0.5));
282
+ totalDamage += r2.damage;
283
+ hits.push(buildDamageHit(ctx, targetId, r2.damage, r2.imbalanceDelta));
298
284
  }
299
285
  }
300
286
  }
@@ -324,9 +310,9 @@ const cookCoreScript = {
324
310
  const lostHp = Math.max(0, self.stats.Mhp - self.runtime.Hp);
325
311
  const lostStepCount = Math.floor((lostHp * 10) / Math.max(1, self.stats.Mhp));
326
312
  const multiplier = 1 + lostStepCount * 0.05;
327
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * multiplier));
328
- totalDamage += damage;
329
- hits.push(buildDamageHit(ctx, target.id, damage));
313
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * multiplier));
314
+ totalDamage += r.damage;
315
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
330
316
  }
331
317
  return {
332
318
  ...buildEmptyResult(ctx, target ? [target.id] : []),
@@ -372,9 +358,9 @@ const kesiCoreScript = {
372
358
  const index = ctx.api.randomInt(pool.length);
373
359
  const [target] = pool.splice(index, 1);
374
360
  hitTargets.push(target.id);
375
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
376
- totalDamage += damage;
377
- hits.push(buildDamageHit(ctx, target.id, damage));
361
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
362
+ totalDamage += r.damage;
363
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
378
364
  if (target.teamId === self.teamId) {
379
365
  ctx.api.gainEnergy(ctx.caster, 10);
380
366
  }
@@ -404,9 +390,9 @@ const kesiUltimateScript = {
404
390
  const target = enemies[ctx.api.randomInt(enemies.length)];
405
391
  hitTargets.push(target.id);
406
392
  attacked.add(target.id);
407
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.8));
408
- totalDamage += damage;
409
- hits.push(buildDamageHit(ctx, target.id, damage));
393
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.8));
394
+ totalDamage += r.damage;
395
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
410
396
  }
411
397
  for (const targetId of attacked) {
412
398
  const target = ctx.api.getUnit(targetId);
@@ -418,9 +404,9 @@ const kesiUltimateScript = {
418
404
  .filter((buff) => buff.stacks > 0 && buff.config.dotRatio)
419
405
  .reduce((sum, buff) => sum + Math.floor(target.stats.Att * (buff.config.dotRatio ?? 0) * buff.stacks), 0);
420
406
  if (dotValue > 0) {
421
- const damage = ctx.api.damage(ctx.caster, targetId, Math.floor(dotValue * 0.6));
422
- totalDamage += damage;
423
- hits.push(buildDamageHit(ctx, target.id, damage));
407
+ const r2 = ctx.api.damage(ctx.caster, targetId, Math.floor(dotValue * 0.6));
408
+ totalDamage += r2.damage;
409
+ hits.push(buildDamageHit(ctx, target.id, r2.damage, r2.imbalanceDelta));
424
410
  }
425
411
  }
426
412
  return {
@@ -636,11 +622,11 @@ const luolaiBasicScript = {
636
622
  let totalDamage = 0;
637
623
  let totalHeal = 0;
638
624
  const hits = [];
639
- const damage = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
640
- totalDamage += damage;
641
- hits.push(buildDamageHit(ctx, target.id, damage));
625
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
626
+ totalDamage += r.damage;
627
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
642
628
  if (frontAlly) {
643
- const heal = ctx.api.heal(frontAlly.id, Math.floor(damage * 0.5));
629
+ const heal = ctx.api.heal(frontAlly.id, Math.floor(r.damage * 0.5));
644
630
  totalHeal += heal;
645
631
  hits.push(buildHealHit(ctx, frontAlly.id, heal));
646
632
  }
@@ -938,9 +924,9 @@ const esCoreScript = {
938
924
  let totalDamage = 0;
939
925
  const hits = [];
940
926
  for (const enemy of enemies) {
941
- const damage = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 0.35));
942
- totalDamage += damage;
943
- hits.push(buildDamageHit(ctx, enemy.id, damage));
927
+ const r = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 0.35));
928
+ totalDamage += r.damage;
929
+ hits.push(buildDamageHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
944
930
  const resisted = ctx.api.randomInt(10000) >= 5000;
945
931
  if (!resisted) {
946
932
  ctx.api.addBuff(ctx.caster, enemy.id, "dazzle");
@@ -9,17 +9,19 @@ function buildEmptyResult(ctx, targets = ctx.targets) {
9
9
  triggeredBy: ctx.trigger
10
10
  };
11
11
  }
12
- function buildDamageHit(ctx, targetId, finalValue) {
12
+ function buildDamageHit(ctx, targetId, finalValue, imbalanceDelta, triggeredImbalanceBreak) {
13
13
  return {
14
14
  targetId,
15
15
  effectType: "damage",
16
16
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
17
- skillType: "basic",
17
+ skillType: ctx.skillType,
18
18
  isCritical: false,
19
19
  baseValue: finalValue,
20
20
  criticalValue: finalValue,
21
21
  finalValue,
22
22
  isRepression: 0,
23
+ imbalanceDelta,
24
+ triggeredImbalanceBreak,
23
25
  damageBreakdown: { preMitigation: finalValue, postMitigation: finalValue, shieldAbsorbed: 0, hpDamage: finalValue }
24
26
  };
25
27
  }
@@ -28,7 +30,7 @@ function buildBuffHit(ctx, targetId, buffId, resisted = false) {
28
30
  targetId,
29
31
  effectType: "buff",
30
32
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
31
- skillType: "basic",
33
+ skillType: ctx.skillType,
32
34
  isCritical: false,
33
35
  baseValue: 0,
34
36
  criticalValue: 0,
@@ -43,7 +45,7 @@ function buildHealHit(ctx, targetId, finalValue) {
43
45
  targetId,
44
46
  effectType: "heal",
45
47
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
46
- skillType: "basic",
48
+ skillType: ctx.skillType,
47
49
  isCritical: false,
48
50
  baseValue: finalValue,
49
51
  criticalValue: finalValue,
@@ -56,7 +58,7 @@ function buildShieldHit(ctx, targetId, finalValue) {
56
58
  targetId,
57
59
  effectType: "shield",
58
60
  element: ctx.api.getUnit(ctx.caster)?.element ?? "fire",
59
- skillType: "basic",
61
+ skillType: ctx.skillType,
60
62
  isCritical: false,
61
63
  baseValue: finalValue,
62
64
  criticalValue: finalValue,
@@ -207,9 +209,9 @@ const m16122Script = {
207
209
  const target = pool.splice(idx, 1)[0];
208
210
  hitTargets.push(target.id);
209
211
  const ratio = (ctx.levelParams.ratio ?? 6000) / 10000;
210
- const dmg = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * ratio));
211
- totalDamage += dmg;
212
- hits.push(buildDamageHit(ctx, target.id, dmg));
212
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * ratio));
213
+ totalDamage += r.damage;
214
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
213
215
  if (ctx.api.randomInt(10000) < 5000) {
214
216
  ctx.api.addBuff(ctx.caster, target.id, "dazzle");
215
217
  hits.push(buildBuffHit(ctx, target.id, "dazzle"));
@@ -1,14 +1,14 @@
1
1
  function buildEmptyResult(ctx, targets = ctx.targets) {
2
2
  return { skillId: ctx.skillId, casterId: ctx.caster, targets, totalDamage: 0, totalHeal: 0, totalShield: 0, triggeredBy: ctx.trigger };
3
3
  }
4
- function buildDamageHit(ctx, targetId, v) {
5
- return { targetId, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
4
+ function buildDamageHit(ctx, targetId, v, imbalanceDelta, triggeredImbalanceBreak) {
5
+ return { targetId, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, imbalanceDelta, triggeredImbalanceBreak, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
6
6
  }
7
7
  function buildBuffHit(ctx, targetId, buffId, resisted = false) {
8
- return { targetId, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: resisted ? undefined : buffId, resisted };
8
+ return { targetId, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: resisted ? undefined : buffId, resisted };
9
9
  }
10
10
  function buildHealHit(ctx, targetId, v) {
11
- return { targetId, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
11
+ return { targetId, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
12
12
  }
13
13
  // ============================================================
14
14
  // 18011 光波比比 普攻:对距离最近的一名敌人造成50%光属性伤害,使自身获得3层【光能】
@@ -56,9 +56,9 @@ const m18013Script = {
56
56
  if (!enemy.runtime.alive)
57
57
  continue;
58
58
  const perHitRatio = (ctx.levelParams.ratio ?? 2000) / 10000;
59
- const dmg = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * perHitRatio));
60
- totalDamage += dmg;
61
- hits.push(buildDamageHit(ctx, enemy.id, dmg));
59
+ const r = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * perHitRatio));
60
+ totalDamage += r.damage;
61
+ hits.push(buildDamageHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
62
62
  if (!hitTargets.includes(enemy.id))
63
63
  hitTargets.push(enemy.id);
64
64
  }
@@ -87,9 +87,9 @@ const m18022Script = {
87
87
  const hits = [];
88
88
  for (const t of targets) {
89
89
  const ratio18022 = (ctx.levelParams.ratio ?? 4000) / 10000;
90
- const dmg = ctx.api.damage(ctx.caster, t.id, Math.floor(self.stats.Att * ratio18022));
91
- totalDamage += dmg;
92
- hits.push(buildDamageHit(ctx, t.id, dmg));
90
+ const r = ctx.api.damage(ctx.caster, t.id, Math.floor(self.stats.Att * ratio18022));
91
+ totalDamage += r.damage;
92
+ hits.push(buildDamageHit(ctx, t.id, r.damage, r.imbalanceDelta));
93
93
  ctx.api.addBuff(ctx.caster, t.id, "erosion");
94
94
  hits.push(buildBuffHit(ctx, t.id, "erosion"));
95
95
  }
@@ -120,9 +120,9 @@ const m18023Script = {
120
120
  // 若有侵蚀则额外伤害
121
121
  if (ctx.api.hasBuff(target.id, "erosion")) {
122
122
  const extraRatio = (ctx.levelParams.extraRatio ?? 15000) / 10000;
123
- const dmg = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * extraRatio));
124
- totalDamage += dmg;
125
- hits.push(buildDamageHit(ctx, target.id, dmg));
123
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * extraRatio));
124
+ totalDamage += r.damage;
125
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
126
126
  }
127
127
  return { ...buildEmptyResult(ctx, [target.id]), totalDamage, hits };
128
128
  }
@@ -141,11 +141,11 @@ const m18033Script = {
141
141
  const target = enemies[0];
142
142
  const hits = [];
143
143
  const ratio18033 = (ctx.levelParams.ratio ?? 3000) / 10000;
144
- const dmg = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * ratio18033));
145
- hits.push(buildDamageHit(ctx, target.id, dmg));
144
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * ratio18033));
145
+ hits.push(buildDamageHit(ctx, target.id, r.damage, r.imbalanceDelta));
146
146
  ctx.api.addBuff(ctx.caster, target.id, "stun");
147
147
  hits.push(buildBuffHit(ctx, target.id, "stun"));
148
- return { ...buildEmptyResult(ctx, [target.id]), totalDamage: dmg, hits };
148
+ return { ...buildEmptyResult(ctx, [target.id]), totalDamage: r.damage, hits };
149
149
  }
150
150
  };
151
151
  // ============================================================
@@ -159,13 +159,13 @@ const m18041Script = {
159
159
  return buildEmptyResult(ctx);
160
160
  const hits = [];
161
161
  let totalDamage = 0;
162
- const dmg = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.5));
163
- totalDamage += dmg;
164
- hits.push(buildDamageHit(ctx, targetId, dmg));
162
+ const r1 = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.5));
163
+ totalDamage += r1.damage;
164
+ hits.push(buildDamageHit(ctx, targetId, r1.damage, r1.imbalanceDelta));
165
165
  if (ctx.api.hasBuff(targetId, "slow")) {
166
- const extra = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.5));
167
- totalDamage += extra;
168
- hits.push(buildDamageHit(ctx, targetId, extra));
166
+ const r2 = ctx.api.damage(ctx.caster, targetId, Math.floor(self.stats.Att * 0.5));
167
+ totalDamage += r2.damage;
168
+ hits.push(buildDamageHit(ctx, targetId, r2.damage, r2.imbalanceDelta));
169
169
  }
170
170
  return { ...buildEmptyResult(ctx, [targetId]), totalDamage, hits };
171
171
  }
@@ -205,9 +205,9 @@ const m18043Script = {
205
205
  let totalDamage = 0;
206
206
  const hits = [];
207
207
  for (const enemy of enemies) {
208
- const dmg = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 0.5));
209
- totalDamage += dmg;
210
- hits.push(buildDamageHit(ctx, enemy.id, dmg));
208
+ const r = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 0.5));
209
+ totalDamage += r.damage;
210
+ hits.push(buildDamageHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
211
211
  ctx.api.addBuff(ctx.caster, enemy.id, "slow");
212
212
  hits.push(buildBuffHit(ctx, enemy.id, "slow"));
213
213
  }
@@ -253,13 +253,13 @@ const m18062Script = {
253
253
  const target = enemies[0];
254
254
  let totalDamage = 0;
255
255
  const hits = [];
256
- const dmg = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 1.3));
257
- totalDamage += dmg;
258
- hits.push(buildDamageHit(ctx, target.id, dmg));
256
+ const r1 = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 1.3));
257
+ totalDamage += r1.damage;
258
+ hits.push(buildDamageHit(ctx, target.id, r1.damage, r1.imbalanceDelta));
259
259
  if (ctx.api.hasBuff(target.id, "static")) {
260
- const extra = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
261
- totalDamage += extra;
262
- hits.push(buildDamageHit(ctx, target.id, extra));
260
+ const r2 = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 0.5));
261
+ totalDamage += r2.damage;
262
+ hits.push(buildDamageHit(ctx, target.id, r2.damage, r2.imbalanceDelta));
263
263
  }
264
264
  return { ...buildEmptyResult(ctx, [target.id]), totalDamage, hits };
265
265
  }
@@ -304,9 +304,9 @@ const m18072Script = {
304
304
  // 引爆:立即结算1次风化DOT伤害
305
305
  const dotDmg = Math.floor(enemy.stats.Att * 0.18 * stacks);
306
306
  if (dotDmg > 0) {
307
- const dmg = ctx.api.damage(ctx.caster, enemy.id, dotDmg);
308
- totalDamage += dmg;
309
- hits.push(buildDamageHit(ctx, enemy.id, dmg));
307
+ const r = ctx.api.damage(ctx.caster, enemy.id, dotDmg);
308
+ totalDamage += r.damage;
309
+ hits.push(buildDamageHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
310
310
  }
311
311
  }
312
312
  }
@@ -1,17 +1,17 @@
1
1
  function empty(ctx, targets = ctx.targets) {
2
2
  return { skillId: ctx.skillId, casterId: ctx.caster, targets, totalDamage: 0, totalHeal: 0, totalShield: 0, triggeredBy: ctx.trigger };
3
3
  }
4
- function dmgHit(ctx, tid, v) {
5
- return { targetId: tid, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
4
+ function dmgHit(ctx, tid, v, ib, tib) {
5
+ return { targetId: tid, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, imbalanceDelta: ib, triggeredImbalanceBreak: tib, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
6
6
  }
7
7
  function buffHit(ctx, tid, bid, r = false) {
8
- return { targetId: tid, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: r ? undefined : bid, resisted: r };
8
+ return { targetId: tid, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: r ? undefined : bid, resisted: r };
9
9
  }
10
10
  function healHit(ctx, tid, v) {
11
- return { targetId: tid, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
11
+ return { targetId: tid, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
12
12
  }
13
13
  function shieldHit(ctx, tid, v) {
14
- return { targetId: tid, effectType: "shield", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
14
+ return { targetId: tid, effectType: "shield", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
15
15
  }
16
16
  // 19011 骨墩墩 普攻:80%火属性伤害+赋予自身【反击】(反击触发的普攻不赋予反击)
17
17
  const m19011Script = {
@@ -23,13 +23,13 @@ const m19011Script = {
23
23
  if (!tid)
24
24
  return empty(ctx);
25
25
  const hits = [];
26
- const d = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Att * 0.8));
27
- hits.push(dmgHit(ctx, tid, d));
26
+ const r = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Att * 0.8));
27
+ hits.push(dmgHit(ctx, tid, r.damage, r.imbalanceDelta));
28
28
  if (ctx.trigger !== "counter") {
29
29
  ctx.api.addBuff(ctx.caster, ctx.caster, "monster-counter-boss");
30
30
  hits.push(buffHit(ctx, ctx.caster, "monster-counter-boss"));
31
31
  }
32
- return { ...empty(ctx, [tid]), totalDamage: d, hits };
32
+ return { ...empty(ctx, [tid]), totalDamage: r.damage, hits };
33
33
  }
34
34
  };
35
35
  // 19012 骨墩墩 技能1:召唤2只骨骨
@@ -144,9 +144,9 @@ const m19023Script = {
144
144
  let totalDamage = 0;
145
145
  const hits = [];
146
146
  for (const enemy of enemies) {
147
- const d = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * (1.5 + bonusRatio)));
148
- totalDamage += d;
149
- hits.push(dmgHit(ctx, enemy.id, d));
147
+ const r = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * (1.5 + bonusRatio)));
148
+ totalDamage += r.damage;
149
+ hits.push(dmgHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
150
150
  if (ctx.api.hasBuff(enemy.id, "slow")) {
151
151
  ctx.api.adjustActionProgress(enemy.id, -0.3);
152
152
  }
@@ -199,9 +199,9 @@ const m19033Script = {
199
199
  const target = enemies[0];
200
200
  const hits = [];
201
201
  let totalDamage = 0;
202
- const d = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 2.2));
203
- totalDamage += d;
204
- hits.push(dmgHit(ctx, target.id, d));
202
+ const r = ctx.api.damage(ctx.caster, target.id, Math.floor(self.stats.Att * 2.2));
203
+ totalDamage += r.damage;
204
+ hits.push(dmgHit(ctx, target.id, r.damage, r.imbalanceDelta));
205
205
  // 检查5层静电 -> 直接死亡
206
206
  const staticBuffs = ctx.api.getBuffs(target.id).filter(b => b.id === "static");
207
207
  const staticStacks = staticBuffs.reduce((s, b) => s + b.stacks, 0);
@@ -1,17 +1,17 @@
1
1
  function empty(ctx, targets = ctx.targets) {
2
2
  return { skillId: ctx.skillId, casterId: ctx.caster, targets, totalDamage: 0, totalHeal: 0, totalShield: 0, triggeredBy: ctx.trigger };
3
3
  }
4
- function dmgHit(ctx, tid, v) {
5
- return { targetId: tid, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
4
+ function dmgHit(ctx, tid, v, ib, tib) {
5
+ return { targetId: tid, effectType: "damage", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0, imbalanceDelta: ib, triggeredImbalanceBreak: tib, damageBreakdown: { preMitigation: v, postMitigation: v, shieldAbsorbed: 0, hpDamage: v } };
6
6
  }
7
7
  function buffHit(ctx, tid, bid, r = false) {
8
- return { targetId: tid, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: r ? undefined : bid, resisted: r };
8
+ return { targetId: tid, effectType: "buff", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: 0, criticalValue: 0, finalValue: 0, isRepression: 0, appliedBuffId: r ? undefined : bid, resisted: r };
9
9
  }
10
10
  function healHit(ctx, tid, v) {
11
- return { targetId: tid, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
11
+ return { targetId: tid, effectType: "heal", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
12
12
  }
13
13
  function shieldHit(ctx, tid, v) {
14
- return { targetId: tid, effectType: "shield", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: "basic", isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
14
+ return { targetId: tid, effectType: "shield", element: ctx.api.getUnit(ctx.caster)?.element ?? "fire", skillType: ctx.skillType, isCritical: false, baseValue: v, criticalValue: v, finalValue: v, isRepression: 0 };
15
15
  }
16
16
  // 19042 大风 技能1:对全体敌人造成50%风属性伤害,并消除敌人20%行动值持续2回合
17
17
  // (伤害由模板处理,这里处理行动值削减)
@@ -245,13 +245,13 @@ const m19071Script = {
245
245
  return empty(ctx);
246
246
  const hits = [];
247
247
  let totalDamage = 0;
248
- const d1 = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Att * 0.8));
249
- totalDamage += d1;
250
- hits.push(dmgHit(ctx, tid, d1));
248
+ const r1 = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Att * 0.8));
249
+ totalDamage += r1.damage;
250
+ hits.push(dmgHit(ctx, tid, r1.damage, r1.imbalanceDelta));
251
251
  // 附加防御力50%额外伤害
252
- const d2 = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Def * 0.5));
253
- totalDamage += d2;
254
- hits.push(dmgHit(ctx, tid, d2));
252
+ const r2 = ctx.api.damage(ctx.caster, tid, Math.floor(self.stats.Def * 0.5));
253
+ totalDamage += r2.damage;
254
+ hits.push(dmgHit(ctx, tid, r2.damage, r2.imbalanceDelta));
255
255
  return { ...empty(ctx, [tid]), totalDamage, hits };
256
256
  }
257
257
  };
@@ -287,9 +287,9 @@ const m19073Script = {
287
287
  const enemies = ctx.api.getAliveUnits().filter(u => u.teamId !== self.teamId);
288
288
  for (const enemy of enemies) {
289
289
  // 额外200%伤害(模板已有150%,这里补充额外的300%总计)
290
- const d = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 3.0));
291
- totalDamage += d;
292
- hits.push(dmgHit(ctx, enemy.id, d));
290
+ const r = ctx.api.damage(ctx.caster, enemy.id, Math.floor(self.stats.Att * 3.0));
291
+ totalDamage += r.damage;
292
+ hits.push(dmgHit(ctx, enemy.id, r.damage, r.imbalanceDelta));
293
293
  ctx.api.addBuff(ctx.caster, enemy.id, "stun");
294
294
  hits.push(buffHit(ctx, enemy.id, "stun"));
295
295
  }
@@ -0,0 +1,28 @@
1
+ import type { ISkillScript } from "../types";
2
+ /**
3
+ * 导出所有示例脚本
4
+ */
5
+ export declare const sampleUpgradeScripts: Record<string, ISkillScript>;
6
+ /**
7
+ * 使用示例:
8
+ *
9
+ * // 在初始化战斗单位时,配置技能等级和参数
10
+ * const skill: SkillDefinition = {
11
+ * id: "hero.skill.1",
12
+ * mode: "script",
13
+ * type: "core",
14
+ * scriptId: "sample.upgradable.damage",
15
+ * energyCost: 30,
16
+ * energyGain: 0,
17
+ * cooldown: 2,
18
+ * targetRule: "nearest",
19
+ * element: "fire",
20
+ * level: 3 as const, // 技能等级 3
21
+ * levelParams: {
22
+ * baseRatio: 0.5, // 基础系数 50%
23
+ * ratioPerLevel: 0.08 // 每级+8%
24
+ * }
25
+ * };
26
+ *
27
+ * // 等级3的实际系数 = 0.5 + (3-1) * 0.08 = 0.66 = 66%
28
+ */
@@ -0,0 +1,223 @@
1
+ /**
2
+ * 示例:支持技能升级的手写技能脚本
3
+ *
4
+ * 升级方案:
5
+ * - 基础伤害系数随等级提升
6
+ * - levelParams 中可配置额外参数
7
+ */
8
+ const upgradableDamageSkill = {
9
+ onCast(ctx) {
10
+ const { level, levelParams } = ctx;
11
+ const self = ctx.api.getUnit(ctx.caster);
12
+ if (!self || ctx.targets.length === 0) {
13
+ return {
14
+ skillId: ctx.skillId,
15
+ casterId: ctx.caster,
16
+ targets: [],
17
+ totalDamage: 0,
18
+ totalHeal: 0,
19
+ totalShield: 0,
20
+ triggeredBy: ctx.trigger
21
+ };
22
+ }
23
+ // 基础系数 + 每级成长
24
+ const baseRatio = levelParams.baseRatio ?? 0.5;
25
+ const ratioPerLevel = levelParams.ratioPerLevel ?? 0.05;
26
+ const currentRatio = baseRatio + (level - 1) * ratioPerLevel;
27
+ // 计算伤害
28
+ const damage = Math.floor(self.stats.Att * currentRatio);
29
+ let totalDamage = 0;
30
+ for (const targetId of ctx.targets) {
31
+ const r = ctx.api.damage(ctx.caster, targetId, damage);
32
+ totalDamage += r.damage;
33
+ }
34
+ return {
35
+ skillId: ctx.skillId,
36
+ casterId: ctx.caster,
37
+ targets: ctx.targets,
38
+ totalDamage,
39
+ totalHeal: 0,
40
+ totalShield: 0,
41
+ triggeredBy: ctx.trigger
42
+ };
43
+ }
44
+ };
45
+ /**
46
+ * 示例:护盾技能,护盾值随等级提升
47
+ */
48
+ const upgradableShieldSkill = {
49
+ onCast(ctx) {
50
+ const { level, levelParams } = ctx;
51
+ const self = ctx.api.getUnit(ctx.caster);
52
+ if (!self) {
53
+ return {
54
+ skillId: ctx.skillId,
55
+ casterId: ctx.caster,
56
+ targets: [],
57
+ totalDamage: 0,
58
+ totalHeal: 0,
59
+ totalShield: 0,
60
+ triggeredBy: ctx.trigger
61
+ };
62
+ }
63
+ // 护盾基础值 + 等级加成
64
+ const baseShield = levelParams.baseShield ?? 100;
65
+ const shieldPerLevel = levelParams.shieldPerLevel ?? 20;
66
+ const shieldValue = baseShield + (level - 1) * shieldPerLevel;
67
+ // 可以基于防御力缩放
68
+ const defRatio = levelParams.defRatio ?? 0.3;
69
+ const finalShield = Math.floor(self.stats.Def * defRatio + shieldValue);
70
+ const totalShield = ctx.api.shield(ctx.caster, finalShield);
71
+ return {
72
+ skillId: ctx.skillId,
73
+ casterId: ctx.caster,
74
+ targets: [ctx.caster],
75
+ totalDamage: 0,
76
+ totalHeal: 0,
77
+ totalShield,
78
+ triggeredBy: ctx.trigger
79
+ };
80
+ }
81
+ };
82
+ /**
83
+ * 示例:治疗技能,治疗量随等级提升
84
+ */
85
+ const upgradableHealSkill = {
86
+ onCast(ctx) {
87
+ const { level, levelParams } = ctx;
88
+ const self = ctx.api.getUnit(ctx.caster);
89
+ if (!self || ctx.targets.length === 0) {
90
+ return {
91
+ skillId: ctx.skillId,
92
+ casterId: ctx.caster,
93
+ targets: [],
94
+ totalDamage: 0,
95
+ totalHeal: 0,
96
+ totalShield: 0,
97
+ triggeredBy: ctx.trigger
98
+ };
99
+ }
100
+ // 治疗基础值 + 等级加成
101
+ const baseHeal = levelParams.baseHeal ?? 50;
102
+ const healPerLevel = levelParams.healPerLevel ?? 10;
103
+ const healValue = baseHeal + (level - 1) * healPerLevel;
104
+ // 基于攻击力缩放
105
+ const attRatio = levelParams.attRatio ?? 0.2;
106
+ const finalHeal = Math.floor(self.stats.Att * attRatio + healValue);
107
+ let totalHeal = 0;
108
+ for (const targetId of ctx.targets) {
109
+ totalHeal += ctx.api.heal(targetId, finalHeal);
110
+ }
111
+ return {
112
+ skillId: ctx.skillId,
113
+ casterId: ctx.caster,
114
+ targets: ctx.targets,
115
+ totalDamage: 0,
116
+ totalHeal,
117
+ totalShield: 0,
118
+ triggeredBy: ctx.trigger
119
+ };
120
+ }
121
+ };
122
+ /**
123
+ * 示例:多段伤害技能,段数或伤害随等级提升
124
+ */
125
+ const upgradableMultiHitSkill = {
126
+ onCast(ctx) {
127
+ const { level, levelParams } = ctx;
128
+ const self = ctx.api.getUnit(ctx.caster);
129
+ if (!self || ctx.targets.length === 0) {
130
+ return {
131
+ skillId: ctx.skillId,
132
+ casterId: ctx.caster,
133
+ targets: [],
134
+ totalDamage: 0,
135
+ totalHeal: 0,
136
+ totalShield: 0,
137
+ triggeredBy: ctx.trigger
138
+ };
139
+ }
140
+ // 基础段数 + 等级解锁额外段数
141
+ const baseHits = levelParams.baseHits ?? 2;
142
+ const bonusHitsAtLevel = levelParams.bonusHitsAtLevel ?? 5;
143
+ const totalHits = level >= bonusHitsAtLevel ? baseHits + 1 : baseHits;
144
+ // 每段伤害系数
145
+ const hitRatio = levelParams.hitRatio ?? 0.3;
146
+ const hitDamage = Math.floor(self.stats.Att * hitRatio);
147
+ let totalDamage = 0;
148
+ const targetId = ctx.targets[0];
149
+ for (let i = 0; i < totalHits; i++) {
150
+ const r = ctx.api.damage(ctx.caster, targetId, hitDamage);
151
+ totalDamage += r.damage;
152
+ }
153
+ return {
154
+ skillId: ctx.skillId,
155
+ casterId: ctx.caster,
156
+ targets: ctx.targets,
157
+ totalDamage,
158
+ totalHeal: 0,
159
+ totalShield: 0,
160
+ triggeredBy: ctx.trigger
161
+ };
162
+ }
163
+ };
164
+ /**
165
+ * 示例:Debuff 技能,概率随等级提升
166
+ */
167
+ const upgradableDebuffSkill = {
168
+ onCast(ctx) {
169
+ const { level, levelParams } = ctx;
170
+ // 基础概率 + 等级加成
171
+ const baseProb = levelParams.baseProb ?? 0.5; // 50%
172
+ const probPerLevel = levelParams.probPerLevel ?? 0.05; // 每级+5%
173
+ const applyChance = Math.min(1, baseProb + (level - 1) * probPerLevel);
174
+ // levelParams 仅支持 number,Debuff 类型固定为 slow
175
+ const buffId = "slow";
176
+ for (const targetId of ctx.targets) {
177
+ // 假设概率判定通过
178
+ ctx.api.addBuff(ctx.caster, targetId, buffId);
179
+ }
180
+ return {
181
+ skillId: ctx.skillId,
182
+ casterId: ctx.caster,
183
+ targets: ctx.targets,
184
+ totalDamage: 0,
185
+ totalHeal: 0,
186
+ totalShield: 0,
187
+ triggeredBy: ctx.trigger
188
+ };
189
+ }
190
+ };
191
+ /**
192
+ * 导出所有示例脚本
193
+ */
194
+ export const sampleUpgradeScripts = {
195
+ "sample.upgradable.damage": upgradableDamageSkill,
196
+ "sample.upgradable.shield": upgradableShieldSkill,
197
+ "sample.upgradable.heal": upgradableHealSkill,
198
+ "sample.upgradable.multihit": upgradableMultiHitSkill,
199
+ "sample.upgradable.debuff": upgradableDebuffSkill
200
+ };
201
+ /**
202
+ * 使用示例:
203
+ *
204
+ * // 在初始化战斗单位时,配置技能等级和参数
205
+ * const skill: SkillDefinition = {
206
+ * id: "hero.skill.1",
207
+ * mode: "script",
208
+ * type: "core",
209
+ * scriptId: "sample.upgradable.damage",
210
+ * energyCost: 30,
211
+ * energyGain: 0,
212
+ * cooldown: 2,
213
+ * targetRule: "nearest",
214
+ * element: "fire",
215
+ * level: 3 as const, // 技能等级 3
216
+ * levelParams: {
217
+ * baseRatio: 0.5, // 基础系数 50%
218
+ * ratioPerLevel: 0.08 // 每级+8%
219
+ * }
220
+ * };
221
+ *
222
+ * // 等级3的实际系数 = 0.5 + (3-1) * 0.08 = 0.66 = 66%
223
+ */
@@ -1,9 +1,14 @@
1
1
  import type { BattleScriptApi, ElementType, ISkillScript, ScriptRuntimeContext, SkillDefinition, SkillResult, SkillTemplate, TriggerContext, UnitModel } from "./types";
2
2
  import { SeedRng } from "./random";
3
+ interface ApplyDamageResult {
4
+ damage: number;
5
+ imbalanceDelta: number;
6
+ triggeredImbalanceBreak: boolean;
7
+ }
3
8
  interface SkillEngineDeps {
4
9
  rng: SeedRng;
5
10
  getUnits: () => UnitModel[];
6
- applyDamage: (sourceId: string, targetId: string, value: number, staggerElement?: ElementType) => number;
11
+ applyDamage: (sourceId: string, targetId: string, value: number, staggerElement?: ElementType) => ApplyDamageResult;
7
12
  applyHeal: (targetId: string, value: number) => number;
8
13
  applyShield: (targetId: string, value: number) => number;
9
14
  applyBuff: (sourceId: string, targetId: string, buffId: string) => void;
@@ -32,5 +37,6 @@ export declare class SkillExecutor {
32
37
  private readonly scriptEngine;
33
38
  constructor(configEngine: ConfigSkillEngine, scriptEngine: ScriptSkillEngine);
34
39
  execute(skill: SkillDefinition, caster: UnitModel, deps: SkillEngineDeps, trigger: SkillResult["triggeredBy"], triggerCtx?: TriggerContext, selectedTargetsOverride?: UnitModel[]): SkillResult;
40
+ private linkBuffHitsToDamage;
35
41
  }
36
42
  export {};
@@ -1,13 +1,13 @@
1
1
  import { calcDamage, calcEffectProbability, calcHealOrShield } from "./formula";
2
2
  import { selectTargets } from "./targeting";
3
3
  export class ScriptSkillEngine {
4
- scripts = new Map();
4
+ scripts = new Map(); // 存储已注册的技能脚本
5
5
  registerScript(scriptId, script) {
6
6
  this.scripts.set(scriptId, script);
7
7
  }
8
8
  cast(scriptId, ctx) {
9
- const script = this.scripts.get(scriptId);
10
- if (!script) {
9
+ const script = this.scripts.get(scriptId); // 获取技能脚本
10
+ if (!script) { // 如果技能脚本不存在
11
11
  return {
12
12
  skillId: ctx.skillId,
13
13
  casterId: ctx.caster,
@@ -103,7 +103,10 @@ export class ConfigSkillEngine {
103
103
  if (step.kind === "damage") {
104
104
  const shieldBefore = target.runtime.Shield;
105
105
  const damageRes = calcDamage({ ...caster, stats: effectiveCaster }, { ...target, stats: effectiveTarget }, raw, skill.type, skill.element, deps.rng);
106
- const done = deps.applyDamage(caster.id, target.id, damageRes.finalDam, skill.element);
106
+ const applyResult = deps.applyDamage(caster.id, target.id, damageRes.finalDam, skill.element);
107
+ const done = applyResult.damage;
108
+ const imbalanceDelta = applyResult.imbalanceDelta;
109
+ const triggeredImbalanceBreak = applyResult.triggeredImbalanceBreak;
107
110
  const shieldAbsorbed = Math.min(shieldBefore, damageRes.finalDam);
108
111
  totalDamage += done;
109
112
  hits.push({
@@ -116,6 +119,8 @@ export class ConfigSkillEngine {
116
119
  criticalValue: damageRes.critDam,
117
120
  finalValue: done,
118
121
  isRepression: damageRes.isRepression,
122
+ imbalanceDelta,
123
+ triggeredImbalanceBreak: triggeredImbalanceBreak || undefined,
119
124
  damageBreakdown: {
120
125
  preMitigation: raw,
121
126
  postMitigation: damageRes.finalDam,
@@ -219,6 +224,7 @@ export class SkillExecutor {
219
224
  caster: caster.id,
220
225
  targets: result.targets,
221
226
  skillId: skill.id,
227
+ skillType: skill.type,
222
228
  trigger,
223
229
  triggerContext: triggerCtx,
224
230
  api: deps.createScriptApi(skill.element),
@@ -241,6 +247,35 @@ export class SkillExecutor {
241
247
  if (triggerCtx && result.targets.length === 0) {
242
248
  result.targets = [...triggerCtx.sourceTargets];
243
249
  }
250
+ if (result.hits && result.hits.length > 0) {
251
+ this.linkBuffHitsToDamage(result.hits);
252
+ }
244
253
  return result;
245
254
  }
255
+ linkBuffHitsToDamage(hits) {
256
+ const latestDamageIndexByTarget = new Map();
257
+ for (let i = 0; i < hits.length; i++) {
258
+ const hit = hits[i];
259
+ hit.hitIndex = i;
260
+ if (hit.effectType === "damage") {
261
+ hit.triggeredBuffHitIndices = undefined;
262
+ latestDamageIndexByTarget.set(hit.targetId, i);
263
+ continue;
264
+ }
265
+ if (hit.effectType !== "buff") {
266
+ continue;
267
+ }
268
+ const sourceIndex = latestDamageIndexByTarget.get(hit.targetId);
269
+ if (sourceIndex !== undefined) {
270
+ hit.sourceDamageHitIndex = sourceIndex;
271
+ const sourceDamageHit = hits[sourceIndex];
272
+ if (sourceDamageHit.effectType === "damage") {
273
+ if (!sourceDamageHit.triggeredBuffHitIndices) {
274
+ sourceDamageHit.triggeredBuffHitIndices = [];
275
+ }
276
+ sourceDamageHit.triggeredBuffHitIndices.push(i);
277
+ }
278
+ }
279
+ }
280
+ }
246
281
  }
@@ -142,6 +142,7 @@ export interface SkillTemplateStep {
142
142
  fixedProbByLevel?: number[];
143
143
  canCrit?: boolean;
144
144
  scaleStat?: "Att" | "Def" | "Mhp";
145
+ scaleStatByLevel?: number[];
145
146
  flat?: number;
146
147
  flatByLevel?: number[];
147
148
  repeat?: number;
@@ -162,6 +163,8 @@ export interface SkillResult {
162
163
  hits?: SkillHitDetail[];
163
164
  }
164
165
  export interface SkillHitDetail {
166
+ /** 当前hit在SkillResult.hits中的索引位置 */
167
+ hitIndex?: number;
165
168
  targetId: UnitId;
166
169
  effectType: "damage" | "heal" | "shield" | "buff";
167
170
  element: ElementType;
@@ -173,6 +176,19 @@ export interface SkillHitDetail {
173
176
  isRepression: -1 | 0 | 1;
174
177
  appliedBuffId?: string;
175
178
  resisted?: boolean;
179
+ /** 本次hit造成的失衡值变化 */
180
+ imbalanceDelta?: number;
181
+ /** 本次hit是否触发了失衡破击 */
182
+ triggeredImbalanceBreak?: boolean;
183
+ /**
184
+ * 当effectType=buff时,若由同一次技能中的某个damage hit触发,
185
+ * 则记录该damage hit在SkillResult.hits中的索引
186
+ */
187
+ sourceDamageHitIndex?: number;
188
+ /**
189
+ * 当effectType=damage时,记录由该damage触发的buff hit索引列表
190
+ */
191
+ triggeredBuffHitIndices?: number[];
176
192
  damageBreakdown?: {
177
193
  preMitigation: number;
178
194
  postMitigation: number;
@@ -260,6 +276,7 @@ export interface ScriptRuntimeContext {
260
276
  caster: UnitId;
261
277
  targets: UnitId[];
262
278
  skillId: string;
279
+ skillType: SkillType;
263
280
  trigger: SkillResult["triggeredBy"];
264
281
  api: BattleScriptApi;
265
282
  triggerContext?: TriggerContext;
@@ -268,8 +285,13 @@ export interface ScriptRuntimeContext {
268
285
  /** 技能升级参数 */
269
286
  levelParams: Record<string, number>;
270
287
  }
288
+ export interface DamageResult {
289
+ damage: number;
290
+ imbalanceDelta: number;
291
+ triggeredImbalanceBreak: boolean;
292
+ }
271
293
  export interface BattleScriptApi {
272
- damage(sourceId: UnitId, targetId: UnitId, value: number): number;
294
+ damage(sourceId: UnitId, targetId: UnitId, value: number): DamageResult;
273
295
  heal(targetId: UnitId, value: number): number;
274
296
  shield(targetId: UnitId, value: number): number;
275
297
  adjustImbalance(targetId: UnitId, delta: number): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hrbattle",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "一个基于回合制战斗引擎,支持技能脚本、buff系统和效果解析。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",