screeps游戏中想查找某个合适的房间,比如想要房间中元素矿的含量大于45000。但是点开房间看又不方便,于是本脚本应运而生。
打开screeps官网(建议使用新版地图),控制台粘贴源码,然后调用适当的方法。
方法速览
- 查看E5S15周围5格房间
helper.search('E5S15', 5).then(() => helper.log().draw())
- 查看E5S15到E10S15之间的房间
helper.search('E5S15', 'E10S15').then(() => helper.log().draw())
- 查看E5S15周围T矿大于45000的房间
helper.search('E5S15', 5).then(() => helper.filter(data => data.T >= 45000).log().draw())
源码
/**
* 寻找合适的房间
*
* 1. helper.search(roomName, roomNameOrRange)得到房间数据
* @param {String} roomName 起始房间
* @param {String|Number} roomNameOrRange 目标房间或者范围,默认为5。如果传入房间名,则搜索由roomName和roomNameOrRange组成的矩形区域内的房间;
* 否则搜索以roomName为中心,roomNameOrRange为半径的范围内的房间
* 2. helper.log()打印房间数据
* 3. helper.draw()在地图上绘制信息,注意,在新版地图才能使用该api
*
* 如果helper配置了myName,则会过滤掉别人的房间或者外矿,如helper.setConfig({myName: 'ganyu'}).search('E13S7').then(() => helper.filter((data) => data.T >= 45000).log().draw())
*/
const utils = {
/**
* 验证房间名是否合法
* @param {String} roomName 房间名
* @returns {Boolean}
*/
validateRoomName(roomName) {
return /^[WE]\d+[NS]\d+$/.test(roomName);
},
/**
* 判断一个房间是否过道房间,如果房间名中带有0则是过道
* @param {String} roomName 房间名
* @returns {Boolean}
*/
isHighWayRoom(roomName) {
return roomName.includes("0");
},
/**
* 判断一个房间是否是九房
* @param {String} roomName 房间名
* @returns {Boolean}
*/
isCenterNineRoom(roomName) {
const [_, x, y] = roomName
.match(/[EW](\d+)[SN](\d+)/)
.map((num) => Number(num) % 10);
return x >= 4 && x <= 6 && y >= 4 && y <= 6;
},
/**
* 将房间名转为坐标,参考点E0S0 -> {x: 0, y: 0}
* @param {String} roomName 房间名
* @returns {{x: number, y: number}} {x, y}
*/
roomNameToXY(roomName) {
// 解析房间名
const parse = roomName.match(/([EW])(\d+)([NS])(\d+)/);
const x = parse[1] === "W" ? ~parseInt(parse[2]) : parseInt(parse[2]);
const y = parse[3] === "N" ? ~parseInt(parse[4]) : parseInt(parse[4]);
return {
x,
y,
};
},
/**
* 将坐标转为房间名
* @param {Number} x x坐标
* @param {Number} y y坐标
* @returns {String} 房间名
*/
xyToRoomName(x, y) {
return (x < 0 ? "W" + ~x : "E" + x) + (y < 0 ? "N" + ~y : "S" + y);
},
/**
* 计算两个房间的距离
* @param roomName1 房间名1
* @param roomName2 房间名2
* @returns {Number} 距离
*/
calcRoomDistance(roomName1, roomName2) {
const { x: x1, y: y1 } = roomNameToXY(roomName1);
const { x: x2, y: y2 } = roomNameToXY(roomName2);
return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
},
};
const helper = {
baseURL: "https://screeps.com",
myName: "",
roomCache: {},
searchResult: {},
token: "",
setConfig(config) {
Object.keys(config).forEach((key) => (this[key] = config[key]));
return this;
},
/**
*
* @param {String} roomName
* @param {String | Number} roomNameOrRange
* @param {*} config
*/
async search(roomName, roomNameOrRange = 5, config = {}) {
if (typeof roomName !== "string" || !utils.validateRoomName(roomName)) {
throw `参数${roomName}不合法,请传入正确的房间名`;
}
let roomNames = [];
const { x: x1, y: y1 } = utils.roomNameToXY(roomName);
if (typeof roomNameOrRange === "string") {
if (!utils.validateRoomName(roomNameOrRange)) {
throw `参数${roomNameOrRange}不合法,请传入正确的房间名`;
}
const { x: x2, y: y2 } = utils.roomNameToXY(roomNameOrRange);
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
roomNames.push(utils.xyToRoomName(x, y));
}
}
} else if (typeof roomNameOrRange === "number" && roomNameOrRange >= 0) {
roomNameOrRange = Math.ceil(roomNameOrRange);
for (let dx = -roomNameOrRange; dx <= roomNameOrRange; dx++) {
for (let dy = -roomNameOrRange; dy <= roomNameOrRange; dy++) {
roomNames.push(utils.xyToRoomName(x1 + dx, y1 + dy));
}
}
} else {
throw `参数${roomNameOrRange}不合法,请传入正确的房间名或者正整数`;
}
roomNames = roomNames.filter(
(r) => !utils.isCenterNineRoom(r) && !utils.isHighWayRoom(r)
);
if (!roomNames.length) return;
this.searchResult = {};
const shard = this.getShard(config.shard);
if (!this.roomCache[shard]) this.roomCache[shard] = {};
const shardCache = this.roomCache[shard];
await Promise.all(
roomNames.map(async (r) => {
if (shardCache[r]) {
this.searchResult[r] = shardCache[r];
return;
}
const data = await this.getRoomObjects(r, shard);
if (!data.ok) return;
const { objects, users } = data;
if (
Object.keys(users).find(
(id) =>
this.myName &&
users[id].username !== this.myName &&
!["Invader", "Source Keeper"].includes(users[id].username)
)
) {
return;
}
this.searchResult[r] = {};
const minerals = objects.filter((o) => o.type === "mineral");
for (const mineral of minerals) {
this.searchResult[r][mineral.mineralType] = mineral.mineralAmount;
}
shardCache[r] = this.searchResult[r];
})
);
return this;
},
/**
* 过滤
*
*/
filter(fn) {
if (typeof fn !== "function") return this;
Object.keys(this.searchResult).forEach((r) => {
if (!fn(this.searchResult[r])) {
delete this.searchResult[r];
}
});
return this;
},
/**
* 打印结果
*/
log() {
console.log(this.searchResult);
return this;
},
/**
* 绘制结果
*/
draw(
fn = (data) =>
Object.entries(data)
.map((v) => v.join(" "))
.join("\\n")
) {
if (!window.location.href.includes("map2")) {
console.log(
`%c请在新地图中使用draw方法:${window.location.href.replace(
"map",
"map2"
)}`,
"color:red;"
);
return this;
}
const colors = ["#ffffff", "#d7efac", "#ff96c7", "#ae47b5", "#bb5626"];
const getColor = (amount) => {
if (amount < 1e4) return colors[0];
return colors[Math.min(Math.floor(amount / 2e4) + 1, colors.length)];
};
const initExpression =
"const R=(r)=>new RoomPosition(0,10,r);const A=(text,r,config)=>[text,R(r),{fontSize:7,align:'left',...config}];";
let expression = initExpression;
let first = true;
for (const r in this.searchResult) {
if (expression.length > 800) {
this.submit(expression);
expression = initExpression;
first = true;
}
const text = fn(this.searchResult[r], r);
if (first) {
expression += `Game.map.visual.text(...A("${text}","${r}",{color:"${getColor(
this.searchResult[r].T || 0
)}"}))`;
first = false;
} else {
expression += `.text(...A("${text}","${r}",{color:"${getColor(
this.searchResult[r].T || 0
)}"}))`;
}
}
console.log(expression);
this.submit(expression);
return this;
},
/**
* 获取当前所在shard
* @param {'shard0' | 'shard1' | 'shard2' | 'shard3' | 'shardSeason'} shard 分片
* @returns {'shard0' | 'shard1' | 'shard2' | 'shard3' | 'shardSeason'}
*/
getShard(shard) {
if (["shard0", "shard1", "shard2", "shard3", "shardSeason"].includes(shard))
return shard;
return window.location.href.match(/(shard\w+)/)[1] || "shardSeason";
},
/**
* 构造url前缀
*/
buildBaseURL() {
if (window.location.href.includes("season"))
return this.baseURL + "/season";
return this.baseURL;
},
/**
* 获取房间对象
* @param {String} roomName 房间名
* @param {'shard0' | 'shard1' | 'shard2' | 'shard3' | 'shardSeason'} shard 分片
*/
async getRoomObjects(roomName, shard = this.getShard()) {
const response = await fetch(
`${this.buildBaseURL()}/api/game/room-objects?room=${roomName}&shard=${shard}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
return response.json();
},
/**
* 提交代码
* @param {String} expression 代码语句
* @param {'shard0' | 'shard1' | 'shard2' | 'shard3' | 'shardSeason'} shard 分片
*/
async submit(expression, shard = this.getShard()) {
const response = await fetch(`${this.buildBaseURL()}/api/user/console`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-token": this.token || localStorage.getItem("auth").slice(1, -1),
},
body: JSON.stringify({
expression,
shard,
}),
});
return response.json();
},
};