ARTICLE · 1006294
三国霸业钩子函数参考文档
0. 钩子机制原理
// js/baye.js:1459 —— C++ 侧调用入口2594649: function($0, $1) {var name = UTF8ToString($0);var rv = 0;if (window.baye == undefined || window.baye.hooks == undefined|| window.baye.hooks[name] == undefined) {rv = -1; // 未注册 → 返回 -1,引擎走默认逻辑} else {var cContext = $1;if (cContext != 0) {var jsContext = baye_bridge_value(cContext);rv = baye.callHook(name, jsContext); // 有上下文} else {rv = baye.callHook(name, undefined); // 无上下文}}return rv;}
// js/bridge.js:387baye.callHook = function(name, context) {var rv = window.baye.hooks[name](context);baye.pushCallback(baye.callback);return rv;};
三条铁律
1. 游戏生命周期类
1.1 didOpenNewGame
baye.hooks.didOpenNewGame = function() {// 初始化自定义全局数据customData = {};customData.Interior = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];// 给所有将领写入扩展属性容器for (var i = 0; i < baye.data.g_Persons.length; i++) {baye.data.g_Persons[i].ext = { ExtraAttr: lzsg.makePersonExtra() };}// 开局执行一次月度结算,避免首月数据为空baye.hooks.tacticStage5();};
📌 LZSG 实战:在 didOpenNewGame 里主动调用 tacticStage5() 完成开局数据铺设。
var customData = {};// ① 造数据:只在开局调用function initCustomData() {customData.extPersons = [];var total = baye.data.g_Persons.length;for (var pid = 0; pid < total; pid++) {var ext = {};ext.lucky = 10; // 扩展字段:吉运值customData.extPersons.push(ext);}}// ② 挂引用:把扩展数据绑到引擎对象上(开局和读档都要调用)function linkCustomData() {var arr = baye.data.g_Persons;for (var i = 0; i < arr.length; i++) {arr[i].ext = customData.extPersons[i];}}baye.hooks.didOpenNewGame = function() {initCustomData(); // 开局:造数据linkCustomData(); // 开局:挂引用};
💡 为什么要拆开:didLoadGame 时数据是从存档 JSON 还原的,再 init 会把玩家进度覆盖掉——那时只调 linkCustomData()。
1.2 willSaveGame
baye.hooks.willSaveGame = function() {// 只有挂在 baye.data 上的数据才会被存档// customData 中无法序列化的部分需先摊平到 g_ 字段for (var i = 0; i < baye.data.g_Persons.length; i++) {var P = baye.data.g_Persons[i];P.Ext = P.Ext || {};P.Ext.ExtraAttr = lzsg.flattenExtra(P.ext.ExtraAttr);}};
⚠️ 存档关键:customData 是脚本局部变量,引擎不认识。想持久化要么写回 baye.data.g_Persons[i].Ext.ExtraAttr 这类引擎字段,要么用官方的 baye.setCustomData() 通道(见下方官方示例)。
var customData = {};baye.hooks.willSaveGame = function() {var data = JSON.stringify(customData);baye.setCustomData(data); // 写进引擎的自定义存档区};
// 字段定义表:数组下标 <-> 对象属性名 的映射,存档和读档共用同一份baye.AddToolsDEFINE = [”belong”,”tooltype”,”tooltlevel”,”raritylv”,”durability”, /* ... */];// 对象数组 -> 二维数组(省掉 key 名)baye.arrayEncode = function(def, array) {return array.map(function(item){var rst = [];for (var i = 0; i < def.length; i++) { rst.push(item[def[i]]); }return rst;});};baye.arrayDecode = function(def, array) { // 二维数组 -> 对象数组return array.map(function(item){var rst = {};for (var i = 0; i < def.length; i++) { rst[def[i]] = item[i]; }return rst;});};baye.hooks.willSaveGame = function() {var tmpData = baye.copy(customData); // 浅拷贝,避免污染运行时数据tmpData[”AddTools”] = baye.arrayEncode(baye.AddToolsDEFINE, tmpData[”AddTools”]);baye.setCustomData(JSON.stringify(tmpData));};
⚠️ def 定义表只能往后追加字段,不能插入或改顺序——否则老存档解码后字段会整体错位。要改结构就得做版本迁移。
1.3 didLoadGame
baye.hooks.didLoadGame = function() {// 从引擎字段还原为脚本易用的对象形态for (var i = 0; i < baye.data.g_Persons.length; i++) {var P = baye.data.g_Persons[i];P.ext = { ExtraAttr: lzsg.expandExtra(P.Ext.ExtraAttr) };}// 老存档补字段(迁移)lzsg.migrateSaveVersion();};
baye.hooks.didLoadGame = function() {var rawJson = baye.getCustomData(); // 取出 willSaveGame 存的字符串var dat = JSON.parse(rawJson);customData = dat ? dat : {}; // 兼容首次开局(无存档 → 空对象)linkCustomData(); // 只需挂引用,不能 init(会覆盖进度)};
⚠️ 必坑点:g_Persons 数组长度会串。读档后引擎里的 g_Persons 长度可能还残留上一局的值,官方示例第一行就是修这个:
baye.data.g_Persons.length = _bayeGetPersonCount(); // 先校正长度再遍历_bayeGetPersonCount() 是 WASM 导出的原始函数,对外暴露的名字是 baye.getPersonCount()(js/bridge.js:373),两者等价。漏掉这行会导致遍历到野数据。
var libversion = ”1.2.3.4”;var customData = {};baye.hooks.didOpenNewGame = function() {customData.libversion = libversion; // 记住”这局是用哪个版本开的”customData.engineVersion = baye.data.g_engineVersion;};baye.hooks.didLoadGame = function() {var dat = JSON.parse(baye.getCustomData());customData = dat ? dat : {};if (customData.libversion != libversion) {lzsg.migrateSaveVersion(customData.libversion); // 版本不一致 → 迁移}};
1.4 loadPeriod
baye.hooks.loadPeriod = function() {baye.centerChoose(100, 80, [”简单”, ”困难”], 0, function(idx) {if (idx == baye.None) {return 0; // 取消 → 回主界面}baye.loadPeriod(1); // 加载时期 1 的数据lzsg.difficulty = idx; // 记录难度供后续使用return 1; // 继续选君主});};
1.5 chooseGameEntry
baye.hooks.chooseGameEntry = function() {baye.centerChoose(100, 50, [”新开局”, ”重返沙场”, ”退出”], 0, function(idx) {if (idx == 2) {return 3; // 重映射:第3项映射到引擎的”退出”(3)}return idx;});};
1.6 chooseActor
baye.hooks.chooseActor = function() {var ids = [1, 2, 3]; // 只允许这 3 个君主var names = ids.map(function(x) { return baye.getPersonName(x); });baye.centerChoose(100, 50, names, 0, function(idx) {if (idx == baye.None) { return baye.None; }return ids[idx]; // 返回真实人物 ID});};
1.7 choosingActorUpdate
baye.hooks.choosingActorUpdate = function(ctx) {// 在选中项变化时绘制势力介绍浮窗var pid = ctx.index;lzsg.drawKingPreview(pid);};
2. 策略阶段类(内政回合)
tacticStage1 → tacticStage2 → tacticStage3 → tacticStage4 → tacticStage5 /* 线性动画参考实现from 起始坐标 [x, y]to 终止坐标 [x, y]duration 动画持续时间(毫秒)frames 总帧数draw 单帧绘图函数 draw(x, y)end 动画结束回调(可选)*/baye.linearAnimation = function(from, to, duration, frames, draw, end) {var frame_time = duration / frames;function nextFrame(n) {var x = from[0] + (to[0] - from[0]) * n / frames;var y = from[1] + (to[1] - from[1]) * n / frames;draw(x, y);if (n < frames) {baye.delay(frame_time, 0, function(){ nextFrame(n+1); });} else if (end) {end(); // 结束回调}}nextFrame(0);};// 挂在策略阶段 2 上跑一个小方块从左上滑到中间baye.hooks.tacticStage2 = function() {baye.linearAnimation([0, 0], [80, 60], 100, 20, function(x, y) {baye.clearRect(x, y, x+4, y+4);baye.drawRect(x, y, x+4, y+4);});};
baye.hooks.showMainHelp = function() {baye.saveScreen(); // ① 先把当前画面存起来baye.linearAnimation([0, 0], [80, 60], 60, 20,function(x, y) {baye.restoreScreen(); // ② 每帧先还原背景baye.clearRect(x, y, x+4, y+4);baye.drawRect(x, y, x+4, y+4);},function() {baye.restoreScreen(); // ③ 结束时再还原一次,清掉残影});};
📌 相关 API:baye.delay(毫秒, ?, 回调) — 定时执行;baye.saveScreen() / baye.restoreScreen() — 存取整个屏幕缓冲;baye.clearRect(x0,y0,x1,y1) / baye.drawRect(x0,y0,x1,y1) — 清矩形 / 画矩形边框。
⚠️ 动画期间引擎仍在跑自己的循环,长动画建议配合状态锁,避免玩家操作打断导致画面错乱。
baye.hooks.tacticStage4 = function() {var allPerson = baye.data.g_Persons;var playerKingId = baye.data.g_PlayerKing + 1;var maxLevel = baye.data.g_engineConfig.maxLevel;for (var i = 0; i < allPerson.length; i++) {var p = allPerson[i];if (p.Belong > 0 && p.Belong != playerKingId&& p.Belong != 255 && p.Level < maxLevel) {if (Math.random() < 0.7) { // 70% 概率升级p.Level = p.Level + 1;}}}};
baye.hooks.tacticStage1 = function() {for (var c = 0; c < baye.data.g_Cities.length; c++) {var Ext = baye.data.g_Cities[c].Ext.ExtraAttr;Ext[7].monthlyBusiness = 5; // 每月商业可开发次数Ext[7].monthlyFarm = 5; // 每月农业可开发次数}// 重置将领的月度培养次数for (var p = 0; p < baye.data.g_Persons.length; p++) {baye.data.g_Persons[p].ext.ExtraAttr.cultivatedTimes = 0;}};
baye.hooks.tacticStageUser = function() {var PKingID = baye.data.g_PlayerKing;// 自定义内政菜单:将领/装备/库房/俘虏/任务/商铺/城池baye.chooseCity(function(city) {if (city == 0xff) { return 0; }var CData = baye.data.g_Cities[city];if (CData.Belong == PKingID + 1) {lzsg.PlayerMenu1(city, 0); // 己方城池菜单} else {lzsg.RobotMenu1(city, 0); // 敌方城池菜单}});};
baye.hooks.tacticStageUser = function() {function tactic() {// 第一级:选城池baye.chooseCity(function(city) {if (city == 0xff) { // 按了 Escreturn 0;}// 第二级:选命令baye.centerChoose(50, 50, [”出征”, ”搜寻”, ”处斩”], 0, function(i) {if (i == 0) { // 出征baye.makeBattle(city, function(){ tactic(); });} else if (i == 1) { // 搜寻(命令号 3)baye.makeCommand(city, 3, function(){ tactic(); });} else if (i == 2) { // 处斩(命令号 7)baye.makeCommand(city, 7, function(){ tactic(); });} else {tactic(); // 继续策略}});});}tactic();return 0;};
💡 官方示例的两个关键手法:
递归回调function tactic(){...} — 命令执行完再调回 tactic(),形成「选城→选命令→执行→再选城」的循环,玩家按 Esc 才退出。这是官方做自定义内政循环的标准写法。 return 0 在最后——注意 tactic() 是异步的(回调驱动),这个 return 0 是立即返回的,不是等菜单走完。
📌 分组边界:ORDER_INTERIOR=1、ORDER_DIPLOMATISM=15、ORDER_ARMAMENT=23。注意 20~22 是空档(原 C 里 TRIBUTE 朝贡 被注释掉了),别踩。
3. 命令与内政类
3.1 willAddOrder
baye.hooks.willAddOrder = function(ctx) {// 拦截”运输”类命令,自定义粮草上限if (ctx.OrderId == 12) { // 假设 12 = 运输var city = baye.data.g_Cities[ctx.City];if (ctx.Food > city.Food * 0.5) {ctx.Food = Math.floor(city.Food * 0.5); // 最多运走一半}return -1; // 改完交给引擎执行}return -1;};
3.2 willExecuteOrder
baye.hooks.willExecuteOrder = function(ctx) {// 记录命令日志,不改变行为lzsg.logOrder(ctx.OrderId, ctx.Person, ctx.City, ctx.Object);return -1;};
3.3 cityMakeCommand
baye.hooks.cityMakeCommand = function(ctx) {var city = baye.data.g_Cities[ctx.cityIndex];// 非己方城池禁止操作if (city.Belong != baye.data.g_PlayerKing + 1) {return 1; // 提示错误,跳过}return -1; // 正常城池交给引擎};
4. 战斗流程 — 阶段类
enterBattle → battleStage1 → ... → battleStage5 → exitBattle 4.1 enterBattle
baye.hooks.enterBattle = function() {// 为每个战场位置生成战斗数据for (var pos = 0; pos < baye.data.g_FgtParam.GenArray.length; pos++) {lzsg.BattleAttrWrite(pos);}};
baye.hooks.enterBattle = function() {// [位置索引, x, y] —— 索引 0~9 是我方,10~19 是敌方var layout = [[0, 18, 3], [1, 16, 3], [2, 20, 3], [3, 19, 3], [4, 16, 1],[10, 17, 3], [11, 15, 16], [12, 14, 15], [13, 16, 15], [14, 15, 14],];for (var i = 0; i < layout.length; i++) {var p = baye.data.g_GenPos[layout[i][0]];p.x = layout[i][1];p.y = layout[i][2];}baye.data.g_LookMovie = 0; // 关掉”观战”模式};
baye.hooks.enterBattle = function() {baye.data.g_GenPos[10].x = baye.data.g_CityX;baye.data.g_GenPos[10].y = baye.data.g_CityY;};
📌 相关全局量:g_GenPos[i] 是可写的战场坐标表(.x / .y / .hp / .mp / .active / .state),g_LookMovie 置 0 表示玩家可控(非观战),g_FoucsX / g_FoucsY 是当前镜头中心(注意引擎拼写是 Foucs 不是 Focus)。
baye.hooks.fightStage1 = function() {baye.data.g_FgtWeather = 0; // 强制晴天};
baye.hooks.battleStage1 = function() { // 注意是 battle 不是 fightbaye.data.g_FgtWeather = 0; // 0=晴 1=雨 2=雪 …(具体枚举待实测)};
📌 这是本手册确认的第二个无效钩子(第一个是 didChangeMenuSelection)。两个都是第三方脚本作者的笔误,属于「写了不报错、但永远不会执行」的静默失效——凡是抄来的钩子名,建议先过一遍第 13 节的速查表确认存在性。
4.2 battleStage1 ~ battleStage5
baye.hooks.battleStage5 = function() {// 回合结束:结算中毒/燃烧等持续状态for (var i = 0; i < baye.data.g_GenPos.length; i++) {lzsg.tickBuffState(i);}};
4.3 exitBattle
baye.hooks.exitBattle = function() {// 清理解放军战场临时数据lzsg.clearBattleTempData();// 处理攻方进攻队列的下一条指令lzsg.advanceAttackQueue();};
5. 战斗流程 — 数值计算类(最常用)
5.1 battleBuildAttackAttriutes ⚠️ 注意拼写
引擎原文拼写为 Attriutes(少了 b),不要写成 Attributes,否则不生效。
baye.hooks.battleBuildAttackAttriutes = function(ctx) {var P = baye.data.g_Persons[baye.data.g_FgtParam.GenArray[ctx.generalIndex] - 1];var Attr = P.ext.ExtraAttr;var g = baye.data.g_GenAtt[ctx.index];// 用自定义 ExtraAttr 覆盖引擎默认属性g.Arms = Attr.troops;g.Force = Attr.strength;g.IQ = Attr.intelligence;g.PhysAtk = Attr.physAtkBase;g.PhysDef = Attr.physDefBase;g.MagAtk = Attr.magAtkBase;g.MagDef = Attr.magDefBase;g.MoveSpeed = Attr.moveSpeed;return 0; // 完全接管,不让引擎再算};
💡 可主动调用:这个钩子还能被脚本自己调用来做"实时试算":
baye.hooks.battleBuildAttackAttriutes({generalIndex: atk, index: 0});baye.hooks.battleBuildAttackAttriutes({generalIndex: def, index: 1});
5.2 countAttackHurt
baye.hooks.countAttackHurt = function(ctx) {var atk = baye.data.g_GenAtt[0];var def = baye.data.g_GenAtt[1];// 自定义伤害公式:物攻 - 物防 + 浮动var base = atk.PhysAtk - def.PhysDef;var rand = 0.85 + Math.random() * 0.3;var crit = (Math.random() < atk.crit) ? 1.5 : 1.0;ctx.hurt = Math.max(1, Math.floor(base * rand * crit));return 0; // 接管伤害计算};
5.3 countSkillHurt
baye.hooks.countSkillHurt = function(ctx) {var atk = baye.data.g_GenAtt[0];var def = baye.data.g_GenAtt[1];// 火系技能额外烧粮草if (lzsg.skillElement(ctx.skillId) == '火') {ctx.hurt = Math.floor(atk.MagAtk * 1.2 - def.MagDef);ctx.prov = Math.floor(atk.MagAtk * 0.5); // 烧粮草ctx.state = 2; // 燃烧状态return 0;}return -1; // 其他技能走引擎};
// 已知两点求直线方程 ax + by + c = 0function resolveEquationWithPoints(x0, y0, x1, y1) {return {a: y1 - y0, b: x0 - x1, c: x1*y0 - x0*y1};}// 点到直线的距离function distanceFromPointToLine(x, y, line) {return Math.abs(line.a*x + line.b*y + line.c) / Math.sqrt(line.a*line.a + line.b*line.b);}baye.hooks.countSkillHurt = function(context) {var iAim = context.origin; // ① 主目标(技能指定的那个)var iSrc = baye.data.g_GenAtt[0].generalIndex; // ② 施法者var iDst = baye.data.g_GenAtt[1].generalIndex; // ③ 本次正在结算的承受者var aim = baye.data.g_GenPos[iAim];var src = baye.data.g_GenPos[iSrc];var dst = baye.data.g_GenPos[iDst];var line = resolveEquationWithPoints(src.x, src.y, aim.x, aim.y);var d2line = distanceFromPointToLine(dst.x, dst.y, line); // 承受者到连线的垂距var d2aim = Math.sqrt(Math.pow(dst.x-aim.x,2) + Math.pow(dst.y-aim.y,2));context.hurt = 0; // 默认无伤害if (d2line < 0.5 && d2aim <= 2) { // 在连线上 且 离主目标 2 格内context.hurt = 100 - 30*d2aim; // 距离越远伤害越低}};
⚠️ 三个"目标"别搞混(这是本钩子最大的坑):
来源 含义 ctx.origin 主目标——玩家用技能时指定的那一格,全程固定 g_GenAtt[0].generalIndex 施法者 g_GenAtt[1].generalIndex 当前正在结算的承受者——本钩子会被逐个单位回调多次 也就是说:一次 AOE 技能,引擎会对每个可能受伤的单位各调一次countSkillHurt,每次 ctx.origin 相同(主目标),但 g_GenAtt[1] 是不同的人。想做「只打主目标」就判断 iDst == iAim。
📌 ctx.skillId 是 1 起算的,取技能名要 -1:baye.getSkillName(ctx.skillId - 1)
📌 想把技能改成「不分敌我」的超级技能,官方示例直接改引擎数据:baye.data.g_Skills[4].aim = 6; // 火攻(下标4)改为伤害不分敌我
5.4 calcAttackRange
baye.hooks.calcAttackRange = function(ctx) {// 远程兵种普攻范围 3x3if (ctx.type == 0 && lzsg.isRanged(ctx.personIndex)) {var size = 3, half = 1;ctx.range = [];for (var y = 0; y < size; y++) {for (var x = 0; x < size; x++) {var dx = Math.abs(x - half), dy = Math.abs(y - half);ctx.push((dx + dy) <= half ? 1 : 0); // 菱形范围}}ctx.rangeSize = size;}// 不设置则引擎用默认范围};
5.5 showSkill
⚠️ 旧版字段名是 ctx.success,新版改为 ctx.result。
baye.hooks.showSkill = function(ctx) {// 自定义成功率:基础 80%,受地形与吉运影响var atk = baye.data.g_GenAtt[0];var rate = 0.8 - ctx.ter * 0.05 + atk.Luck * 0.02;ctx.result = (Math.random() < rate) ? 1 : 0;};
baye.hooks.showSkill = function(context) {context.result = 1; // 1 = 成功(100% 命中),0 = 失败};
💡 这个钩子只管"是否放出来",不管伤害多少——伤害在 countSkillHurt 里算。调试期两个钩子配合用:showSkill 保证必中 + countSkillHurt 打印坐标,就能稳定复现 AOE 逻辑。
5.6 canUseSkill
baye.hooks.canUseSkill = function(ctx) {// 禁止对同阵营使用攻击类技能if (ctx.same && lzsg.isAttackSkill(ctx.skillIndex)) {return false;}return true;};
5.7 getSkillIds
baye.hooks.getSkillIds = function(ctx) {var pid = baye.data.g_FgtParam.GenArray[ctx.generalIndex] - 1;var P = baye.data.g_Persons[pid];var ids = [];// 按武将等级解锁技能if (P.Level >= 5) { ids.push(2); }if (P.Level >= 10) { ids.push(3); }if (P.Level >= 20) { ids.push(4); }ctx.skillIds = ids;};
5.8 getMaxArms
baye.hooks.getMaxArms = function(ctx) {var P = baye.data.g_Persons[ctx.personIndex];// 最大兵力 = 等级 × 100 + 统率 × 50ctx.maxArms = P.Level * 100 + P.Force * 50;};
6. 战斗流程 — AI 与状态类
6.1 aiFightCommand
baye.hooks.aiFightCommand = function(ctx) {var me = ctx.sIdx;// 策略:找血量最低的敌人,能秒就秒,否则休息回血var target = lzsg.findWeakestEnemy(me);if (target == baye.None) {ctx.type = 3; // 无目标 → 休息return;}if (lzsg.canKill(me, target)) {ctx.type = 0; // 普攻ctx.aIdx = target;} else {ctx.type = 1; // 技能ctx.param = lzsg.bestSkill(me);ctx.aIdx = target;}};
6.2 battleDrivePersonState
baye.hooks.battleDrivePersonState = function(ctx) {var pos = baye.data.g_GenPos[ctx.generalIndex];var state = lzsg.getCustomState(ctx.generalIndex);if (state.poison > 0) {pos.Arms -= 50; // 中毒每回合掉兵state.poison -= 1;return 0; // 有变化}return 1; // 无变化};
6.3 willShowPKAnimation / didShowPKAnimation ⚠️ 非官方
来源:官方文档未记载,baye.wasm 实测存在,LZSG 用于天赋系统。
baye.hooks.willShowPKAnimation = function() {var atk = baye.data.g_FgtParam.AtkGen;var def = baye.data.g_FgtParam.DefGen;// 攻击方天赋:万人莫敌 → 临时加攻if (lzsg.hasTalent(atk, '万人莫敌')) {baye.data.g_GenAtt[0].PhysAtk *= 1.5;lzsg.tempBuffApplied = true; // 标记,供 did 钩子还原}};baye.hooks.didShowPKAnimation = function() {// 还原 will 钩子做的临时修改if (lzsg.tempBuffApplied) {baye.data.g_GenAtt[0].PhysAtk /= 1.5;lzsg.tempBuffApplied = false;}// 防守方天赋:铁壁 → 反弹伤害lzsg.triggerDefenderTalent();};
7. 菜单与 UI 类 ⚠️(多数非官方文档,但 WASM 实测存在)
7.1 willOpenMenu / willCloseMenu / willChangeMenuSelection
// 进入某个菜单时动态挂载,实现”光标跟随显示说明”function showMyMenu() {var options = ['将领管理', '装备管理', '库房管理', '俘虏管理'];// ① 挂载光标钩子baye.hooks.willChangeMenuSelection = function(option) {lzsg.drawMenuDescription(options[option.index]); // 右侧显示说明};// ② 打开菜单lzsg.SelMenu1(options, 0, function(one) {if (one == baye.None) {return; // 取消}lzsg.handleMenuChoice(one);});}// ③ 关闭时清理,避免钩子泄漏到别的菜单baye.hooks.willCloseMenu = function() {baye.hooks.willOpenMenu = undefined;baye.hooks.willChangeMenuSelection = undefined;};
⚠️ 必坑点:willChangeMenuSelection 是全局单例,用完必须在 willCloseMenu 里置 undefined,否则会污染后续所有菜单。
baye.hooks.willChangeMenuSelection = function(c) {console.log(sprintf(”(%d,%d)-(%d,%d) selection changing to:%d”,baye.data.c_Sx, baye.data.c_Sy,baye.data.c_Ex, baye.data.c_Ey, // 当前菜单的绘制边界c.index));baye.drawText(0, 0, ”选中:” + c.index);};baye.hooks.willCloseMenu = function(c) {console.log(”菜单退出, 删除钩子”);baye.hooks.willChangeMenuSelection = undefined; // 用完必须清baye.hooks.willCloseMenu = undefined;};
📌 baye.data.c_Sx/c_Sy/c_Ex/c_Ey 是当前菜单的绘制边界(左上角/右下角坐标),由引擎在开菜单时填好。想让说明文字画在菜单旁边,用它们算位置最稳。
// 临时设置绘制边界,返回旧值以便还原function setDrawBounds(bounds) {var old = [baye.data.c_Sx, baye.data.c_Sy, baye.data.c_Ex, baye.data.c_Ey];baye.data.c_Sx = bounds[0];baye.data.c_Sy = bounds[1];baye.data.c_Ex = bounds[2];baye.data.c_Ey = bounds[3];return old;}// 在指定矩形内画文字(不传 bounds 则全屏)function drawText(x, y, text, bounds) {bounds = bounds ? bounds : [0, 0, baye.data.g_screenWidth, baye.data.g_screenHeight];var old = setDrawBounds(bounds);baye.drawText(x, y, text);setDrawBounds(old); // 关键:画完还原,否则影响后续所有绘制}baye.hooks.willCloseMenu = function() {baye.hooks.willOpenMenu = undefined;baye.hooks.willChangeMenuSelection = undefined;};baye.hooks.showMainHelp = function(context) {baye.clearRect(3, 3, 138, 141); // 清矩形 (x0,y0,x1,y1)baye.drawRect(3, 3, 138, 141); // 画边框drawText(8, 8, ”人物简介”);var items = ['张三', '李四'];// 第一级菜单:光标移动时在下方显示当前人名baye.hooks.willChangeMenuSelection = function(c) {drawText(8, 8+13, items[c.index]);};baye.choose(144, 6, 60, 60, items, 0, function(choice) {if (choice == baye.None) {console.log(”操作取消”);} else {baye.clearRect(141, 73, 210, 141);baye.drawRect(141, 73, 210, 141);drawText(143, 75, ”指令简介”);nextLevel(choice); // 进第二级菜单}});function nextLevel(choice) {var items = ['招降俘虏', '交换俘虏', '出售俘虏', '流放俘虏', '取消操作'];// 换一个新的光标钩子(覆盖上一级的)baye.hooks.willChangeMenuSelection = function(c) {drawText(143, 75+13, items[c.index]);};baye.choose(144, 21, 60, 48, items, 0, function(c) {if (c == baye.None) { console.log(”操作取消”); }else { console.log(c); }});}}
💡 baye.choose(x, y, w, h, items, default, cb) — 在任意位置开菜单。与 baye.centerChoose(x, y, items, default, cb)(居中菜单)的区别:choose 多两个尺寸参数,可以精确摆位置。
⚠️ 多级菜单时,每进一级都要重新给 willChangeMenuSelection 赋值——因为钩子是单例,第二级的闭包 items 跟第一级不是同一个数组,不覆盖就会显示错内容或 undefined。
baye.hooks.didChangeMenuSelection = undefined; // script (7)/(8)/(10).js baye.hooks.willCloseMenu = function(ctx) {baye.hooks.willChangeMenuSelection = null; // 注意是 will 开头,且用 null};
7.2 onMenuIdle
baye.hooks.onMenuIdle = function(ctx) {baye.drawText(0, 0, ”selected:” + ctx.index);// 光标反显闪烁var x = 8 * ctx.index, y = 20, w = 8, h = 8;baye.revertRect(x, y, x + w - 1, y + h - 1);};
7.3 showMainHelp
baye.hooks.showMainHelp = function() {// 接管帮助,改成一个字体预览菜单baye.hooks.willChangeMenuSelection = function(ctx) {var oldFont = currentFont;setFont(ctx.index);baye.clearRect(0, 0, 60, 30);baye.drawText(0, 0, ”字体预览”);setFont(oldFont);};baye.centerChoose(28, 50, [”默认”, ”仿宋”, ”黑体”, ”楷体”, ”宋体”],currentFont, function(idx) {if (idx != baye.None) { setFont(idx); }});};
var libversion = ”1.2.3.4”;var customData = {};// 在主策略界面按[帮助]键或点击右下角触发baye.hooks.showMainHelp = function() {baye.clearScreen();var text = ”版本信息\n”;text += ”引擎 :” + baye.data.g_engineVersion + ”\n”;text += ”数据 :” + libversion + ”\n”;text += ”开局引擎:” + customData.engineVersion + ”\n”; // 这局开局时的引擎版本text += ”开局数据:” + customData.libversion + ”\n”;baye.drawText(0, 0, text);};baye.hooks.didOpenNewGame = function() {customData.libversion = libversion; // 开局时记住版本customData.engineVersion = baye.data.g_engineVersion;};
💡 同时记录「开局版本」和「当前版本」,是为了识别老存档——对不上就走迁移逻辑(见 1.3)。
baye.hooks.showMainHelp = function() {// GenArray: 下标 0~9 我方将领槽位,10~19 敌方;值为 人物ID+1,0 表示空位baye.data.g_FgtParam.GenArray = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 我方:1 号人物2, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 敌方:2 号人物];baye.data.g_FgtParam.MapId = 110 + 1; // 地图起始 ID 110baye.data.g_FgtParam.Way = 1; // 进攻方向baye.data.g_FgtParam.CityIndex = 1; // 城池 IDbaye.data.g_FgtParam.Mode = 0; // 1=进攻 / 0=防守baye.data.g_FgtParam.MProvender = 500; // 我方粮草baye.data.g_FgtParam.EProvender = 500; // 敌方粮草baye.enterBattle(function() {console.log(”战斗结束”); // 战斗结束回调});};
📌 baye.enterBattle(cb) 是脚本主动进战场的 API(与同名钩子 baye.hooks.enterBattle 是两回事)。先填好 g_FgtParam 再调它即可。
g_FgtParam 字段 含义 GenArray[20] 参战将领槽位,值 = 人物 ID + 1,0 为空 MapId 战场地图 ID Way 进攻方向 CityIndex 城池 ID Mode 1 = 进攻,0 = 防守 MProvender / EProvender 我方 / 敌方粮草
7.4 mainSystemMenu
baye.hooks.mainSystemMenu = function() {baye.centerChoose(60, 30, [”策略结束”, ”存储进度”, ”退出游戏”], 0, function(idx) {return idx; // 映射回引擎菜单项});};
7.5 fightOpenMainMenu
baye.hooks.fightOpenMainMenu = function() {baye.centerChoose(60, 30, [”回合结束”, ”全军撤退”], 0, function(idx) {return idx;});};
7.6 fightChooseAction
baye.hooks.fightChooseAction = function(ctx) {console.log(”person = ” + ctx.index);baye.centerChoose(30, 60, [”攻击”, ”计谋”, ”查看”, ”待机”], 0, function(idx) {return idx;});};
7.7 fightChooseSkill
baye.hooks.fightChooseSkill = function(ctx) {var skills = [2, 3, 4];var names = skills.map(function(x) { return baye.getSkillName(x); });baye.centerChoose(30, 60, names, 0, function(idx) {if (idx == baye.None) { return baye.None; }return skills[idx]; // 返回真实技能 ID});};
7.8 meetFight
baye.hooks.meetFight = function(ctx) {console.log(”meet fight at ” + ctx.city);var items = [”出战”, ”谈判”];baye.centerChoose(40, 30, items, 0, function(index) {switch (items[index]) {case ”出战”: return 1; // 进入选将界面case ”谈判”: return 0; // 敌军撤退default: return 1; // 不能取消,默认出战}});};
📌 官方示例用的是解构写法 function({city}) {...},ES6 语法;如需兼容老浏览器请改用 function(ctx){ var city = ctx.city; }。
7.9 fightStatusBarTouched
baye.hooks.fightStatusBarTouched = function(ctx) {console.log(sprintf(”status bar is touched at (%d,%d)”, ctx.x, ctx.y));// 把状态栏三个区域映射成按键if (ctx.x < 30) {baye.sendKey(baye.VK_EXIT); // 左区 → 退出键} else if (ctx.x < 60) {baye.sendKey(baye.VK_SEARCH); // 中区 → 查看键} else {baye.sendKey(baye.VK_HELP); // 右区 → 帮助键}return 0;};
7.10 fightWillShowHelp ⚠️ 非官方
baye.hooks.fightWillShowHelp = function() {// 自定义战场帮助内容lzsg.drawBattleHelp();};
7.11 willShowCityInfo ⚠️ 未在脚本中使用
⚠️ 该钩子为邻域扫描发现,行为未经验证,使用前请先打日志确认调用时机。
8. 绘制与字体类
8.1 didShowMainMap
baye.hooks.didShowMainMap = function() {// 在大地图右侧绘制自定义侧边栏lzsg.drawSideBar();};
8.2 showMiniMap
baye.hooks.showMiniMap = function() {lzsg.drawAttackRouteMap();};
8.3 didShowFightSituation
baye.hooks.didShowFightSituation = function() {lzsg.drawBattleSituationOverlay();};
8.4 didRefreshFightStateBar
baye.hooks.didRefreshFightStateBar = function() {// 重绘状态栏,显示自定义战斗属性lzsg.drawCustomStateBar();};// 也可主动触发刷新baye.hooks.didRefreshFightStateBar();
8.5 didRefreshFightSituation / didShowFightStateBar ⚠️ 未在脚本中使用
⚠️ 两者仅在 baye.wasm 字符串中发现的"近亲"钩子(与 didShowFightSituation/didRefreshFightStateBar 成对),未在官方文档或 LZSG 中出现,需实测确认。
8.6 drawMapUnit
baye.hooks.drawMapUnit = function(ctx) {var pic = baye.data.g_FightMap[ctx.tile];var color = baye.data.g_paintColor;baye.data.g_paintColor = 170; // 地图色稍浅baye.drawImage(ctx.x, ctx.y, baye.data.g_TileId, 0, pic, !ctx.flag);baye.data.g_paintColor = color; // 用完必须还原};
⚠️ baye.data.g_paintColor 是全局状态,改完必须还原。
8.7 drawOneGeneral
baye.hooks.drawOneGeneral = function(ctx) {var color = baye.data.g_paintColor;baye.data.g_paintColor = 200;baye.drawImage(ctx.x, ctx.y, 4, 0, ctx.pic, 1);baye.data.g_paintColor = color;};
8.8 fontImageForChar
function configFont(fontSize, fontName) {var fontId = { 48: 5, 24: 1, 12: 0 }[fontSize];baye.setFont(fontId);baye.clearFontCache();baye.data.g_engineConfig.useCustomFont = 1; // 开启中文字体baye.data.g_engineConfig.useCustomFontEn = 1; // 开启英文字体// 挂载取字模钩子baye.hooks.fontImageForChar = function(ctx) {ctx.zmCode = getFontImage(ctx.code);};}
baye.hooks.fontImageForChar = function(c) {// index 为 assets/003_MAIN_SPE.assets/1 下的图片序号var index = {0x82e0: 0, // 傕0x90aa: 1, // 惇0xb574: 2, // 祎0x8faa: 3, // 彧0xe041: 4, // 郃0xab64: 5, // 玠0xae8f: 6, // 畯0x85b1: 7, // 叡}[c.code];if (index != undefined) {c.index = index; // 只改认识的字,其余交给引擎}};
🔍 c.code 是 GBK 内码,不是 Unicode!(已实测证实)
把示例里 8 个汉字逐一取 GBK 编码比对,8/8 全部吻合:
汉字 Unicode GBK 示例中的 code 傕 U+5095 0x82E0 0x82e0 ✅ 惇 U+60C7 0x90AA 0x90aa ✅ 祎 U+794E 0xB574 0xb574 ✅ 彧 U+5F67 0x8FAA 0x8faa ✅ 郃 U+90C3 0xE041 0xe041 ✅ 玠 U+73A0 0xAB64 0xab64 ✅ 畯 U+756F 0xAE8F 0xae8f ✅ 叡 U+53E1 0x85B1 0x85b1 ✅ 用 ch.charCodeAt(0) 拿到的是 Unicode,直接当 key 会全部落空。想动态生成映射表,得先把字符转成 GBK 再取值。
📌 模式 B 下必须 if (index != undefined) 才赋值——不认识的字要让引擎继续用内置字库,否则整个字库只剩你补的这几个字。
9. 道具类
9.1 giveTool / takeOffTool
📌 官方文档对这两个钩子的参数描述有笔误(写成"地形/技能ID"),实际应为 personIndex / toolIndex。
// 赏赐永远成功baye.hooks.giveTool = function(ctx) {ctx.result = 1;};// 没收:绑定的装备不可没收baye.hooks.takeOffTool = function(ctx) {var tool = baye.data.g_Tools[ctx.toolIndex];if (tool.Add.ExtraAttr[0].companionId != 0) {ctx.result = 0; // 已绑定 → 失败} else {ctx.result = 1;}};
9.2 willGiveTool / willTakeOffTool
baye.hooks.willGiveTool = function(ctx) {var tool = baye.data.g_Tools[ctx.toolIndex];var name = baye.getToolName(ctx.toolIndex);// 神兵只赏赐给忠诚度 > 90 的将领if (lzsg.isLegendary(name)) {var P = baye.data.g_Persons[ctx.personIndex];if (P.Devotion < 90) {baye.showMessage(”忠心不足,无法赐予神兵”);return 0; // 跳过引擎,赏赐失败}}return -1; // 交给引擎};
💡 与 giveTool/takeOffTool 的区别:will* 是前置拦截(能阻止动作发生),不带 will 的是结果判定(动作已发生,只改成败)。
10. 列表 UI 自定义类 ⚠️ 全部非官方
来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 下 10 个实战脚本全部在用。这是做「自定义人物表 / 道具表」的核心入口。
baye.data.g_uiCfg.personPropertiesCount = 13; // 人物表 列数量baye.data.g_uiCfg.personPropertiesDisplayWitdh = [ // 每列宽度(字符数)4, 4, 4, 4, 4, 4, 4, 4, 4, 10, 10, 8, 4,];
🐛 引擎拼写错误:personPropertiesDisplay**Witdh** —— 少了一个 d(正确英文是 Width)。这是引擎侧的真实字段名,照抄即可,写对了反而不生效。三个约束:列数 = 表头数组长度 = 宽度数组长度,三者必须一致。
10.1 getPersonPropertyTitle
10.2 getPersonPropertyValue
10.3 getToolPropertyTitle
10.4 getToolPropertyValue
// 表头数组决定列数;两个钩子的 propertyIndex 都指向这个数组var baye_person_heads = ['姓名', '等级', '武力', '智力', '兵力', '特 技'];baye.hooks.getPersonPropertyTitle = function(c) {c.value = baye_person_heads[c.propertyIndex];return 0;};baye.hooks.getPersonPropertyValue = function(c) {var person = baye.data.g_Persons[c.personIndex];switch (baye_person_heads[c.propertyIndex]) {case '姓名': c.value = baye.getPersonName(c.personIndex); break;case '等级': c.value = person.Level; break;case '武力': c.value = person.Force; break;case '智力': c.value = person.IQ; break;case '兵力': c.value = person.Arms; break;case '特 技': c.value = privateSkillName(c.personIndex); break;}return 0; // 忘了 return 0 引擎会忽略本次填充};
// ① 先声明列数(1 列)baye.data.g_uiCfg.personPropertiesCount = 1;baye.data.g_uiCfg.personPropertiesDisplayWitdh = [4];// ② 表头数组 —— 两个钩子共用,propertyIndex 就是它的下标var baye_person_heads = [”吉运”];baye.hooks.getPersonPropertyTitle = function(c) { // 填写人物表头c.value = baye_person_heads[c.propertyIndex];return 0;};baye.hooks.getPersonPropertyValue = function(c) { // 填写人物单元格var person = baye.data.g_Persons[c.personIndex];switch (baye_person_heads[c.propertyIndex]) {case ”吉运”:c.value = person.ext.lucky; // 取自 didOpenNewGame 初始化的数据break;}return 0;};
baye.data.g_uiCfg.personPropertiesCount = 13;baye.data.g_uiCfg.personPropertiesDisplayWitdh = [4, 4, 4, 4, 4, 4, 4, 4, 4, 10, 10, 8, 4,];var baye_person_heads = [”等级”, ”统率”, ”武力”, ”智力”, ”兵种”, ”兵力”, ”忠诚”,”体力”, ”经验”, ”主道具”, ”副道具”, ”归属”, ”城池”,];baye.hooks.getPersonPropertyTitle = function(c) {c.value = baye_person_heads[c.propertyIndex];return 0;};baye.hooks.getPersonPropertyValue = function(c) {var person = baye.data.g_Persons[c.personIndex];// 道具 ID 是 1 起算,0 表示没有function toolName(id) {return id == 0 ? '' : baye.getToolName(id - 1);}// 归属:0=在野 0xff=俘虏 自己=君主 其余=某君主function belongName(id) {if (id == 0) return '无';if (id == 0xff) return '俘虏';if (id - 1 == c.personIndex) return '君主';return baye.getPersonName(id - 1);}// 查人物所在城池:遍历所有城,看谁的人里有他function getPersonCityName(pindex) {var cities = baye.data.g_Cities;for (var i = 0; i < cities.length; i++) {for (var j = 0; j < cities[i].Persons.length; j++) {if (cities[i].Persons[j] == pindex) {return baye.getCityName(i);}}}return '未知';}switch (baye_person_heads[c.propertyIndex]) {case ”等级”: c.value = person.Level; break;case ”统率”: c.value = person.Force; break;case ”武力”: c.value = person.Force; break;case ”智力”: c.value = person.IQ; break;case ”兵力”: c.value = person.Arms; break;case ”忠诚”: c.value = person.Devotion; break;case ”体力”: c.value = person.HP; break;case ”经验”: c.value = person.Exp; break;case ”主道具”: c.value = toolName(person.Tool1); break;case ”副道具”: c.value = toolName(person.Tool2); break;case ”归属”: c.value = belongName(person.Belong); break;case ”城池”: c.value = getPersonCityName(c.personIndex); break;}return 0;};
💡 两个官方示例的共同点:用 switch (baye_person_heads[c.propertyIndex]) 按列名分发,而不是 switch (c.propertyIndex) 按下标分发。这样做的好处是调整列顺序时不用改钩子代码——推荐照抄这个写法。
var baye_tool_heads = ['类型', '武力', '智力', '兵种'];baye.hooks.getToolPropertyTitle = function(c) {c.title = baye_tool_heads[c.propertyIndex]; // ⚠️ 这里是 title 不是 valuereturn 0;};baye.hooks.getToolPropertyValue = function(c) {var tool = baye.data.g_Tools[c.toolIndex];switch (baye_tool_heads[c.propertyIndex]) {case '类型': c.value = ['装备', '使用'][tool.useflag]; break;case '武力': c.value = tool.at; break;case '智力': c.value = tool.iq; break;case '兵种': c.value = ['--','禁军','神机','骠骑'][tool.arm]; break;}return 0;};
11. 战场地图与信息面板类 ⚠️ 全部非官方
来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 中 4~5 个脚本在用。这一组是自定义战场与自定义信息面板的入口,威力很大。
11.1 loadFightMap
⚠️ 返回值是本钩子的核心契约(官方示例明确演示)。不 return -1 的话,未覆盖到的城池会拿到一份空地图或者上一次的残留数据。官方写法是 switch (城池名),default 分支统一 return -1 兜底。
baye.hooks.loadFightMap = function() {var city = baye.data.g_FgtParam.CityIndex;var name = baye.getCityName(city);console.log('城池序号:' + city);console.log('城池名称:' + name);console.log('进攻方向:' + baye.data.g_FgtParam.Way);switch (name) {case '汉中': {baye.data.g_MapWid = 17;baye.data.g_MapHgt = 18;baye.data.g_FightMapData = [1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 3, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1,1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1,1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 5, 1, 2, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 1,20,16,16,16,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,17,20,16,24,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1,17,17,17,17,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1,17,23,16,21,1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 5, 1, 1,23,16,16,16,];break;}default:return -1; // ← 其余城池交给引擎默认地图}return 0; // ← 我提供了地图};
📌 改完地图记得摆人:官方示例末尾注释提醒——换了地图后将领起始位置常常不对,要在 enterBattle 里手动摆(见 4.1 官方示例)。
🗺️ 地形编号(据官方示例数据推断,待实测确认):1 = 平地(占比最高,作背景);2 = 成片障碍(3×3 连块,疑为山地);3 / 4 / 5 = 散布的特殊地形(零星出现);16~`24` = 城池建筑区(示例里集中在右下角)。
🐛 官方示例的一个小 bug:配套的 logMap() 打印函数把宽高写反了(for (row < w) for (col < h),索引却用 row*w + col),非正方形地图会打印错乱。自己写打印时记得用 row*MapWid + col 且外层循环走 MapHgt。
⚠️ g_FightMapData 元素个数必须严格等于 MapWid × MapHgt,短了会读到越界数据、长了尾部被忽略。
11.2 drawGenerals
baye.hooks.drawGenerals = function(c) {// 画出全部屏幕范围内的将领图var g_GenPos = baye.data.g_GenPos;// 计算屏幕坐标范围(16 像素一格)var left = baye.data.g_MapSX;var top = baye.data.g_MapSY;var right = Math.floor(left + baye.data.g_screenWidth / 16 - 1);var bottom = Math.floor(top + baye.data.g_screenHeight / 16 - 2);var frame = c.frame; // 0 / 1 两帧切换for (var i = 0; i < 20; i++) {var p = g_GenPos[i];if (p.state == 8) continue; // 死亡, 不画// 在屏幕外, 不画if (p.x < left || p.x > right || p.y < top || p.y > bottom) continue;var point = baye.getFighterXY(i); // 战场格坐标 -> 屏幕像素坐标var picind;if (frame == 0) {picind = 3; // 第 0 帧图片序号} else {picind = 7; // 第 1 帧图片序号}baye.drawImage(point.x, point.y, 5, 0, picind, 1);}};
📌 baye.drawImage(x, y, 资源组, ?, 图片序号, ?) — 官方示例传的是 (x, y, 5, 0, picind, 1),第 3、4、6 个参数含义未在示例中说明(待实测)。第 5 个 picind 是资源组内的图片序号。
baye.hooks.drawGenerals = function(c) {var g_GenPos = baye.data.g_GenPos;var frame = c.frame;// 屏幕裁剪范围(16 像素一格)var left = baye.data.g_MapSX;var top = baye.data.g_MapSY;var right = Math.floor(left + baye.data.g_screenWidth / 16 - 1);var bottom = Math.floor(top + baye.data.g_screenHeight / 16 - 2);for (var i = 0; i < 20; i++) {var p = g_GenPos[i];if (p.state == 8) { continue; } // 8 = 死亡,不画if (p.x < left || p.x > right || p.y < top || p.y > bottom) { continue; }var point = baye.getFighterXY(i); // 取屏幕像素坐标var picind = (frame == 0) ? 30 : 31; // 两帧切换baye.drawImage(point.x, point.y, 34, 0, picind, 1);}return 0; // 关键:接管};
11.3 getTerrainInfo
baye.hooks.getTerrainInfo = function(context) {var terNames = ['草原','平原','山地','森林','堡垒','城池','火焰','河流'];context.info ='【地形】' + terNames[context.ter] + '\n' +'——————————————————-\n' +'兵种 攻击系数 防御系数\n' +'骑兵 120% 105%\n';};
11.4 getFighterInfo
baye.hooks.getFighterInfo = function(context) {var person = baye.data.g_GenAtt[context.index]; // 战场数据var pIndex = baye.data.g_FgtParam.GenArray[context.generalIndex] - 1; // 全局人物索引var P = baye.data.g_Persons[pIndex];var pos = baye.data.g_GenPos[context.generalIndex]; // 坐标/生命/状态context.info = '【' + baye.getPersonName(pIndex) + '】\n' +'兵力 ' + P.Arms + ' 等级 ' + P.Level + '\n' +'状态 ' + ['正常','混乱','禁咒','定身'][pos.state] + '\n' +privateSkillInfo(pIndex);};
💡 可在同一钩子里调用 baye.clearRect(...) 自行清屏重绘(实战脚本用来做半透明面板)。
12. 移动与地形计算类 ⚠️ 全部非官方
来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 中 4~6 个脚本在用。
12.1 countMove
baye.hooks.countMove = function(c) {if (baye.data.g_FgtWeather == 2) { // 雪天:移动力锁死为 2baye.data.g_GenPos[c.generalIndex].move = 2;return; // 已处理,不 return -1}return -1; // 其余情况交给引擎};
12.2 countMoveRange ⚠️ 存疑
⚠️ 可信度存疑:10 个实战脚本中,唯一出现此钩子的 script (9).js 把它整段注释掉了(L1538-1549)。说明作者实测后放弃了它。名字确实存在于 baye.wasm,但可能引擎并不真正回调。优先用 countMove 代替。
// 脚本中被注释掉的写法(未验证是否会被回调)// baye.hooks.countMoveRange = function(context) {// if (person.ArmsType == 3 && context.ter != 7) {// baye.data.g_GenPos[context.generalIndex].move += 1;// }// return -1;// };
12.3 countLandResistance
baye.hooks.countLandResistance = function(context) {var pIndex = baye.data.g_FgtParam.GenArray[context.generalIndex];if (baye.getPersonNameByID(pIndex) == '赵云') {for (var i = 0; i < 46; i++) {context.result[i] = 0x81; // 赵云:全地形低阻力}return;}return -1; // 其他人交还引擎};
⚠️ context.result 是引擎传入的数组,只能逐项赋值,不要整体替换(context.result = [...] 会丢失引用,引擎读不到)。
13. 完整钩子速查表
验证口径:
第一轮(候选名 71 个):逐一在 4.4 MB 的 baye.wasm 中做字节匹配,仅 didChangeMenuSelection 未命中,其余 70 个全部存在。 第二轮(候选名 21 个,含 exscript 全部钩子):19 个命中,fightStage1 与前缀 fightStage 均未命中(0 次)。 两轮共证伪 2 个无效钩子。详见文末「附:数据来源与验证方法」。
14. 实战避坑清单
15. 调试技巧
// 一次性给所有钩子打日志,摸清调用顺序(function() {var names = ['tacticStage1','tacticStage2','tacticStage3','tacticStage4','tacticStage5','enterBattle','battleStage1','battleStage5','exitBattle','battleBuildAttackAttriutes','countAttackHurt','countSkillHurt'];names.forEach(function(name) {var orig = baye.hooks[name];baye.hooks[name] = function(ctx) {console.log('[HOOK] ' + name, JSON.stringify(ctx));return orig ? orig.apply(this, arguments) : -1;};});})();
附:数据来源与验证方法
命中: enterBattle, battleStage1, battleStage5, tacticStage1, tacticStage5,tacticStageUser, drawGenerals, loadFightMap, countSkillHurt, showSkill,showMainHelp, fontImageForChar, willCloseMenu, willChangeMenuSelection,didOpenNewGame, willSaveGame, didLoadGame,getPersonPropertyTitle, getPersonPropertyValue (共 19 个)未命中: fightStage1, fightStage ← 本轮新增证伪
官方手册记载 31WASM 实测命中 72 ← 有效钩子总数scripts/ 实战使用 54exscript 官方示例 18 ← 权威性最高,本版示例主体→ 72 − 31 = 41 个钩子官方手册从未记载(含新增的 11 个)→ 这就是「官方手册只记了不到一半」的由来
⚠️ 免责:第一轮新增的 12 个钩子均无官方文档背书,ctx 字段名是从第三方脚本实现反推的,语义命名(如"地形阻力""移动力")为整理时的推断,实机行为请以调试日志为准。
第二轮已用 exscript 官方示例校正了其中 5 个(getPersonPropertyTitle/Value、loadFightMap、drawGenerals、fontImageForChar),这几条可放心使用。