fix(game): 修复底部塔面板不可见问题,改用React层实现,并用AI素材替换Graphics像素块
This commit is contained in:
@@ -1,19 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
|
||||
const TOWER_META = [
|
||||
{ type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤 1.5/s', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场:5%概率秒杀' },
|
||||
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
||||
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' },
|
||||
{ type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助 +20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' },
|
||||
] as const
|
||||
|
||||
type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp'
|
||||
|
||||
/**
|
||||
* 游戏页面
|
||||
* Phaser 通过动态 import 加载,确保 SSR 安全(不在服务端执行)
|
||||
*/
|
||||
export default function GamePage() {
|
||||
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
||||
const [hc, setHc] = useState(200)
|
||||
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
|
||||
const [gameReady, setGameReady] = useState(false)
|
||||
// expose setter to game scene via window
|
||||
const selectedTowerRef = useRef<TowerType | null>(null)
|
||||
|
||||
const handleSelectTower = useCallback((type: TowerType) => {
|
||||
const next = selectedTowerRef.current === type ? null : type
|
||||
selectedTowerRef.current = next
|
||||
setSelectedTower(next)
|
||||
// notify scene
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__gameSelectTower?.(next)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const initGame = async () => {
|
||||
// 动态导入 Phaser,避免 SSR 环境报错
|
||||
const Phaser = (await import('phaser')).default
|
||||
const { createGameConfig } = await import('@/game/config')
|
||||
const { createGameScene } = await import('@/game/GameScene')
|
||||
@@ -21,20 +40,31 @@ export default function GamePage() {
|
||||
if (!mounted) return
|
||||
|
||||
const GameScene = createGameScene(Phaser)
|
||||
const config = createGameConfig('game-container')
|
||||
|
||||
// 注入场景
|
||||
const config = createGameConfig('game-canvas-container')
|
||||
config.scene = [GameScene]
|
||||
|
||||
// 手动设置 scale mode(避免 SSR 时引用 Phaser.Scale 常量报错)
|
||||
if (config.scale) {
|
||||
config.scale.mode = Phaser.Scale.FIT
|
||||
config.scale.autoCenter = Phaser.Scale.CENTER_BOTH
|
||||
}
|
||||
|
||||
// type 设置
|
||||
config.type = Phaser.AUTO
|
||||
|
||||
// expose HC update to React
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__gameOnHCChange = (val: number) => {
|
||||
if (mounted) setHc(val)
|
||||
}
|
||||
(window as any).__gameOnTowerDeselect = () => {
|
||||
if (mounted) {
|
||||
selectedTowerRef.current = null
|
||||
setSelectedTower(null)
|
||||
}
|
||||
}
|
||||
;(window as any).__gameReady = () => {
|
||||
if (mounted) setGameReady(true)
|
||||
}
|
||||
}
|
||||
|
||||
gameRef.current = new Phaser.Game(config)
|
||||
}
|
||||
|
||||
@@ -44,12 +74,98 @@ export default function GamePage() {
|
||||
mounted = false
|
||||
gameRef.current?.destroy(true)
|
||||
gameRef.current = null
|
||||
if (typeof window !== 'undefined') {
|
||||
delete (window as any).__gameOnHCChange
|
||||
delete (window as any).__gameOnTowerDeselect
|
||||
delete (window as any).__gameSelectTower
|
||||
delete (window as any).__gameReady
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen overflow-hidden" style={{ backgroundColor: '#0F0F23' }}>
|
||||
<div id="game-container" className="w-full h-full" />
|
||||
<div
|
||||
className="w-full h-screen flex flex-col overflow-hidden"
|
||||
style={{ backgroundColor: '#0A1628' }}
|
||||
>
|
||||
{/* Phaser canvas 区域,flex-1 填满剩余高度 */}
|
||||
<div
|
||||
id="game-canvas-container"
|
||||
className="flex-1 min-h-0 w-full"
|
||||
style={{ backgroundColor: '#0A1628' }}
|
||||
/>
|
||||
|
||||
{/* 底部塔选择面板 — 纯 React DOM,不被 Canvas 遮挡 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgba(10,18,40,0.97)',
|
||||
borderTop: '2px solid #1e3a5f',
|
||||
padding: '8px 16px',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
zIndex: 50,
|
||||
boxShadow: '0 -4px 20px rgba(124,58,237,0.15)',
|
||||
}}
|
||||
>
|
||||
{/* 选塔提示 */}
|
||||
<div style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#7C3AED', marginRight: '8px', whiteSpace: 'nowrap', opacity: gameReady ? 1 : 0.3 }}>
|
||||
{selectedTower ? '点击格子建造' : '选择塔 ▼'}
|
||||
</div>
|
||||
|
||||
{TOWER_META.map((meta) => {
|
||||
const canAfford = hc >= meta.cost
|
||||
const isSelected = selectedTower === meta.type
|
||||
return (
|
||||
<button
|
||||
key={meta.type}
|
||||
title={meta.tip}
|
||||
onClick={() => canAfford && handleSelectTower(meta.type)}
|
||||
disabled={!canAfford}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
padding: '6px 10px',
|
||||
backgroundColor: isSelected ? '#1e3a5f' : '#0F1B2D',
|
||||
border: `2px solid ${isSelected ? meta.color : '#1e3a5f'}`,
|
||||
borderRadius: '8px',
|
||||
cursor: canAfford ? 'pointer' : 'not-allowed',
|
||||
minWidth: '90px',
|
||||
opacity: canAfford ? 1 : 0.4,
|
||||
transition: 'all 0.15s ease',
|
||||
boxShadow: isSelected ? `0 0 12px ${meta.color}66` : 'none',
|
||||
transform: isSelected ? 'translateY(-3px)' : 'none',
|
||||
}}
|
||||
>
|
||||
{/* 角色图片 */}
|
||||
<div style={{ width: '48px', height: '48px', position: 'relative', flexShrink: 0 }}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={meta.img}
|
||||
alt={meta.name}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
{/* 名称 */}
|
||||
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: meta.color, textAlign: 'center', lineHeight: 1.1 }}>
|
||||
{meta.name}
|
||||
</span>
|
||||
{/* 费用 */}
|
||||
<span style={{ fontFamily: "'Press Start 2P', monospace", fontSize: '8px', color: '#A78BFA' }}>
|
||||
{meta.cost} HC
|
||||
</span>
|
||||
{/* 描述 */}
|
||||
<span style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#64748B', textAlign: 'center' }}>
|
||||
{meta.desc}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
10
app/page.tsx
10
app/page.tsx
@@ -15,8 +15,16 @@ export default function GameCover() {
|
||||
return (
|
||||
<main
|
||||
className="crt-overlay relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden"
|
||||
style={{ backgroundColor: '#0F0F23' }}
|
||||
style={{
|
||||
backgroundColor: '#0F0F23',
|
||||
backgroundImage: 'url(/game-assets/cover-bg.png)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
{/* 深色叠加层,确保文字可读 */}
|
||||
<div className="absolute inset-0" style={{ backgroundColor: 'rgba(10,10,30,0.72)' }} />
|
||||
|
||||
{/* CRT 流动扫描线 */}
|
||||
<div className="game-cover-scanline" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user