diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index a8a879a..2c4b7f6 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -33,16 +33,16 @@ const msgBadge = computed(() => { return n > 99 ? '99+' : n }) -// 仅饥荒游玩页:导航变悬浮小条,默认隐藏,鼠标靠近顶部再展示 +// 仅饥荒游玩页:导航变悬浮小条,默认隐藏;由 GamePlay 的吸顶按钮统一展开/收起 const navFloat = computed(() => route.path.startsWith('/play/starve')) const navShow = ref(true) watch(navFloat, (v) => { navShow.value = !v }, { immediate: true }) -function onNavMouseMove(e) { +function onStarveNavToggle() { if (!navFloat.value) return - navShow.value = e.clientY <= 60 + navShow.value = !navShow.value } -onMounted(() => window.addEventListener('mousemove', onNavMouseMove)) -onBeforeUnmount(() => window.removeEventListener('mousemove', onNavMouseMove)) +onMounted(() => window.addEventListener('nlg:starve-nav-toggle', onStarveNavToggle)) +onBeforeUnmount(() => window.removeEventListener('nlg:starve-nav-toggle', onStarveNavToggle)) // 当前路由是否命中某导航 function isActive(path) { diff --git a/src/games/components/StarveGame.vue b/src/games/components/StarveGame.vue index 5fb2583..899210f 100644 --- a/src/games/components/StarveGame.vue +++ b/src/games/components/StarveGame.vue @@ -35,6 +35,8 @@ const emit = defineEmits(['score', 'end', 'save', 'progress']) const canvas = ref(null) const clockCv = ref(null) const mapCv = ref(null) +const fullMapCv = ref(null) +const showFullMap = ref(false) // ---- HUD 响应式状态 ---- const playing = ref(false) @@ -46,6 +48,7 @@ const hunger = ref(CAP.hunger) const san = ref(CAP.san) const temp = ref(20) const day = ref(1) +const cursorMode = ref('normal') const seasonTag = ref({ name: '秋', dayIn: 1, len: 20, tint: '#c98548', code: 'autumn' }) const tips = ref([]) const hint = ref('') @@ -661,6 +664,48 @@ function onCanvasClick(ev) { if (ent) return setMyTarget({ kind: 'ent', id: ent.id }) setMyTarget({ kind: 'walk', x: wxp, y: wyp }) } +function updateCursor(ev) { + if (!playing.value || !canvas.value) { + cursorMode.value = 'normal' + return + } + const rect = canvas.value.getBoundingClientRect() + const wxp = ((ev.clientX - rect.left) / rect.width) * canvas.value.width + camX + const wyp = ((ev.clientY - rect.top) / rect.height) * canvas.value.height + camY + let mon = null + let md = Infinity + monsters.forEach((m) => { + const r = m.kind === 'deerclops' ? 70 : m.kind === 'treeguard' ? 50 : m.kind === 'beefalo' ? 36 : 28 + const d = Math.hypot(m.x - wxp, m.y - wyp) + if (d < r && d < md) { md = d; mon = m } + }) + if (mon) { + cursorMode.value = (mon.kind === 'pig' || mon.kind === 'beefalo' || modRt.mobKinds[mon.kind]?.friendly) ? 'hand' : 'attack' + return + } + const f = fires.find((f2) => Math.hypot(f2.x - wxp, f2.y - wyp) < 36) + if (f) { + cursorMode.value = 'hand' + return + } + let ent = null + let ed = Infinity + entities.forEach((e) => { + if (e.deco && e.type !== 'stump') return + if (e.stub || e.solid) 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 } + }) + if (ent) { + const attackable = ent.hp > 0 && (ent.mobile || ent.nest || ent.lurk) + cursorMode.value = attackable ? 'attack' : 'hand' + return + } + cursorMode.value = 'normal' +} +function onCanvasLeave() { + cursorMode.value = 'normal' +} // 设置本地目标:客机同时上报主机 function setMyTarget(t) { player.target = resolveTarget(t) @@ -1388,6 +1433,9 @@ function setPaused(v) { function openManualFromGame() { window.dispatchEvent(new CustomEvent('nlg:open-manual')) } +function openSettingsFromGame() { + window.dispatchEvent(new CustomEvent('nlg:open-settings')) +} function togglePause() { setPaused(!paused.value) } @@ -2361,7 +2409,8 @@ function draw() { } } ctx.restore() - drawClock() + drawClockV2() + if (showFullMap.value) drawFullMap() } function drawEntWrap(e, env) { const sx = e.x - camX @@ -2535,8 +2584,242 @@ function drawNight(W, H) { }) ctx.drawImage(layer, 0, 0) } +// 无限接近原版:外木圈 + 三段昼夜盘 + 中心羊皮纸日数盘 + 黑针从中心指向边缘 +function drawClockV2() { + const c = clockCv.value?.getContext('2d') + if (!c) return + const S = 112 + c.clearRect(0, 0, S, S) + const cx2 = S / 2 + const cy2 = S / 2 + const R = S / 2 - 5 + const [duskAt, nightAt] = daySplit(seasonNow.code) + + // 外层木圈 + c.beginPath() + c.arc(cx2, cy2, R, 0, 7) + c.fillStyle = '#6b4a2c' + c.fill() + c.strokeStyle = '#1d1409' + c.lineWidth = 3 + c.stroke() + c.beginPath() + c.arc(cx2, cy2, R * 0.94, 0, 7) + c.strokeStyle = '#8a6238' + c.lineWidth = 2 + c.stroke() + + // 昼夜三段扇区 + const segs = [ + [0, duskAt / DAY_LEN, '#f0c94a'], + [duskAt / DAY_LEN, nightAt / DAY_LEN, '#d88a3e'], + [nightAt / DAY_LEN, 1, '#2a2742'], + ] + segs.forEach(([a, b, col]) => { + c.beginPath() + c.moveTo(cx2, cy2) + c.arc(cx2, cy2, R * 0.82, -Math.PI / 2 + a * Math.PI * 2, -Math.PI / 2 + b * Math.PI * 2) + c.closePath() + c.fillStyle = col + c.fill() + c.strokeStyle = 'rgba(20,14,8,0.75)' + c.lineWidth = 1.4 + c.stroke() + }) + + // 外圈 16 段刻度 + for (let i = 0; i < 16; i++) { + const a = -Math.PI / 2 + (i / 16) * Math.PI * 2 + const long = i % 4 === 0 + const r1 = R * 0.94 + const r2 = long ? R * 0.82 : R * 0.88 + c.beginPath() + c.moveTo(cx2 + Math.cos(a) * r1, cy2 + Math.sin(a) * r1) + c.lineTo(cx2 + Math.cos(a) * r2, cy2 + Math.sin(a) * r2) + c.strokeStyle = 'rgba(20,14,8,0.9)' + c.lineWidth = long ? 2.2 : 1.1 + c.stroke() + } + + // 中心羊皮纸盘 + c.beginPath() + c.arc(cx2, cy2, R * 0.46, 0, 7) + c.fillStyle = '#eadfc4' + c.fill() + c.strokeStyle = '#3a2a17' + c.lineWidth = 2 + c.stroke() + c.beginPath() + c.arc(cx2, cy2, R * 0.46, 0, 7) + c.strokeStyle = seasonTag.value.tint + c.lineWidth = 2 + c.stroke() + c.fillStyle = '#2b1d10' + c.font = 'bold 16px "Ma Shan Zheng", serif' + c.textAlign = 'center' + c.textBaseline = 'middle' + c.fillText(`${day.value}`, cx2, cy2 - 6) + c.font = 'bold 11px "Ma Shan Zheng", serif' + c.fillStyle = seasonTag.value.tint + c.fillText(`${seasonTag.value.name} ${seasonTag.value.dayIn}/${seasonTag.value.len}`, cx2, cy2 + 10) + + // 原版黑针:从中心向外,尖端略过中心盘与外圈之间 + const ang = -Math.PI / 2 + (dayTime / DAY_LEN) * Math.PI * 2 + c.save() + c.translate(cx2, cy2) + c.rotate(ang) + c.beginPath() + c.moveTo(0, -2.6) + c.lineTo(R * 0.92, -0.8) + c.lineTo(R * 0.92, 0.8) + c.lineTo(0, 2.6) + c.closePath() + c.fillStyle = '#17120b' + c.fill() + c.strokeStyle = '#f4ead2' + c.lineWidth = 1.4 + c.stroke() + // 指针尖端圆点 + const tipIcon = dayTime >= nightAt || dayTime < 0 ? '#c9d4e8' : '#f0c94a' + c.beginPath() + c.arc(R * 0.92, 0, 2.8, 0, 7) + c.fillStyle = tipIcon + c.fill() + c.strokeStyle = '#17120b' + c.lineWidth = 1.2 + c.stroke() + c.restore() + + // 中心铜轴 + c.beginPath() + c.arc(cx2, cy2, 4, 0, 7) + c.fillStyle = '#b89a5c' + c.fill() + c.strokeStyle = '#1d1409' + c.lineWidth = 1.4 + c.stroke() +} +// 原版风格罗盘时钟:外圈木纹时刻环 + 昼夜扇区 + 中心天数季节盘 + 日月指针 +function drawClockV2Legacy() { + const c = clockCv.value?.getContext('2d') + if (!c) return + const S = 104 + c.clearRect(0, 0, S, S) + const cx2 = S / 2 + const cy2 = S / 2 + const R = S / 2 - 6 + const [duskAt, nightAt] = daySplit(seasonNow.code) + + // 外圈木纹底 + c.beginPath() + c.arc(cx2, cy2, R, 0, 7) + c.fillStyle = '#4a341f' + c.fill() + c.strokeStyle = '#1d1409' + c.lineWidth = 3 + c.stroke() + // 木纹环线 + c.strokeStyle = 'rgba(31,20,9,0.55)' + c.lineWidth = 1.4 + ;[0.82, 0.9, 0.96].forEach((k) => { + c.beginPath() + c.arc(cx2, cy2, R * k, 0, 7) + c.stroke() + }) + + // 昼夜扇区(黄 / 橙 / 深蓝) + const segs = [ + [0, duskAt / DAY_LEN, '#e8bc5a'], + [duskAt / DAY_LEN, nightAt / DAY_LEN, '#c97b3a'], + [nightAt / DAY_LEN, 1, '#25223d'], + ] + segs.forEach(([a, b, col]) => { + c.beginPath() + c.moveTo(cx2, cy2) + c.arc(cx2, cy2, R * 0.78, -Math.PI / 2 + a * Math.PI * 2, -Math.PI / 2 + b * Math.PI * 2) + c.closePath() + c.fillStyle = col + c.fill() + c.strokeStyle = 'rgba(20,14,8,0.65)' + c.lineWidth = 1.2 + c.stroke() + }) + + // 16 段时刻刻度:整点长刻度 + for (let i = 0; i < 16; i++) { + const a = -Math.PI / 2 + (i / 16) * Math.PI * 2 + const long = i % 4 === 0 + const r1 = R * 0.96 + const r2 = long ? R * 0.82 : R * 0.88 + c.beginPath() + c.moveTo(cx2 + Math.cos(a) * r1, cy2 + Math.sin(a) * r1) + c.lineTo(cx2 + Math.cos(a) * r2, cy2 + Math.sin(a) * r2) + c.strokeStyle = 'rgba(20,14,8,0.85)' + c.lineWidth = long ? 2 : 1 + c.stroke() + } + + // 指针:当前时刻,针头挂太阳/月亮 + const ang = -Math.PI / 2 + (dayTime / DAY_LEN) * Math.PI * 2 + c.save() + c.translate(cx2, cy2) + c.rotate(ang) + c.strokeStyle = '#f4ead2' + c.lineWidth = 3 + c.beginPath() + c.moveTo(0, 0) + c.lineTo(R * 0.86, 0) + c.stroke() + const icon = dayTime >= nightAt || dayTime < 0 ? 'moon' : 'sun' + c.fillStyle = icon === 'sun' ? '#f0c04a' : '#dfe8f0' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.4 + if (icon === 'sun') { + c.beginPath(); c.arc(R * 0.74, 0, 5, 0, 7); c.fill(); c.stroke() + } else { + c.beginPath(); c.arc(R * 0.74, 0, 5, 0, 7); c.fill(); c.stroke() + c.fillStyle = '#25223d' + c.beginPath(); c.arc(R * 0.76, 1, 3.4, 0, 7); c.fill() + } + c.restore() + // 中心铜轴,原版罗盘指针的固定点 + c.beginPath() + c.arc(cx2, cy2, 4.4, 0, 7) + c.fillStyle = '#b89a5c' + c.fill() + c.strokeStyle = '#1d1409' + c.lineWidth = 1.4 + c.stroke() + + // 中心季节盘 + c.beginPath() + c.arc(cx2, cy2, R * 0.5, 0, 7) + c.fillStyle = '#f4ead2' + c.fill() + c.strokeStyle = seasonTag.value.tint + c.lineWidth = 3 + c.stroke() + c.fillStyle = '#3a2d18' + c.font = 'bold 16px "Ma Shan Zheng", serif' + c.textAlign = 'center' + c.textBaseline = 'middle' + c.fillText(`${day.value}`, cx2, cy2 - 7) + c.font = 'bold 11px "Ma Shan Zheng", serif' + c.fillStyle = seasonTag.value.tint + c.fillText(`${seasonTag.value.name} ${seasonTag.value.dayIn}/${seasonTag.value.len}`, cx2, cy2 + 10) + + // 顶部小提环 + c.strokeStyle = '#3a2a17' + c.lineWidth = 3 + c.beginPath() + c.moveTo(cx2 - 6, cy2 - R) + c.lineTo(cx2 - 4, cy2 - R - 7) + c.lineTo(cx2 + 4, cy2 - R - 7) + c.lineTo(cx2 + 6, cy2 - R) + c.stroke() +} // 右上角昼夜时钟(16 段刻度 + 季节配色与标签) -function drawClock() { +function drawClockLegacy() { const c = clockCv.value?.getContext('2d') if (!c) return const S = 92 @@ -2600,10 +2883,11 @@ function drawClock() { c.stroke() } // 小地图(2Hz 更新):群系底色 + 战争迷雾 + 标记 -function drawMinimap() { - const c = mapCv.value?.getContext('2d') - if (!c || !tiles || !explored) return - const s = 5 +function drawMapTo(cv, s) { + if (!cv || !tiles || !explored) return + const c = cv.getContext('2d') + // s 由调用方传入 + // s 为像素缩放 c.fillStyle = '#0b0906' c.fillRect(0, 0, GW * s, GH * s) for (let ty = 0; ty < GH; ty++) { @@ -2627,7 +2911,33 @@ function drawMinimap() { if (e.struct && explored[tileIdx(e.x, e.y)]) dot(e.x, e.y, e.type === 'pighouse' ? '#e8a8a0' : '#7fd8f0', 2) }) monsters.forEach((m) => { if (m.kind === 'deerclops') dot(m.x, m.y, '#e04840', 3.4) }) - players.forEach((p) => { if (!p.dead || coop) dot(p.x, p.y, p === player ? '#fff' : '#ffe89a', p === player ? 3 : 2.4) }) + players.forEach((p) => { + if (!p.dead || coop) { + const mx = (p.x / TILE) * s + const my = (p.y / TILE) * s + const r = p === player ? (s >= 8 ? 7 : 4.5) : (s >= 8 ? 5.5 : 3.2) + c.fillStyle = p === player ? '#ffffff' : '#ffe89a' + c.strokeStyle = '#1d1409' + c.lineWidth = s >= 8 ? 3 : 1.5 + c.beginPath() + c.arc(mx, my, r, 0, 7) + c.fill() + c.stroke() + if (p === player && s >= 8) { + c.strokeStyle = 'rgba(255,255,255,0.7)' + c.lineWidth = 2 + c.beginPath() + c.arc(mx, my, r + 5 + Math.sin(time * 4) * 2, 0, 7) + c.stroke() + } + } + }) +} +function drawMinimap() { + drawMapTo(mapCv.value, 3) +} +function drawFullMap() { + drawMapTo(fullMapCv.value, 12) } // ===================================================================== @@ -2911,8 +3221,8 @@ function freshClass(f) {