From e70b7aa3d7e4efffd663a8b543af87d42591fbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 15 Aug 2026 09:52:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A5=A5=E8=8D=92=E6=94=B9=E5=90=8D=E5=B9=B4?= =?UTF-8?q?=E7=B3=95=E7=94=9F=E5=AD=98=EF=BC=8C=E4=BC=98=E5=8C=96=E5=8A=A8?= =?UTF-8?q?=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/GameCard.vue | 6 +- src/components/GameCarousel.vue | 5 +- src/games/components/StarveGame.vue | 173 +++++++++++++++++++++++++++- src/games/starve/mobs.js | 8 +- src/games/starve/mods/chill.js | 13 +++ src/games/starve/mods/index.js | 5 +- src/games/starve/mods/lucky.js | 18 +++ src/games/starve/mods/traveler.js | 13 +++ src/games/starveSkins.js | 10 +- src/views/GamePlay.vue | 153 +++++++++++++++++++++++- 10 files changed, 385 insertions(+), 19 deletions(-) create mode 100644 src/games/starve/mods/chill.js create mode 100644 src/games/starve/mods/lucky.js create mode 100644 src/games/starve/mods/traveler.js diff --git a/src/components/GameCard.vue b/src/components/GameCard.vue index 91ba102..a0a11e7 100644 --- a/src/components/GameCard.vue +++ b/src/components/GameCard.vue @@ -1,11 +1,13 @@ @@ -20,7 +22,7 @@ defineEmits(['play', 'buy', 'addCart'])
- {{ game.name }} + {{ displayName }} {{ game.category }}

{{ game.description }}

diff --git a/src/components/GameCarousel.vue b/src/components/GameCarousel.vue index 4e4c301..2805406 100644 --- a/src/components/GameCarousel.vue +++ b/src/components/GameCarousel.vue @@ -14,6 +14,9 @@ const props = defineProps({ interval: { type: Number, default: 5 }, // 自动播放间隔(秒) }) const emit = defineEmits(['play']) +function gameName(g) { + return g.code === 'starve' ? '年糕求生' : g.name +} const idx = ref(0) const n = computed(() => props.games.length) @@ -112,7 +115,7 @@ onBeforeUnmount(pause) {{ g.engine === 'online' ? '⚔️ 联机对战' : '🕹️ 单机' }} ⚡ 本周周免
-

{{ g.name }}

+

{{ gameName(g) }}

{{ g.description }}

diff --git a/src/games/components/StarveGame.vue b/src/games/components/StarveGame.vue index 853fa52..5fb2583 100644 --- a/src/games/components/StarveGame.vue +++ b/src/games/components/StarveGame.vue @@ -64,6 +64,16 @@ const uiTech = ref(0) const uiUnlocked = ref([]) const potView = ref(null) // {id, ing:[code], state, t, dish} const uiMods = ref([]) // 本局启用的 mod 元数据(HUD 展示) +const paused = ref(false) +const touchMode = ref(localStorage.getItem('starve_touch_mode') || 'both') +const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0) +const joyActive = ref(false) +const joyKnobX = ref(0) +const joyKnobY = ref(0) +let joyDX = 0 +let joyDY = 0 +let joyBaseX = 0 +let joyBaseY = 0 const ICONS = buildIcons() // {url, cv} @@ -854,15 +864,18 @@ function hitEntity(p, e) { e.hp -= dmg e.shake = 0.25 markDirty(e) - if (e.type === 'beehive' && e.beeCd <= 0) { + if (e.type === 'beehive') { + const bees = monsters.filter((m) => m.kind === 'bee' && m.hiveId === e.id).length + if (bees < 6 && e.beeCd <= 0) { e.beeCd = 3 - const n = randInt(2, 3) + const n = Math.min(2, 6 - bees) for (let i = 0; i < n; i++) { const spot = findLandNear(tiles, e.x, e.y, 10, 40) - spawnMob('bee', spot.x, spot.y, { angryAt: p, life: 16 }) + spawnMob('bee', spot.x, spot.y, { angryAt: p, life: 16, hiveId: e.id }) } ptip(p, '惹恼了蜂巢!') } + } if (e.hp <= 0) killEntity(p, e) return } @@ -1332,6 +1345,55 @@ function updateThreats(dt) { monsters = monsters.filter((m) => m.kind !== 'shadow') } } +function joyStart(e) { + if (touchMode.value === 'tap') return + const t = e.touches?.[0] || e + joyActive.value = true + joyBaseX = t.clientX + joyBaseY = t.clientY + joyDX = 0 + joyDY = 0 +} +function joyMove(e) { + if (!joyActive.value) return + const t = e.touches?.[0] || e + let dx = t.clientX - joyBaseX + let dy = t.clientY - joyBaseY + const max = 42 + const d = Math.hypot(dx, dy) + if (d > max) { + dx *= max / d + dy *= max / d + } + joyKnobX.value = dx + joyKnobY.value = dy + joyDX = dx / max + joyDY = dy / max +} +function joyEnd() { + joyActive.value = false + joyKnobX.value = 0 + joyKnobY.value = 0 + joyDX = 0 + joyDY = 0 +} +function setPaused(v) { + if (coop) return // 联机模式不暂停 + if (paused.value === v) return + paused.value = v + if (v) loop?.stop() + else if (playing.value) loop?.start() +} + +function openManualFromGame() { + window.dispatchEvent(new CustomEvent('nlg:open-manual')) +} +function togglePause() { + setPaused(!paused.value) +} +function onStarveTouchMode(e) { + touchMode.value = e.detail || 'both' +} function updatePlayers(dt) { players.forEach((p) => { p.walking = false @@ -1348,6 +1410,15 @@ function updatePlayers(dt) { if (p === player) { dx = (keys.has('ArrowRight') || keys.has('d') ? 1 : 0) - (keys.has('ArrowLeft') || keys.has('a') ? 1 : 0) dy = (keys.has('ArrowDown') || keys.has('s') ? 1 : 0) - (keys.has('ArrowUp') || keys.has('w') ? 1 : 0) + if (touchMode.value !== 'tap' && joyActive.value) { + dx = joyDX + dy = joyDY + } + const inputLen = Math.hypot(dx, dy) + if (inputLen > 1) { + dx /= inputLen + dy /= inputLen + } p.input.dx = dx p.input.dy = dy } @@ -2689,7 +2760,7 @@ function stop() { function useProp() { return false } -defineExpose({ start, stop, useProp }) +defineExpose({ start, stop, useProp, setPaused, togglePause }) onMounted(() => { ctx = canvas.value.getContext('2d') @@ -2698,6 +2769,10 @@ onMounted(() => { keys.attach() keys.onPress((k) => { if (!playing.value) return + if ((k === 'p' || k === 'P') || (k === 'Escape' && !potOpenId && !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 @@ -2714,8 +2789,10 @@ onMounted(() => { }) window.addEventListener('resize', fitCanvas) fitCanvas() + window.addEventListener('starve-touch-mode', onStarveTouchMode) // 调试后门(e2e / 人工验证) window.__starve = { + touchModeDebug: true, time: (t) => { dayTime = Math.max(0, Math.min(DAY_LEN - 1, t)) }, day: (d) => { day.value = d; onNewDay() }, season: (code) => { @@ -2810,6 +2887,7 @@ onBeforeUnmount(() => { loop?.stop() keys?.detach() window.removeEventListener('resize', fitCanvas) + window.removeEventListener('starve-touch-mode', onStarveTouchMode) coop?.offMessage?.() delete window.__starve }) @@ -2836,6 +2914,31 @@ function freshClass(f) {
+ +
+ + +
+ + +
+
+

已暂停

+

快捷键 P / Esc 继续

+
+ + +
+
+
+
@@ -2862,6 +2965,7 @@ function freshClass(f) {
+ Mod×{{ uiMods.length }} @@ -3503,4 +3607,65 @@ function freshClass(f) { color: #f4ead2; font-size: 20px; } + +/* ---- 手机摇杆 ---- */ +.joy-stick { + position: absolute; + left: 18px; + bottom: 92px; + width: 112px; + height: 112px; + touch-action: none; + user-select: none; + -webkit-user-select: none; + z-index: 20; +} +.joy-base { + position: absolute; + inset: 10px; + border-radius: 50%; + border: 2px solid rgba(244, 234, 210, 0.55); + background: rgba(0, 0, 0, 0.28); + box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.12); +} +.joy-knob { + position: absolute; + left: 50%; + top: 50%; + width: 46px; + height: 46px; + margin: -23px 0 0 -23px; + border-radius: 50%; + background: rgba(244, 234, 210, 0.9); + border: 2px solid rgba(0, 0, 0, 0.5); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35); +} +/* ---- 暂停 ---- */ +.pause-overlay { + position: absolute; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + background: rgba(8, 5, 4, 0.6); + backdrop-filter: blur(2px); +} +.pause-card { + background: var(--bg-panel, #1d1f3a); + border: 1px solid var(--border, #34386b); + border-radius: 16px; + padding: 26px 30px; + text-align: center; + display: flex; + flex-direction: column; + gap: 10px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} +.pause-btns { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 8px; +} diff --git a/src/games/starve/mobs.js b/src/games/starve/mobs.js index b1e09e4..6d70ed0 100644 --- a/src/games/starve/mobs.js +++ b/src/games/starve/mobs.js @@ -47,13 +47,17 @@ function nearestVictim(m, g, playersOnly = false) { // 对受害者造成伤害(玩家走减伤管线;猪人直接扣血并反击) function biteVictim(m, victim, dmg, label, g) { if (victim.kind === 'p') { - g.damagePlayer(victim.ref, dmg, label) + if (typeof g.damagePlayer === 'function') { + g.damagePlayer(victim.ref, dmg, label) + } else if (victim.ref && victim.ref.hp !== undefined) { + victim.ref.hp = Math.max(0, victim.ref.hp - dmg) + } } else { const pig = victim.ref pig.hp -= dmg pig.shake = 0.22 pig.targetMob = m // 猪人被咬会还手 - if (pig.hp <= 0) g.killMob(pig, null) + if (pig.hp <= 0 && typeof g.killMob === 'function') g.killMob(pig, null) } } // 通用追咬:追近 → 咬一口 → 弹开 diff --git a/src/games/starve/mods/chill.js b/src/games/starve/mods/chill.js new file mode 100644 index 0000000..88c6813 --- /dev/null +++ b/src/games/starve/mods/chill.js @@ -0,0 +1,13 @@ +// 悠闲时光:降低生存压力,适合轻松体验 +export default { + id: 'chill', + name: '悠闲时光', + desc: '饥饿减缓 40% · 采集更快 · 猎犬袭击间隔更长', + factor: 1.1, + tag: 'easy', + tune: { + hungerRate: 0.6, // 原 75/DAY_LEN + gatherMul: 0.7, + houndGapMul: 1.5, + }, +} diff --git a/src/games/starve/mods/index.js b/src/games/starve/mods/index.js index a03623e..a6e3213 100644 --- a/src/games/starve/mods/index.js +++ b/src/games/starve/mods/index.js @@ -7,5 +7,8 @@ import revive from './revive.js' import peaceful from './peaceful.js' import nightmare from './nightmare.js' import magic from './magic.js' +import chill from './chill.js' +import traveler from './traveler.js' +import lucky from './lucky.js' -export const MODS = [qol, farm, chest, chester, revive, peaceful, nightmare, magic] +export const MODS = [qol, farm, chest, chester, revive, peaceful, nightmare, magic, chill, traveler, lucky] diff --git a/src/games/starve/mods/lucky.js b/src/games/starve/mods/lucky.js new file mode 100644 index 0000000..f2e66dc --- /dev/null +++ b/src/games/starve/mods/lucky.js @@ -0,0 +1,18 @@ +// 幸运矿工:矿脉收益更高 +export default { + id: 'lucky', + name: '幸运矿工', + desc: '金矿额外掉金块 · 普通岩石也有概率出金', + factor: 1.15, + tag: 'easy', + hooks: { + onGather(g, p, e) { + if (!p) return + if (e.type === 'goldrock') { + g.addItem(p, 'gold', g.rand(1, 2)) + } else if (e.type === 'rock' && g.rand(1, 4) === 1) { + g.addItem(p, 'gold', 1) + } + }, + }, +} diff --git a/src/games/starve/mods/traveler.js b/src/games/starve/mods/traveler.js new file mode 100644 index 0000000..e5b8f45 --- /dev/null +++ b/src/games/starve/mods/traveler.js @@ -0,0 +1,13 @@ +// 旅者行囊:更大的随身空间 +export default { + id: 'traveler', + name: '旅者行囊', + desc: '主物品栏 +5 · 背包 +4 · 物品堆叠上限 60', + factor: 1.1, + tag: 'qol', + tune: { + invSlots: 20, + packSlots: 12, + stackMax: 60, + }, +} diff --git a/src/games/starveSkins.js b/src/games/starveSkins.js index 3abe339..b1c9559 100644 --- a/src/games/starveSkins.js +++ b/src/games/starveSkins.js @@ -184,11 +184,11 @@ function drawToolLegacy(c, toolCode) { } // 增强手持物:更大、轮廓更清楚、材质区分更明显(斧/镐/铲/矛/触手/火把) -function drawToolEnhanced(c, toolCode) { +function drawToolEnhanced(c, toolCode, toolAng = 0) { if (!toolCode) return c.save() - c.translate(0, 10) - c.rotate(-0.45) + c.translate(0, 9) + c.rotate(toolAng) c.lineJoin = 'round' c.lineCap = 'round' const wood = '#8a5a2b' @@ -511,6 +511,8 @@ export function drawPlayerFig(c, skin, pose = {}) { else if (act === 'eat') swingAng = -0.4 - Math.sin(t * Math.PI) * 1.95 // 抬手到嘴边 else swingAng = -2.1 + t * 2.7 // 砍树/通用横劈 } + // 工具在手中的基础角度:挥击时跟手,平时按动作/待机姿态微调 + const toolAng = swing > 0 ? 0 : act === 'eat' ? 0.9 : act === 'attack' ? 0 : act === 'chop' ? -0.25 : (act === 'mine' || act === 'dig') ? -0.5 : -0.15 c.save() c.translate(5 + thrust, -11 + breathe) c.rotate(swingAng) @@ -520,7 +522,7 @@ export function drawPlayerFig(c, skin, pose = {}) { c.moveTo(0, 0) c.lineTo(0, 11) c.stroke() - drawToolEnhanced(c, toolCode) + drawToolEnhanced(c, toolCode, toolAng) c.restore() c.restore() } diff --git a/src/views/GamePlay.vue b/src/views/GamePlay.vue index de6b6b3..48edd98 100644 --- a/src/views/GamePlay.vue +++ b/src/views/GamePlay.vue @@ -28,6 +28,9 @@ const detail = ref(null) // {game, owned, best_score} const phase = ref('loading') // loading / locked / ready / playing / over const liveScore = ref(0) // 实时得分 const finalScore = ref(0) // 结束得分 +const displayName = computed(() => (code === 'starve' ? '年糕求生' : detail.value?.game?.name || '游戏')) +const showManual = ref(false) +const showSettings = ref(false) const result = ref(null) // 结算响应 const useDouble = ref(false) // 是否使用双倍积分卡 const bag = ref([]) // 我的道具背包 @@ -61,6 +64,31 @@ function onHeadMouseMove(e) { if (!floatHead.value) return headShow.value = e.clientY <= 130 } +const touchMode = ref(localStorage.getItem('starve_touch_mode') || 'both') +function saveTouchMode(mode) { + touchMode.value = mode + localStorage.setItem('starve_touch_mode', mode) + window.dispatchEvent(new CustomEvent('starve-touch-mode', { detail: mode })) +} +function openManual() { + showManual.value = true + if (phase.value === 'playing') gameRef.value?.setPaused?.(true) +} +function closeManual() { + showManual.value = false + if (phase.value === 'playing') gameRef.value?.setPaused?.(false) +} +function openSettings() { + showSettings.value = true + if (phase.value === 'playing') gameRef.value?.setPaused?.(true) +} +function closeSettings() { + showSettings.value = false + if (phase.value === 'playing') gameRef.value?.setPaused?.(false) +} +function togglePause() { + gameRef.value?.togglePause?.() +} const playHeadClasses = computed(() => ({ 'play-head-float': floatHead.value, 'play-head-hidden': floatHead.value && !headShow.value, @@ -549,10 +577,11 @@ watch(() => coopStore.roomState?.code, (code) => { }) onMounted(() => { - window.addEventListener('mousemove', onHeadMouseMove) + // play-head 通过吸顶按钮手动展开/收起,不再跟随鼠标自动弹出 // 宽屏游戏(如饥荒):给 body 打标解除全局 1200px 限宽 if (meta?.wide) document.body.classList.add('wide-page') load() + window.addEventListener('nlg:open-manual', openManual) if (coopable) { window.addEventListener('nlg:pending-join', tryPendingJoin) tryPendingJoin() @@ -560,8 +589,9 @@ onMounted(() => { }) onBeforeUnmount(() => { document.body.classList.remove('wide-page') + window.removeEventListener('nlg:open-manual', openManual) window.removeEventListener('nlg:pending-join', tryPendingJoin) - window.removeEventListener('mousemove', onHeadMouseMove) + // play-head 展开状态由组件自行管理 clearInterval(inviteTickTimer) gameRef.value?.stop() // 离开游玩页断开联机连接(房间自动退出) @@ -571,18 +601,24 @@ onBeforeUnmount(() => {