TL;DR (English)
First off — this codebase is genuinely impressive. A solo-built browser FPS with ~117 weapons, 39 maps, bots, a killcam, a full shop economy, and an AI chat is a lot of moving parts, and the fact that it all hangs together is real engineering.
This issue is about a pro habit, not a bug: a few important lists are currently copy-pasted into several files, and you have to remember to edit every copy by hand. That's called the DRY principle — "Don't Repeat Yourself," a.k.a. having a single source of truth. Right now most copies agree (nice!), but I found one place where they've already started to drift — which is exactly the trap this habit prevents.
The big idea: make one file, shared/tables.js, that holds the map pool and the weapon tables, and have both server.js and game.js read from that one copy.
What I verified (real file:line evidence)
1. The map pool exists in 4 JavaScript copies — all the same big 39-map array:
2. The map list is ALSO hardcoded twice in the menu UI — and these two have ALREADY drifted:
👀 This is the whole point in a nutshell: two lists that were supposed to match no longer do. Nobody did anything wrong — it's just what happens to copies over time. One source of truth makes this impossible.
3. The weapon tables are mirrored across server.js and game.js — and right now they DO agree (great!):
The fact that you wrote those "keep in sync" comments yourself shows you already felt the problem. This issue is just the next step: make the computer keep them in sync so you don't have to.
Why one copy is safer (even though they agree today)
Two copies that agree today is luck plus discipline, and discipline runs out at 11pm when you're adding weapon #118. The risk isn't "it's wrong now" — it's:
- Add a map → you must edit it in 5 places (and the UI row that's easy to forget is the one that already drifted).
- Add a weapon → edit
WEAPON_COSTS in 2 files; miss one and the client shows a price the server won't honor.
- The bug is silent: nothing crashes, the lists just quietly disagree, and you debug a "weird map/price bug" weeks later.
With one source of truth, "add a map" or "add a weapon" becomes a one-line edit in one file, and every consumer updates for free. 🎯
Suggested first step (small, safe, high-payoff)
Start with just the map pool — it has the most copies and already shows drift, so it's the clearest win. Tackle the weapon tables in a follow-up.
-
Create shared/tables.js. One catch: public/game.js is loaded as a plain <script src="game.js"> (public/index.html:828), not a module — so the browser can't import. A friendly pattern that works in both Node and the browser:
// shared/tables.js
(function (root) {
const MAP_POOL = ['blank','urban','warehouse','forest','volcano','cyber','desert',
'tundra','space','airport','trenches','chernobyl','refinery','skydock','sewer',
'gravity_lab','glassworks','carrier','overgrowth','orbital_station','foundry',
'carnival','biosphere','lockdown','studio','temple','holiday','labyrinth','arena',
'opera','doomsday','train','dreamscape','pearl_harbor','titanic','supermarket',
'pyongyang','traffic_cone_republic','flying_moai'];
const tables = { MAP_POOL };
if (typeof module !== 'undefined' && module.exports) module.exports = tables; // Node (server.js)
else root.SHARED_TABLES = tables; // browser (game.js)
})(typeof window !== 'undefined' ? window : globalThis);
-
In server.js: const { MAP_POOL } = require('./shared/tables'); near the top, then delete the two inline arrays at 722-727 and 792-797 and use the shared one.
-
In public/index.html: add <script src="shared/tables.js"></script> before the game.js tag at line 828.
-
In public/game.js: replace the inline pool at 15018 with SHARED_TABLES.MAP_POOL.
-
Bonus (optional, the satisfying part): instead of hand-writing the two <div class="map-card"> rows in the HTML, give each map a tiny entry (label + color + emoji) and loop to build both rows from MAP_POOL. That deletes ~60 lines of HTML and makes the two-rows-disagree bug above structurally impossible.
⚠️ One small heads-up so it doesn't bite you: BUNDLES is an object in server.js:132 but an array in game.js:1169. When you get to that one, pick a single shape in shared/tables.js and adapt whichever consumer needs the other shape — don't try to make it "both" at once. The map pool is identical in shape everywhere, which is why it's the perfect place to start.
How to verify when you're done
中文详细说明
一句话总结
先说真心话:你一个人做出一个浏览器 FPS —— 约 117 把武器、39 张地图、机器人、击杀回放、商店经济系统、还有 AI 聊天 —— 这个工程量非常惊人,而且这么多系统居然能稳稳跑起来,是真功夫。👏
这个 issue 讲的不是 bug,而是一个专业习惯:现在有几个重要的列表被复制粘贴到了好几个文件里,每次改都得手动改所有副本。这个原则叫 DRY(Don't Repeat Yourself,不要重复自己),也叫单一数据源(single source of truth)。目前大部分副本还是一致的(很棒!),但我发现已经有一处悄悄对不上了—— 而这正是这个习惯要帮你避免的坑。
核心想法:建一个文件 shared/tables.js,把地图池和武器表都放进去,让 server.js 和 game.js 都从这一份里读。
我核对到的证据(真实的 文件:行号)
1. 地图池在 4 处 JS 代码里被复制(都是同一个 39 张地图的数组):
2. 地图列表在菜单 UI 里又写死了两遍 —— 而且这两遍已经对不上了:
👀 这就是整件事的最佳例子:两个本来应该一样的列表,现在不一样了。这不是谁犯了错 —— 副本随着时间推移就是会这样。只要改成单一数据源,这种情况根本不可能发生。
3. 武器表在 server.js 和 game.js 之间各有一份 —— 目前两边是一致的(很好!):
你自己写下那几句 "keep them in sync"(记得保持同步),说明你已经感觉到这个问题了。这个 issue 只是下一步:让电脑替你保持同步,这样你就不用自己记着了。😄
为什么"一份"更安全(虽然现在两边是一致的)
两份现在一致,靠的是运气 + 自律。可自律是会用完的 —— 比如晚上 11 点加第 118 把武器的时候。真正的风险不是"现在错了",而是:
- 加一张地图 → 你得在 5 个地方改(而且最容易忘的那行 UI,恰恰就是已经对不上的那行)。
- 加一把武器 → 要改 2 个文件里的
WEAPON_COSTS;漏掉一个,客户端显示的价格服务器就不认。
- 这种 bug 是静悄悄的:不会崩溃,列表只是默默地对不上,然后你几周后对着一个"奇怪的地图/价格 bug"抓狂。
改成单一数据源后,"加地图""加武器"就变成只在一个文件里改一行,所有用到它的地方自动更新。🎯
建议的第一步(小、安全、收益高)
先只搞地图池就好 —— 它副本最多、而且已经出现漂移,是最明显的胜利。武器表放到后面再单独处理。
-
新建 shared/tables.js。 有个小坑:public/game.js 是用普通的 <script src="game.js"> 加载的(public/index.html:828),不是 module,所以浏览器里不能用 import。上面英文段给了一个在 Node 和浏览器里都能用的小写法(module.exports + 挂全局变量),照抄即可。
-
server.js 里: 顶部加 const { MAP_POOL } = require('./shared/tables');,然后删掉 722-727 和 792-797 这两段内联数组,改用共享的那个。
-
public/index.html 里: 在第 828 行的 game.js 之前,加一行 <script src="shared/tables.js"></script>。
-
public/game.js 里: 把 15018 行的内联 pool 换成 SHARED_TABLES.MAP_POOL。
-
加分项(可选,但最爽的部分): 与其手写两行 <div class="map-card">,不如给每张地图存一个小条目(名字 + 颜色 + emoji),然后用循环从 MAP_POOL 生成这两行。这样能删掉约 60 行 HTML,顺便让上面那个"两行对不上"的 bug 结构上不可能再发生。
⚠️ 一个小提醒,免得到时候被坑:BUNDLES 在 server.js:132 是对象,在 game.js:1169 是数组。等你处理这个的时候,在 shared/tables.js 里选一种形状,在需要另一种形状的那一端做个转换 —— 不要想着一次就让它"两种都行"。地图池在所有地方形状都一样,所以它是最适合开刀的第一刀。
完成后怎么验证
你已经做到了大多数人不会做的一步 —— 给自己写下"保持同步"的注释,说明你的工程直觉很准。这一步只是把那份责任从你脑子里搬到代码里。搞定第一个表之后,你会上瘾的 —— 加新东西从此只改一个地方,爽得很。继续加油,期待第 118 把武器!🚀
🔗 Related / 相关: the duplicate id="map-row" is also tracked as a quick fix in #4 (good first issue). This issue is the bigger "one source of truth" picture — its optional step 5 (generate the map rows from data) would dissolve that duplicate entirely. / 重复的 id="map-row" 也作为快速修复记录在 #4(good first issue)里;本 issue 是更大的「单一数据源」蓝图,这里的可选第 5 步(用数据循环生成地图行)能从结构上彻底消除那个重复。
TL;DR (English)
First off — this codebase is genuinely impressive. A solo-built browser FPS with ~117 weapons, 39 maps, bots, a killcam, a full shop economy, and an AI chat is a lot of moving parts, and the fact that it all hangs together is real engineering.
This issue is about a pro habit, not a bug: a few important lists are currently copy-pasted into several files, and you have to remember to edit every copy by hand. That's called the DRY principle — "Don't Repeat Yourself," a.k.a. having a single source of truth. Right now most copies agree (nice!), but I found one place where they've already started to drift — which is exactly the trap this habit prevents.
The big idea: make one file,
shared/tables.js, that holds the map pool and the weapon tables, and have bothserver.jsandgame.jsread from that one copy.What I verified (real file:line evidence)
1. The map pool exists in 4 JavaScript copies — all the same big 39-map array:
server.js:722-727(insidecheckLobbyStart)server.js:792-797(insidetryPairPvpQueue)public/game.js:15018(theconst pool = [...]for match startup)2. The map list is ALSO hardcoded twice in the menu UI — and these two have ALREADY drifted:
public/index.html:483-519— first MAP row. Missingairport,trenches,chernobyl, and uses labels likeCLASSIC.public/index.html:537-562— second MAP row. Includesairport/trenches/chernobyl, uses emoji labels like🟩 BLANK.3. The weapon tables are mirrored across
server.jsandgame.js— and right now they DO agree (great!):WEAPON_COSTS—server.js:47andpublic/game.js:1085. Both 52 entries; spot-checkedak20: 250identical in both. The server file even has a comment atserver.js:45: "Mirrors the client-side WEAPON_COSTS table in game.js — keep them in sync."BUNDLES—server.js:132(an object) andpublic/game.js:1169(an array). Both 25 entries; comment atserver.js:130-131says "Keep in sync with public/game.js."WEAPON_DAMAGE— defined atserver.js:812.The fact that you wrote those "keep in sync" comments yourself shows you already felt the problem. This issue is just the next step: make the computer keep them in sync so you don't have to.
Why one copy is safer (even though they agree today)
Two copies that agree today is luck plus discipline, and discipline runs out at 11pm when you're adding weapon #118. The risk isn't "it's wrong now" — it's:
WEAPON_COSTSin 2 files; miss one and the client shows a price the server won't honor.With one source of truth, "add a map" or "add a weapon" becomes a one-line edit in one file, and every consumer updates for free. 🎯
Suggested first step (small, safe, high-payoff)
Start with just the map pool — it has the most copies and already shows drift, so it's the clearest win. Tackle the weapon tables in a follow-up.
Create
shared/tables.js. One catch:public/game.jsis loaded as a plain<script src="game.js">(public/index.html:828), not a module — so the browser can'timport. A friendly pattern that works in both Node and the browser:In
server.js:const { MAP_POOL } = require('./shared/tables');near the top, then delete the two inline arrays at722-727and792-797and use the shared one.In
public/index.html: add<script src="shared/tables.js"></script>before thegame.jstag at line 828.In
public/game.js: replace the inlinepoolat15018withSHARED_TABLES.MAP_POOL.Bonus (optional, the satisfying part): instead of hand-writing the two
<div class="map-card">rows in the HTML, give each map a tiny entry (label + color + emoji) and loop to build both rows fromMAP_POOL. That deletes ~60 lines of HTML and makes the two-rows-disagree bug above structurally impossible.How to verify when you're done
shared/tables.js).node --check server.jspasses, server boots, a match still loads a random map.中文详细说明
一句话总结
先说真心话:你一个人做出一个浏览器 FPS —— 约 117 把武器、39 张地图、机器人、击杀回放、商店经济系统、还有 AI 聊天 —— 这个工程量非常惊人,而且这么多系统居然能稳稳跑起来,是真功夫。👏
这个 issue 讲的不是 bug,而是一个专业习惯:现在有几个重要的列表被复制粘贴到了好几个文件里,每次改都得手动改所有副本。这个原则叫 DRY(Don't Repeat Yourself,不要重复自己),也叫单一数据源(single source of truth)。目前大部分副本还是一致的(很棒!),但我发现已经有一处悄悄对不上了—— 而这正是这个习惯要帮你避免的坑。
核心想法:建一个文件
shared/tables.js,把地图池和武器表都放进去,让server.js和game.js都从这一份里读。我核对到的证据(真实的 文件:行号)
1. 地图池在 4 处 JS 代码里被复制(都是同一个 39 张地图的数组):
server.js:722-727(checkLobbyStart函数里)server.js:792-797(tryPairPvpQueue函数里)public/game.js:15018(开局选地图的const pool = [...])2. 地图列表在菜单 UI 里又写死了两遍 —— 而且这两遍已经对不上了:
public/index.html:483-519—— 第一行 MAP,缺少airport、trenches、chernobyl,标签是CLASSIC之类。public/index.html:537-562—— 第二行 MAP,包含airport/trenches/chernobyl,标签是🟩 BLANK之类带 emoji 的。3. 武器表在
server.js和game.js之间各有一份 —— 目前两边是一致的(很好!):WEAPON_COSTS——server.js:47和public/game.js:1085。都是 52 条;抽查ak20: 250两边一致。server.js:45你自己还写了注释:"Mirrors the client-side WEAPON_COSTS table in game.js — keep them in sync."BUNDLES——server.js:132(是个对象)和public/game.js:1169(是个数组)。都是 25 条;server.js:130-131注释写着 "Keep in sync with public/game.js."WEAPON_DAMAGE—— 定义在server.js:812。你自己写下那几句 "keep them in sync"(记得保持同步),说明你已经感觉到这个问题了。这个 issue 只是下一步:让电脑替你保持同步,这样你就不用自己记着了。😄
为什么"一份"更安全(虽然现在两边是一致的)
两份现在一致,靠的是运气 + 自律。可自律是会用完的 —— 比如晚上 11 点加第 118 把武器的时候。真正的风险不是"现在错了",而是:
WEAPON_COSTS;漏掉一个,客户端显示的价格服务器就不认。改成单一数据源后,"加地图""加武器"就变成只在一个文件里改一行,所有用到它的地方自动更新。🎯
建议的第一步(小、安全、收益高)
先只搞地图池就好 —— 它副本最多、而且已经出现漂移,是最明显的胜利。武器表放到后面再单独处理。
新建
shared/tables.js。 有个小坑:public/game.js是用普通的<script src="game.js">加载的(public/index.html:828),不是 module,所以浏览器里不能用import。上面英文段给了一个在 Node 和浏览器里都能用的小写法(module.exports+ 挂全局变量),照抄即可。server.js里: 顶部加const { MAP_POOL } = require('./shared/tables');,然后删掉722-727和792-797这两段内联数组,改用共享的那个。public/index.html里: 在第 828 行的game.js之前,加一行<script src="shared/tables.js"></script>。public/game.js里: 把15018行的内联pool换成SHARED_TABLES.MAP_POOL。加分项(可选,但最爽的部分): 与其手写两行
<div class="map-card">,不如给每张地图存一个小条目(名字 + 颜色 + emoji),然后用循环从MAP_POOL生成这两行。这样能删掉约 60 行 HTML,顺便让上面那个"两行对不上"的 bug 结构上不可能再发生。完成后怎么验证
shared/tables.js里)。node --check server.js通过,服务器能启动,开局还是会随机一张地图。你已经做到了大多数人不会做的一步 —— 给自己写下"保持同步"的注释,说明你的工程直觉很准。这一步只是把那份责任从你脑子里搬到代码里。搞定第一个表之后,你会上瘾的 —— 加新东西从此只改一个地方,爽得很。继续加油,期待第 118 把武器!🚀