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,36 @@
|
|
|
1
|
+
import type { BattleScriptApi, ElementType, ISkillScript, ScriptRuntimeContext, SkillDefinition, SkillResult, SkillTemplate, TriggerContext, UnitModel } from "./types";
|
|
2
|
+
import { SeedRng } from "./random";
|
|
3
|
+
interface SkillEngineDeps {
|
|
4
|
+
rng: SeedRng;
|
|
5
|
+
getUnits: () => UnitModel[];
|
|
6
|
+
applyDamage: (sourceId: string, targetId: string, value: number, staggerElement?: ElementType) => number;
|
|
7
|
+
applyHeal: (targetId: string, value: number) => number;
|
|
8
|
+
applyShield: (targetId: string, value: number) => number;
|
|
9
|
+
applyBuff: (sourceId: string, targetId: string, buffId: string) => void;
|
|
10
|
+
getEffectiveStats: (unit: UnitModel) => UnitModel["stats"];
|
|
11
|
+
createScriptApi: (skillElement: ElementType) => BattleScriptApi;
|
|
12
|
+
}
|
|
13
|
+
export declare class ScriptSkillEngine {
|
|
14
|
+
private scripts;
|
|
15
|
+
registerScript(scriptId: string, script: ISkillScript): void;
|
|
16
|
+
cast(scriptId: string, ctx: ScriptRuntimeContext): SkillResult;
|
|
17
|
+
}
|
|
18
|
+
export declare class ConfigSkillEngine {
|
|
19
|
+
private templates;
|
|
20
|
+
private normalizeSkillLevel;
|
|
21
|
+
private getLevelIndex;
|
|
22
|
+
private resolveLevelValue;
|
|
23
|
+
private resolveStepRatio;
|
|
24
|
+
private resolveStepFlat;
|
|
25
|
+
private resolveStepFixedProb;
|
|
26
|
+
private resolveBuffApplyProb;
|
|
27
|
+
registerTemplates(templates: SkillTemplate[]): void;
|
|
28
|
+
cast(skill: SkillDefinition, caster: UnitModel, deps: SkillEngineDeps, selectedTargets?: UnitModel[]): SkillResult;
|
|
29
|
+
}
|
|
30
|
+
export declare class SkillExecutor {
|
|
31
|
+
private readonly configEngine;
|
|
32
|
+
private readonly scriptEngine;
|
|
33
|
+
constructor(configEngine: ConfigSkillEngine, scriptEngine: ScriptSkillEngine);
|
|
34
|
+
execute(skill: SkillDefinition, caster: UnitModel, deps: SkillEngineDeps, trigger: SkillResult["triggeredBy"], triggerCtx?: TriggerContext, selectedTargetsOverride?: UnitModel[]): SkillResult;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { calcDamage, calcEffectProbability, calcHealOrShield } from "./formula";
|
|
2
|
+
import { selectTargets } from "./targeting";
|
|
3
|
+
export class ScriptSkillEngine {
|
|
4
|
+
scripts = new Map();
|
|
5
|
+
registerScript(scriptId, script) {
|
|
6
|
+
this.scripts.set(scriptId, script);
|
|
7
|
+
}
|
|
8
|
+
cast(scriptId, ctx) {
|
|
9
|
+
const script = this.scripts.get(scriptId);
|
|
10
|
+
if (!script) {
|
|
11
|
+
return {
|
|
12
|
+
skillId: ctx.skillId,
|
|
13
|
+
casterId: ctx.caster,
|
|
14
|
+
targets: ctx.targets,
|
|
15
|
+
totalDamage: 0,
|
|
16
|
+
totalHeal: 0,
|
|
17
|
+
totalShield: 0,
|
|
18
|
+
triggeredBy: "normal",
|
|
19
|
+
hits: []
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
script.onPreCast?.(ctx);
|
|
23
|
+
const result = script.onCast(ctx);
|
|
24
|
+
script.onPostCast?.(ctx, result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class ConfigSkillEngine {
|
|
29
|
+
templates = new Map();
|
|
30
|
+
normalizeSkillLevel(level) {
|
|
31
|
+
return Math.max(1, Math.floor(level ?? 1));
|
|
32
|
+
}
|
|
33
|
+
getLevelIndex(skillLevel) {
|
|
34
|
+
return Math.max(0, this.normalizeSkillLevel(skillLevel) - 1);
|
|
35
|
+
}
|
|
36
|
+
resolveLevelValue(values, fallback, skillLevel) {
|
|
37
|
+
if (!values || values.length === 0) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
const levelIndex = this.getLevelIndex(skillLevel);
|
|
41
|
+
const clampedIndex = Math.min(levelIndex, values.length - 1);
|
|
42
|
+
return values[clampedIndex] ?? fallback;
|
|
43
|
+
}
|
|
44
|
+
resolveStepRatio(step, skillLevel) {
|
|
45
|
+
return this.resolveLevelValue(step.ratioByLevel, step.ratio, skillLevel);
|
|
46
|
+
}
|
|
47
|
+
resolveStepFlat(step, skillLevel) {
|
|
48
|
+
return this.resolveLevelValue(step.flatByLevel, step.flat ?? 0, skillLevel);
|
|
49
|
+
}
|
|
50
|
+
resolveStepFixedProb(step, skillLevel) {
|
|
51
|
+
if (step.fixedProbByLevel && step.fixedProbByLevel.length > 0) {
|
|
52
|
+
return this.resolveLevelValue(step.fixedProbByLevel, step.fixedProb ?? 10000, skillLevel);
|
|
53
|
+
}
|
|
54
|
+
return step.fixedProb;
|
|
55
|
+
}
|
|
56
|
+
resolveBuffApplyProb(stepFixedProb, skill, skillLevel) {
|
|
57
|
+
const levelParams = skill.levelParams ?? {};
|
|
58
|
+
const baseProb = stepFixedProb ?? 10000;
|
|
59
|
+
const probDeltaPerLevel = levelParams.effectProbDeltaPerLevel ?? 0;
|
|
60
|
+
return baseProb + Math.max(0, skillLevel - 1) * probDeltaPerLevel;
|
|
61
|
+
}
|
|
62
|
+
registerTemplates(templates) {
|
|
63
|
+
for (const template of templates) {
|
|
64
|
+
this.templates.set(template.id, template);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
cast(skill, caster, deps, selectedTargets) {
|
|
68
|
+
const template = skill.configTemplateId ? this.templates.get(skill.configTemplateId) : undefined;
|
|
69
|
+
if (!template) {
|
|
70
|
+
return {
|
|
71
|
+
skillId: skill.id,
|
|
72
|
+
casterId: caster.id,
|
|
73
|
+
targets: [],
|
|
74
|
+
totalDamage: 0,
|
|
75
|
+
totalHeal: 0,
|
|
76
|
+
totalShield: 0,
|
|
77
|
+
triggeredBy: "normal",
|
|
78
|
+
hits: []
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const units = deps.getUnits();
|
|
82
|
+
const targets = selectedTargets ?? selectTargets(skill.targetRule, caster, units, deps.rng);
|
|
83
|
+
let totalDamage = 0;
|
|
84
|
+
let totalHeal = 0;
|
|
85
|
+
let totalShield = 0;
|
|
86
|
+
const skillLevel = this.normalizeSkillLevel(skill.level);
|
|
87
|
+
const hits = [];
|
|
88
|
+
for (const step of template.steps) {
|
|
89
|
+
const stepRatio = this.resolveStepRatio(step, skillLevel);
|
|
90
|
+
const stepFlat = this.resolveStepFlat(step, skillLevel);
|
|
91
|
+
const stepFixedProb = this.resolveStepFixedProb(step, skillLevel);
|
|
92
|
+
const repeat = Math.max(1, step.repeat ?? 1);
|
|
93
|
+
for (const target of targets) {
|
|
94
|
+
if (!target.runtime.alive) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (let i = 0; i < repeat; i++) {
|
|
98
|
+
const scaleStat = step.scaleStat ?? "Att";
|
|
99
|
+
const effectiveCaster = deps.getEffectiveStats(caster);
|
|
100
|
+
const effectiveTarget = deps.getEffectiveStats(target);
|
|
101
|
+
const baseScale = effectiveCaster[scaleStat];
|
|
102
|
+
const raw = Math.floor(baseScale * stepRatio + stepFlat);
|
|
103
|
+
if (step.kind === "damage") {
|
|
104
|
+
const shieldBefore = target.runtime.Shield;
|
|
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);
|
|
107
|
+
const shieldAbsorbed = Math.min(shieldBefore, damageRes.finalDam);
|
|
108
|
+
totalDamage += done;
|
|
109
|
+
hits.push({
|
|
110
|
+
targetId: target.id,
|
|
111
|
+
effectType: "damage",
|
|
112
|
+
element: skill.element,
|
|
113
|
+
skillType: skill.type,
|
|
114
|
+
isCritical: damageRes.isCritical,
|
|
115
|
+
baseValue: damageRes.baseDam,
|
|
116
|
+
criticalValue: damageRes.critDam,
|
|
117
|
+
finalValue: done,
|
|
118
|
+
isRepression: damageRes.isRepression,
|
|
119
|
+
damageBreakdown: {
|
|
120
|
+
preMitigation: raw,
|
|
121
|
+
postMitigation: damageRes.finalDam,
|
|
122
|
+
shieldAbsorbed,
|
|
123
|
+
hpDamage: done
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (step.kind === "heal") {
|
|
128
|
+
const healRes = calcHealOrShield({ ...caster, stats: effectiveCaster }, { ...target, stats: effectiveTarget }, raw, deps.rng);
|
|
129
|
+
const applied = deps.applyHeal(target.id, healRes.finalValue);
|
|
130
|
+
totalHeal += applied;
|
|
131
|
+
hits.push({
|
|
132
|
+
targetId: target.id,
|
|
133
|
+
effectType: "heal",
|
|
134
|
+
element: skill.element,
|
|
135
|
+
skillType: skill.type,
|
|
136
|
+
isCritical: healRes.isCritical,
|
|
137
|
+
baseValue: healRes.baseValue,
|
|
138
|
+
criticalValue: healRes.critValue,
|
|
139
|
+
finalValue: applied,
|
|
140
|
+
isRepression: 0
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if (step.kind === "shield") {
|
|
144
|
+
const shieldRes = calcHealOrShield({ ...caster, stats: effectiveCaster }, { ...target, stats: effectiveTarget }, raw, deps.rng);
|
|
145
|
+
const applied = deps.applyShield(target.id, shieldRes.finalValue);
|
|
146
|
+
totalShield += applied;
|
|
147
|
+
hits.push({
|
|
148
|
+
targetId: target.id,
|
|
149
|
+
effectType: "shield",
|
|
150
|
+
element: skill.element,
|
|
151
|
+
skillType: skill.type,
|
|
152
|
+
isCritical: shieldRes.isCritical,
|
|
153
|
+
baseValue: shieldRes.baseValue,
|
|
154
|
+
criticalValue: shieldRes.critValue,
|
|
155
|
+
finalValue: applied,
|
|
156
|
+
isRepression: 0
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (step.kind === "buff" && step.buffId) {
|
|
160
|
+
const baseProb = this.resolveBuffApplyProb(stepFixedProb, skill, skillLevel);
|
|
161
|
+
const canApply = calcEffectProbability(baseProb, stepFixedProb !== undefined, { ...caster, stats: effectiveCaster }, { ...target, stats: effectiveTarget }, deps.rng);
|
|
162
|
+
if (canApply) {
|
|
163
|
+
deps.applyBuff(caster.id, target.id, step.buffId);
|
|
164
|
+
}
|
|
165
|
+
hits.push({
|
|
166
|
+
targetId: target.id,
|
|
167
|
+
effectType: "buff",
|
|
168
|
+
element: skill.element,
|
|
169
|
+
skillType: skill.type,
|
|
170
|
+
isCritical: false,
|
|
171
|
+
baseValue: 0,
|
|
172
|
+
criticalValue: 0,
|
|
173
|
+
finalValue: 0,
|
|
174
|
+
isRepression: 0,
|
|
175
|
+
appliedBuffId: canApply ? step.buffId : undefined,
|
|
176
|
+
resisted: !canApply
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
skillId: skill.id,
|
|
184
|
+
casterId: caster.id,
|
|
185
|
+
targets: targets.map((t) => t.id),
|
|
186
|
+
totalDamage,
|
|
187
|
+
totalHeal,
|
|
188
|
+
totalShield,
|
|
189
|
+
triggeredBy: "normal",
|
|
190
|
+
hits
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export class SkillExecutor {
|
|
195
|
+
configEngine;
|
|
196
|
+
scriptEngine;
|
|
197
|
+
constructor(configEngine, scriptEngine) {
|
|
198
|
+
this.configEngine = configEngine;
|
|
199
|
+
this.scriptEngine = scriptEngine;
|
|
200
|
+
}
|
|
201
|
+
execute(skill, caster, deps, trigger, triggerCtx, selectedTargetsOverride) {
|
|
202
|
+
const selectedTargets = selectedTargetsOverride ?? selectTargets(skill.targetRule, caster, deps.getUnits(), deps.rng);
|
|
203
|
+
let result = {
|
|
204
|
+
skillId: skill.id,
|
|
205
|
+
casterId: caster.id,
|
|
206
|
+
targets: selectedTargets.map((item) => item.id),
|
|
207
|
+
totalDamage: 0,
|
|
208
|
+
totalHeal: 0,
|
|
209
|
+
totalShield: 0,
|
|
210
|
+
triggeredBy: trigger,
|
|
211
|
+
hits: []
|
|
212
|
+
};
|
|
213
|
+
if (skill.mode === "config" || skill.mode === "hybrid") {
|
|
214
|
+
result = this.configEngine.cast(skill, caster, deps, selectedTargets);
|
|
215
|
+
result.triggeredBy = trigger;
|
|
216
|
+
}
|
|
217
|
+
if ((skill.mode === "script" || skill.mode === "hybrid") && skill.scriptId) {
|
|
218
|
+
const scriptCtx = {
|
|
219
|
+
caster: caster.id,
|
|
220
|
+
targets: result.targets,
|
|
221
|
+
skillId: skill.id,
|
|
222
|
+
trigger,
|
|
223
|
+
triggerContext: triggerCtx,
|
|
224
|
+
api: deps.createScriptApi(skill.element),
|
|
225
|
+
level: skill.level ?? 1,
|
|
226
|
+
levelParams: skill.levelParams ?? {}
|
|
227
|
+
};
|
|
228
|
+
const scriptRes = this.scriptEngine.cast(skill.scriptId, scriptCtx);
|
|
229
|
+
const resolvedTargets = skill.mode === "script"
|
|
230
|
+
? (scriptRes.targets.length > 0 ? scriptRes.targets : result.targets)
|
|
231
|
+
: (result.targets.length > 0 ? result.targets : scriptRes.targets);
|
|
232
|
+
result = {
|
|
233
|
+
...result,
|
|
234
|
+
targets: resolvedTargets,
|
|
235
|
+
totalDamage: result.totalDamage + scriptRes.totalDamage,
|
|
236
|
+
totalHeal: result.totalHeal + scriptRes.totalHeal,
|
|
237
|
+
totalShield: result.totalShield + scriptRes.totalShield,
|
|
238
|
+
hits: [...(result.hits ?? []), ...(scriptRes.hits ?? [])]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (triggerCtx && result.targets.length === 0) {
|
|
242
|
+
result.targets = [...triggerCtx.sourceTargets];
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function living(units) {
|
|
2
|
+
return units.filter((u) => u.runtime.alive);
|
|
3
|
+
}
|
|
4
|
+
export function selectTargets(rule, caster, allUnits, rng) {
|
|
5
|
+
const allies = living(allUnits.filter((u) => u.teamId === caster.teamId)).sort((a, b) => a.position - b.position);
|
|
6
|
+
const enemies = living(allUnits.filter((u) => u.teamId !== caster.teamId)).sort((a, b) => a.position - b.position);
|
|
7
|
+
if (rule === "self") {
|
|
8
|
+
return [caster];
|
|
9
|
+
}
|
|
10
|
+
if (rule === "allEnemies") {
|
|
11
|
+
return enemies;
|
|
12
|
+
}
|
|
13
|
+
if (rule === "allAllies") {
|
|
14
|
+
return allies;
|
|
15
|
+
}
|
|
16
|
+
if (rule === "lowestHp" || rule === "lowestHpEnemy") {
|
|
17
|
+
const target = enemies.sort((a, b) => a.runtime.Hp - b.runtime.Hp)[0];
|
|
18
|
+
return target ? [target] : [];
|
|
19
|
+
}
|
|
20
|
+
if (rule === "highestHpEnemy") {
|
|
21
|
+
const target = enemies.sort((a, b) => b.runtime.Hp - a.runtime.Hp)[0];
|
|
22
|
+
return target ? [target] : [];
|
|
23
|
+
}
|
|
24
|
+
if (rule === "randomSingleEnemy") {
|
|
25
|
+
if (enemies.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return [enemies[rng.nextInt(enemies.length)]];
|
|
29
|
+
}
|
|
30
|
+
if (rule === "enemyAdjacent") {
|
|
31
|
+
if (enemies.length <= 2) {
|
|
32
|
+
return enemies;
|
|
33
|
+
}
|
|
34
|
+
const anchor = enemies[0];
|
|
35
|
+
return enemies.filter((u) => Math.abs(u.position - anchor.position) <= 1);
|
|
36
|
+
}
|
|
37
|
+
return enemies.length > 0 ? [enemies[0]] : [];
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UnitModel, UnitId } from "./types";
|
|
2
|
+
export declare class TurnBarSystem {
|
|
3
|
+
private extraTurnQueue;
|
|
4
|
+
grantExtraTurn(unitId: UnitId): void;
|
|
5
|
+
grantActionProgress(unit: UnitModel, ratio: number): void;
|
|
6
|
+
fillUntilAction(units: UnitModel[]): UnitModel | undefined;
|
|
7
|
+
consumeAction(unit: UnitModel): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const ACTION_THRESHOLD = 1000000;
|
|
2
|
+
export class TurnBarSystem {
|
|
3
|
+
extraTurnQueue = [];
|
|
4
|
+
grantExtraTurn(unitId) {
|
|
5
|
+
this.extraTurnQueue.push(unitId);
|
|
6
|
+
}
|
|
7
|
+
grantActionProgress(unit, ratio) {
|
|
8
|
+
const clampedRatio = Math.max(0, Math.floor(ratio));
|
|
9
|
+
const progress = Math.floor((ACTION_THRESHOLD * clampedRatio) / 10000);
|
|
10
|
+
unit.runtime.AP = Math.min(ACTION_THRESHOLD, unit.runtime.AP + progress);
|
|
11
|
+
}
|
|
12
|
+
fillUntilAction(units) {
|
|
13
|
+
const aliveUnits = units.filter((u) => u.runtime.alive);
|
|
14
|
+
if (aliveUnits.length === 0) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (this.extraTurnQueue.length > 0) {
|
|
18
|
+
const targetId = this.extraTurnQueue.shift();
|
|
19
|
+
const target = aliveUnits.find((u) => u.id === targetId);
|
|
20
|
+
if (target) {
|
|
21
|
+
target.runtime.AP = ACTION_THRESHOLD;
|
|
22
|
+
return target;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
while (true) {
|
|
26
|
+
let acted = aliveUnits
|
|
27
|
+
.filter((u) => u.runtime.AP >= ACTION_THRESHOLD)
|
|
28
|
+
.sort((a, b) => b.runtime.AP - a.runtime.AP || b.stats.Spd - a.stats.Spd)[0];
|
|
29
|
+
if (acted) {
|
|
30
|
+
return acted;
|
|
31
|
+
}
|
|
32
|
+
for (const unit of aliveUnits) {
|
|
33
|
+
unit.runtime.AP += Math.max(1, unit.stats.Spd);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
consumeAction(unit) {
|
|
38
|
+
unit.runtime.AP = 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export type TeamId = 1 | 2;
|
|
2
|
+
export type UnitId = string;
|
|
3
|
+
export type ElementType = "fire" | "thunder" | "water" | "rock" | "wind" | "light" | "dark";
|
|
4
|
+
export type SkillType = "basic" | "core" | "ultimate";
|
|
5
|
+
export type SkillExecMode = "config" | "script" | "hybrid";
|
|
6
|
+
export type BattleEventName = "OnTurnStart" | "OnTurnEnd" | "OnBeforeCast" | "OnAfterCast" | "OnBeforeDamage" | "OnAfterDamage" | "OnKill" | "OnDeath" | "OnBuffAdd" | "OnBuffRemove" | "OnExtraTurnGranted" | "OnImbalanceBreak" | "OnDamageDealt" | "BeforeDamageTaken";
|
|
7
|
+
export interface UnitStats {
|
|
8
|
+
Mhp: number;
|
|
9
|
+
Def: number;
|
|
10
|
+
Att: number;
|
|
11
|
+
Spd: number;
|
|
12
|
+
pER: number;
|
|
13
|
+
pAR: number;
|
|
14
|
+
pEHR: number;
|
|
15
|
+
pERes: number;
|
|
16
|
+
pBSDI: number;
|
|
17
|
+
pESDI: number;
|
|
18
|
+
pFSDI: number;
|
|
19
|
+
pCTR: number;
|
|
20
|
+
pCTD: number;
|
|
21
|
+
pCTR_Def: number;
|
|
22
|
+
pCTD_Def: number;
|
|
23
|
+
pHE: number;
|
|
24
|
+
pFDI: number;
|
|
25
|
+
pFDI_Def: number;
|
|
26
|
+
pTDI: number;
|
|
27
|
+
pTDI_Def: number;
|
|
28
|
+
pWDI: number;
|
|
29
|
+
pWDI_Def: number;
|
|
30
|
+
pRDI: number;
|
|
31
|
+
pRDI_Def: number;
|
|
32
|
+
pADI: number;
|
|
33
|
+
pADI_Def: number;
|
|
34
|
+
pLDI: number;
|
|
35
|
+
pLDI_Def: number;
|
|
36
|
+
pDDI: number;
|
|
37
|
+
pDDI_Def: number;
|
|
38
|
+
pIM: number;
|
|
39
|
+
pITM: number;
|
|
40
|
+
pVulnerability: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RuntimeState {
|
|
43
|
+
Hp: number;
|
|
44
|
+
AP: number;
|
|
45
|
+
Energy: number;
|
|
46
|
+
EnergyMax: number;
|
|
47
|
+
Shield: number;
|
|
48
|
+
Imbalance: number;
|
|
49
|
+
alive: boolean;
|
|
50
|
+
stunned: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface UnitSkillSlot {
|
|
53
|
+
id: string;
|
|
54
|
+
type: SkillType;
|
|
55
|
+
cooldown: number;
|
|
56
|
+
energyCost: number;
|
|
57
|
+
energyGain: number;
|
|
58
|
+
}
|
|
59
|
+
export interface UnitModel {
|
|
60
|
+
id: UnitId;
|
|
61
|
+
name: string;
|
|
62
|
+
teamId: TeamId;
|
|
63
|
+
position: 1 | 2 | 3 | 4;
|
|
64
|
+
element: ElementType;
|
|
65
|
+
stats: UnitStats;
|
|
66
|
+
runtime: RuntimeState;
|
|
67
|
+
skills: UnitSkillSlot[];
|
|
68
|
+
}
|
|
69
|
+
export interface BuffConfig {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
maxStacks: number;
|
|
73
|
+
durationPerStack: number;
|
|
74
|
+
expireRounds?: number;
|
|
75
|
+
onRoundEndDelta: number;
|
|
76
|
+
stun?: boolean;
|
|
77
|
+
taunt?: boolean;
|
|
78
|
+
dotRatio?: number;
|
|
79
|
+
dotElement?: ElementType;
|
|
80
|
+
speedDelta?: number;
|
|
81
|
+
attackDelta?: number;
|
|
82
|
+
defDelta?: number;
|
|
83
|
+
speedRateDelta?: number;
|
|
84
|
+
attackRateDelta?: number;
|
|
85
|
+
defenseRateDelta?: number;
|
|
86
|
+
critRateDelta?: number;
|
|
87
|
+
critDamageDelta?: number;
|
|
88
|
+
healEffectDelta?: number;
|
|
89
|
+
effectHitDelta?: number;
|
|
90
|
+
damageRateDelta?: number;
|
|
91
|
+
effectResDelta?: number;
|
|
92
|
+
energyRecoveryRateDelta?: number;
|
|
93
|
+
vulnerabilityDelta?: number;
|
|
94
|
+
imbalanceRateDelta?: number;
|
|
95
|
+
damageShareToSourceRatio?: number;
|
|
96
|
+
reactiveCounter?: boolean;
|
|
97
|
+
reactiveCounterPriority?: number;
|
|
98
|
+
applyOnImbalanceBreak?: boolean;
|
|
99
|
+
applyOnElementBreakElements?: ElementType[];
|
|
100
|
+
}
|
|
101
|
+
export interface BuffInstance {
|
|
102
|
+
id: string;
|
|
103
|
+
ownerId: UnitId;
|
|
104
|
+
stacks: number;
|
|
105
|
+
remainingRounds: number;
|
|
106
|
+
config: BuffConfig;
|
|
107
|
+
sourceId: UnitId;
|
|
108
|
+
}
|
|
109
|
+
export interface SkillDefinition {
|
|
110
|
+
id: string;
|
|
111
|
+
mode: SkillExecMode;
|
|
112
|
+
type: SkillType;
|
|
113
|
+
configTemplateId?: string;
|
|
114
|
+
scriptId?: string;
|
|
115
|
+
energyCost: number;
|
|
116
|
+
energyGain: number;
|
|
117
|
+
cooldown: number;
|
|
118
|
+
targetRule: TargetRule;
|
|
119
|
+
element: ElementType;
|
|
120
|
+
/** 技能等级,用于数值升级 */
|
|
121
|
+
level?: number;
|
|
122
|
+
/** 技能升级参数,传递给脚本使用 */
|
|
123
|
+
levelParams?: Record<string, number>;
|
|
124
|
+
activateOnBattleStart?: boolean;
|
|
125
|
+
chaseRule?: {
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
triggerSourceSkillIds?: string[];
|
|
128
|
+
triggerSourceTriggers?: Array<"normal" | "chase" | "assist" | "counter">;
|
|
129
|
+
sameTeamOnly?: boolean;
|
|
130
|
+
allowSelfTrigger?: boolean;
|
|
131
|
+
requireSourceTargets?: boolean;
|
|
132
|
+
chance?: number;
|
|
133
|
+
useSkillId?: string;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export interface SkillTemplateStep {
|
|
137
|
+
kind: "damage" | "heal" | "shield" | "buff";
|
|
138
|
+
ratio: number;
|
|
139
|
+
ratioByLevel?: number[];
|
|
140
|
+
buffId?: string;
|
|
141
|
+
fixedProb?: number;
|
|
142
|
+
fixedProbByLevel?: number[];
|
|
143
|
+
canCrit?: boolean;
|
|
144
|
+
scaleStat?: "Att" | "Def" | "Mhp";
|
|
145
|
+
flat?: number;
|
|
146
|
+
flatByLevel?: number[];
|
|
147
|
+
repeat?: number;
|
|
148
|
+
}
|
|
149
|
+
export interface SkillTemplate {
|
|
150
|
+
id: string;
|
|
151
|
+
steps: SkillTemplateStep[];
|
|
152
|
+
}
|
|
153
|
+
export type TargetRule = "nearest" | "lowestHp" | "lowestHpEnemy" | "highestHpEnemy" | "randomSingleEnemy" | "lastRowEnemy" | "firstAndLastEnemy" | "enemyAdjacent" | "allEnemies" | "allAllies" | "self" | "selfWithAdjacent";
|
|
154
|
+
export interface SkillResult {
|
|
155
|
+
skillId: string;
|
|
156
|
+
casterId: UnitId;
|
|
157
|
+
targets: UnitId[];
|
|
158
|
+
totalDamage: number;
|
|
159
|
+
totalHeal: number;
|
|
160
|
+
totalShield: number;
|
|
161
|
+
triggeredBy: "normal" | "chase" | "assist" | "counter";
|
|
162
|
+
hits?: SkillHitDetail[];
|
|
163
|
+
}
|
|
164
|
+
export interface SkillHitDetail {
|
|
165
|
+
targetId: UnitId;
|
|
166
|
+
effectType: "damage" | "heal" | "shield" | "buff";
|
|
167
|
+
element: ElementType;
|
|
168
|
+
skillType: SkillType;
|
|
169
|
+
isCritical: boolean;
|
|
170
|
+
baseValue: number;
|
|
171
|
+
criticalValue: number;
|
|
172
|
+
finalValue: number;
|
|
173
|
+
isRepression: -1 | 0 | 1;
|
|
174
|
+
appliedBuffId?: string;
|
|
175
|
+
resisted?: boolean;
|
|
176
|
+
damageBreakdown?: {
|
|
177
|
+
preMitigation: number;
|
|
178
|
+
postMitigation: number;
|
|
179
|
+
shieldAbsorbed: number;
|
|
180
|
+
hpDamage: number;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export interface BattleEvent<T = unknown> {
|
|
184
|
+
name: BattleEventName;
|
|
185
|
+
payload: T;
|
|
186
|
+
priority?: number;
|
|
187
|
+
}
|
|
188
|
+
export interface BattleActionLog {
|
|
189
|
+
turn: number;
|
|
190
|
+
actorId: UnitId;
|
|
191
|
+
skillId: string;
|
|
192
|
+
triggeredBy: SkillResult["triggeredBy"];
|
|
193
|
+
result: SkillResult;
|
|
194
|
+
}
|
|
195
|
+
export interface BattleSnapshot {
|
|
196
|
+
turn: number;
|
|
197
|
+
phase: "beforeAction" | "afterSkill" | "turnEnd";
|
|
198
|
+
units: Array<{
|
|
199
|
+
id: UnitId;
|
|
200
|
+
hp: number;
|
|
201
|
+
shield: number;
|
|
202
|
+
ap: number;
|
|
203
|
+
energy: number;
|
|
204
|
+
alive: boolean;
|
|
205
|
+
position: number;
|
|
206
|
+
effectiveAllStats: UnitStats;
|
|
207
|
+
buffs: Array<{
|
|
208
|
+
id: string;
|
|
209
|
+
name: string;
|
|
210
|
+
stacks: number;
|
|
211
|
+
remainingRounds: number;
|
|
212
|
+
sourceId: UnitId;
|
|
213
|
+
}>;
|
|
214
|
+
imbalance: number;
|
|
215
|
+
imbalanceMax: number;
|
|
216
|
+
}>;
|
|
217
|
+
/** 回合开始时触发的DOT伤害记录,用于飘字展示 */
|
|
218
|
+
dotDamages?: Array<{
|
|
219
|
+
targetId: UnitId;
|
|
220
|
+
value: number;
|
|
221
|
+
buffId: string;
|
|
222
|
+
element?: ElementType;
|
|
223
|
+
}>;
|
|
224
|
+
/** 失衡破击记录 */
|
|
225
|
+
imbalanceBreaks?: Array<{
|
|
226
|
+
targetId: UnitId;
|
|
227
|
+
attackerId: UnitId;
|
|
228
|
+
element: ElementType;
|
|
229
|
+
}>;
|
|
230
|
+
/** 失衡值变化来源记录 */
|
|
231
|
+
imbalanceChanges?: Array<{
|
|
232
|
+
targetId: UnitId;
|
|
233
|
+
delta: number;
|
|
234
|
+
reason: "turn_start_clear" | "skill_effect";
|
|
235
|
+
sourceActorId?: UnitId;
|
|
236
|
+
sourceSkillId?: string;
|
|
237
|
+
}>;
|
|
238
|
+
}
|
|
239
|
+
export interface BattleInitUnit {
|
|
240
|
+
id: string;
|
|
241
|
+
name: string;
|
|
242
|
+
teamId: TeamId;
|
|
243
|
+
position: 1 | 2 | 3 | 4;
|
|
244
|
+
element: ElementType;
|
|
245
|
+
stats: UnitStats;
|
|
246
|
+
skills: SkillDefinition[];
|
|
247
|
+
}
|
|
248
|
+
export interface BattleConfig {
|
|
249
|
+
maxTurns: number;
|
|
250
|
+
seed: number;
|
|
251
|
+
}
|
|
252
|
+
export interface TriggerContext {
|
|
253
|
+
turn: number;
|
|
254
|
+
sourceSkillId: string;
|
|
255
|
+
sourceActorId: UnitId;
|
|
256
|
+
sourceTargets: UnitId[];
|
|
257
|
+
sourceTriggeredBy?: SkillResult["triggeredBy"];
|
|
258
|
+
}
|
|
259
|
+
export interface ScriptRuntimeContext {
|
|
260
|
+
caster: UnitId;
|
|
261
|
+
targets: UnitId[];
|
|
262
|
+
skillId: string;
|
|
263
|
+
trigger: SkillResult["triggeredBy"];
|
|
264
|
+
api: BattleScriptApi;
|
|
265
|
+
triggerContext?: TriggerContext;
|
|
266
|
+
/** 技能等级 */
|
|
267
|
+
level: number;
|
|
268
|
+
/** 技能升级参数 */
|
|
269
|
+
levelParams: Record<string, number>;
|
|
270
|
+
}
|
|
271
|
+
export interface BattleScriptApi {
|
|
272
|
+
damage(sourceId: UnitId, targetId: UnitId, value: number): number;
|
|
273
|
+
heal(targetId: UnitId, value: number): number;
|
|
274
|
+
shield(targetId: UnitId, value: number): number;
|
|
275
|
+
adjustImbalance(targetId: UnitId, delta: number): number;
|
|
276
|
+
addBuff(sourceId: UnitId, targetId: UnitId, buffId: string): void;
|
|
277
|
+
removeBuff(targetId: UnitId, buffId: string): void;
|
|
278
|
+
grantExtraTurn(unitId: UnitId): void;
|
|
279
|
+
adjustActionProgress(unitId: UnitId, ratio: number): void;
|
|
280
|
+
gainEnergy(unitId: UnitId, value: number): void;
|
|
281
|
+
getUnit(unitId: UnitId): UnitModel | undefined;
|
|
282
|
+
getAliveUnits(): UnitModel[];
|
|
283
|
+
hasBuff(unitId: UnitId, buffId: string): boolean;
|
|
284
|
+
getBuffs(unitId: UnitId): BuffInstance[];
|
|
285
|
+
randomInt(maxExclusive: number): number;
|
|
286
|
+
pushPosition(unitId: UnitId, delta: number): void;
|
|
287
|
+
preventDeath(unitId: UnitId): void;
|
|
288
|
+
isUnitTurn(unitId: UnitId): boolean;
|
|
289
|
+
castSkill(actorId: UnitId, skillId: string, trigger: SkillResult["triggeredBy"], targetIds?: UnitId[]): SkillResult | null;
|
|
290
|
+
onAnyEvent(listenerUnitId: UnitId, eventName: BattleEventName, handler: (payload: any) => void): void;
|
|
291
|
+
/** 动态召唤一个新单位加入战斗 */
|
|
292
|
+
spawnUnit(initUnit: BattleInitUnit): UnitModel | null;
|
|
293
|
+
/** 延长目标单位所有主动技能的冷却回合数 */
|
|
294
|
+
extendCooldown(unitId: UnitId, delta: number): void;
|
|
295
|
+
/** 击杀指定单位(将其HP设为0并标记死亡) */
|
|
296
|
+
killUnit(unitId: UnitId): void;
|
|
297
|
+
}
|
|
298
|
+
export interface ISkillScript {
|
|
299
|
+
onPreCast?(ctx: ScriptRuntimeContext): void;
|
|
300
|
+
onCast(ctx: ScriptRuntimeContext): SkillResult;
|
|
301
|
+
onPostCast?(ctx: ScriptRuntimeContext, result: SkillResult): void;
|
|
302
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BattleCore, type BattleResult } from "../battle/battleCore";
|
|
2
|
+
import type { BattleConfig, BattleInitUnit } from "../battle/types";
|
|
3
|
+
export declare class BattleFacade {
|
|
4
|
+
private core;
|
|
5
|
+
start(units: BattleInitUnit[], config: BattleConfig, initialize?: (core: BattleCore) => void): BattleResult;
|
|
6
|
+
stop(): void;
|
|
7
|
+
private toInstanceId;
|
|
8
|
+
}
|