feat: 加天气 / 谷仓 / 建筑三套系统,收获改为入库 + 择时卖出
动手前先修掉了两处「死机制」,新增的三个系统各自激活一个: * 天气:小麦 2 天成熟、初始水分 2、每天只掉 1 点,等于永远不需要浇水。 现在水分消耗由天气决定(晴 1 / 多云 0 / 干旱 2,雨天反补 3),而 `state.forecast` 把明天的天气提前告诉你——「今晚浇不浇」成了能算的决策。 5 种天气、权重合计 100、同种不连出;暴风雨会毁未熟作物,温室免疫。 * 谷仓:市场波动原本纯装饰(收获当场按市价变现,没有择时空间)。现在收获与 收取只进 `state.inventory`,靠新增的 `sell` 换金币,价格波动才真正参与决策。 仓满时 `_store()` 抛 `granary_full` 且不改状态,作物原样留在地里。 * 建筑:新增 `build`,买的是「以后不用天天手动做」——水井自动浇干地、 喂食器自动喂食(只吃库存饲料,绝不自动花钱)、温室、谷仓扩容。 每天在 `advance_day` 里先于生长执行,同类越造越贵(×1.6)。 存档升到 `SAVE_VERSION=2`,配 `MIN_SAVE_VERSION=1` 向后兼容,旧档缺的新字段 一律走默认值,不作废玩家手里的档。`advance_day(weather=...)` 是留给测试的 确定性入口。 顺带修两个真 bug: * `daemon.py` 三处 `makedirs(os.path.dirname(path))`:`--pid-file pid.txt` 这种只给文件名的用法会让 dirname 变成空串,启动阶段直接崩。抽出 `ensure_parent_dir()` 兜底。 * `RemoteFarm.snapshot()` 是本地缓存,非实时客户端轮询会一直读到旧数据, 看起来像服务器卡死。补 docstring 指明要自己调 `refresh()`。 质量门禁:ruff format/check 全绿、pyright 0 错误、503 项测试 OK(skip 35)、 bench 无回归(brief 19.5µs / snapshot 48.5µs / POST full=1 400µs)。 另起真实服务器(--tick 3 + 预置存档)跑了 38 项端到端验收,全过。
This commit is contained in:
1 parent
a92c29ead2
commit
cf24cb43f1
21 files changed
+1349
-112
No files matched your search
+65
-11
@@ -142,6 +142,17 @@ DASHBOARD_HTML = r"""<!DOCTYPE html>
|
||||
<h2>市场行情 <em>红涨绿跌</em></h2>
|
||||
<table id="market"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>谷仓 <em id="granaryhint"></em></h2>
|
||||
<table id="granary"></table>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<button class="primary" data-act="sell">全部卖出</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>建筑 <em>花金币换自动化</em></h2>
|
||||
<div class="row" id="buildings"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>服务器排行榜 <em id="worldinfo"></em></h2>
|
||||
<table id="board"></table>
|
||||
@@ -165,6 +176,13 @@ const cropNames = {
|
||||
corn: "玉米", pumpkin: "南瓜", strawberry: "草莓"
|
||||
};
|
||||
|
||||
function goodsName(key) {
|
||||
const g = ((catalog && catalog.goods) || []).find((x) => x.key === key);
|
||||
if (g) return g.name;
|
||||
if (key.indexOf("animal:") === 0) return key.slice(7);
|
||||
return cropNames[key] || key;
|
||||
}
|
||||
|
||||
function toast(text) {
|
||||
const el = $("toast");
|
||||
el.textContent = text;
|
||||
@@ -277,8 +295,12 @@ function render() {
|
||||
if (!state) return;
|
||||
const s = state;
|
||||
$("stats").innerHTML = [
|
||||
["第几天", s.day], ["金币", s.gold], ["等级", "Lv." + s.level + " (" + s.exp + "/" + s.exp_needed + ")"],
|
||||
["行动点", s.ap + "/" + s.max_ap], ["饲料", s.feed], ["净资产", Math.round(s.net_worth)],
|
||||
["第几天", s.day],
|
||||
["天气", (s.weather_name || "—") + " → " + (s.forecast_name || "?")],
|
||||
["金币", s.gold], ["等级", "Lv." + s.level + " (" + s.exp + "/" + s.exp_needed + ")"],
|
||||
["行动点", s.ap + "/" + s.max_ap], ["饲料", s.feed],
|
||||
["仓储", (s.storage_used || 0) + "/" + (s.granary_capacity || 0)],
|
||||
["净资产", Math.round(s.net_worth)],
|
||||
["在线", s.online !== undefined ? s.online : "—"],
|
||||
["下一天", fmtLeft(s.seconds_to_tick)]
|
||||
].map((p) => '<div class="stat"><small>' + p[0] + '</small><b>' + p[1] + '</b></div>').join("");
|
||||
@@ -314,18 +336,48 @@ function render() {
|
||||
(a.pending > 0 ? '<button data-c="' + a.id + '">收取</button>' : '') + '</li>'
|
||||
).join("") : '<li style="color:#6b7a66">牧场空空的,先买一只吧</li>';
|
||||
|
||||
/* 市场 */
|
||||
const rows = Object.entries(s.market).filter((kv) => kv[0].indexOf("animal:") !== 0);
|
||||
const base = { wheat: 14, carrot: 26, tomato: 52, corn: 95, pumpkin: 180, strawberry: 320 };
|
||||
$("market").innerHTML = '<tr><th>作物</th><th class="num">现价</th><th class="num">基准</th></tr>' +
|
||||
rows.sort((a, b) => b[1] - a[1]).map((kv) => {
|
||||
const b = base[kv[0]] || kv[1];
|
||||
/* 市场:基准价直接用 catalog.goods,别再在 JS 里抄一份常量表 */
|
||||
const gmap = {};
|
||||
((catalog && catalog.goods) || []).forEach((g) => { gmap[g.key] = g; });
|
||||
$("market").innerHTML = '<tr><th>货物</th><th class="num">现价</th><th class="num">基准</th></tr>' +
|
||||
Object.entries(s.market).sort((a, b) => b[1] - a[1]).map((kv) => {
|
||||
const g = gmap[kv[0]] || {};
|
||||
const b = g.base_price || kv[1];
|
||||
const cls = kv[1] > b * 1.03 ? "up" : (kv[1] < b * 0.97 ? "down" : "flat");
|
||||
const arrow = cls === "up" ? "↑" : (cls === "down" ? "↓" : "→");
|
||||
return '<tr><td>' + (cropNames[kv[0]] || kv[0]) + '</td><td class="num ' + cls + '">' +
|
||||
kv[1].toFixed(0) + " " + arrow + '</td><td class="num">' + b + '</td></tr>';
|
||||
return '<tr><td>' + goodsName(kv[0]) + '</td><td class="num ' + cls + '">' +
|
||||
kv[1].toFixed(0) + " " + arrow + '</td><td class="num">' + b.toFixed(0) + '</td></tr>';
|
||||
}).join("");
|
||||
|
||||
/* 谷仓:卖货是唯一把存货变成金币的出口,所以每个存项都直接给一个卖出按钮 */
|
||||
const inv = Object.entries(s.inventory || {}).filter((kv) => kv[1] > 0);
|
||||
$("granaryhint").textContent =
|
||||
"仓容 " + (s.storage_used || 0) + "/" + (s.granary_capacity || 0);
|
||||
$("granary").innerHTML =
|
||||
'<tr><th>货物</th><th class="num">存量</th><th class="num">现价</th>' +
|
||||
'<th class="num">基准</th><th></th></tr>' +
|
||||
(inv.length ? inv.sort((a, b) => b[1] - a[1]).map((kv) => {
|
||||
const g = gmap[kv[0]] || {};
|
||||
const price = g.price !== undefined ? g.price : ((s.market || {})[kv[0]] || 0);
|
||||
const b = g.base_price || 0;
|
||||
const cls = b && price > b * 1.03 ? "up" : (b && price < b * 0.97 ? "down" : "flat");
|
||||
return '<tr><td>' + goodsName(kv[0]) + '</td><td class="num">' + kv[1] +
|
||||
'</td><td class="num ' + cls + '">' + price.toFixed(0) + '</td>' +
|
||||
'<td class="num">' + (b ? b.toFixed(0) : "—") + '</td>' +
|
||||
'<td class="num"><button data-s="' + kv[0] + '">卖</button></td></tr>';
|
||||
}).join("")
|
||||
: '<tr><td colspan="5" style="color:#6b7a66">空仓:收获与收取的产出会自动入库</td></tr>');
|
||||
|
||||
/* 建筑 */
|
||||
const builds = (catalog && catalog.buildings) || [];
|
||||
$("buildings").innerHTML = builds.length ? builds.map((b) => {
|
||||
const disabled = b.locked || b.maxed || s.gold < b.next_cost;
|
||||
const label = b.name + " " + (b.maxed ? "已满" : b.next_cost + " 金") +
|
||||
"(" + b.owned + "/" + b.max_level + ")";
|
||||
return '<button data-b="' + b.key + '"' + (disabled ? " disabled" : "") +
|
||||
' title="' + b.effect + '">' + label + '</button>';
|
||||
}).join("") : '<span style="color:#6b7a66">目录加载中…</span>';
|
||||
|
||||
renderLog(null);
|
||||
renderBoard();
|
||||
}
|
||||
@@ -383,6 +435,8 @@ document.addEventListener("click", (ev) => {
|
||||
}
|
||||
else if (t.dataset.f) act("feed", { animal_id: +t.dataset.f });
|
||||
else if (t.dataset.c) act("collect", { animal_id: +t.dataset.c });
|
||||
else if (t.dataset.s) act("sell", { key: t.dataset.s });
|
||||
else if (t.dataset.b) act("build", { key: t.dataset.b });
|
||||
else if (t.dataset.act === "buy_animal") act("buy_animal", { kind: $("buyanimal").value });
|
||||
else if (t.dataset.act === "buy_feed") act("buy_feed", { quantity: 10 });
|
||||
else if (t.dataset.act === "reset") { if (confirm("确定重置牧场?所有进度都会清空")) act("reset"); }
|
||||
@@ -443,7 +497,7 @@ def dashboard_text(host: str, port: int) -> str:
|
||||
'POST /api/actions {"action":"...","params":{...}} 通用入口\n'
|
||||
'POST /api/batch {"actions":[{...},{...}]} 批量\n'
|
||||
"POST /api/plant|water|harvest|harvest_all|buy_animal|feed|\n"
|
||||
" collect|buy_feed|buy_plot|pause|save|reset 便捷入口\n"
|
||||
" collect|buy_feed|buy_plot|sell|build|pause|save|reset 便捷入口\n"
|
||||
'POST /api/farms {"farm":"名字"} 创建/进入牧场\n'
|
||||
"POST /api/admin/save 全部存档\n"
|
||||
"POST /api/admin/shutdown 优雅停机\n"
|
||||
|
||||
Reference in new issue
Block a user