feat(app): 主页新增难度选择UI,游戏页读取localStorage难度参数

This commit is contained in:
Cloud Bot
2026-03-21 09:45:37 +00:00
parent 0f335e7722
commit 9704881fd8
2 changed files with 113 additions and 16 deletions

View File

@@ -16,14 +16,12 @@ export default function GamePage() {
const [hc, setHc] = useState(200) const [hc, setHc] = useState(200)
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null) const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false) const [gameReady, setGameReady] = useState(false)
// expose setter to game scene via window
const selectedTowerRef = useRef<TowerType | null>(null) const selectedTowerRef = useRef<TowerType | null>(null)
const handleSelectTower = useCallback((type: TowerType) => { const handleSelectTower = useCallback((type: TowerType) => {
const next = selectedTowerRef.current === type ? null : type const next = selectedTowerRef.current === type ? null : type
selectedTowerRef.current = next selectedTowerRef.current = next
setSelectedTower(next) setSelectedTower(next)
// notify scene
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
(window as any).__gameSelectTower?.(next) (window as any).__gameSelectTower?.(next)
} }
@@ -39,6 +37,16 @@ export default function GamePage() {
if (!mounted) return if (!mounted) return
// 从 localStorage 读取难度,通过 window 变量传给场景
if (typeof window !== 'undefined') {
const storedDifficulty = localStorage.getItem('game-difficulty')
if (storedDifficulty === 'easy' || storedDifficulty === 'normal' || storedDifficulty === 'hard') {
;(window as any).__gameDifficulty = storedDifficulty
} else {
;(window as any).__gameDifficulty = 'normal'
}
}
const GameScene = createGameScene(Phaser) const GameScene = createGameScene(Phaser)
const config = createGameConfig('game-canvas-container') const config = createGameConfig('game-canvas-container')
config.scene = [GameScene] config.scene = [GameScene]
@@ -49,7 +57,6 @@ export default function GamePage() {
} }
config.type = Phaser.AUTO config.type = Phaser.AUTO
// expose HC update to React
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
(window as any).__gameOnHCChange = (val: number) => { (window as any).__gameOnHCChange = (val: number) => {
if (mounted) setHc(val) if (mounted) setHc(val)
@@ -79,6 +86,7 @@ export default function GamePage() {
delete (window as any).__gameOnTowerDeselect delete (window as any).__gameOnTowerDeselect
delete (window as any).__gameSelectTower delete (window as any).__gameSelectTower
delete (window as any).__gameReady delete (window as any).__gameReady
delete (window as any).__gameDifficulty
} }
} }
}, []) }, [])
@@ -88,14 +96,14 @@ export default function GamePage() {
className="w-full h-screen flex flex-col overflow-hidden" className="w-full h-screen flex flex-col overflow-hidden"
style={{ backgroundColor: '#0A1628' }} style={{ backgroundColor: '#0A1628' }}
> >
{/* Phaser canvas 区域flex-1 填满剩余高度 */} {/* Phaser canvas 区域 */}
<div <div
id="game-canvas-container" id="game-canvas-container"
className="flex-1 min-h-0 w-full" className="flex-1 min-h-0 w-full"
style={{ backgroundColor: '#0A1628' }} style={{ backgroundColor: '#0A1628' }}
/> />
{/* 底部塔选择面板 — 纯 React DOM不被 Canvas 遮挡 */} {/* 底部塔选择面板 */}
<div <div
style={{ style={{
backgroundColor: 'rgba(10,18,40,0.97)', backgroundColor: 'rgba(10,18,40,0.97)',
@@ -141,7 +149,6 @@ export default function GamePage() {
transform: isSelected ? 'translateY(-3px)' : 'none', transform: isSelected ? 'translateY(-3px)' : 'none',
}} }}
> >
{/* 角色图片 */}
<div style={{ width: '48px', height: '48px', position: 'relative', flexShrink: 0 }}> <div style={{ width: '48px', height: '48px', position: 'relative', flexShrink: 0 }}>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
@@ -150,15 +157,12 @@ export default function GamePage() {
style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'auto' }} style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'auto' }}
/> />
</div> </div>
{/* 名称 */}
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: meta.color, textAlign: 'center', lineHeight: 1.1 }}> <span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: meta.color, textAlign: 'center', lineHeight: 1.1 }}>
{meta.name} {meta.name}
</span> </span>
{/* 费用 */}
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#A78BFA' }}> <span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#A78BFA' }}>
{meta.cost} HC {meta.cost} HC
</span> </span>
{/* 描述 */}
<span style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#64748B', textAlign: 'center' }}> <span style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#64748B', textAlign: 'center' }}>
{meta.desc} {meta.desc}
</span> </span>

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import Link from 'next/link' import { useState } from 'react'
import { useRouter } from 'next/navigation'
// 背景装饰黑话词条 // 背景装饰黑话词条
const BUZZ_WORDS = [ const BUZZ_WORDS = [
@@ -11,7 +12,53 @@ const BUZZ_WORDS = [
'向上管理', '自驱力', '全链路', '数据驱动', '业务增长', '向上管理', '自驱力', '全链路', '数据驱动', '业务增长',
] ]
type DifficultyLevel = 'easy' | 'normal' | 'hard'
const DIFFICULTY_OPTIONS: {
key: DifficultyLevel
label: string
desc: string
color: string
borderColor: string
glowColor: string
}[] = [
{
key: 'easy',
label: '简单',
desc: '带薪摸鱼,准点下班',
color: '#22C55E',
borderColor: '#16a34a',
glowColor: 'rgba(34,197,94,0.35)',
},
{
key: 'normal',
label: '普通',
desc: '常规打工,偶有加班',
color: '#3B82F6',
borderColor: '#2563eb',
glowColor: 'rgba(59,130,246,0.35)',
},
{
key: 'hard',
label: '困难',
desc: '地狱模式007全开',
color: '#EF4444',
borderColor: '#dc2626',
glowColor: 'rgba(239,68,68,0.35)',
},
]
export default function GameCover() { export default function GameCover() {
const [selectedDifficulty, setSelectedDifficulty] = useState<DifficultyLevel>('normal')
const router = useRouter()
const handleStart = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('game-difficulty', selectedDifficulty)
}
router.push('/game')
}
return ( return (
<main <main
className="crt-overlay relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden" className="crt-overlay relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden"
@@ -22,7 +69,7 @@ export default function GameCover() {
backgroundPosition: 'center', backgroundPosition: 'center',
}} }}
> >
{/* 深色叠加层,确保文字可读 */} {/* 深色叠加层 */}
<div className="absolute inset-0" style={{ backgroundColor: 'rgba(10,10,30,0.72)' }} /> <div className="absolute inset-0" style={{ backgroundColor: 'rgba(10,10,30,0.72)' }} />
{/* CRT 流动扫描线 */} {/* CRT 流动扫描线 */}
@@ -108,9 +155,54 @@ export default function GameCover() {
线 HC 线 HC
</p> </p>
{/* 难度选择 */}
<div className="w-full">
<p
className="font-pixel text-center mb-3"
style={{ fontSize: '9px', color: '#7C3AED', letterSpacing: '0.2em' }}
>
</p>
<div className="flex gap-3 justify-center">
{DIFFICULTY_OPTIONS.map(opt => {
const isSelected = selectedDifficulty === opt.key
return (
<button
key={opt.key}
onClick={() => setSelectedDifficulty(opt.key)}
style={{
flex: 1,
padding: '10px 8px',
backgroundColor: isSelected ? `rgba(${opt.key === 'easy' ? '34,197,94' : opt.key === 'normal' ? '59,130,246' : '239,68,68'},0.15)` : '#0F1B2D',
border: `2px solid ${isSelected ? opt.color : '#1e3a5f'}`,
borderRadius: '10px',
cursor: 'pointer',
transition: 'all 0.15s ease',
boxShadow: isSelected ? `0 0 16px ${opt.glowColor}` : 'none',
transform: isSelected ? 'translateY(-2px)' : 'none',
}}
>
<div
className="font-vt323"
style={{ fontSize: '22px', color: opt.color, marginBottom: '2px' }}
>
{opt.label}
</div>
<div
className="font-vt323"
style={{ fontSize: '13px', color: '#9CA3AF', lineHeight: 1.2 }}
>
{opt.desc}
</div>
</button>
)
})}
</div>
</div>
{/* 开始游戏按钮 */} {/* 开始游戏按钮 */}
<Link <button
href="/game" onClick={handleStart}
className="font-pixel text-white rounded-lg transition-all duration-200 cursor-pointer" className="font-pixel text-white rounded-lg transition-all duration-200 cursor-pointer"
style={{ style={{
backgroundColor: '#F43F5E', backgroundColor: '#F43F5E',
@@ -118,22 +210,23 @@ export default function GameCover() {
fontSize: '12px', fontSize: '12px',
letterSpacing: '0.1em', letterSpacing: '0.1em',
boxShadow: '0 0 20px rgba(244, 63, 94, 0.4)', boxShadow: '0 0 20px rgba(244, 63, 94, 0.4)',
border: 'none',
}} }}
onMouseEnter={e => { onMouseEnter={e => {
const el = e.currentTarget as HTMLAnchorElement const el = e.currentTarget as HTMLButtonElement
el.style.opacity = '0.9' el.style.opacity = '0.9'
el.style.transform = 'translateY(-2px)' el.style.transform = 'translateY(-2px)'
el.style.boxShadow = '0 0 30px rgba(244, 63, 94, 0.6)' el.style.boxShadow = '0 0 30px rgba(244, 63, 94, 0.6)'
}} }}
onMouseLeave={e => { onMouseLeave={e => {
const el = e.currentTarget as HTMLAnchorElement const el = e.currentTarget as HTMLButtonElement
el.style.opacity = '1' el.style.opacity = '1'
el.style.transform = 'translateY(0)' el.style.transform = 'translateY(0)'
el.style.boxShadow = '0 0 20px rgba(244, 63, 94, 0.4)' el.style.boxShadow = '0 0 20px rgba(244, 63, 94, 0.4)'
}} }}
> >
</Link> </button>
{/* 提示文字 */} {/* 提示文字 */}
<p <p