diff --git a/.workbuddy/memory/2026-09-25.md b/.workbuddy/memory/2026-09-25.md index cec5120..8217078 100644 --- a/.workbuddy/memory/2026-09-25.md +++ b/.workbuddy/memory/2026-09-25.md @@ -42,3 +42,21 @@ - 真机验收:登录、canvas 点击浇水(AP 4→3、水分 2→3、toast)、 悬停 tooltip、截图目检全过;errors/console 干净。534 项测试全绿。 +## 2026-09-25 移除行动点系统 + +- 用户拍板:「谁家浇水还要体力啊 体力删掉 都已经有时间了」——时间 + (40 分钟一天、只能自然流动)本身就是唯一节奏阀门,AP 是重复惩罚。 +- 连根拔的触点:config(START_AP/AP_COST)、models(ap/max_ap 字段,旧档 + payload 的 ap 键 from_dict 自然忽略)、engine(_spend_ap + 8 调用点 + + 每日恢复)、bot(AP 分支)、client/TUI/main/README/规则文本。 +- **SQLite 老库迁移**:ap 是标量 NOT NULL 列,connect() 里 + `PRAGMA table_info` 发现即 `ALTER TABLE farms DROP COLUMN ap` + (Python 3.13 的 SQLite ≥3.35 支持)。实测 saves/farm.db 自动迁完。 +- 网页端:删"行动点"统计卡;"下一天"倒计时改**客户端本地走秒** + (render 记 tickLeft/tickStamp 基点,1s 定时器补 SSE 推送间隙); + 字体栈补 MiSans/HarmonyOS Sans/PingFang、正文 15px、数字 + tabular-nums、canvas 小字加粗。 +- 测试 -2(AP 专属用例没了)改 5,532 项全绿;提交 15d290a 已推送。 + 真机:老库迁移 ✅、无行动点卡 ✅、倒计时 39:04→39:02 走秒 ✅。 + + diff --git a/farm/server/webui.py b/farm/server/webui.py index c0ea525..63cc28b 100644 --- a/farm/server/webui.py +++ b/farm/server/webui.py @@ -288,6 +288,7 @@ async function act(action, params) { }); if (data) { toast(data.message); + mapJuice(action, params); if (data.state) refreshFromBrief(data.state); } } @@ -493,8 +494,57 @@ async function renderBoard() { const MAP = { cv: null, ctx: null, scale: 1, w: 0, h: 0, field: null, pen: null, builds: [], stripH: 0, - walkers: [], animalIds: "", drops: null, clouds: null, hover: null + walkers: [], animalIds: "", drops: null, clouds: null, hover: null, fx: [] }; + +/* 动作粒子:浇水溅水花、收获爆金点、播种扬土,配一枚上浮的表情小字。 + act() 成功后调 mapJuice(action, params),坐标由地块号/牲畜 id 反查。 */ +function plotXY(position) { + const f = MAP.field; + const i = (state.plots || []).findIndex((p) => p.position === position); + if (!f || i < 0) return null; + return { x: f.x + (i % f.cols) * (f.ts + f.gap) + f.ts / 2, + y: f.y + Math.floor(i / f.cols) * (f.ts + f.gap) + f.ts / 2 }; +} +function animalXY(id) { + const wd = (MAP.walkers || []).find((w) => w.a.id === id); + return wd ? { x: wd.x, y: wd.y } : null; +} +function mapJuice(action, params) { + if (!MAP.field || !params) return; + let at = null; + if (params.plot !== undefined) at = plotXY(params.plot); + else if (params.animal_id !== undefined) at = animalXY(params.animal_id); + if (!at) return; + const conf = { + water: { color: "#7cc4f5", n: 14, text: "💧" }, + plant: { color: "#a97b4f", n: 12, text: "🌱" }, + harvest: { color: "#ffd54d", n: 22, text: "✨" }, + feed: { color: "#e8b04a", n: 12, text: "🍽" }, + collect: { color: "#ffd54d", n: 18, text: "✨" } + }[action]; + if (!conf) return; + for (let i = 0; i < conf.n; i++) { + const a = Math.random() * 6.28, v = 34 + Math.random() * 74; + MAP.fx.push({ x: at.x, y: at.y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - 52, + t: 0, life: 0.55 + Math.random() * 0.45, color: conf.color, + r: 1.6 + Math.random() * 2.4 }); + } + MAP.fx.push({ x: at.x, y: at.y - 8, floater: true, t: 0, life: 1.1, text: conf.text }); +} + +/* 一天的进度 → 画面光色:清晨淡金 → 正午不叠色 → 黄昏橙 → 入夜靛紫。 + 时间不该只在倒计时数字上流动,田里的光也要跟着走。 */ +function dayTint(prog) { + if (prog < 0.12) { + const k = prog / 0.12; + return [255, 190, 110, 0.14 * (1 - k)]; + } + if (prog < 0.70) return null; + const k = (prog - 0.70) / 0.30; + return [Math.round(mapLerp(255, 104, k)), Math.round(mapLerp(150, 80, k)), + Math.round(mapLerp(72, 190, k)), 0.05 + 0.15 * k]; +} const cropEmoji = { wheat: "🌾", carrot: "🥕", tomato: "🍅", corn: "🌽", pumpkin: "🎃", strawberry: "🍓" }; const animalEmoji = { chicken: "🐔", sheep: "🐑", cow: "🐄" }; const buildingInfo = { @@ -584,6 +634,14 @@ function mapStep(dt, now) { } } (MAP.clouds || []).forEach((c) => { c.x += c.v * dt; if (c.x > MAP.w + 70) c.x = -70; }); + /* 特效粒子:受重力下坠、寿命到了消失;浮字匀速上飘 */ + for (let i = MAP.fx.length - 1; i >= 0; i--) { + const f = MAP.fx[i]; + f.t += dt; + if (f.t >= f.life) { MAP.fx.splice(i, 1); continue; } + if (f.floater) { f.y -= 26 * dt; } + else { f.vy += 190 * dt; f.x += f.vx * dt; f.y += f.vy * dt; } + } /* 牲畜漫步 + 边界回弹(pen 未布局时直接跳过) */ const pen = MAP.pen; if (!pen) return; @@ -607,7 +665,7 @@ function mapRoundRect(x, y, w, h, r) { c.closePath(); } -function mapDrawTile(p, x, y, ts) { +function mapDrawTile(p, x, y, ts, now) { const c = MAP.ctx; const wet = p.moisture / (p.water_capacity || 3); const r = Math.round(mapLerp(206, 118, wet)); @@ -628,7 +686,10 @@ function mapDrawTile(p, x, y, ts) { const ratio = p.grow_days ? p.growth / p.grow_days : 1; const size = Math.round(16 + ratio * 20); c.save(); - if (p.is_ripe) { c.shadowColor = "rgba(255,200,40,.9)"; c.shadowBlur = 12; } + if (p.is_ripe) { + c.shadowColor = "rgba(255,200,40,.9)"; + c.shadowBlur = 10 + Math.sin(now / 320 + p.position * 1.7) * 5; + } c.font = size + 'px "Segoe UI Emoji","Noto Color Emoji",sans-serif'; c.textAlign = "center"; c.textBaseline = "middle"; c.fillText(cropEmoji[p.crop] || "🌱", x + ts / 2, y + ts / 2 - 3); @@ -698,7 +759,7 @@ function mapDraw(now) { const f = MAP.field, ts = f.ts; (state.plots || []).forEach((p, i) => { const col = i % f.cols, row = Math.floor(i / f.cols); - mapDrawTile(p, f.x + col * (ts + f.gap), f.y + row * (ts + f.gap), ts); + mapDrawTile(p, f.x + col * (ts + f.gap), f.y + row * (ts + f.gap), ts, now); }); /* 牧场围栏 */ const pen = MAP.pen; @@ -756,6 +817,26 @@ function mapDraw(now) { c.fillText(kv[1] > 1 ? info[1] + " ×" + kv[1] : info[1], cx, y0 + 32); }); } + /* 昼夜光色 + 动作特效:盖在场地上、悬停提示之下 */ + const dayTotal = state.tick_seconds || 2400; + const tint = dayTint(1 - Math.min(1, (state.seconds_to_tick || 0) / dayTotal)); + if (tint && tint[3] > 0.004) { + c.fillStyle = "rgba(" + tint[0] + "," + tint[1] + "," + tint[2] + "," + tint[3].toFixed(3) + ")"; + c.fillRect(0, 0, MAP.w, MAP.h); + } + (MAP.fx || []).forEach((f) => { + const k = 1 - f.t / f.life; + c.globalAlpha = f.floater ? Math.min(1, k * 1.8) : k; + if (f.floater) { + c.font = '700 16px "Segoe UI Emoji","Noto Color Emoji",sans-serif'; + c.textAlign = "center"; c.textBaseline = "middle"; + c.fillText(f.text, f.x, f.y); + } else { + c.fillStyle = f.color; + c.beginPath(); c.arc(f.x, f.y, f.r, 0, 6.29); c.fill(); + } + c.globalAlpha = 1; + }); /* 悬停提示框 */ const h = MAP.hover; if (h) { @@ -821,8 +902,9 @@ function mapBind() { const pt = mapPoint(ev); const hit = mapHit(pt.x, pt.y); MAP.hover = hit ? { type: hit.type, p: hit.p, a: hit.a, x: pt.x, y: pt.y } : null; + MAP.cv.style.cursor = hit ? "pointer" : "default"; }); - MAP.cv.addEventListener("pointerleave", () => { MAP.hover = null; }); + MAP.cv.addEventListener("pointerleave", () => { MAP.hover = null; MAP.cv.style.cursor = "default"; }); MAP.cv.addEventListener("click", (ev) => { const pt = mapPoint(ev); const hit = mapHit(pt.x, pt.y);