前言
在游玩Screeps中,不可避免的会需要手动调节参数、发起指令,传统的手操一般有以下方式
- 在控制台中直接修改 Memory: Memory.xxx.xxx = xxx
- 在控制台调用global上提前挂载好的方法: BuyOrder("orderid",10000)
- 插旗子、移动旗子
- 手动放置construction sites,摧毁建筑等
从笔者个人的实践出发,第一、二种方式的体验是比较糟糕的,在控制台敲代码既没有补全,也没有提示,参数也比较多。而且随着业务量的增大,挂载的方法也越来越多,如何统一管理这些方法,避免冲突也越来越重要。
系统介绍
因此,在“懒”的驱动下,笔者设计并实现了一套控制台系统,统一管理全局挂载的方法。这套系统有以下特点
- 节点和指令以树状形式构成了整个系统,节点包含了若干个子节点、指令
- 键入节点的名称或缩写,可以进入节点的语境,同时挂载其子节点和指令
- 键入指令的名称或缩写,以及参数(如有),可以发起指令
- 节点可以是静态的(写死),也可以是动态的(进入语境时生成)
- 每个节点,都有唯一的“路径”,路径本身就包含了一些信息,这些信息可以作为默认的参数,在调用指令时,不需要再传入这些信息。
- 节点和节点之间相互隔离,即使指令重名了,但因为语境不同,也不会产生冲突。
- 支持添加钩子函数,在节点挂载,退出时调用
效果展示
基础指令效果展示
在候选菜单中,子节点,都是以"/"结尾的,键入子节点的名称或缩写,会进入子节点
值得注意的是每次键入节点的名称,都会自动触发 list 指令,展示当前节点下所有可用的子节点、指令。图中的"myroom"下的子节点,以及他的缩写,都是动态生成出来的。而且因为都配置了缩写,敲1-2个字母,就能在菜单间快速切换了。
数据结构
// 指令
interface Command {
// 名称
name: string;
// 缩写
alias?: string;
// 描述
description: string;
// 参数
parameters: string[];
// 回调函数
callback(...args: any[]): string;
}
// 节点
interface Dir {
name: string;
alias?: string;
// 离开节点时的钩子函数
onLeave?(): string;
description: string;
// 子节点
dirs: Dir[];
// 指令
cmds: Command[];
}
如何实现动态的节点和指令
使用 getter 和 setter 在访问时计算出节点
动态节点的一个例子
export const myroom: Dir = {
name: "myroom",
alias: "mr",
description: "我的房间",
cmds: [],
get dirs() {
const roomNames = getMyRoomNames();
let i = 0;
let dirs: Dir[] = [];
_.forEach(roomNames, roomName => {
let dir: Dir = {
name: roomName,
alias: `r${i}`,
description: "管理我的房间",
cmds: [],
get dirs() {
// 子节点的子节点也是动态的
return myRoomDirs(roomName);
}
};
dirs.push(dir);
i += 1;
});
return dirs;
}
};
控制多个房间时,缩写会按照 r0,r1,r2 .... 的顺序自动生成
动态指令的一个例子
function level(roomName: string): Dir {
return {
name: "level",
alias: "l",
description: "每级建筑规划",
onLeave: () => {
Memory._lpRoomName = "";
return `关闭${roomName}建筑级别可视化`;
},
dirs: [],
get cmds() {
Memory._lpRoomName = roomName;
let cmds: Command[] = [];
for (let i = 1; i <= 8; i++) {
let cmd: Command = {
name: `level${i}`,
alias: `l${i}`,
description: `等级${i}的建筑规划`,
parameters: [],
callback: () => {
global._lpStructures = getLevelPlan(roomName, i);
return `查看等级${i}的建筑规划`;
}
};
cmds.push(cmd);
}
return cmds;
}
};
}
同样用到了 getter setter
如何挂载节点和指令
使用 Object.defineProperty,记得设置 configurable为true
Object.defineProperty(global, "help", {
// 这个不加的话,就不能修改了
configurable: true,
get: () => {
let output = "";
output += "home\t返回主页\n";
output += "dir\t当前路径\n";
output += "back\t返回上一级\n";
output += "list\t可用路径/命令\n";
output += "help\t帮助";
return output;
}
});
进入某个节点的语境时,需要挂载他所有的子节点、指令,还要记得挂载它们的缩写。对于有参数的指令,需要挂载到 value上,而不是使用 getter。
为了记录路径,需要维护一个 list,存放当前进入过的节点,每进入一个子节点,就push一次,每退出一个节点,就pop一次,然后再挂载最右侧节点的子节点及指令。如果list为空,就挂载home节点。
一个提升体验的优化
因为节点的信息是对象,所以存放在global下,每次global reset的时候,都会丢失这部分信息,这在sim环境下是很不方便的,因此我还序列化存储了路径:home/myroom/sim/constructionPlan/design,在global reset后,自动按次序键入这些节点,以此来保证sim环境下的流畅使用。
if (Memory._currentDirs) {
const paths = Memory._currentDirs.split("/");
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
if (!(global as any)[path]) {
break;
}
}
}
结合可视化的使用案例
Screeps通过 RoomVisual 支持房间中的可视化。在实际使用中,除开那些一直开启的报表类的可视化,一些可选的可视化功能,经常需要手操启用或关闭,笔者的控制台系统非常适合对接这些可选的可视化功能。下面展示一个使用案例,或许会给你带来一些灵感。
show2.gif
网友评论