饥荒改名年糕生存,优化动画
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
// 游戏卡片:大厅与商城通用(图标、名称、分类、价格/拥有标识、热度)
|
||||
import { computed } from 'vue'
|
||||
import GameIcon from './GameIcon.vue'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
game: { type: Object, required: true }, // 游戏对象(含 owned 字段)
|
||||
showBuy: { type: Boolean, default: false }, // 商城模式显示购买按钮
|
||||
})
|
||||
const displayName = computed(() => (props.game.code === 'starve' ? '年糕求生' : props.game.name))
|
||||
defineEmits(['play', 'buy', 'addCart'])
|
||||
</script>
|
||||
|
||||
@@ -20,7 +22,7 @@ defineEmits(['play', 'buy', 'addCart'])
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="name-row">
|
||||
<span class="name">{{ game.name }}</span>
|
||||
<span class="name">{{ displayName }}</span>
|
||||
<span class="tag">{{ game.category }}</span>
|
||||
</div>
|
||||
<p class="desc text-dim">{{ game.description }}</p>
|
||||
|
||||
@@ -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)
|
||||
<span class="tag">{{ g.engine === 'online' ? '⚔️ 联机对战' : '🕹️ 单机' }}</span>
|
||||
<span v-if="g.weekly_free" class="tag tag-accent">⚡ 本周周免</span>
|
||||
</div>
|
||||
<h3 class="s-name">{{ g.name }}</h3>
|
||||
<h3 class="s-name">{{ gameName(g) }}</h3>
|
||||
<p class="s-desc text-dim">{{ g.description }}</p>
|
||||
<div class="s-foot">
|
||||
<button class="btn" @click.stop="emit('play', g)">{{ actionText(g) }}</button>
|
||||
|
||||
@@ -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) {
|
||||
<div class="starve-wrap">
|
||||
<canvas ref="canvas" class="starve-canvas" @click="onCanvasClick"></canvas>
|
||||
|
||||
<!-- 手机摇杆(设置里可切换) -->
|
||||
<div
|
||||
v-if="isTouchDevice && playing && touchMode !== 'tap'"
|
||||
class="joy-stick"
|
||||
@touchstart.prevent="joyStart"
|
||||
@touchmove.prevent="joyMove"
|
||||
@touchend.prevent="joyEnd"
|
||||
@touchcancel.prevent="joyEnd"
|
||||
>
|
||||
<span class="joy-base"></span>
|
||||
<span class="joy-knob" :style="{ transform: `translate(${joyKnobX}px, ${joyKnobY}px)` }"></span>
|
||||
</div>
|
||||
|
||||
<!-- 暂停遮罩 -->
|
||||
<div v-if="paused" class="pause-overlay">
|
||||
<div class="pause-card">
|
||||
<h3>已暂停</h3>
|
||||
<p class="text-dim">快捷键 P / Esc 继续</p>
|
||||
<div class="pause-btns">
|
||||
<button class="ds-btn" @click="setPaused(false)">继续游戏</button>
|
||||
<button class="ds-btn ghost" @click="openManualFromGame">操作手册</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右上:时钟 + 三围 + 体温 -->
|
||||
<div v-show="playing || dead" class="hud-right">
|
||||
<canvas ref="clockCv" width="92" height="92" class="clock"></canvas>
|
||||
@@ -2862,6 +2965,7 @@ function freshClass(f) {
|
||||
<!-- 左上:存档 / 地图开关 / mod 标签 -->
|
||||
<div v-show="playing" class="hud-left">
|
||||
<button v-if="!isCoop" class="ds-btn" :disabled="saving" @click="doSave(true)">{{ saving ? '已保存' : '保存' }}</button>
|
||||
<button class="ds-btn" @click="togglePause">{{ paused ? '继续' : '暂停 P' }}</button>
|
||||
<button class="ds-btn" @click="showMap = !showMap; showMap && drawMinimap()">地图 M</button>
|
||||
<span v-if="uiMods.length" class="mod-tags" :title="uiMods.map((m) => m.name).join('、')">
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
// 通用追咬:追近 → 咬一口 → 弹开
|
||||
|
||||
13
src/games/starve/mods/chill.js
Normal file
13
src/games/starve/mods/chill.js
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
18
src/games/starve/mods/lucky.js
Normal file
18
src/games/starve/mods/lucky.js
Normal file
@@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
13
src/games/starve/mods/traveler.js
Normal file
13
src/games/starve/mods/traveler.js
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="detail" class="play-wrap" :class="{ 'play-fill': meta?.wide }">
|
||||
<button v-if="floatHead" class="head-toggle" @click="headShow = !headShow">
|
||||
{{ headShow ? '▲ 收起' : '☰ 年糕求生' }}
|
||||
</button>
|
||||
<!-- 顶部信息条 -->
|
||||
<div class="play-head panel" :class="playHeadClasses">
|
||||
<button class="btn btn-ghost btn-sm" @click="router.back()">← 返回</button>
|
||||
<GameIcon class="g-icon" :code="code" :icon="detail.game.icon" :size="34" />
|
||||
<div class="g-info">
|
||||
<b>{{ detail.game.name }}</b>
|
||||
<b>{{ displayName }}</b>
|
||||
<span class="text-dim" style="font-size: 12px">{{ meta.controls }}</span>
|
||||
</div>
|
||||
<div class="g-stats">
|
||||
<span class="stat-chip">本局 <b class="text-accent">{{ liveScore }}</b></span>
|
||||
<span class="stat-chip">最高 <b class="text-primary">{{ detail.best_score }}</b></span>
|
||||
<span class="stat-chip">💰 {{ userStore.user?.points ?? 0 }}</span>
|
||||
<button class="btn btn-ghost btn-sm" @click="openManual">📖 手册</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="openSettings">⚙️ 设置</button>
|
||||
<button v-if="phase === 'playing'" class="btn btn-ghost btn-sm" @click="togglePause">⏸ 暂停</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 游戏区域 -->
|
||||
@@ -595,7 +631,7 @@ onBeforeUnmount(() => {
|
||||
<!-- 1. 模式选择 -->
|
||||
<template v-if="coopStage === 'mode'">
|
||||
<div class="ov-icon"><GameIcon :code="code" :icon="detail.game.icon" :size="56" /></div>
|
||||
<h3>{{ detail.game.name }}</h3>
|
||||
<h3>{{ displayName }}</h3>
|
||||
<p class="text-dim ov-desc">{{ detail.game.description }}</p>
|
||||
<div class="mode-cards">
|
||||
<button class="mode-card" @click="coopStage = 'solo'">
|
||||
@@ -617,6 +653,10 @@ onBeforeUnmount(() => {
|
||||
<i>2~4 人共享同一世界</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ov-btns">
|
||||
<button class="btn btn-ghost btn-sm" @click="openManual">📖 操作手册</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="openSettings">⚙️ 设置</button>
|
||||
</div>
|
||||
<button class="btn btn-ghost" @click="coopStage = 'skins'">
|
||||
更衣室 · 30 款人物皮肤
|
||||
</button>
|
||||
@@ -783,9 +823,13 @@ onBeforeUnmount(() => {
|
||||
<!-- ============ 普通游戏:原有开始面板 ============ -->
|
||||
<template v-else>
|
||||
<div class="ov-icon"><GameIcon :code="code" :icon="detail.game.icon" :size="levelsMeta ? 40 : 56" /></div>
|
||||
<h3>{{ detail.game.name }}</h3>
|
||||
<h3>{{ displayName }}</h3>
|
||||
<p v-if="!levelsMeta" class="text-dim ov-desc">{{ detail.game.description }}</p>
|
||||
<p class="text-dim" style="font-size: 12px">🎮 {{ meta.controls }}</p>
|
||||
<div class="ov-btns">
|
||||
<button class="btn btn-ghost btn-sm" @click="openManual">📖 操作手册</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="openSettings">⚙️ 设置</button>
|
||||
</div>
|
||||
<label v-if="meta.supports.includes('double_points') && propCount('double_points') > 0" class="double-check">
|
||||
<input v-model="useDouble" type="checkbox" />
|
||||
使用 ✨双倍积分卡(剩 {{ propCount('double_points') }} 张),本局积分翻倍
|
||||
@@ -893,6 +937,54 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作手册 -->
|
||||
<div v-if="showManual" class="modal-mask" @click.self="closeManual">
|
||||
<div class="mod-modal panel manual-modal">
|
||||
<button class="bm-close" @click="closeManual">✕</button>
|
||||
<h3>操作手册</h3>
|
||||
<p class="text-dim" style="font-size: 12px; line-height: 1.6; text-align: left">
|
||||
{{ meta.controls }}<br />
|
||||
移动端:点击地面移动{{ touchMode === 'both' || touchMode === 'joystick' ? ',也可使用左下角摇杆' : '' }};点击目标会自动攻击/采集。<br />
|
||||
快捷键:WASD 移动 · 空格采集 · F 攻击 · C 合成 · M 地图 · P/Esc 暂停。
|
||||
</p>
|
||||
<div class="ov-btns">
|
||||
<button class="btn" @click="closeManual">知道了</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置 -->
|
||||
<div v-if="showSettings" class="modal-mask" @click.self="closeSettings">
|
||||
<div class="mod-modal panel settings-modal">
|
||||
<button class="bm-close" @click="closeSettings">✕</button>
|
||||
<h3>设置</h3>
|
||||
<div class="setting-block">
|
||||
<b>移动端移动方式</b>
|
||||
<div class="setting-options">
|
||||
<button class="chip" :class="{ on: touchMode === 'both' }" @click="saveTouchMode('both')">点击 + 摇杆</button>
|
||||
<button class="chip" :class="{ on: touchMode === 'tap' }" @click="saveTouchMode('tap')">仅点击</button>
|
||||
<button class="chip" :class="{ on: touchMode === 'joystick' }" @click="saveTouchMode('joystick')">仅摇杆</button>
|
||||
</div>
|
||||
<p class="text-dim" style="font-size: 12px">默认都支持;摇杆未选中时自动隐藏。</p>
|
||||
</div>
|
||||
<div class="setting-block">
|
||||
<b>PC 快捷键</b>
|
||||
<ul class="shortcut-list">
|
||||
<li><span>WASD / 方向键</span>移动</li>
|
||||
<li><span>空格</span>采集 / 交互</li>
|
||||
<li><span>F</span>攻击</li>
|
||||
<li><span>1~9</span>使用物品栏</li>
|
||||
<li><span>C</span>合成</li>
|
||||
<li><span>M</span>地图</li>
|
||||
<li><span>P / Esc</span>暂停 / 继续</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ov-btns">
|
||||
<button class="btn" @click="closeSettings">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未拥有提示 -->
|
||||
@@ -1657,4 +1749,55 @@ onBeforeUnmount(() => {
|
||||
.overlay.room-entry {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 二级导航:吸顶小按钮手动展开/收起,不随鼠标弹出 */
|
||||
.head-toggle {
|
||||
position: fixed;
|
||||
top: 2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 160;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-panel);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
/* 手册/设置弹窗 */
|
||||
.manual-modal,
|
||||
.settings-modal {
|
||||
width: min(560px, 100%);
|
||||
text-align: left;
|
||||
}
|
||||
.setting-block {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.setting-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.shortcut-list {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.shortcut-list span {
|
||||
display: inline-block;
|
||||
min-width: 96px;
|
||||
color: var(--primary-2);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user