chenanghui 4 days ago
commit ecd3905e23
  1. 2589
      assets/bundles/battleMap/prefab/地图/mapLayer1.prefab
  2. 2923
      assets/bundles/battleMap/prefab/地图/mapLayer2.prefab
  3. 2547
      assets/bundles/battleMap/prefab/地图/mapLayer3.prefab
  4. 14
      assets/bundles/first/config/Const.json
  5. 90
      assets/bundles/first/config/Level.json
  6. 2
      assets/bundles/first/config/Skill.json
  7. 32
      assets/bundles/popBattle/popSelect/prefab/PopSelect.prefab
  8. 6
      assets/script/battle/ai/decision/AiActionPlanner.ts
  9. 13
      assets/script/battle/ai/runtime/AiHeadlessBagHost.ts
  10. 55
      assets/script/battle/ai/runtime/AiLog.ts
  11. 14
      assets/script/battle/ai/runtime/AiOpponentController.ts
  12. 170
      assets/script/battle/combat/BattleBuffDigitStatHost.ts
  13. 6
      assets/script/battle/combat/BattleBuffInGameRefresh.ts
  14. 16
      assets/script/battle/combat/TowerQueryService.ts
  15. 100
      assets/script/battle/runtime/BattleRuntime.ts
  16. 3
      assets/script/battle/runtime/DeploySync.ts
  17. 5
      assets/script/battle/view/CampHpBarPresenter.ts
  18. 2
      assets/script/sdk/sdkConfig.ts
  19. 3
      assets/script/ui/BattleGame/UIBattleGame.ts
  20. 15
      assets/script/ui/BattleWeaponDesc1/UIBattleWeaponDesc1.ts
  21. 5
      assets/script/ui/PopSelect/UIPopSelect.ts
  22. BIN
      配置表/excelList/关卡表.xlsx
  23. BIN
      配置表/excelList/常量表.xlsx
  24. BIN
      配置表/excelList/技能表.xlsx
  25. BIN
      配置表/excelList/随机buff.xlsx
  26. BIN
      配置表/策划配置/数字2048资源.xlsx

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -111,37 +111,37 @@
"id": 19,
"desc": "星期2地图id",
"para": 0,
"para1": "3,4,5"
"para1": "1,2,3"
},
{
"id": 20,
"desc": "星期3地图id",
"para": 0,
"para1": "5,6,7"
"para1": "1,2,3"
},
{
"id": 21,
"desc": "星期4地图id",
"para": 0,
"para1": "7,1,2"
"para1": "1,2,3"
},
{
"id": 22,
"desc": "星期5地图id",
"para": 0,
"para1": "2,3,4"
"para1": "1,2,3"
},
{
"id": 23,
"desc": "星期6地图id",
"para": 0,
"para1": "4,5,6"
"para1": "1,2,3"
},
{
"id": 24,
"desc": "星期7地图id",
"para": 0,
"para1": "6,1,2"
"para1": "1,2,3"
},
{
"id": 25,
@ -153,7 +153,7 @@
"id": 26,
"desc": "解锁格子次数等级加值",
"para": 0,
"para1": "0,2,2,4,4,4,5,5,5,5,6,6,6,6,6"
"para1": "0,3,3,5,5,5,7,7,7,7,8,8,8,8,8"
},
{
"id": 27,

@ -14,15 +14,15 @@
"bosspro": 1200,
"unlockland1": "0,2,5",
"unlockland2": "0,5,5",
"unlockland3": "0,6,5",
"unlockland4": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland5": "0,7,5",
"unlockland6": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland7": "0,8,5",
"unlockland8": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland9": "0,9,5",
"unlockland10": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"unlockland11": "0,10,5",
"unlockland3": "1,5,1|2,5,1|3,5,1|4,5,1|5,5,1",
"unlockland4": "0,6,5",
"unlockland5": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland6": "0,7,5",
"unlockland7": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland8": "0,8,5",
"unlockland9": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland10": "0,9,5",
"unlockland11": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"normalland": [
6,
7,
@ -52,15 +52,15 @@
"bosspro": 1200,
"unlockland1": "0,2,5",
"unlockland2": "0,5,5",
"unlockland3": "0,6,5",
"unlockland4": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland5": "0,7,5",
"unlockland6": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland7": "0,8,5",
"unlockland8": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland9": "0,9,5",
"unlockland10": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"unlockland11": "0,10,5",
"unlockland3": "1,5,1|2,5,1|3,5,1|4,5,1|5,5,1",
"unlockland4": "0,6,5",
"unlockland5": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland6": "0,7,5",
"unlockland7": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland8": "0,8,5",
"unlockland9": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland10": "0,9,5",
"unlockland11": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"normalland": [
6,
7,
@ -90,15 +90,15 @@
"bosspro": 1200,
"unlockland1": "0,2,5",
"unlockland2": "0,5,5",
"unlockland3": "0,6,5",
"unlockland4": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland5": "0,7,5",
"unlockland6": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland7": "0,8,5",
"unlockland8": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland9": "0,9,5",
"unlockland10": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"unlockland11": "0,10,5",
"unlockland3": "1,5,1|2,5,1|3,5,1|4,5,1|5,5,1",
"unlockland4": "0,6,5",
"unlockland5": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland6": "0,7,5",
"unlockland7": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland8": "0,8,5",
"unlockland9": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland10": "0,9,5",
"unlockland11": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"normalland": [
6,
7,
@ -128,15 +128,15 @@
"bosspro": 1200,
"unlockland1": "0,2,5",
"unlockland2": "0,5,5",
"unlockland3": "0,6,5",
"unlockland4": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland5": "0,7,5",
"unlockland6": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland7": "0,8,5",
"unlockland8": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland9": "0,9,5",
"unlockland10": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"unlockland11": "0,10,5",
"unlockland3": "1,5,1|2,5,1|3,5,1|4,5,1|5,5,1",
"unlockland4": "0,6,5",
"unlockland5": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland6": "0,7,5",
"unlockland7": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland8": "0,8,5",
"unlockland9": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland10": "0,9,5",
"unlockland11": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"normalland": [
6,
7,
@ -166,15 +166,15 @@
"bosspro": 1200,
"unlockland1": "0,2,5",
"unlockland2": "0,5,5",
"unlockland3": "0,6,5",
"unlockland4": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland5": "0,7,5",
"unlockland6": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland7": "0,8,5",
"unlockland8": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland9": "0,9,5",
"unlockland10": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"unlockland11": "0,10,5",
"unlockland3": "1,5,1|2,5,1|3,5,1|4,5,1|5,5,1",
"unlockland4": "0,6,5",
"unlockland5": "1,6,1|2,6,1|3,6,1|4,6,1|5,6,1",
"unlockland6": "0,7,5",
"unlockland7": "1,7,1|2,7,1|3,7,1|4,7,1|5,7,1",
"unlockland8": "0,8,5",
"unlockland9": "1,8,1|2,8,1|3,8,1|4,8,1|5,8,1",
"unlockland10": "0,9,5",
"unlockland11": "1,9,1|2,9,1|3,9,1|4,9,1|5,9,1",
"normalland": [
6,
7,

@ -1216,7 +1216,7 @@
"icon": "",
"type": 1,
"level": 0,
"cd": 0,
"cd": 20,
"priority": 100,
"animation": "悟空专属技能效果",
"showeffect": "",

@ -987,10 +987,7 @@
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "3f45d54f-19f3-4967-b58a-b3a8eb1af318@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_spriteFrame": null,
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
@ -1123,10 +1120,7 @@
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "8322760d-efd8-4f76-a499-d547ccca506d@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_spriteFrame": null,
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
@ -1319,7 +1313,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 98.514,
"y": 102.816,
"z": 0
},
"_lrot": {
@ -1331,8 +1325,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 0.5,
"y": 0.5,
"z": 1
},
"_mobility": 0,
@ -1359,8 +1353,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 152
"width": 106,
"height": 146
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -1492,7 +1486,7 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 64,
"width": 104,
"height": 54.4
},
"_anchorPoint": {
@ -1528,7 +1522,7 @@
"b": 255,
"a": 255
},
"_string": "轩辕剑",
"_string": "暴击伤害Ⅰ",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 20,
@ -1611,7 +1605,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 11.658,
"y": 7,
"z": 0
},
"_lrot": {
@ -1652,7 +1646,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 125.19999999999999
"height": 65.19999999999999
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -1678,10 +1672,10 @@
"__id__": 69
},
"_lineHeight": 20,
"_string": "<color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color><color=#00ff00>Rich</color><color=#0fffff>Text</color>",
"_string": "一定几率召唤出属性卡,拖到数字上随机变换一个属性",
"_horizontalAlign": 0,
"_verticalAlign": 0,
"_fontSize": 20,
"_fontSize": 16,
"_fontColor": {
"__type__": "cc.Color",
"r": 0,

@ -76,11 +76,7 @@ export class AiActionPlanner {
const dst = m.target.kind === 'bag' ? `bag[${m.target.slotIndex}]` : `map[${m.target.gridIndex}]`;
detail += ` ${src}->${dst}`;
}
if (label === '解锁地块' || label === '产卡使用' || label === '数字合成' || label === '上阵') {
console.warn('[AI操作]', `决策 ${label} | ${detail} | emptyPut=${emptyPut} dig=${dig}`);
} else {
aiOp(`决策 ${label} | ${detail} | emptyPut=${emptyPut} dig=${dig}`);
}
aiOp(`决策 ${label} | ${detail} | emptyPut=${emptyPut} dig=${dig}`);
return action;
}

@ -134,7 +134,7 @@ export class AiHeadlessBagHost {
}
if (best < 0) return false;
if (!bag.discardBagSlot(best)) return false;
console.log('[AI操作]', `卡死丢弃 bag[${best}] Lv${bestLv} 腾召唤位`);
aiOp(`卡死丢弃 bag[${best}] Lv${bestLv} 腾召唤位`);
return true;
}
@ -250,15 +250,10 @@ export class AiHeadlessBagHost {
const land = this._land.stateOf(i);
parts.push(`[${i}|${land}]=${this._resolveTileName(s.weapon2Id)}Lv${s.level ?? 1}`);
}
const line =
aiOp(
`背包[${tag}] filled=${filled}/${slots.length} gold=${this._battle?.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}`
+ ` | ${parts.join(' ')}`;
// 首刷/召唤用 console,便于确认 AI 是否有可拖数字
if (tag.indexOf('firstFill') === 0 || tag === 'summon' || tag.indexOf('卡死') >= 0) {
console.warn('[AI操作]', line);
} else {
aiOp(line);
}
+ ` | ${parts.join(' ')}`,
);
}
/** Text 表中文名;缺表回退 id */

@ -1,24 +1,31 @@
import { sdkConfig } from '../../../sdk/sdkConfig';
/**
* AI 调试打印。统一前缀 `[AI操作]`,控制台可过滤。
* 默认全关;控制台 `setAiLogVerbose(true)` 后 `aiLog` / `aiOp` / `aiWarn` 才输出。
* AI 调试打印。统一前缀 `[AI操作]`,紫色普通 log;由 sdkConfig.enableAiDecisionLog 控制。
* 控制台亦可 `setAiLogVerbose(true)` 临时强制开启。
*/
/** 所有 AI 日志统一前缀(控制台过滤用) */
export const AI_LOG_PREFIX = '[AI操作]';
let _verbose = false;
/** 紫色样式(与打点绿字同用 %c) */
const AI_LOG_STYLE = 'color:#9c27b0';
/** 运行时强制开关;null 表示跟随 sdkConfig */
let _forceVerbose: boolean | null = null;
/**
* 开关 AI 细节 console 打印。
* 开关 AI 细节 console 打印(覆盖 sdkConfig,传 null 恢复跟随配置)。
* @param enabled 是否开启
*/
export function setAiLogVerbose(enabled: boolean): void {
_verbose = !!enabled;
_forceVerbose = !!enabled;
}
/** 当前是否开启 AI 细节打印 */
/** 当前是否开启 AI 决策打印 */
export function isAiLogVerbose(): boolean {
return _verbose;
if (_forceVerbose !== null) return _forceVerbose;
return !!sdkConfig.enableAiDecisionLog;
}
/**
@ -50,13 +57,33 @@ function _withPrefix(args: unknown[]): unknown[] {
return out;
}
/**
* 紫色普通 log 输出。
* @param args 打印参数
*/
function _printPurple(args: unknown[]): void {
const out = _withPrefix(args);
const head = out[0];
if (typeof head === 'string') {
const rest: unknown[] = [];
for (let i = 1; i < out.length; i++) rest.push(out[i]);
if (rest.length <= 0) {
console.log(`%c${head}`, AI_LOG_STYLE);
return;
}
console.log(`%c${head}`, AI_LOG_STYLE, ...rest);
return;
}
console.log(`%c${AI_LOG_PREFIX}`, AI_LOG_STYLE, ...out);
}
/**
* AI 细节日志;关闭时为空操作。
* @param args 打印参数
*/
export function aiLog(...args: unknown[]): void {
if (!_verbose) return;
console.log(..._withPrefix(args));
if (!isAiLogVerbose()) return;
_printPurple(args);
}
/**
@ -64,17 +91,17 @@ export function aiLog(...args: unknown[]): void {
* @param args 打印参数
*/
export function aiOp(...args: unknown[]): void {
if (!_verbose) return;
console.log(..._withPrefix(args));
if (!isAiLogVerbose()) return;
_printPurple(args);
}
/**
* AI 告警;关闭时为空操作(用 log 避免浏览器 warn 刷屏卡顿)。
* AI 告警;关闭时为空操作(紫色普通 log,避免浏览器 warn 刷屏)。
* @param args 打印参数
*/
export function aiWarn(...args: unknown[]): void {
if (!_verbose) return;
console.log(..._withPrefix(args));
if (!isAiLogVerbose()) return;
_printPurple(args);
}
try {

@ -168,7 +168,7 @@ export class AiOpponentController {
return;
}
// 规划成功但落子拒绝:本拍回退刷新,避免整拍空转
console.warn('[AI操作] 动作失败回退刷新', action.kind);
aiWarn('动作失败回退刷新', action.kind);
}
// 升级/放字已穷尽:先乾坤袋回收背包多余字(场上不回收),再刷新
@ -187,7 +187,7 @@ export class AiOpponentController {
}
if (r === 'pool_empty') {
this._poolEmpty = true;
console.warn('[AI操作] 字池耗尽,停止召唤');
aiWarn('字池耗尽,停止召唤');
return;
}
if (r === 'ok') {
@ -195,8 +195,7 @@ export class AiOpponentController {
return;
}
if (r === 'not_enough' || r === 'no_slot') {
console.warn(
'[AI操作]',
aiOp(
`待机 refresh=${r} gold=${this._bagHost!.aiGold}`
+ ` emptyPut=${this._planner.countEmptyTextPut(ctx)}`,
);
@ -231,8 +230,7 @@ export class AiOpponentController {
} else if (action) {
detail = String(action.kind);
}
console.warn(
'[AI操作]',
aiOp(
`tick filled=${filled} openDrag=${openDrag} maxLv=${maxLv}`
+ ` emptyPut=${emptyPut} unlockArea=${unlockId} gold=${this._bagHost!.aiGold}`
+ ` | ${detail}`,
@ -306,8 +304,8 @@ export class AiOpponentController {
const mg = this._map?.getMapGridByIndex(target.gridIndex);
if (mg?.isAreaUnlock) {
const ok = this._applyLogicalDrop(source, target, tag);
if (ok) console.warn('[AI操作]', `解锁落子 ${this._fmtSrc(source)}->${this._fmtDst(target)}`);
else console.error('[AI操作]', `解锁落子失败 ${this._fmtSrc(source)}->${this._fmtDst(target)}`);
if (ok) aiOp(`解锁落子 ${this._fmtSrc(source)}->${this._fmtDst(target)}`);
else aiWarn(`解锁落子失败 ${this._fmtSrc(source)}->${this._fmtDst(target)}`);
return ok;
}
}

@ -1,12 +1,16 @@
import type { BattleSession } from 'db://assets/mx/module/battle';
import { AttrId, AttributeComp, CombatComp } from 'db://assets/mx/module/battle/ecs/Components';
import { AttrId, AttributeComp, CombatComp, TeamComp } from 'db://assets/mx/module/battle/ecs/Components';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { TableNames } from '../../game/ConfigTableData';
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes';
import { BattleBuffTargetParse, parseFunctionParNums } from '../../manager/battleBuff/BattleBuffParamParse';
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
import { isElementHero } from '../config/BattleHeroTable';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { getTowerLoadoutService } from './TowerLoadoutService';
import type { TowerCombatStats } from './TowerQueryTypes';
/** modifier source 前缀 */
const DIGIT_STAT_SOURCE = 'battle_buff_digit_';
@ -45,14 +49,76 @@ export class BattleBuffDigitStatHost {
*/
reapplyPlayer(playerId: number): void {
const battle = this._battle;
if (!battle) return;
if (!battle) {
console.warn('[BattleBuffDigitStat] reapply 未 bind', playerId);
return;
}
const pid = Math.max(0, Math.floor(Number(playerId) || 0));
const session = battle.session;
const seen: EntityId[] = [];
const player = battle.getPlayer(pid);
if (!player) return;
const deployed = player.deployedUnits;
const deployed = player?.deployedUnits ?? [];
for (let i = 0; i < deployed.length; i++) {
this.refreshEntity(battle.session, deployed[i], pid);
const eid = deployed[i];
if (this._hasSeen(seen, eid)) continue;
seen.push(eid);
this.refreshEntity(session, eid, pid);
}
// deployedUnits 与 ECS 偶发不同步时,按 TeamComp+CombatComp 补刷
session.world.view2(CombatComp, TeamComp).forEach((entityId, _combat, team) => {
if (team.ownerId !== pid) return;
if (this._hasSeen(seen, entityId)) return;
seen.push(entityId);
this.refreshEntity(session, entityId, pid);
});
if (this._hasDigitPassives(pid) && seen.length <= 0) {
console.warn('[BattleBuffDigitStat] 有 101~104 但场上无塔可刷', pid);
}
GEvent.Ins.emit(GEvent.UpdateBestPlace);
}
/**
* 未部署/配表预览:把 101~104 叠到快照数值上(与 ECS 通道一致)。
* @param stats 基础战斗数值
* @param weapon1Id Hero.id
* @param level 合成等级
* @param playerId 携带方
*/
previewOntoStats(stats: TowerCombatStats, weapon1Id: number, level: number, playerId: number): TowerCombatStats {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id);
if (!hero) return stats;
const heroType = this._resolveElementType(weapon1Id, hero);
const lv = this._resolveMatchLevel(hero, level);
const pid = Math.max(0, Math.floor(Number(playerId) || 0));
let attackMul = 1;
let aspdAdd = 0;
let critRateAdd = 0;
let critDmgAdd = 0;
const passives = BattleBuffCarryStore.getPassives(pid);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
const ft = Math.floor(Number(row.functiontype) || 0);
if (!this._isDigitFt(ft)) continue;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, lv)) continue;
const nums = parseFunctionParNums(row.functionpar);
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (wan === 0) continue;
if (ft === BattleBuffFunctionType.DigitDamage) attackMul *= 1 + wan / 10000;
else if (ft === BattleBuffFunctionType.DigitCritRate) critRateAdd += wan / 10000;
else if (ft === BattleBuffFunctionType.DigitCritDamage) critDmgAdd += wan / 10000;
else if (ft === BattleBuffFunctionType.DigitAttackSpeed) aspdAdd += wan;
}
if (attackMul === 1 && aspdAdd === 0 && critRateAdd === 0 && critDmgAdd === 0) return stats;
const attackSpeed = stats.attackSpeed * (1 + aspdAdd / 10000);
return {
...stats,
attack: Math.floor(stats.attack * attackMul),
attackSpeed,
attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0,
critRate: Math.min(1, stats.critRate + critRateAdd),
critDamage: stats.critDamage + critDmgAdd,
};
}
/**
@ -64,25 +130,52 @@ export class BattleBuffDigitStatHost {
refreshEntity(session: BattleSession, entityId: EntityId, ownerPlayerId: number): void {
this._clearDigitMods(session, entityId);
const pid = Math.max(0, Math.floor(Number(ownerPlayerId) || 0));
if (!this._hasDigitPassives(pid)) return;
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (!loadout) return;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, loadout.weapon1Id);
if (!hero) return;
const heroType = Math.floor(Number(hero.herotype) || 0);
const level = Math.floor(Number(loadout.level) || 0);
const passives = BattleBuffCarryStore.getPassives(pid);
if (!hero) {
console.warn('[BattleBuffDigitStat] Hero 缺失', loadout.weapon1Id, entityId);
return;
}
const heroType = this._resolveElementType(loadout.weapon1Id, hero);
const level = this._resolveMatchLevel(hero, loadout.level);
const world = session.world;
if (!world.isAlive(entityId)) return;
if (!world.getComponent(entityId, CombatComp)) {
console.warn('[BattleBuffDigitStat] 无 CombatComp', entityId, loadout.weapon1Id);
return;
}
this._applyPassivesToEntity(session, entityId, pid, heroType, level);
}
/**
* 匹配用等级:五行以 Hero.level 为准(weapon1Id 已定级)。
* @param hero Hero 行
* @param loadoutLevel 装载等级
*/
private _resolveMatchLevel(hero: ITableHero, loadoutLevel: number): number {
if (isElementHero(hero)) {
const hlv = Math.floor(Number(hero.level) || 0);
if (hlv > 0) return hlv;
}
return Math.max(1, Math.floor(Number(loadoutLevel) || 0));
}
/**
* @param session 会话
* @param entityId 实体
* @param playerId 携带方
* @param heroType 五行
* @param level 等级
*/
private _applyPassivesToEntity(session: BattleSession, entityId: EntityId, playerId: number, heroType: number, level: number): void {
const passives = BattleBuffCarryStore.getPassives(playerId);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
const ft = Math.floor(Number(row.functiontype) || 0);
if (
ft !== BattleBuffFunctionType.DigitDamage
&& ft !== BattleBuffFunctionType.DigitCritRate
&& ft !== BattleBuffFunctionType.DigitCritDamage
&& ft !== BattleBuffFunctionType.DigitAttackSpeed
) {
continue;
}
if (!this._isDigitFt(ft)) continue;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, level)) continue;
const nums = parseFunctionParNums(row.functionpar);
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
@ -90,10 +183,53 @@ export class BattleBuffDigitStatHost {
console.warn('[BattleBuffDigitStat] functionpar 无效', row.id, row.functionpar);
continue;
}
if (wan > 0 && wan < 100) {
console.warn('[BattleBuffDigitStat] functionpar 过小(疑非万分比)', row.id, wan, row.desc);
}
this._applyOne(session, entityId, ft, wan, `${DIGIT_STAT_SOURCE}${row.id}_${i}`);
}
}
private _hasSeen(seen: readonly EntityId[], eid: EntityId): boolean {
for (let i = 0; i < seen.length; i++) {
if (seen[i] === eid) return true;
}
return false;
}
private _hasDigitPassives(playerId: number): boolean {
const passives = BattleBuffCarryStore.getPassives(playerId);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
if (this._isDigitFt(Math.floor(Number(row.functiontype) || 0))) return true;
}
return false;
}
private _isDigitFt(ft: number): boolean {
return ft === BattleBuffFunctionType.DigitDamage
|| ft === BattleBuffFunctionType.DigitCritRate
|| ft === BattleBuffFunctionType.DigitCritDamage
|| ft === BattleBuffFunctionType.DigitAttackSpeed;
}
/**
* 五行 type:优先 Text.type,否则 Hero.herotype。
* @param weapon1Id Hero.id
* @param hero Hero 行
*/
private _resolveElementType(weapon1Id: number, hero: ITableHero): number {
const provider = WeaponComposeConfigProvider.instance;
provider.load();
const w2 = provider.getWeapon2IdForWeapon1(weapon1Id);
if (w2 != null) {
const t = Math.floor(Number(provider.getWeapon2Type(w2)) || 0);
if (t >= 1 && t <= 5) return t;
}
return Math.floor(Number(hero.herotype) || 0);
}
private _clearDigitMods(session: BattleSession, entityId: EntityId): void {
const world = session.world;
if (!world.isAlive(entityId)) return;

@ -11,6 +11,11 @@ interface InGameRefreshBattle {
getPlayer(playerId: number): { deployedUnits: readonly number[] } | null | undefined;
/** 会话 */
session: import('db://assets/mx/module/battle').BattleSession;
/**
* 气血丸/双生盾增量加基地血。
* @param playerId 携带方
*/
applyInGameBaseHpCarry(playerId: number): boolean;
}
/**
@ -27,5 +32,6 @@ export class BattleBuffInGameRefresh {
getBattleBuffUnitAttackSpeedHost().bind(battle as never);
getBattleBuffMoveSpeedHost().bind(battle as never);
getBattleBuffBaseThunderHost().refreshCarry(battle as never);
battle.applyInGameBaseHpCarry(playerId);
}
}

@ -14,6 +14,7 @@ import {
findEntityIdByGridIndex as queryEntityByGrid,
} from '../runtime/DeployEntityQuery';
import { getTowerLoadoutService, resolveHeroBattleWeaponId } from './TowerLoadoutService';
import { getBattleBuffDigitStatHost } from './BattleBuffDigitStatHost';
import { deriveTowerSetupStats } from './TowerStatResolver';
import { parseEntryBaseAttr } from '../skill/SkillEntryParse';
import { entriesAffectingWeapon } from '../skill/GlobalSkillEffects';
@ -168,7 +169,11 @@ export class TowerQueryService {
const entries = p?.selectedSkills
? entriesAffectingWeapon(p.selectedSkills, weapon1Id)
: [];
const stats = this._applySelectedSkillsPreview(setup, entries);
const withSkills = this._applySelectedSkillsPreview(setup, entries);
const pid = p?.playerId ?? gg.game.CurentBattle?.localPlayerId ?? 0;
const stats = getBattleBuffDigitStatHost().previewOntoStats(
withSkills, weapon1Id, setup.effectiveLevel, pid,
);
const normalSkillId = heroNormalSkillId(w1, setup.effectiveLevel);
const activeSkillId = p?.getActiveSkillBaseAtkId(weapon1Id, setup.effectiveLevel)
?? heroUltSkillId(w1, setup.effectiveLevel);
@ -287,7 +292,14 @@ export class TowerQueryService {
const map = gg.game.CurentBattle?.Map;
const player = gg.game.localPlayer;
if (!s || !map) return null;
return queryEntityByGrid(gridIndex, s, map, player);
const fromDeploy = queryEntityByGrid(gridIndex, s, map, player);
if (fromDeploy != null) return fromDeploy;
// DeployedUnits 对账滞后时回退 GridMap.occupant
const g = map.getMapGridByIndex(gridIndex);
if (!g) return null;
const occ = s.grid.getCell({ row: g.Row, col: g.Col })?.occupant ?? null;
if (occ == null || !s.world.isAlive(occ)) return null;
return occ;
}
/** 从 AttributeSystem 读取最终战斗数值 */

@ -403,6 +403,10 @@ export class BattleRuntime implements IBattleRuntimeHandle {
private _reviveSpawnSuspended = false;
private _localPlayerId = 0;
private _players: Player[] = [];
/**
* 已应用到基地的气血丸/双生盾累计(按携带方),局内三选一用增量避免重复加血。
*/
private readonly _appliedBaseHp: { playerId: number; baseHp: number; bothSelf: number; bothEnemy: number }[] = [];
/** 本局结算已落地(防止 WallDamaged / BattleEnd 重复弹窗) */
private _resultSettled = false;
/** 已上报 inLevel 打点的波次;-1 表示尚未上报(波形事件与变化检测共用,避免重复上报) */
@ -793,41 +797,80 @@ export class BattleRuntime implements IBattleRuntimeHandle {
this._addPlayerGold(this._localPlayerId, amount);
}
/** 气血丸 + 双生盾:本机/对方基地血 */
private _applyCarriedBaseHpBonus(localPlayerId: number): void {
const pid = Math.max(0, Math.floor(Number(localPlayerId) || 0));
/** 开战:按 CarryStore 为各方套用气血丸/双生盾 */
private _applyCarriedBaseHpBonus(_localPlayerId: number): void {
this._appliedBaseHp.length = 0;
for (let i = 0; i < this.session.players.length; i++) {
this.applyInGameBaseHpCarry(this.session.players[i].id);
}
}
/**
* 局内三选一获得气血丸/双生盾后:按 CarryStore 增量加基地血。
* @param carrierPlayerId 携带方 playerId
* @returns 是否有血量变化
*/
applyInGameBaseHpCarry(carrierPlayerId: number): boolean {
const pid = Math.max(0, Math.floor(Number(carrierPlayerId) || 0));
const selfSlot = this.session.players[pid];
if (!selfSlot) {
console.log('[BattleRuntime] 基地加血 passive 应用失败:玩家槽缺失', pid);
return;
console.warn('[BattleRuntime] 基地加血失败:玩家槽缺失', pid);
return false;
}
const base = resolveCarriedBaseHpBonus();
if (base.active && base.bonusHp > 0) {
selfSlot.wallMaxHp += base.bonusHp;
selfSlot.wallHp += base.bonusHp;
}
const both = resolveCarriedBothBaseHp();
if (!both.active) return;
if (both.selfHp > 0) {
selfSlot.wallMaxHp += both.selfHp;
selfSlot.wallHp += both.selfHp;
}
if (both.enemyHp > 0) {
let enemyPid = -1;
for (let i = 0; i < this.session.players.length; i++) {
if (this.session.players[i].id !== pid) {
enemyPid = this.session.players[i].id;
break;
}
}
const passives = BattleBuffCarryStore.getPassives(pid);
const base = resolveCarriedBaseHpBonus(passives);
const both = resolveCarriedBothBaseHp(passives);
let prev = this._findAppliedBaseHp(pid);
if (!prev) {
prev = { playerId: pid, baseHp: 0, bothSelf: 0, bothEnemy: 0 };
this._appliedBaseHp.push(prev);
}
const dBase = Math.max(0, base.bonusHp - prev.baseHp);
const dSelf = Math.max(0, both.selfHp - prev.bothSelf);
const dEnemy = Math.max(0, both.enemyHp - prev.bothEnemy);
let changed = false;
const selfAdd = dBase + dSelf;
if (selfAdd > 0) {
selfSlot.wallMaxHp += selfAdd;
selfSlot.wallHp += selfAdd;
changed = true;
}
if (dEnemy > 0) {
const enemyPid = this._opponentSlotId(pid);
const enemySlot = enemyPid >= 0 ? this.session.players[enemyPid] : null;
if (!enemySlot) {
console.log('[BattleRuntime] 双生盾对方槽缺失');
return;
console.warn('[BattleRuntime] 双生盾对方槽缺失', pid);
} else {
enemySlot.wallMaxHp += dEnemy;
enemySlot.wallHp += dEnemy;
changed = true;
}
enemySlot.wallMaxHp += both.enemyHp;
enemySlot.wallHp += both.enemyHp;
}
prev.baseHp = Math.max(prev.baseHp, base.bonusHp);
prev.bothSelf = Math.max(prev.bothSelf, both.selfHp);
prev.bothEnemy = Math.max(prev.bothEnemy, both.enemyHp);
return changed;
}
/**
* 双人对战对手槽 id。
* @param playerId 当前玩家
*/
private _opponentSlotId(playerId: number): number {
for (let i = 0; i < this.session.players.length; i++) {
if (this.session.players[i].id !== playerId) return this.session.players[i].id;
}
return -1;
}
/**
* @param playerId 携带方
*/
private _findAppliedBaseHp(playerId: number): { playerId: number; baseHp: number; bothSelf: number; bothEnemy: number } | null {
for (let i = 0; i < this._appliedBaseHp.length; i++) {
if (this._appliedBaseHp[i].playerId === playerId) return this._appliedBaseHp[i];
}
return null;
}
private _initPlayers(localPlayerId: number): void {
@ -1181,6 +1224,7 @@ export class BattleRuntime implements IBattleRuntimeHandle {
getBattleBuffUnitAttackSpeedHost().unbind();
getBattleBuffBaseThunderHost().unbind();
getBattleBuffDigitStatHost().unbind();
this._appliedBaseHp.length = 0;
getNezhaChargeHost().abortAll();
getEarthChargeHost().abortAll();
getArrowRainHost().abortAll();

@ -10,6 +10,7 @@ import type { MapManager } from '../map/legacy/MapManager';
import { getTowerLoadoutService } from '../combat/TowerLoadoutService';
import { refreshWangMuFieldAtkSteal } from '../skill/WangMuConfig';
import { setupTowerCombat } from '../combat/TowerCombatSetup';
import { getBattleBuffDigitStatHost } from '../combat/BattleBuffDigitStatHost';
import { HeroActorHost } from '../heros/HeroActorHost';
import { isHeroActorWeapon1 } from '../heros/HeroActorRes';
import { mapGridToLogicCell } from '../map/BattleMapSpec';
@ -496,6 +497,8 @@ export class DeploySync {
const loadout = loadoutSvc.getLoadout(entityId);
if (loadout && loadout.level !== comp.level && player) {
setupTowerCombat(session, entityId, comp.weapon1Id, comp.level, player);
} else {
getBattleBuffDigitStatHost().refreshEntity(session, entityId, pid);
}
}

@ -117,6 +117,11 @@ export class CampHpBarPresenter {
}
}
/** 气血丸等改 wallMaxHp 后按槽位重建格宽/格数 */
syncFromSlots(): void {
this._rebuildAllFromSlots();
}
/** 回收节点、停 tween、解事件 */
unbind(): void {
const battle = this._battle;

@ -19,6 +19,8 @@ export class sdkConfig {
static readonly enableConsolePfLog = true;
/** SDK 流程跟踪日志;默认关 */
static readonly enableSdkTraceLog = false;
/** AI 决策打印(紫色 `[AI操作]`);默认开 */
static readonly enableAiDecisionLog = false;
/**当前配置文件,不同项目分支设置好对应项目配置文件 */
static readonly config: IConfigData = DefaultConfig;

@ -906,11 +906,12 @@ export class UIBattleGame extends Auto_BattleGame {
void gg.ui.openPanel(UIID.PopSelect);
};
/** 本局获得 buff 后刷新技能栏 */
/** 本局获得 buff 后刷新技能栏与基地血条 */
refreshBattleBuffSkills(): void {
this._dailyShopSkillView?.refresh();
this._passiveSkillView?.refresh();
this._otherSkillView?.refresh();
this._campHpBar.syncFromSlots();
}
/**

@ -24,6 +24,7 @@ import { GEvent } from '../../../mx/module/event/GEvent';
import { Auto_BattleWeaponDesc1 } from './Auto_BattleWeaponDesc1';
import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes';
import { TowerQueryService } from '../../battle/combat/TowerQueryService';
import { getBattleBuffDigitStatHost } from '../../battle/combat/BattleBuffDigitStatHost';
import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver';
import { heroNormalSkillId, textIconName } from '../../battle/config/BattleHeroTable';
import { applyGlyphSpineSkin } from '../../battle/view/GlyphSpineSkin';
@ -421,15 +422,23 @@ export class UIBattleWeaponDesc1 extends Auto_BattleWeaponDesc1 {
return live;
}
/** 优先 ECS 最终攻速/攻击;未部署才回落配表快照 */
/** 优先 ECS 最终攻速/攻击;未部署才回落配表快照(叠 101~104) */
private _resolveLiveStats(runtime: TowerRuntimeSnapshot | null, entity: TileEntitySnapshot): TowerRuntimeSnapshot['stats'] | undefined {
const session = gg.game.CurentBattle?.session ?? null;
const battle = gg.game.CurentBattle;
const session = battle?.session ?? null;
const pid = battle?.localPlayerId ?? gg.game.localPlayer?.playerId ?? 0;
const entityId = runtime?.entityId ?? 0;
if (entityId > 0 && session) {
getBattleBuffDigitStatHost().refreshEntity(session, entityId, pid);
const live = TowerQueryService.instance.readLiveStats(session, entityId);
if (live) return live;
}
return runtime?.stats ?? entity.tower?.stats;
const base = runtime?.stats ?? entity.tower?.stats;
if (!base) return undefined;
const w1 = entity.weapon1Id > 0 ? entity.weapon1Id : (entity.tower?.config?.id ?? 0);
const level = runtime?.level ?? entity.level;
if (!(w1 > 0)) return base;
return getBattleBuffDigitStatHost().previewOntoStats(base, w1, level, pid);
}
private _applyPanelPosition(data: BattleWeaponDesc1PanelData | null): void {

@ -32,7 +32,8 @@ export class UIPopSelect extends Auto_PopSelect {
private readonly _itemNodes: Node[] = [];
onInit(): void {
this._playerId = 0;
const battle = gg.game.CurentBattle as ProjectBattleHandle | null;
this._playerId = battle?.localPlayerId ?? 0;
if (!this.lb_haveCount) {
this.lb_haveCount = find('ui_center/btnRefresh/lb_haveCount', this.node)?.getComponent(Label) ?? null;
}
@ -89,7 +90,7 @@ export class UIPopSelect extends Auto_PopSelect {
private _refreshCountUi(): void {
const remain = BattleBuffSelectService.remainRefreshTotal(this._playerId);
if (this.lb_haveCount) {
this.lb_haveCount.string = `剩余${remain}次`;
this.lb_haveCount.string = `${remain}`;
}
if (this.btnRefresh) {
this.btnRefresh.active = BattleBuffSelectService.canRefresh(this._playerId);

Loading…
Cancel
Save