diff --git a/src/games/components/StarveGame.vue b/src/games/components/StarveGame.vue index 39815de..1182e57 100644 --- a/src/games/components/StarveGame.vue +++ b/src/games/components/StarveGame.vue @@ -56,6 +56,9 @@ const hint = ref('') const nearActions = ref([]) const craftOpen = ref(false) const craftTab = ref('tools') +const packOpen = ref(false) +const boxView = ref(null) // { id, slots: uiSlot[] } +const slotTip = ref({ show: false, text: '', x: 0, y: 0 }) const showMap = ref(false) const saving = ref(false) const isCoop = ref(false) @@ -182,6 +185,8 @@ let shakeT = 0 let shakeAmp = 0 let flashRed = 0 let potOpenId = 0 +let boxOpenId = 0 +const BOX_SLOTS = 12 let invDirty = true let hudT = 0 let mapT = 0 @@ -411,6 +416,43 @@ function useToolDur(p) { invDirty = true if (eq.dur <= 0) breakEquip(p, 'hand', `${ITEMS[eq.code].name}用坏了!`) } +// 默认工具配方:斧/镐/铲(mod 高级工具走库存匹配,不自动合成) +const TOOL_CRAFT = { axe: 'axe', pick: 'pickaxe', dig: 'shovel' } +// 在背包/物品栏找对应工具类型的格子 +function findToolSlot(p, toolType) { + for (const area of ['main', 'pack']) { + const arr = slotArea(p, area) + if (!arr) continue + for (let i = 0; i < arr.length; i++) { + const s = arr[i] + if (s && ITEMS[s.code]?.tool === toolType) return { area, i } + } + } + return null +} +// 确保手上是指定工具:已装备→跳过;背包有→自动装备;没有→尝试合成默认配方再装备 +function ensureHandTool(p, toolType) { + if (!toolType) return true + if (handItem(p)?.tool === toolType) return true + const found = findToolSlot(p, toolType) + if (found) { + equipFromSlot(p, found.area, found.i) + return handItem(p)?.tool === toolType + } + const code = TOOL_CRAFT[toolType] + if (!code) { + ptip(p, `需要${toolType === 'axe' ? '斧头' : toolType === 'pick' ? '镐子' : '铲子'}`) + return false + } + craftFor(p, code) + if (handItem(p)?.tool === toolType) return true + const again = findToolSlot(p, toolType) + if (again) { + equipFromSlot(p, again.area, again.i) + return handItem(p)?.tool === toolType + } + return false +} // 承伤管线:护甲减免 → 扣血 → 死亡 function damagePlayer(p, dmg, label) { if (p.dead) return @@ -476,7 +518,7 @@ function makeEnt(type, x, y, extra = {}) { tool: conf.tool || '', drops: conf.drops || null, regrow: conf.regrow || 0, regrowT: 0, stub: false, vanish: !!conf.vanish, mobile: !!conf.mobile, nest: !!conf.nest, - struct: !!conf.struct, pot: !!conf.pot, lurk: !!conf.lurk, + struct: !!conf.struct, pot: !!conf.pot, chest: !!conf.chest, lurk: !!conf.lurk, solid: !!conf.solid, loot: !!conf.loot, hp: conf.hp || 0, maxHp: conf.hp || 0, deco: !RES[type], @@ -484,6 +526,12 @@ function makeEnt(type, x, y, extra = {}) { } if (type === 'trap') e.state = 'armed' if (type === 'crockpot') { e.ing = []; e.cookT = 0; e.dish = '' ; e.state = 'idle' } + if (type === 'woodchest') { + if (!e.mx) e.mx = {} + if (!Array.isArray(e.mx.slots)) e.mx.slots = makeSlots(BOX_SLOTS) + while (e.mx.slots.length < BOX_SLOTS) e.mx.slots.push(null) + e.mx.open = false + } if (type === 'tentacle') { e.state = 'lurk'; e.atkT = 0; e.cd = 0; e.dir = 1 } if (type === 'rabbit') { e.vx = 0; e.vy = 0; e.dir = 1; e.hop = Math.random() * 7; e.flee = false; e.wanderT = 0 } if (type === 'rabbithole') { e.respawnT = 0; e.rabbitId = 0 } @@ -540,11 +588,12 @@ function killMob(m, credit) { if (Math.random() < prob) dropLoot(m.x, m.y, code, n) }) if (m.kind === 'deerclops') tip('独眼巨鹿倒下了!它掉落了眼球与大肉') + callHook(modRt, 'onKillMob', gCtx, m, credit) } // 巨鹿拆家:建筑损毁并崩出部分材料 function smashStructure(e) { tip(`${NAMES[e.type] || '建筑'}被独眼巨鹿砸毁了!`) - const refund = { sciencemachine: { rock: 2, log: 2 }, alchemyengine: { rock: 2, gold: 2 }, crockpot: { rock: 3 }, pighouse: { pigskin: 1, log: 2 } }[e.type] + const refund = { sciencemachine: { rock: 2, log: 2 }, alchemyengine: { rock: 2, gold: 2 }, crockpot: { rock: 3 }, woodchest: { log: 2 }, pighouse: { pigskin: 1, log: 2 } }[e.type] if (refund) Object.entries(refund).forEach(([code, n]) => dropLoot(e.x, e.y, code, n)) removeEntity(e) shake(6) @@ -662,10 +711,46 @@ function eventToWorld(ev) { function screenDeltaToLocal(dx, dy) { return pageLandscapeOn() ? { x: -dy, y: dx } : { x: dx, y: dy } } +// 框选拖拽:若 mod 处理了 pointer 序列,则抑制随后的 click 选中/走路 +let suppressClick = false +function onCanvasPointerDown(ev) { + if (!playing.value || !player || player.dead || waitingWorld.value) return + if (ev.button != null && ev.button !== 0) return + const { x: wxp, y: wyp } = eventToWorld(ev) + if (callHandled(modRt, 'onPointerDown', gCtx, wxp, wyp, ev)) { + suppressClick = true + ev.preventDefault?.() + } +} +function onCanvasPointerMove(ev) { + if (!playing.value || !player || player.dead || waitingWorld.value) { + updateCursor(ev) + return + } + const { x: wxp, y: wyp } = eventToWorld(ev) + if (callHandled(modRt, 'onPointerMove', gCtx, wxp, wyp, ev)) { + suppressClick = true + ev.preventDefault?.() + return + } + updateCursor(ev) +} +function onCanvasPointerUp(ev) { + if (!playing.value || !player || player.dead || waitingWorld.value) return + const { x: wxp, y: wyp } = eventToWorld(ev) + if (callHandled(modRt, 'onPointerUp', gCtx, wxp, wyp, ev)) { + suppressClick = true + ev.preventDefault?.() + } +} // 画布点击:优先怪 > 火 > 实体 > 走路 function onCanvasClick(ev) { ev.preventDefault?.() canvas.value?.blur?.() + if (suppressClick) { + suppressClick = false + return + } if (!playing.value || !player || player.dead || waitingWorld.value) return const { x: wxp, y: wyp } = eventToWorld(ev) // mod 点击钩子:任一 mod 处理并返回 true 则吃掉本次点击(不选中怪物/火/实体,也不点地移动) @@ -684,7 +769,9 @@ function onCanvasClick(ev) { let ed = Infinity entities.forEach((e) => { if (e.deco && e.type !== 'stump') return - if (e.stub || e.solid) return + if (e.stub) return + // 栅栏等 solid 不可点选;门始终可点以开关 + if (e.solid && e.type !== 'gate' && e.type !== 'campgate') return const d = Math.hypot(e.x - wxp, e.y - wyp) if (d < Math.max(26, e.size * 0.9) && d < ed) { ed = d; ent = e } }) @@ -717,7 +804,8 @@ function updateCursor(ev) { let ed = Infinity entities.forEach((e) => { if (e.deco && e.type !== 'stump') return - if (e.stub || e.solid) return + if (e.stub) return + if (e.solid && e.type !== 'gate' && e.type !== 'campgate') return const d = Math.hypot(e.x - wxp, e.y - wyp) if (d < Math.max(26, e.size * 0.9) && d < ed) { ed = d; ent = e } }) @@ -753,16 +841,20 @@ function resolveTarget(t) { return { kind: 'walk', x: t.x, y: t.y } } function canInteractEnt(e) { - if (!e || e.stub || e.solid) return false + if (!e || e.stub) return false + // 关着的门仍可点开;其它 solid 不可交互 + if (e.solid && e.type !== 'gate' && e.type !== 'campgate') return false if (e.deco && e.type !== 'stump') return false return !!(e.loot || e.type === 'trap' || (e.pot && (e.state === 'done' || e.state === 'idle')) || (e.drops && !e.mobile) || e.type === 'stump' || e.struct || modRt.entKinds[e.type]?.interact) } function entActionLabel(e) { + if (e.type === 'gate' || e.type === 'campgate') return e.mx?.open ? '关门' : '开门' if (modRt.entKinds[e.type]) return NAMES[e.type] || '交互' if (e.loot) return `拾取${ITEMS[e.code]?.name || NAMES[e.type] || ''}` if (e.type === 'trap') return e.state === 'caught' ? '收取猎物' : '收回陷阱' if (e.pot) return e.state === 'done' ? '取出料理' : '打开锅' + if (e.chest || e.type === 'woodchest') return '打开木箱' if (e.type === 'stump') return '挖掘树桩' if (e.struct) return NAMES[e.type] || '查看' return `采集${NAMES[e.type] || ''}` @@ -984,6 +1076,24 @@ function hitEntity(p, e) { markDirty(e) } else if (p === player) { potOpenId = e.id + boxOpenId = 0 + packOpen.value = false + } + p.target = null + return + } + if (e.chest || e.type === 'woodchest') { + if (p === player) { + ensureBoxSlots(e) + e.mx.open = true + boxOpenId = e.id + potOpenId = 0 + potView.value = null + packOpen.value = false + craftOpen.value = false + markDirty(e) + invDirty = true + ptip(p, '打开了木箱') } p.target = null return @@ -997,8 +1107,7 @@ function hitEntity(p, e) { return } if (e.type === 'stump') { - if (handItem(p)?.tool !== 'dig') { - ptip(p, '需要铲子才能挖树桩') + if (!ensureHandTool(p, 'dig')) { p.target = null return } @@ -1036,9 +1145,7 @@ function hitEntity(p, e) { // 资源采集 if (!e.drops) { p.target = null; return } if (e.tool) { - const need = e.tool === 'axe' ? 'axe' : 'pick' - if (handItem(p)?.tool !== need) { - ptip(p, `需要${need === 'axe' ? '斧头' : '镐子'}(装备到手上)`) + if (!ensureHandTool(p, e.tool)) { p.target = null return } @@ -1126,9 +1233,124 @@ function feedFire(p, f) { } ptip(p, '没有燃料(木头/草/树枝)') } -// 点击物品格:吃 / 用 / 装备 / 放置 +function ensureBoxSlots(e) { + if (!e.mx) e.mx = {} + if (!Array.isArray(e.mx.slots)) e.mx.slots = makeSlots(BOX_SLOTS) + while (e.mx.slots.length < BOX_SLOTS) e.mx.slots.push(null) + if (e.mx.slots.length > BOX_SLOTS) e.mx.slots = e.mx.slots.slice(0, BOX_SLOTS) + return e.mx.slots +} +function closeBox() { + if (boxOpenId) { + const e = entities.find((x) => x.id === boxOpenId) + if (e?.mx) { + e.mx.open = false + markDirty(e) + } + } + boxOpenId = 0 + boxView.value = null +} +function depositToBox(p, area, i) { + const e = entities.find((x) => x.id === boxOpenId) + if (!e) return false + const arr = slotArea(p, area) + const s = arr?.[i] + if (!s) return false + const slots = ensureBoxSlots(e) + const max = stackMax(s.code) + // 先合并堆叠 + for (const bs of slots) { + if (bs && bs.code === s.code && bs.n < max) { + const take = Math.min(s.n, max - bs.n) + if (ITEMS[s.code]?.perish) { + bs.fresh = ((bs.fresh ?? 1) * bs.n + (s.fresh ?? 1) * take) / (bs.n + take) + } + bs.n += take + s.n -= take + if (s.n <= 0) arr[i] = null + markDirty(e) + invDirty = true + ptip(p, `存入${ITEMS[s.code]?.name || s.code}`) + return true + } + } + const empty = slots.findIndex((x) => !x) + if (empty < 0) { + ptip(p, '箱子已满') + return true + } + slots[empty] = { ...s } + arr[i] = null + markDirty(e) + invDirty = true + ptip(p, `存入${ITEMS[s.code]?.name || s.code}`) + return true +} +function withdrawFromBox(p, bi) { + const e = entities.find((x) => x.id === boxOpenId) + if (!e) return + const slots = ensureBoxSlots(e) + const s = slots[bi] + if (!s) return + addItem(p, s.code, s.n, s.fresh ?? 1, s.dur ?? null) + slots[bi] = null + markDirty(e) + invDirty = true + ptip(p, `取出${ITEMS[s.code]?.name || s.code}`) +} +function showSlotTip(ev, text) { + if (!text) { + slotTip.value = { show: false, text: '', x: 0, y: 0 } + return + } + const wrap = ev.currentTarget?.closest?.('.starve-wrap') || canvas.value?.parentElement + const rect = wrap?.getBoundingClientRect?.() || { left: 0, top: 0 } + slotTip.value = { + show: true, + text, + x: ev.clientX - rect.left, + y: ev.clientY - rect.top - 12, + } +} +function hideSlotTip() { + slotTip.value.show = false +} +function tipTextForSlot(s) { + if (!s) return '' + let t = s.name || s.code + if (s.n > 1) t += ` ×${s.n}` + return t +} +function tipTextForEquip(sl) { + const eq = uiEquip.value[sl] + if (!eq) return sl === 'hand' ? '手部' : sl === 'body' ? '身体' : '头部' + return eq.name || eq.code +} +function togglePack() { + if (!uiPack.value) { + tip('需要装备背包才能打开') + return + } + packOpen.value = !packOpen.value + if (packOpen.value) { + craftOpen.value = false + closeBox() + potOpenId = 0 + potView.value = null + } +} +// 点击物品格:吃 / 用 / 装备 / 放置;箱子打开时改为存入 function clickSlot(area, i) { if (!player || player.dead || !playing.value) return + if (boxOpenId) { + if (coop && !coop.isHost) { + coop.send('starve_input', { seat: coop.mySeat, act: { type: 'boxdep', id: boxOpenId, area, i } }) + return + } + depositToBox(player, area, i) + return + } if (potOpenId) { const pot = entities.find((e) => e.id === potOpenId) const s = slotArea(player, area)?.[i] @@ -1144,6 +1366,14 @@ function clickSlot(area, i) { } useItemAt(player, area, i) } +function clickBoxSlot(bi) { + if (!player || player.dead || !boxOpenId) return + if (coop && !coop.isHost) { + coop.send('starve_input', { seat: coop.mySeat, act: { type: 'boxwit', id: boxOpenId, i: bi } }) + return + } + withdrawFromBox(player, bi) +} // 使用某格物品(主机侧) function useItemAt(p, area, i) { const arr = slotArea(p, area) @@ -1334,6 +1564,7 @@ const gCtx = { get season() { return seasonNow.code }, get time() { return time }, get day() { return day.value }, + get dayTime() { return dayTime }, get mon() { return MON }, get items() { return ITEMS }, get tune() { return TUNE }, @@ -1341,6 +1572,20 @@ const gCtx = { get worldH() { return WORLD.h }, isLand: (x, y) => !tiles || isLand(tiles, x, y), litAt: (x, y) => litAt(x, y), + // 推进游戏内时刻;跨日时与 updateDayNight 一致触发 day++ / onNewDay + setDayTime: (t) => { + let nt = Number(t) + if (!Number.isFinite(nt)) return + while (nt >= DAY_LEN) { + nt -= DAY_LEN + day.value += 1 + addScore(100) + onNewDay() + } + if (nt < 0) nt = 0 + dayTime = nt + phaseName = phaseOf(seasonNow.code, dayTime) + }, damagePlayer: (p, dmg, label) => damagePlayer(p, dmg, label), addEntity: (type, x, y, extra) => addEntity(type, x, y, extra), removeEntity: (e) => removeEntity(e), @@ -1376,6 +1621,24 @@ const gCtx = { }, swing: (p, act) => swingAct(p, act), heatAt: (p) => heatAt(p), + cookPot: (codes) => cookPot(codes), + // 按物品 code 对玩家施加 food 效果(食堂点餐等,不扣背包) + applyFood: (p, code, fresh = 1) => { + const it = ITEMS[code] + if (!it?.food || code === 'rot') return false + const st = freshStage({ fresh }) + const f = it.food + p.hunger = Math.min(CAP.hunger, p.hunger + Math.max(0, f.hunger) * st.mult) + if (f.hp > 0 && st.hpOk) p.hp = Math.min(CAP.hp, p.hp + f.hp) + else if (f.hp < 0) p.hp = Math.max(1, p.hp + f.hp) + if (f.san) p.san = Math.max(0, Math.min(CAP.san, p.san + f.san)) + swingAct(p, 'eat') + const parts = [`饥饿 +${Math.round(f.hunger * st.mult)}`] + if (f.hp) parts.push(`生命 ${f.hp > 0 ? '+' : ''}${st.hpOk || f.hp < 0 ? f.hp : 0}`) + if (f.san) parts.push(`理智 ${f.san > 0 ? '+' : ''}${f.san}`) + ptip(p, `吃下${it.name}${st.label} · ${parts.join(' ')}`) + return true + }, // 新增火焰(供 mod 使用):eternal 火焰永久燃烧、免疫消耗与雨天加速 addFire: (x, y, o = {}) => { const f = { fid: fid++, x, y, ttl: o.ttl ?? 50, max: o.max ?? 90, pit: !!o.pit, eternal: !!o.eternal } @@ -1888,6 +2151,18 @@ function syncHud(force = false) { potView.value = { id: pot.id, ing: [...(pot.ing || [])], state: pot.state, t: Math.ceil(pot.cookT || 0), dish: pot.dish } } } else if (potView.value) potView.value = null + // 木箱面板 + if (boxOpenId) { + const box = entities.find((e) => e.id === boxOpenId) + if (!box || Math.hypot(box.x - player.x, box.y - player.y) > 130) { + closeBox() + } else { + const slots = ensureBoxSlots(box) + boxView.value = { id: box.id, slots: slots.map(packUiSlot) } + } + } else if (boxView.value) boxView.value = null + // 卸下背包时关闭面板 + if (!player.pack) packOpen.value = false updateHint() } function updateHint() { @@ -2267,6 +2542,12 @@ function applyClientInput(data) { } else if (act.type === 'potcook') { const pot = entities.find((e) => e.id === act.id) if (pot?.pot) potCook(p, pot) + } else if (act.type === 'boxdep') { + boxOpenId = act.id + depositToBox(p, act.area, act.i) + } else if (act.type === 'boxwit') { + boxOpenId = act.id + withdrawFromBox(p, act.i) } else if (act.type === 'need_world') { fullSyncPending = true } @@ -3106,6 +3387,10 @@ function resetState() { mid = 1 fid = 1 potOpenId = 0 + boxOpenId = 0 + boxView.value = null + packOpen.value = false + slotTip.value = { show: false, text: '', x: 0, y: 0 } shakeT = 0 flashRed = 0 pendingEv = [] @@ -3211,18 +3496,24 @@ onMounted(() => { keys.attach() keys.onPress((k) => { if (!playing.value) return - if ((k === 'p' || k === 'P') || (k === 'Escape' && !potOpenId && !showMap.value && !craftOpen.value)) { + if ((k === 'p' || k === 'P') || (k === 'Escape' && !potOpenId && !boxOpenId && !packOpen.value && !showMap.value && !craftOpen.value)) { togglePause() return } if (k === ' ') doGather() else if (k === 'f' || k === 'F') doAttack() - else if (k === 'c' || k === 'C') craftOpen.value = !craftOpen.value + else if (k === 'e' || k === 'E') doQuickInteract() + else if (k === 'c' || k === 'C') { + craftOpen.value = !craftOpen.value + if (craftOpen.value) { packOpen.value = false; closeBox() } + } else if (k === 'b' || k === 'B') togglePack() else if (k === 'm' || k === 'M') { showMap.value = !showMap.value if (showMap.value) drawMinimap() } else if (k === 'Escape') { - if (potOpenId) { potOpenId = 0; potView.value = null } + if (boxOpenId) closeBox() + else if (packOpen.value) packOpen.value = false + else if (potOpenId) { potOpenId = 0; potView.value = null } else if (showMap.value) showMap.value = false else craftOpen.value = false } else if (k >= '1' && k <= '9') { @@ -3356,7 +3647,16 @@ function freshClass(f) { @@ -895,18 +988,28 @@ onBeforeUnmount(() => { :friends="progress.friends" @pick="startGame" /> - - @@ -990,6 +1093,26 @@ onBeforeUnmount(() => { + + +