霸业开发指导手册
想改玩法、自己做版本,就得会写钩子。这一本管怎么上手:JavaScript 简要基础、霸业脚本系统原理、编写向导、浏览器里的调试方法,外加六个可直接抄改的示范脚本。查条目请去配套的《霸业 API 及函数手册》。
第一章 · 简要说明与 JS 基础
简要说明:这份手册是什么
这是写给想做游戏、想改玩法的人的,不是写给只玩版本的玩家。三国霸业的网页版(baye)把战斗、AI、内政、道具这些规则做成了可以改的钩子函数:你在脚本里定义 baye.hooks.xxx = function(context){...},引擎跑到那一步就会来问你的脚本该怎么算。学会写钩子,就等于拿到了这套引擎的规则改写在手里。
正文摘录自上游文档仓库 bgwp/baye-doc 的开发者部分(《Javascript 简要基础》《霸业脚本系统原理》《编写向导》《调试方法》《示范脚本》),我们做了排版整理和少量勘误,技术口径以原文为准。同一次摘录里的 API 手册(baye.data 数据项、77 个 baye.hooks 钩子、baye 功能函数)单独放在配套的《霸业 API 及函数手册》里:文档原本没写的 16 个钩子,是对照 iBaye(引擎源码)与 baye-alpha(网页版)的调用点补写的,条目里标了「补写」和源码位置,方便你自己复核。
阅读顺序建议:先扫一遍「Javascript 简要基础」(会写就跳过),再读「霸业脚本系统原理」搞懂钩子是什么,然后照「编写向导」动手写第一个,卡住了看「调试方法」,最后照「示范脚本」抄改,查条目去配套的《霸业 API 及函数手册》。
引擎里那些「原版是怎么算的」的具体数值没有全部收录,那部分属于版本数据;这份手册只讲怎么写脚本改规则。
得益于HTML优势,网页版霸业在原版基础上扩展出Javascript脚本系统,使得lib制作能高度定制游戏逻辑。
Javascript简要基础
基本概念
脚本是一种编程语言,不同于普通文档(如XML),脚本主要由一系列的 数据 和对数据的 操作 (语句)构成。
脚本中语句是一条接着一条顺序执行下去的。如
a = 1; //让对象a的值为1 (赋值)
b = a; //让b的值为a的值,此后b的值也为1
a = 2; //让a的值为2,此后a为2,b仍然为1
c = a; //让c的值为a的值,此后a为2,b为1,c为2
以上脚本有4条语句,顺序执行。
XML文档描述“ 做什么 ”,脚本描述“ 怎么做 ”。
霸业的脚本系统采用javascript语言, 本教程仅讲一些最基本的javascript脚本知识,欲了解更全面,请参考相关资料。
注释
注释的内容是为了帮助程序员阅读用的,代码执行时注释会被忽略。
// 标记单行注释开始,每一行的 // 开始到行末的内容为注释。
/ 和 / 对,用于标记多行注释,如
/*
这是注释
这也是注释
*/
Javascript的数据类型
数字: 123
字符串: "赵云"
对象: {name: "赵云", age: 20}
列表: [1, 2, 3]
赋值
最基础的动作语句是赋值,赋值就是让一个 名字 关联一个 值 , 其后,我们就可以在需要用到这个 值 的地方使用这个 名字 来代替。
基本语法:
变量名称=值;
示例
/*
简单赋值语句
让 a 关联值 1,分号指示语句结束
javascript里面分号是可选的,但为避免歧义,最好每条语句都加上分号
*/
var a = 1; // var 定义并赋值,首次使用时加上var
a = a + 2; // 重新赋值, 此后a的值是3
没接触过编程语言的朋友一定要注意: 程序里面的 = 符号代表的是一个动作,区别于我们数学里面学的 = 代表一个相等关系 .
条件判断
有时候程序需要根据不同的条件执行不同的代码,Javasctipt 用 if 来进行条件判断。
示例
var a = 1;
var b = 2;
if (a > b) {
// 如果 a > b
console.log("a > b");
}
示例2
var a = 1;
if (a > b) {
// 如果 a > b
// console.log语句通常用于调试,它将字符串输出到控制台。
console.log("a > b");
} else if (a == 1) {
console.log("a = 1");
} else if (a == 0) {
console.log("a = 2");
} else {
// 其它情况
console.log("其它情况");
}
遍历数据
有时候我们需要遍历一个集合里面的每一个元素做操作,比如霸业里面找出符合某种条件的人物。
我们通常使用 for 循环来实现遍历数据,示例
var numbers = [1, 3, 2, 4];
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] < 3) {
console.log(numbers[i]);
}
}
for 循环语法构成如下
for (表达式1; 表达式2; 表达式3) {
循环体
}
其它语句
执行流程:
函数(function)
如果一段代码是可复用的或是逻辑相对独立,我们可以将其封装称为函数。
基本语法
function 函数名称(参数列表){
函数体
}
函数名称可以忽略,称为匿名函数。
// 这种格式是普通的赋值语句, “=”右边的的函数整体是一个值。
变量名称 = function(参数列表) {
函数体
}
示例
function add(x, y) {
return x + y;
}
var a = add(1, 2); // now, c = 3
var b = add(a, 1);
或
var add = function(x, y) {
return x + y;
}
var a = add(1, 2); // now, c = 3
var b = add(a, 1);
第二章 · 系统原理、编写向导与调试方法
霸业脚本系统原理
霸业的脚本系统定义了一个对象 baye.hooks ,称之为 钩子对象 ,此对象作为引擎与脚本衔接的纽带。引擎在一系列特定的时间点会调用 baye.hooks 里面的 钩子函数 来完成特定任务。
当lib开启了脚本功能后,引擎在启动时会执行 script.js 文件内的代码。因此, script.js 文件的主要作用就是往 baye.hooks 对象里面填充我们需要关注的 钩子函数 来实现特定功能。
示例
baye.hooks.giveTool = function(context) {
//简单示例,赏赐成功
context.result = 1;
}
如上,每当引擎执行到赏赐功能的时候会调用 baye.hooks.giveTool 这个钩子函数,达到" 询问 脚本是否要允许赏赐“这个目的。
霸业脚本编写向导
了解了原理后,我们就可以一步一步来编写霸业脚本了。
假如我们要实现这么一个功能: 每月对AI控制的人物执行升级
为实现此功能,我们需要在写一些功能代码,并且让引擎在每次策略结束后执行我们的代码。
Step1: 让引擎每次策略结束后执行我们的代码
我们设置钩子函数 baye.hooks.tacticStage4 (详细介绍见配套的《霸业 API 及函数手册》· 钩子函数)
baye.hooks.tacticStage4 = function() {
//待续
};
Step2: 遍历所有人物,如果有归属,且归属不是玩家君主, 且等级未满上限,升级之
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.Level < maxLevel) {
p.Level = p.Level + 1;
}
}
};
注:
- 关于
baye.data内的数据见配套的《霸业 API 及函数手册》· 霸业 API allPerson[i]: 取列表里第i个元素(也就是第i个人物的数据)&&: 并且!=: 不等于
Step3: 进一步完善
当人物处于被俘虏状态时,归属被设置为255,因此进一步排除俘虏被升级
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
) {
p.Level = p.Level + 1;
}
}
};
Step4: 加入概率因素
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
) {
var chance = Math.random(); //产生一个0~1之间的随机数
if (chance < 0.7) {// 随机数小于0.7升级,也就是70%的概率升级
p.Level = p.Level + 1;
}
}
}
};
调试方法
我们使用PC浏览器的 "开发者工具" 来调试脚本。以Chrome浏览器为例,打开baye页面后按 F12 打开“开发者工具”。如下图:
实时查看和修改引擎数据
霸业的引擎数据存放在 baye.data 对象中,浏览器控制台中输入 baye.data. 浏览器会弹出自动补全菜单,对象中有哪些属性一览无余。如图:
查看第一个城池的归属:
修改第一个城池的归属:
修改后移动一下城池光标,可以看到已经实时生效。
实时替换钩子函数
调试钩子函数时,如果每次修改一点脚本,又重新打包lib->重新加载运行,会非常麻烦。当我们修改了钩子函数后,只需在浏览器控制台对该钩子重新赋值,该钩子就即时生效。如图:
添加调试日志
有时候为调试代码中的逻辑是否按预想的逻辑正常执行,我们需要得到一些实时输出信息。我们用 console.log 来实现日志功能。
例如,前面的升级功能,我们要先看升级功能有没有正常执行,可以添加一些日志
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.Level < maxLevel) {
p.Level = p.Level + 1;
var name = baye.getPersonName(i);
console.log(name + " 升级为: " + p.Level);
}
}
};
策略结束后,可见到控制台打印如下:
第三章 · 示范脚本
下面六段是上游文档里的完整示例,都是能直接贴进脚本跑的。
计算攻防
baye.hooks.battleBuildAttackAttriutes = function(context) {
/*
脚本计算攻防示例
context.index: 计算结果存放位置序号
context.generalIndex: 战场将领序号
*/
var result = baye.data.g_GenAtt[context.index];
var pIndex = baye.data.g_FgtParam.GenArray[context.generalIndex] - 1;
var person = baye.data.g_Persons[pIndex];
// 地形对兵种的影响
var terEffects = [
// 草地, 平原, 山地, 森林, 村庄, 城池, 营寨, 河流
[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, 1, 1, 1], // 极兵
[1, 1, 1, 1, 1, 1, 1, 1], // 玄兵
];
var AtkModulus = [1.0, 0.8, 0.9, 0.8, 1.3, 0.4]; /* 各兵种攻击系数 */
var DfModulus = [0.7, 1.2, 1.0, 1.1, 1.2, 0.6]; /* 各兵种防御系数 */
var TerrDfModu = [1.0, 1.0, 1.3, 1.15, 1.1, 1.5, 1.2, 0.8]; /* 各种地形防御系数 */
// 武力/等级/攻击系数影响攻击力
var at = person.Force * (person.Level + 10) * AtkModulus[person.ArmsType];
// 智力/等级/防御系数影响防御力
var df = person.IQ * (person.Level + 10) * DfModulus[person.ArmsType];
// 叠加地形对攻防的影响
at = terEffects[person.ArmsType][result.ter] * at;
df = terEffects[person.ArmsType][result.ter] * df;
// 地形固有防御系数
df *= TerrDfModu[result.ter];
// 输出最终结果
result.at = at;
result.df = df;
};
计算伤害
查阅文档可知, 可以使用计算普通攻击伤害的钩子http://bgwp.oschina.io/baye-doc/script/index.html#baye-hooks-countattackhurt
baye.hooks.countAttackHurt 这个钩子是用来计算普通攻击兵力伤害的。
进入这个钩子的时候,攻防双方的战力参数已经计算好,并放到了这:
- 攻击方:baye.data.g_GenAtt[0]
- 防守方:baye.data.g_GenAtt[1]
这是之前引擎内部或引擎调用钩子:baye.hooks.battleBuildAttackAttriutes 来算好的。
g_GenAtt里面包含了攻防双方将领的战场序号,可用来提取双方将领的其它数据。
那么baye.hooks.countAttackHurt要完成的目标就是根据攻防双方的战力参数或其它条件(如队友的特殊道具)来计算出对防守方造成多少伤害。引擎内置算法是完全根据g_GenAtt里面的战力参数来计算伤害值的。
本示例扩展出逻辑是:查找我方队友是否有携带特殊道具的,如果有,则在原伤害基础上按比例加成。
如下是引擎默认算法
baye.hooks.countAttackHurt = function(context) {
var KeZhiMatrix = [
[1.0, 1.2, 0.8, 1.0, 0.7, 1.3],
[0.8, 1.0, 1.2, 1.0, 0.6, 1.2],
[1.2, 0.8, 1.0, 1.0, 1.1, 1.2],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
[1.1, 1.3, 0.9, 1.0, 1.0, 1.5],
[0.6, 0.6, 0.6, 0.6, 0.6, 0.6]
];
var p0 = baye.data.g_GenAtt[0]; //攻击方数据
var p1 = baye.data.g_GenAtt[1]; //防守方数据
var person = baye.getPersonByGeneralIndex(p0.generalIndex);
/* 基本伤害 hurt = (at / df) * arms / 8 */
var hurt = p0.at / p1.df * (person.Arms >> 3);
/* 相克加层 hurt *= modu */
hurt *= KeZhiMatrix[p0.armsType][p1.armsType];
context.hurt = hurt;
};
引入其它条件(如特殊道具)影响伤害输出
baye.hooks.countAttackHurt = function(context) {
var KeZhiMatrix = [
[1.0, 1.2, 0.8, 1.0, 0.7, 1.3],
[0.8, 1.0, 1.2, 1.0, 0.6, 1.2],
[1.2, 0.8, 1.0, 1.0, 1.1, 1.2],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
[1.1, 1.3, 0.9, 1.0, 1.0, 1.5],
[0.6, 0.6, 0.6, 0.6, 0.6, 0.6]
];
var p0 = baye.data.g_GenAtt[0]; //攻击方数据
var p1 = baye.data.g_GenAtt[1]; //防守方数据
var person = baye.getPersonByGeneralIndex(p0.generalIndex);
/* 基本伤害 hurt = (at / df) * arms / 8 */
var hurt = p0.at / p1.df * (person.Arms >> 3);
/* 相克加层 hurt *= modu */
hurt *= KeZhiMatrix[p0.armsType][p1.armsType];
// 查找攻击方是否携带有NB道具
// baye.data.g_GenAtt里面有20个位置, 前十个为攻城方, 后十个为守城方
//
// 首先需要判断攻击者是攻城方还是守城放
var startIndex = p0.generalIndex < 10 ? 0 : 10;
var hasNewBeeTool = false;
var newBeeToolID = 5; //NB道具的ID
// 查找攻击方有没有NB道具
for (var ind = startIndex; ind < startIndex + 10; ind++) {
if (baye.data.g_GenPos[ind].state != 8) { // 8是死亡状态
var p = baye.getPersonByGeneralIndex(ind);
if (p.Tool1 == newBeeToolID || p.Tool2 == newBeeToolID) {
hasNewBeeTool = true; // 找到NB道具
break;
}
}
}
if (hasNewBeeTool) {
// 攻击方团队有NB道具, 百倍伤害
hurt *= 100;
}
context.hurt = hurt;
};
关于状态
STATE_ZC = 0 /* 正常 */
STATE_HL = 1 /* 混乱 */
STATE_JZ = 2 /* 禁咒 */
STATE_DS = 3 /* 定身 */
STATE_QM = 4 /* 奇门 */
STATE_DJ = 5 /* 遁甲 */
STATE_SZ = 6 /* 石阵 */
STATE_QZ = 7 /* 潜踪 */
STATE_SW = 8 /* 死亡 */
道具为人物加额外属性
群魔乱舞将年龄数据用作了统率
// 禁止年龄自动增长
baye.data.g_engineConfig.disableAgeGrow = 1;
// 定义数据表
baye.TOOL_AGE_TAB = [10, 10, 0, 10 /* ... */ ];
// 赏赐时加属性
baye.hooks.giveTool = function(ctx) {
var up = baye.TOOL_AGE_TAB[ctx.toolIndex];
if (up > 0) {
var person = baye.data.g_Persons[ctx.personIndex];
person.Age += up;
}
ctx.result = 1;
};
// 没收时减属性
baye.hooks.takeOffTool = function(ctx) {
var up = baye.TOOL_AGE_TAB[ctx.toolIndex];
if (up > 0) {
var person = baye.data.g_Persons[ctx.personIndex];
person.Age -= up;
}
ctx.result = 1;
};
进一步增加在道具表格中显示统率属性
// 配置道具表格 列数量
baye.data.g_uiCfg.toolPropertiesCount = 6;
// 配置表格 列宽度
baye.data.g_uiCfg.toolPropertiesDisplayWitdh = [4, 6, 6, 6, 6, 6];
// 定义道具表格表头
var baye_tool_heads = ["类型", "加武力", "加智力", "加统率", "加移动", "变兵种"];
// 填写表头的钩子
baye.hooks.getToolPropertyTitle = function(c) {
c.title = baye_tool_heads[c.propertyIndex];
return 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 = baye.TOOL_AGE_TAB[c.toolIndex];
break;
case "加移动":
c.value = tool.move;
break;
case "变兵种":
c.value = ['无', '水', '玄', '极', "骑兵", "步兵", "弓兵", "水兵", "极兵", "玄兵"][tool.arm];
break;
}
return 0;
};
不复用年龄数据,额外为人物增加统率属性
引擎已经固定人物有哪些属性, 因此不能修改固有的人物属性。要增加属性,只能采用"外挂式", 将属性值存储于引擎外(脚本环境中)。
//待续...
弹出提示,和对话
目前脚本系统支持两个UI相关API
| baye.alert | 弹出文字提示 |
| baye.say | 人物对话 |
部分钩子支持调用这些API。
示例, 没收物品时拒绝, 并对话
baye.hooks.willTakeOffTool = function(ctx) {
if (ctx.personIndex == baye.data.g_PlayerKing) return -1;
baye.say(ctx.personIndex, "你算哪根葱?\n我的宝贝岂是你想拿走就拿走?", function() {
baye.alert("没收失败!");
return 0;
});
};
"工匠"人物
可利用"赏赐"功能打造类似工匠的人物。
以马腾作为示例
baye.hooks.willGiveTool = function(c) {
// 1号道具 + 2号道具 + 3000金钱 可合成 3号道具
if (baye.getPersonName(c.personIndex) != "马腾") {
return -1;
}
var person = baye.data.g_Persons[c.personIndex];
var city = baye.data.g_Cities[c.cityIndex];
// 检查道具是否满足条件
if (person.Tool1 - 1 == 1 && c.toolIndex == 2) {
// 限制条件
if (city.Money < 10000) {
baye.say(c.personIndex, "城池不够富足,无法开工!");
return 0;
}
if (city.PeopleDevotion < 99) {
baye.say(c.personIndex, "城民们对你的治理不太满意,无法开工!");
return 0;
}
if (city.State != 0) {
baye.say(c.personIndex, "城池灾害,无法开工!");
return 0;
}
city.Money -= 3000; // 扣除3000钱
baye.deleteToolInCity(c.cityIndex, c.toolIndex); // 删除材料道具
baye.putToolInCity(c.cityIndex, 3); // 生产出新道具
baye.say(c.personIndex, "全新[" + baye.getToolName(3) + "]已打造成功!");
return 0;
}
return -1;
};
配套手册:霸业 API 及函数手册
这一本讲怎么学、怎么写、怎么调;写的时候要知道有哪些数据、哪些钩子、哪些函数可用,
那就查另一本:霸业 API 及函数手册。
它把 baye.data 的全部数据项、77 个 baye.hooks 钩子和 baye 功能函数
做成左侧一棵可折叠的树,点一条就在右边展开它的定义、调用时机、输入输出和示例。