Compare commits

...

11 Commits

Author SHA1 Message Date
shiyi
cd9c70473b fix 2026-04-03 14:17:23 +08:00
Cloud Bot
7253a882b0 Backup 2026-03-25 2026-03-25 02:22:11 +00:00
Cloud Bot
ee7222929a fix(hydration): PuaPanel placeholder 改用 useEffect 随机选取,修复 SSR/客户端 hydration 不匹配 2026-03-24 11:58:48 +00:00
Cloud Bot
dc9904b3f9 feat(ui): 添加首次游玩教程弹窗——5步引导含故事背景/部署操作/波次机制/开会激励/兵力建议,localStorage记录已读 2026-03-24 09:33:47 +00:00
Cloud Bot
de17de46e1 feat(ui): 底部塔卡片添加悬浮信息弹窗——显示角色名言、属性数值、特殊技能、建议打法 2026-03-24 09:21:18 +00:00
Cloud Bot
b8ba572ffb feat(game): 添加开会暂停系统——点击开会按钮暂停游戏,可安心发激励,结束开会后恢复 2026-03-24 09:13:14 +00:00
Cloud Bot
6843d2b74c balance: 回调过度难度——精力消耗4(持续时间翻倍)/恢复8,初始HC170,怪物HP/速度适度降低至合理区间 2026-03-24 09:02:02 +00:00
Cloud Bot
4fec1ebe9d balance: 全面提升游戏难度——怪物HP/速度/KPI扣减上调,精力消耗加快,初始HC降至150,波次加量缩间隔,Boss技能冷却12s,自动下一波等待2s 2026-03-24 08:56:14 +00:00
Cloud Bot
e460d90b3a Backup 2026-03-24 2026-03-24 08:48:48 +00:00
Cloud Bot
361a1bed5d fix(game): 关卡切换时重置HC为200,不带入上一关余量,修复游戏平衡性问题 2026-03-24 08:48:19 +00:00
Cloud Bot
a36c8af344 feat(pua): 新增重复检测——LLM语义相似度判断,相似60%效果降级,相似80%强制翻车,额外扣HC惩罚 2026-03-24 08:44:35 +00:00
19 changed files with 1059 additions and 122 deletions

9
.env Normal file
View File

@@ -0,0 +1,9 @@
DATABASE_URL=postgresql+asyncpg://postgres:preiMQc4OsHHL4p7bAH@172.16.193.3:15400/db_vibe_next_template_371a88b5
LOG_DIR=/var/log/vibe-app
NOVA_BASE_URL=https://pre-nova-api.betteryeah.com
NOVA_TENANT_ID=2f3ea083e766407ca8f6b0d9e19ba3c0
NOVA_ACCESS_KEY=oapi-ak-467E17CB-5252-4DC1-B998-E9E4A9A8D0C3
LLM_BASE_URL=https://pre-llm-server-internal.betteryeah.com
LLM_API_KEY=sk-skills-default-dev-key

View File

@@ -4,6 +4,11 @@
"agent_id": "d730c266fe6748839d9c93ece8e58b84", "agent_id": "d730c266fe6748839d9c93ece8e58b84",
"agent_name": "Agent", "agent_name": "Agent",
"agent_description": "" "agent_description": ""
},
{
"agent_id": "83e1cd25be424ecda678641501555531",
"agent_name": "Nova示例Agent",
"agent_description": "一个Nova的Agent示例包含Agent具备的通用功能文件操作、沙箱环境执行、浏览器操作等等。"
} }
] ]
} }

1
.project Normal file
View File

@@ -0,0 +1 @@
{"project_id": "371a88b57cff44999c4cf61b4708638a", "user_id": 1009}

View File

@@ -3,7 +3,6 @@ FROM node:22-slim AS builder
WORKDIR /app WORKDIR /app
ENV CI=true ENV CI=true
COPY .npmrc /root/.npmrc
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@latest --activate
COPY . . COPY . .
@@ -17,7 +16,6 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV CI=true ENV CI=true
COPY .npmrc /root/.npmrc
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=builder /app/package.json ./ COPY --from=builder /app/package.json ./

View File

@@ -2,70 +2,118 @@ import { NextResponse } from 'next/server'
import { llmClient } from '@/app/api/llm-client' import { llmClient } from '@/app/api/llm-client'
const SYSTEM_PROMPT = `你是一个专门分析互联网大厂老板PUA话术的AI裁判。 const SYSTEM_PROMPT = `你是一个专门分析互联网大厂老板PUA话术的AI裁判。
用户输入一段老板/领导对员工说的"打鸡血"或"PUA"的话,你需要分析这段话并给出评分和游戏效果。
评分标准1-10分 你的任务分两步
1-3分废话/没营养("大家加油"之类)→ 轻微效果 1. 判断当前输入是否与历史记录重复或高度相似(意思相近算相似,不只是字面相同)
4-6分标准大厂黑话"对齐""闭环""狼性")→ 中等效果 2. 对当前输入进行鸡血值评分
7-8分强力PUA"996是福报""不拼搏对不起父母")→ 强力效果
9-10分终极毒鸡汤极限施压/情感绑架)→ 全场爆发 相似度判断标准similarity字段0.0-1.0
- 0.0-0.3:全新内容,没有相似之处
- 0.3-0.6:有一定相关,但表达不同
- 0.6-0.8:明显相似,换汤不换药
- 0.8-1.0:基本重复,几乎一样的意思
评分标准score字段1-10分
- 1-3分废话/没营养 → backfire效果
- 4-6分标准大厂黑话 → 中等效果
- 7-8分强力PUA → 攻速暴增
- 9-10分终极毒鸡汤 → 全场狂暴
只返回如下JSON不要其他内容 只返回如下JSON不要其他内容
{ {
"similarity": 数字(0.0-1.0),
"similarTo": "最相似的历史内容摘要,没有则为空字符串",
"score": 数字(1-10), "score": 数字(1-10),
"title": "效果名称2-6个汉字有创意", "title": "效果名称2-6个汉字有创意",
"desc": "对话语的一句话点评15字以内带点讽刺", "desc": "对话语的一句话点评15字以内带点讽刺",
"effect": "attack_boost" | "speed_boost" | "money_rain" | "rage_mode" | "backfire" "effect": "attack_boost" | "speed_boost" | "money_rain" | "rage_mode" | "backfire"
} }`
effect说明 // 重复惩罚倍率:根据相似度递增
- attack_boost: 攻击力提升score 4-6 function getDuplicatePenaltyMultiplier(similarity: number): number {
- speed_boost: 攻击速度提升score 7-8 if (similarity >= 0.8) return 2.0 // 严重重复:再扣双倍
- money_rain: HC暴增score 5-7话语中强调利益 if (similarity >= 0.6) return 1.0 // 明显相似:再扣一倍
- rage_mode: 全场狂暴score 9-10 return 0 // 不额外惩罚
- backfire: 话太废了反而debuffscore 1-2` }
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { text } = await req.json() const { text, history } = await req.json() as {
text: string
history: string[] // 最近N条历史原文
}
if (!text || typeof text !== 'string' || text.trim().length < 2) { if (!text || typeof text !== 'string' || text.trim().length < 2) {
return NextResponse.json({ error: '输入内容太短了' }, { status: 400 }) return NextResponse.json({ error: '输入内容太短了' }, { status: 400 })
} }
// 构建带历史的 prompt
const historyBlock = history && history.length > 0
? `\n\n历史记录已经说过的话按时间倒序\n${history.slice(0, 5).map((h, i) => `${i + 1}. "${h}"`).join('\n')}`
: '\n\n历史记录暂无'
const userMsg = `当前输入:"${text.slice(0, 200)}"${historyBlock}`
const resp = await llmClient.chat({ const resp = await llmClient.chat({
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
messages: [ messages: [
{ role: 'system', content: SYSTEM_PROMPT }, { role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: text.slice(0, 200) }, { role: 'user', content: userMsg },
], ],
maxTokens: 150, maxTokens: 200,
temperature: 0.7, temperature: 0.6,
}) })
const raw = resp.choices?.[0]?.message?.content ?? '' const raw = resp.choices?.[0]?.message?.content ?? ''
// 提取 JSON防止 LLM 多输出文字)
const match = raw.match(/\{[\s\S]*\}/) const match = raw.match(/\{[\s\S]*\}/)
if (!match) throw new Error('LLM 返回格式异常') if (!match) throw new Error('LLM 返回格式异常')
const result = JSON.parse(match[0]) const result = JSON.parse(match[0])
const score = Math.max(1, Math.min(10, Number(result.score) || 5)) const score = Math.max(1, Math.min(10, Number(result.score) || 5))
const similarity = Math.max(0, Math.min(1, Number(result.similarity) || 0))
const penaltyMultiplier = getDuplicatePenaltyMultiplier(similarity)
const isDuplicate = similarity >= 0.6
// 如果重复,效果强制降级或 backfire
let effect = result.effect || 'attack_boost'
let adjustedScore = score
if (isDuplicate && similarity >= 0.8) {
effect = 'backfire'
adjustedScore = Math.max(1, score - 3)
} else if (isDuplicate && similarity >= 0.6) {
// 效果打半折rage_mode→speed_boostspeed_boost→attack_boostetc.
const downgrade: Record<string, string> = {
rage_mode: 'speed_boost',
speed_boost: 'attack_boost',
money_rain: 'attack_boost',
attack_boost: 'backfire',
backfire: 'backfire',
}
effect = downgrade[effect] ?? 'backfire'
adjustedScore = Math.max(1, score - 2)
}
return NextResponse.json({ return NextResponse.json({
score, score: adjustedScore,
title: result.title || '打鸡血', title: result.title || '打鸡血',
desc: result.desc || '还行吧', desc: result.desc || '还行吧',
effect: result.effect || 'attack_boost', effect,
similarity,
similarTo: result.similarTo || '',
isDuplicate,
penaltyMultiplier, // 前端据此额外扣 HC
}) })
} catch (e) { } catch (e) {
console.error('[pua-score]', e) console.error('[pua-score]', e)
// 降级:随机给个分数
const score = Math.floor(Math.random() * 7) + 3
return NextResponse.json({ return NextResponse.json({
score, score: 3,
title: '随机鸡血', title: '随机鸡血',
desc: 'AI开小差了随机发力', desc: 'AI开小差了随机发力',
effect: 'attack_boost', effect: 'attack_boost',
similarity: 0,
similarTo: '',
isDuplicate: false,
penaltyMultiplier: 0,
}) })
} }
} }

430
app/game/TutorialModal.tsx Normal file
View File

@@ -0,0 +1,430 @@
'use client'
import { useState, useEffect } from 'react'
const STORAGE_KEY = 'dachang-tutorial-seen-v1'
// ── 教程步骤数据 ─────────────────────────────────────────────────────────────
const STEPS = [
{
id: 'story',
icon: '🏢',
title: '故事背景',
subtitle: '2026年某大厂总部',
content: (
<div style={{ lineHeight: 1.7 }}>
<p>
"组织架构调整"<span style={{ color: '#FBBF24' }}>VP</span>
"职场怪物"<b></b><b></b>
</p>
<p style={{ marginTop: '10px' }}>
<span style={{ color: '#EF4444' }}>KPI归零</span>
"毕业"
</p>
<p style={{ marginTop: '10px' }}>
<br />
<span style={{ color: '#A78BFA' }}>HC</span>线<br />
KPI
</p>
</div>
),
},
{
id: 'deploy',
icon: '🗺️',
title: '如何部署防线',
subtitle: '拖拽?不,点击就够了',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#A78BFA' }}></b> = </span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#22C55E' }}></b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#F59E0B' }}></b> 10 HC</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#EC4899' }}></b></span>
</div>
<div style={{
marginTop: '6px',
backgroundColor: 'rgba(167,139,250,0.1)',
border: '1px solid rgba(167,139,250,0.3)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#CBD5E1',
}}>
💡 <b></b>
</div>
</div>
</div>
),
},
{
id: 'wave',
icon: '⚔️',
title: '战斗与波次',
subtitle: '召唤敌人才能赚HC',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#C4B5FD' }}></b> HC</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🕐</span>
<span><b style={{ color: '#FBBF24' }}>2</b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>📋</span>
<span>3<b style={{ color: '#FCD34D' }}></b>3</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>👑</span>
<span>5<b style={{ color: '#FBBF24' }}>BOSS波</b>VP会随机摧毁你的防御塔</span>
</div>
<div style={{
marginTop: '2px',
backgroundColor: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.25)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#FCA5A5',
}}>
<b>3 × 5</b> HC
</div>
</div>
</div>
),
},
{
id: 'meeting',
icon: '📋',
title: '开会 & 发激励',
subtitle: '暂停游戏PUA员工',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🟢</span>
<span><b style={{ color: '#4ADE80' }}>📋 </b><b></b></span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span><b style={{ color: '#EC4899' }}>PUA激励台</b>//...AI会评分并给出游戏加成</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}>🎯</span>
<span>
<span style={{ color: '#22C55E' }}> 4-6</span>
<span style={{ color: '#FBBF24' }}> 7-8</span>
<span style={{ color: '#FF4E00' }}> 9-10</span>
</span>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
<span style={{ fontSize: '22px', flexShrink: 0 }}></span>
<span> = HC × 15%<b style={{ color: '#F97316' }}></b>HC</span>
</div>
<div style={{
marginTop: '2px',
backgroundColor: 'rgba(74,222,128,0.08)',
border: '1px solid rgba(74,222,128,0.25)',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '13px',
color: '#86EFAC',
}}>
💡 <b></b>
</div>
</div>
</div>
),
},
{
id: 'tactics',
icon: '🧠',
title: '兵力部署建议',
subtitle: '老板传授的排兵布阵',
content: (
<div style={{ lineHeight: 1.7 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '9px' }}>
<div style={{
backgroundColor: 'rgba(34,197,94,0.1)',
border: '1px solid rgba(34,197,94,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#22C55E' }}>1-2</b>
<br />002-3
</div>
<div style={{
backgroundColor: 'rgba(59,130,246,0.1)',
border: '1px solid rgba(59,130,246,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#3B82F6' }}>3-4</b>
<br />PPT大师减速P6资深开发远程输出HRBP放中央提速
</div>
<div style={{
backgroundColor: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#EF4444' }}>BOSS波5</b>
<br />BOSS路径上
</div>
<div style={{
backgroundColor: 'rgba(167,139,250,0.1)',
border: '1px solid rgba(167,139,250,0.3)',
borderRadius: '6px', padding: '7px 10px',
}}>
<b style={{ color: '#A78BFA' }}></b>
<br /> &gt; 线HRBP永远放在其他塔的中间
</div>
</div>
</div>
),
},
]
// ── 教程弹窗组件 ─────────────────────────────────────────────────────────────
interface TutorialModalProps {
onClose: () => void
}
export function TutorialModal({ onClose }: TutorialModalProps) {
const [step, setStep] = useState(0)
const current = STEPS[step]
const isLast = step === STEPS.length - 1
const isFirst = step === 0
const handleClose = () => {
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, '1')
}
onClose()
}
return (
<div style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.82)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '16px',
backdropFilter: 'blur(4px)',
}}>
<div style={{
backgroundColor: '#0a1628',
border: '2px solid #7C3AED',
borderRadius: '16px',
width: '100%',
maxWidth: '520px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 0 60px rgba(124,58,237,0.5), 0 24px 64px rgba(0,0,0,0.8)',
overflow: 'hidden',
}}>
{/* 顶部:进度条 */}
<div style={{
height: '3px',
backgroundColor: '#1e3a5f',
flexShrink: 0,
}}>
<div style={{
height: '100%',
width: `${((step + 1) / STEPS.length) * 100}%`,
background: 'linear-gradient(90deg, #7C3AED, #EC4899)',
transition: 'width 0.3s ease',
borderRadius: '2px',
}} />
</div>
{/* 步骤指示点 */}
<div style={{
display: 'flex',
justifyContent: 'center',
gap: '6px',
padding: '12px 24px 0',
flexShrink: 0,
}}>
{STEPS.map((s, i) => (
<button
key={s.id}
onClick={() => setStep(i)}
style={{
width: i === step ? '20px' : '8px',
height: '8px',
borderRadius: '4px',
border: 'none',
backgroundColor: i === step ? '#7C3AED' : i < step ? '#4C1D95' : '#1e3a5f',
cursor: 'pointer',
transition: 'all 0.2s ease',
padding: 0,
}}
/>
))}
</div>
{/* 内容区 */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '20px 28px',
}}>
{/* 图标 + 标题 */}
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '40px', marginBottom: '8px', lineHeight: 1 }}>
{current.icon}
</div>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '11px',
color: '#A78BFA',
marginBottom: '6px',
letterSpacing: '0.5px',
}}>
{current.title}
</div>
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '16px',
color: '#64748B',
}}>
{current.subtitle}
</div>
</div>
{/* 分割线 */}
<div style={{
height: '1px',
background: 'linear-gradient(90deg, transparent, #1e3a5f, transparent)',
marginBottom: '16px',
}} />
{/* 正文 */}
<div style={{
fontFamily: 'VT323, monospace',
fontSize: '15px',
color: '#CBD5E1',
}}>
{current.content}
</div>
</div>
{/* 底部导航 */}
<div style={{
borderTop: '1px solid #1e3a5f',
padding: '14px 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.5)',
}}>
{/* 步骤文字 */}
<span style={{
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#334155',
}}>
{step + 1} / {STEPS.length}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
{/* 上一步 */}
{!isFirst && (
<button
onClick={() => setStep(s => s - 1)}
style={{
padding: '8px 16px',
backgroundColor: '#0F1B2D',
border: '1px solid #1e3a5f',
borderRadius: '8px',
color: '#94A3B8',
fontFamily: 'VT323, monospace',
fontSize: '16px',
cursor: 'pointer',
}}
>
</button>
)}
{/* 跳过(仅前几步) */}
{!isLast && (
<button
onClick={handleClose}
style={{
padding: '8px 16px',
backgroundColor: 'transparent',
border: '1px solid #1e293b',
borderRadius: '8px',
color: '#475569',
fontFamily: 'VT323, monospace',
fontSize: '16px',
cursor: 'pointer',
}}
>
</button>
)}
{/* 下一步 / 开始战斗 */}
<button
onClick={isLast ? handleClose : () => setStep(s => s + 1)}
style={{
padding: '8px 20px',
backgroundColor: isLast ? '#7C3AED' : '#1e3a5f',
border: `2px solid ${isLast ? '#A78BFA' : '#7C3AED'}`,
borderRadius: '8px',
color: isLast ? '#E2E8F0' : '#C4B5FD',
fontFamily: isLast ? "'Press Start 2P', monospace" : 'VT323, monospace',
fontSize: isLast ? '9px' : '16px',
cursor: 'pointer',
boxShadow: isLast ? '0 0 16px rgba(124,58,237,0.5)' : 'none',
letterSpacing: isLast ? '0.5px' : 'normal',
transition: 'all 0.15s',
}}
>
{isLast ? '开始战斗 ▶' : '下一步 →'}
</button>
</div>
</div>
</div>
</div>
)
}
// ── Hook判断是否需要显示教程 ────────────────────────────────────────────────
export function useTutorial() {
const [show, setShow] = useState(false)
useEffect(() => {
// 避免 SSR 问题,在客户端检查
if (typeof window === 'undefined') return
const seen = localStorage.getItem(STORAGE_KEY)
if (!seen) setShow(true)
}, [])
const dismiss = () => {
localStorage.setItem(STORAGE_KEY, '1')
setShow(false)
}
return { show, dismiss }
}

View File

@@ -1,16 +1,91 @@
'use client' 'use client'
import { useEffect, useRef, useState, useCallback } from 'react' import { useEffect, useRef, useState, useCallback } from 'react'
import { TutorialModal, useTutorial } from './TutorialModal'
const TOWER_META = [ // ── 塔的完整元数据(用于底部面板 + Tooltip ──────────────────────────────
{ type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空5%自伤' }, interface TowerInfo {
{ type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场5%概率秒杀' }, type: string
{ type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助+20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' }, name: string
{ type: 'ops', name: '运营专员', cost: 90, desc: '溅射 18伤', color: '#8B5CF6', img: '/game-assets/tower-ops.png', tip: '增长黑客20%双倍HC' }, cost: number
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域减速40%' }, color: string
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' }, img: string
{ type: 'pm', name: '产品经理', cost: 160, desc: '需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更每4次把怪打回去' }, // Tooltip 内容
] as const quote: string // 人物一句话名言/自我介绍
atk: string // 攻击类型
dmg: string // 伤害数值
range: string // 射程
speed: string // 攻速
skill: string // 特殊技能名
skillDesc: string // 技能描述
tactics: string // 建议打法
}
const TOWER_META: TowerInfo[] = [
{
type: 'outsource', name: '外包程序员', cost: 30, color: '#94A3B8',
img: '/game-assets/tower-outsource.png',
quote: '"能跑就行,文档?没有!"',
atk: '近战', dmg: '8', range: '2格', speed: '0.7次/s',
skill: 'Bug自助餐',
skillDesc: '每次攻击30%概率丢空5%概率Bug反弹精力瞬间归零',
tactics: '前期凑数用,价格最便宜。不要指望他,放在路口堵一堵就够了。',
},
{
type: 'intern', name: '00后实习生', cost: 50, color: '#22C55E',
img: '/game-assets/tower-intern.png',
quote: '"准点下班,法律护身。"',
atk: '近战', dmg: '15', range: '1.5格', speed: '1.5次/s',
skill: '整顿职场',
skillDesc: '每次攻击5%概率直接秒杀普通怪物每秒有1%概率自动"跑路"退还25HC',
tactics: '性价比最高的前排攒够HC先多建几个。秒杀概率虽低但惊喜不断。',
},
{
type: 'hrbp', name: 'HRBP', cost: 80, color: '#EC4899',
img: '/game-assets/tower-hrbp.png',
quote: '"公司是你家,福报靠大家。"',
atk: '辅助(无伤害)', dmg: '0', range: '1格光环', speed: '每0.5s触发',
skill: '打鸡血',
skillDesc: '持续给周围1格内所有塔附加+20%攻速BUFF每次消耗自身5点精力',
tactics: '永远放在塔群中央搭配P6或实习生使用效果翻倍但别让她精力耗完。',
},
{
type: 'ops', name: '运营专员', cost: 90, color: '#8B5CF6',
img: '/game-assets/tower-ops.png',
quote: '"数据不好看A/B测"',
atk: '远程 范围溅射', dmg: '18', range: '3格', speed: '1.2次/s',
skill: '增长黑客',
skillDesc: '每次命中20%概率触发"双倍HC",目标死亡时奖励翻倍;命中范围内相邻怪物也受溅射伤害',
tactics: '人多的时候最强专门对付密集的应届生群体。顺手赚HC。',
},
{
type: 'ppt', name: 'PPT大师', cost: 100, color: '#F59E0B',
img: '/game-assets/tower-ppt.png',
quote: '"底层逻辑要打通,顶层设计要对齐。"',
atk: 'AOE范围', dmg: '5范围', range: '3格圆形', speed: '1.5次/s',
skill: '黑话领域',
skillDesc: '攻击圆形范围内所有怪物并降低移速40%持续2秒越多怪物越划算',
tactics: '减速神器放在路径转弯处效果最佳。搭配P6的DOT伤害组合拳秒杀老员工。',
},
{
type: 'senior', name: 'P6资深开发', cost: 120, color: '#3B82F6',
img: '/game-assets/tower-senior.png',
quote: '"这行代码谁写的?别问,是我。"',
atk: '远程直线', dmg: '30 + DOT 10/s×3s', range: '5格', speed: '1.0次/s',
skill: '代码屎山',
skillDesc: '子弹命中后附加DOT每秒10伤持续3秒远程射程5格直线穿透感',
tactics: '主力输出射程超长。优先打护盾老员工和BossDOT能绕过护盾持续掉血。',
},
{
type: 'pm', name: '产品经理', cost: 160, color: '#06B6D4',
img: '/game-assets/tower-pm.png',
quote: '"这个需求很简单,下班前能上线吗?"',
atk: '远程曲线', dmg: '20', range: '4格', speed: '0.8次/s',
skill: '需求变更',
skillDesc: '每4次攻击触发"需求变更"将目标强制打回2个路径节点大幅拖延到达时间',
tactics: '最贵但效果独特把Boss和老员工不断打回去配合其他输出塔可以让怪走不出去。',
},
]
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm' type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
@@ -21,7 +96,11 @@ interface PuaResult {
title: string title: string
desc: string desc: string
effect: EffectType effect: EffectType
cost?: number // 实际扣除的 HC前端填写 cost?: number
similarity?: number
similarTo?: string
isDuplicate?: boolean
penaltyMultiplier?: number
} }
const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = { const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = {
@@ -40,6 +119,130 @@ const PUA_PLACEHOLDERS = [
'大家加油,相信自己!', '大家加油,相信自己!',
] ]
// ── 塔的悬浮信息卡片 ─────────────────────────────────────────────────────────
function TowerTooltip({
tower,
pos,
canAfford,
}: {
tower: TowerInfo
pos: { left: number; bottom: number }
canAfford: boolean
}) {
return (
<div style={{
position: 'fixed',
left: pos.left,
bottom: pos.bottom,
transform: 'translateX(-50%)',
zIndex: 9999,
width: '220px',
backgroundColor: '#0a1628',
border: `2px solid ${tower.color}`,
borderRadius: '10px',
padding: '12px',
boxShadow: `0 0 24px ${tower.color}44, 0 8px 32px rgba(0,0,0,0.7)`,
pointerEvents: 'none',
}}>
{/* 小三角指示器 */}
<div style={{
position: 'absolute', bottom: '-8px', left: '50%',
transform: 'translateX(-50%)',
width: 0, height: 0,
borderLeft: '7px solid transparent',
borderRight: '7px solid transparent',
borderTop: `7px solid ${tower.color}`,
}} />
{/* 头部:头像 + 名字 + 费用 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={tower.img} alt={tower.name}
style={{ width: '40px', height: '40px', objectFit: 'contain', flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '8px', color: tower.color, lineHeight: 1.4,
}}>
{tower.name}
</div>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px',
color: canAfford ? '#A78BFA' : '#EF4444',
marginTop: '3px',
}}>
{tower.cost} HC {!canAfford && '(不足)'}
</div>
</div>
</div>
{/* 名言 */}
<div style={{
fontFamily: 'VT323, monospace', fontSize: '13px',
color: '#94A3B8', fontStyle: 'italic',
borderLeft: `2px solid ${tower.color}66`,
paddingLeft: '8px', marginBottom: '8px', lineHeight: 1.3,
}}>
{tower.quote}
</div>
{/* 数值属性 */}
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr',
gap: '3px 8px', marginBottom: '8px',
}}>
{[
['攻击', tower.atk],
['射程', tower.range],
['伤害', tower.dmg],
['攻速', tower.speed],
].map(([label, val]) => (
<div key={label} style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '10px', color: '#475569' }}>
{label}
</span>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#E2E8F0' }}>
{val}
</span>
</div>
))}
</div>
{/* 技能 */}
<div style={{
backgroundColor: `${tower.color}15`,
border: `1px solid ${tower.color}40`,
borderRadius: '6px',
padding: '6px 8px',
marginBottom: '7px',
}}>
<div style={{
fontFamily: "'Press Start 2P', monospace",
fontSize: '7px', color: tower.color, marginBottom: '4px',
}}>
{tower.skill}
</div>
<div style={{
fontFamily: 'VT323, monospace', fontSize: '12px',
color: '#CBD5E1', lineHeight: 1.35,
}}>
{tower.skillDesc}
</div>
</div>
{/* 打法建议 */}
<div style={{
fontFamily: 'VT323, monospace', fontSize: '12px',
color: '#64748B', lineHeight: 1.35,
borderTop: '1px solid #1e3a5f', paddingTop: '6px',
}}>
💡 {tower.tactics}
</div>
</div>
)
}
// ── 费用计算基于当前HC的15%最低20最高200取整到10的倍数 ──────────── // ── 费用计算基于当前HC的15%最低20最高200取整到10的倍数 ────────────
function calcPuaCost(hc: number): number { function calcPuaCost(hc: number): number {
const raw = Math.ceil(hc * 0.15) const raw = Math.ceil(hc * 0.15)
@@ -53,9 +256,16 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [result, setResult] = useState<PuaResult | null>(null) const [result, setResult] = useState<PuaResult | null>(null)
const [history, setHistory] = useState<PuaResult[]>([]) const [history, setHistory] = useState<PuaResult[]>([])
// 历史原文列表(用于发给服务端做相似度检测)
const historyTexts = useRef<string[]>([])
const [insufficient, setInsufficient] = useState(false) const [insufficient, setInsufficient] = useState(false)
const [dupWarning, setDupWarning] = useState<string | null>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const placeholder = useRef(PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)]).current const [placeholder, setPlaceholder] = useState(PUA_PLACEHOLDERS[0])
useEffect(() => {
// 只在客户端随机选取,避免 SSR/客户端 hydration 不匹配
setPlaceholder(PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)])
}, [])
const cost = calcPuaCost(hc) const cost = calcPuaCost(hc)
const canAfford = hc >= cost const canAfford = hc >= cost
@@ -63,7 +273,6 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
if (!text.trim() || loading || !gameReady || !waveStarted) return if (!text.trim() || loading || !gameReady || !waveStarted) return
// 先从游戏扣除 HC扣不到则拒绝
const spendHC: ((n: number) => boolean) | undefined = const spendHC: ((n: number) => boolean) | undefined =
typeof window !== 'undefined' ? (window as any).__gameSpendHC : undefined typeof window !== 'undefined' ? (window as any).__gameSpendHC : undefined
@@ -74,6 +283,7 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const actualCost = calcPuaCost(currentHC) const actualCost = calcPuaCost(currentHC)
// 扣除基础费用
if (!spendHC?.(actualCost)) { if (!spendHC?.(actualCost)) {
setInsufficient(true) setInsufficient(true)
setTimeout(() => setInsufficient(false), 2000) setTimeout(() => setInsufficient(false), 2000)
@@ -82,18 +292,43 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
setLoading(true) setLoading(true)
setResult(null) setResult(null)
setDupWarning(null)
setInsufficient(false) setInsufficient(false)
try { try {
const res = await fetch('/api/pua-score', { const res = await fetch('/api/pua-score', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim() }), body: JSON.stringify({
text: text.trim(),
history: historyTexts.current,
}),
}) })
const data: PuaResult & { cost?: number } = await res.json() const data: PuaResult = await res.json()
data.cost = actualCost data.cost = actualCost
// 处理重复惩罚:额外扣 HC
const penalty = data.penaltyMultiplier ?? 0
if (penalty > 0 && data.isDuplicate) {
const extraCost = Math.round(actualCost * penalty)
const penaltyPaid = spendHC?.(extraCost) ?? false
if (penaltyPaid) {
data.cost = actualCost + extraCost
// 显示重复警告
const sim = data.similarity ?? 0
const level = sim >= 0.8 ? '严重重复' : '内容相似'
setDupWarning(
`检测到${level}!额外扣除 ${extraCost} HC` +
(data.similarTo ? `\n与"${data.similarTo}"相似)` : '')
)
}
}
setResult(data) setResult(data)
// 更新历史原文列表最近8条
historyTexts.current = [text.trim(), ...historyTexts.current].slice(0, 8)
setHistory(prev => [data, ...prev].slice(0, 5)) setHistory(prev => [data, ...prev].slice(0, 5))
// 通知游戏场景应用 buff
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff?.(data.effect, data.score, data.title) ;(window as any).__gamePuaBuff?.(data.effect, data.score, data.title)
} }
@@ -183,7 +418,7 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
</div> </div>
</div> </div>
{/* HC 不足闪烁提示 */} {/* HC 不足提示 */}
{insufficient && ( {insufficient && (
<div style={{ <div style={{
backgroundColor: '#7F1D1D', backgroundColor: '#7F1D1D',
@@ -194,12 +429,28 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
fontSize: '14px', fontSize: '14px',
color: '#FCA5A5', color: '#FCA5A5',
textAlign: 'center', textAlign: 'center',
animation: 'none',
}}> }}>
HC HC
</div> </div>
)} )}
{/* 重复惩罚警告 */}
{dupWarning && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '6px',
padding: '7px 8px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#FED7AA',
lineHeight: 1.4,
whiteSpace: 'pre-line',
}}>
{dupWarning}
</div>
)}
{/* 输入框 */} {/* 输入框 */}
<textarea <textarea
ref={textareaRef} ref={textareaRef}
@@ -295,6 +546,35 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
}}> }}>
{result.desc} {result.desc}
</div> </div>
{/* 相似度指示(有重复时显示) */}
{(result.similarity ?? 0) >= 0.3 && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '4px',
padding: '4px 6px',
fontFamily: 'VT323, monospace',
fontSize: '12px',
color: '#FED7AA',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
<span></span>
<span style={{ color: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316' }}>
{Math.round((result.similarity ?? 0) * 100)}%
</span>
</div>
{/* 相似度进度条 */}
<div style={{ height: '3px', backgroundColor: '#1c0a04', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${(result.similarity ?? 0) * 100}%`,
backgroundColor: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316',
borderRadius: '2px',
transition: 'width 0.3s ease',
}} />
</div>
</div>
)}
{/* 效果标签 + 花费 */} {/* 效果标签 + 花费 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ <div style={{
@@ -376,13 +656,15 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
paddingTop: '8px', paddingTop: '8px',
}}> }}>
<span style={{ color: '#475569' }}> = HC × 15%</span><br /> <span style={{ color: '#475569' }}> = HC × 15%</span><br />
<span style={{ color: '#334155' }}>20 200</span><br />
<br /> <br />
Enter <br /> 60%: <br />
1-2分: 废话翻<br /> 80%: <br />
4-6分: 攻击+HC<br /> HC<br />
<br />
9-10分: 全场狂暴<br />
7-8分: 攻速暴增<br /> 7-8分: 攻速暴增<br />
9-10: 全场狂暴 4-6: 攻击+HC<br />
1-2分: 废话翻车
</div> </div>
</div> </div>
) )
@@ -391,16 +673,28 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
// ── 主游戏页面 ──────────────────────────────────────────────────────────────── // ── 主游戏页面 ────────────────────────────────────────────────────────────────
export default function GamePage() { export default function GamePage() {
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null) const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
const [hc, setHc] = useState(200) const [hc, setHc] = useState(170)
const [selectedTower, setSelectedTower] = useState<TowerType | null>(null) const [selectedTower, setSelectedTower] = useState<TowerType | null>(null)
const [gameReady, setGameReady] = useState(false) const [gameReady, setGameReady] = useState(false)
const [waveStarted, setWaveStarted] = useState(false) const [waveStarted, setWaveStarted] = useState(false)
// 召唤按钮状态(由 HUD 通过 window.__gameSetWaveBtn 驱动) const [inMeeting, setInMeeting] = useState(false)
const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({ const [waveBtn, setWaveBtn] = useState<{ text: string; disabled: boolean }>({
text: '▶ 召唤下一波', text: '▶ 召唤下一波',
disabled: false, disabled: false,
}) })
const selectedTowerRef = useRef<TowerType | null>(null) const selectedTowerRef = useRef<TowerType | null>(null)
const { show: showTutorial, dismiss: dismissTutorial } = useTutorial()
const handleMeeting = useCallback(() => {
if (!gameReady) return
if (!inMeeting) {
const ok = typeof window !== 'undefined' && (window as any).__gamePause?.()
if (ok) setInMeeting(true)
} else {
const ok = typeof window !== 'undefined' && (window as any).__gameResume?.()
if (ok) setInMeeting(false)
}
}, [gameReady, inMeeting])
const handleSelectTower = useCallback((type: TowerType) => { const handleSelectTower = useCallback((type: TowerType) => {
const next = selectedTowerRef.current === type ? null : type const next = selectedTowerRef.current === type ? null : type
@@ -411,6 +705,10 @@ export default function GamePage() {
} }
}, []) }, [])
// Tooltip 状态
const [hoveredTower, setHoveredTower] = useState<TowerInfo | null>(null)
const [tooltipPos, setTooltipPos] = useState<{ left: number; bottom: number } | null>(null)
useEffect(() => { useEffect(() => {
let mounted = true let mounted = true
const initGame = async () => { const initGame = async () => {
@@ -466,7 +764,8 @@ export default function GamePage() {
;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower', ;['__gameOnHCChange','__gameOnTowerDeselect','__gameSelectTower',
'__gameReady','__gameDifficulty','__gamePuaBuff', '__gameReady','__gameDifficulty','__gamePuaBuff',
'__gameGetHC','__gameSpendHC','__gameWaveStarted', '__gameGetHC','__gameSpendHC','__gameWaveStarted',
'__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick'].forEach(k => { '__gameSetWaveBtn','__gameWaveBtnState','__gameOnWaveClick',
'__gamePause','__gameResume','__gameIsPaused'].forEach(k => {
delete (window as any)[k] delete (window as any)[k]
}) })
} }
@@ -486,79 +785,126 @@ export default function GamePage() {
style={{ backgroundColor: '#0A1628' }} style={{ backgroundColor: '#0A1628' }}
/> />
{/* 右侧面板HC + 召唤按钮 + PUA激励台 */} {/* 右侧面板HC + 开会按钮 + 召唤按钮 + PUA激励台 */}
<div style={{ <div style={{
width: '240px', width: '240px',
flexShrink: 0, flexShrink: 0,
backgroundColor: 'rgba(10,18,40,0.97)', backgroundColor: 'rgba(10,18,40,0.97)',
borderLeft: '2px solid #1e3a5f', borderLeft: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden', overflow: 'hidden',
transition: 'border-color 0.2s',
boxShadow: inMeeting ? 'inset 0 0 20px rgba(34,197,94,0.08)' : 'none',
}}> }}>
{/* HC 数量显示 */} {/* HC 数量 + 开会按钮(同一行) */}
<div style={{ <div style={{
padding: '10px 12px 8px', padding: '8px 12px',
borderBottom: '1px solid #1e3a5f', borderBottom: '1px solid #1e3a5f',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <div style={{ flex: 1 }}>
<span style={{ fontFamily: 'VT323, monospace', fontSize: '13px', color: '#64748B' }}> <div style={{ fontFamily: 'VT323, monospace', fontSize: '11px', color: '#475569', marginBottom: '1px' }}>
</span> </div>
<span style={{ <div style={{
fontFamily: "'Press Start 2P', monospace", fontFamily: "'Press Start 2P', monospace",
fontSize: '12px', fontSize: '11px',
color: '#A78BFA', color: '#A78BFA',
letterSpacing: '1px', letterSpacing: '1px',
}}> }}>
{hc} HC {hc} HC
</span>
</div> </div>
</div> </div>
{/* 开会按钮 */}
<button
onClick={handleMeeting}
disabled={!gameReady || !waveStarted}
title={inMeeting ? '结束开会,恢复游戏' : '开会暂停,发起激励'}
style={{
flexShrink: 0,
padding: '6px 10px',
backgroundColor: inMeeting ? '#14532D' : '#0F1B2D',
border: `2px solid ${inMeeting ? '#22C55E' : '#1e3a5f'}`,
borderRadius: '8px',
color: inMeeting ? '#4ADE80' : (!gameReady || !waveStarted ? '#334155' : '#94A3B8'),
fontFamily: 'VT323, monospace',
fontSize: '14px',
cursor: !gameReady || !waveStarted ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
whiteSpace: 'nowrap',
lineHeight: 1.2,
boxShadow: inMeeting ? '0 0 10px rgba(34,197,94,0.3)' : 'none',
}}
>
{inMeeting ? '📋 结束开会' : '📋 开会'}
</button>
</div>
{/* 召唤下一波按钮 */} {/* 开会中提示条 */}
<div style={{ padding: '10px 12px', borderBottom: '2px solid #1e3a5f' }}> {inMeeting && (
<div style={{
backgroundColor: '#14532D',
borderBottom: '1px solid #22C55E',
padding: '5px 12px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#86EFAC',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span style={{ animation: 'pulse 1.5s ease-in-out infinite' }}></span>
</div>
)}
{/* 召唤下一波按钮(开会时禁用) */}
<div style={{ padding: '8px 12px', borderBottom: '2px solid #1e3a5f' }}>
<button <button
onClick={() => { onClick={() => {
if (inMeeting) return
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
(window as any).__gameOnWaveClick?.() (window as any).__gameOnWaveClick?.()
} }
}} }}
disabled={waveBtn.disabled || !gameReady} disabled={waveBtn.disabled || !gameReady || inMeeting}
style={{ style={{
width: '100%', width: '100%',
padding: '10px 8px', padding: '10px 8px',
backgroundColor: waveBtn.disabled || !gameReady ? '#0F172A' : '#1e3a5f', backgroundColor: waveBtn.disabled || !gameReady || inMeeting ? '#0F172A' : '#1e3a5f',
border: `2px solid ${waveBtn.disabled || !gameReady ? '#1e293b' : '#7C3AED'}`, border: `2px solid ${waveBtn.disabled || !gameReady || inMeeting ? '#1e293b' : '#7C3AED'}`,
borderRadius: '8px', borderRadius: '8px',
color: waveBtn.disabled || !gameReady ? '#4B5563' : '#C4B5FD', color: waveBtn.disabled || !gameReady || inMeeting ? '#4B5563' : '#C4B5FD',
fontFamily: 'VT323, monospace', fontFamily: 'VT323, monospace',
fontSize: '20px', fontSize: '20px',
cursor: waveBtn.disabled || !gameReady ? 'not-allowed' : 'pointer', cursor: waveBtn.disabled || !gameReady || inMeeting ? 'not-allowed' : 'pointer',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
letterSpacing: '1px', letterSpacing: '1px',
lineHeight: 1.2, lineHeight: 1.2,
boxShadow: waveBtn.disabled || !gameReady ? 'none' : '0 0 12px rgba(124,58,237,0.3)', boxShadow: waveBtn.disabled || !gameReady || inMeeting ? 'none' : '0 0 12px rgba(124,58,237,0.3)',
}} }}
onMouseEnter={e => { onMouseEnter={e => {
if (!waveBtn.disabled && gameReady) { if (!waveBtn.disabled && gameReady && !inMeeting) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#2d3a5e' (e.currentTarget as HTMLButtonElement).style.backgroundColor = '#2d3a5e'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 18px rgba(124,58,237,0.5)' ;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 18px rgba(124,58,237,0.5)'
} }
}} }}
onMouseLeave={e => { onMouseLeave={e => {
if (!waveBtn.disabled && gameReady) { if (!waveBtn.disabled && gameReady && !inMeeting) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = '#1e3a5f' (e.currentTarget as HTMLButtonElement).style.backgroundColor = '#1e3a5f'
;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 12px rgba(124,58,237,0.3)' ;(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 12px rgba(124,58,237,0.3)'
} }
}} }}
> >
{waveBtn.text} {inMeeting ? '开会中...' : waveBtn.text}
</button> </button>
</div> </div>
{/* PUA 激励台 */} {/* PUA 激励台 */}
<PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted} /> <PuaPanel gameReady={gameReady} hc={hc} waveStarted={waveStarted || inMeeting} />
</div> </div>
</div> </div>
@@ -592,9 +938,20 @@ export default function GamePage() {
return ( return (
<button <button
key={meta.type} key={meta.type}
title={meta.tip} onClick={() => canAfford && handleSelectTower(meta.type as TowerType)}
onClick={() => canAfford && handleSelectTower(meta.type)}
disabled={!canAfford} disabled={!canAfford}
onMouseEnter={(e) => {
const rect = (e.currentTarget as HTMLButtonElement).getBoundingClientRect()
setTooltipPos({
left: rect.left + rect.width / 2,
bottom: window.innerHeight - rect.top + 10,
})
setHoveredTower(meta)
}}
onMouseLeave={() => {
setHoveredTower(null)
setTooltipPos(null)
}}
style={{ style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: '2px', padding: '5px 8px', gap: '2px', padding: '5px 8px',
@@ -625,6 +982,20 @@ export default function GamePage() {
) )
})} })}
</div> </div>
{/* 塔 Tooltip 弹窗 */}
{hoveredTower && tooltipPos && (
<TowerTooltip
tower={hoveredTower}
pos={tooltipPos}
canAfford={hc >= hoveredTower.cost}
/>
)}
{/* 首次游玩教程弹窗 */}
{showTutorial && (
<TutorialModal onClose={dismissTutorial} />
)}
</div> </div>
) )
} }

View File

@@ -725,3 +725,8 @@
/* 深色背景(终端/代码块保留不覆盖bg-slate-900 bg-slate-950 */ /* 深色背景(终端/代码块保留不覆盖bg-slate-900 bg-slate-950 */
} }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}

View File

@@ -58,6 +58,12 @@ export class GameManager {
// difficulty 保留,不重置 // difficulty 保留,不重置
} }
/** 重置 HC 到初始值(进入下一关时调用,不带入上一关的余量) */
resetHC(): void {
this.hc = INITIAL_HC
this.onHCChange.forEach(cb => cb(this.hc))
}
/** /**
* 扣除 HC余额不足时返回 false * 扣除 HC余额不足时返回 false
*/ */
@@ -96,6 +102,22 @@ export class GameManager {
this.onKPIChange.forEach(cb => cb(this.kpi)) this.onKPIChange.forEach(cb => cb(this.kpi))
} }
/** 暂停游戏(仅 playing 状态下有效) */
pause(): boolean {
if (this.gameState !== 'playing') return false
this.gameState = 'paused'
return true
}
/** 恢复游戏(仅 paused 状态下有效) */
resume(): boolean {
if (this.gameState !== 'paused') return false
this.gameState = 'playing'
return true
}
get isPaused(): boolean { return this.gameState === 'paused' }
/** 触发胜利 */ /** 触发胜利 */
triggerVictory(): void { triggerVictory(): void {
if (this.gameState === 'playing') { if (this.gameState === 'playing') {

View File

@@ -64,6 +64,10 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
private bgObject: Phaser.GameObjects.Image | null = null private bgObject: Phaser.GameObjects.Image | null = null
private mapInTransition: boolean = false private mapInTransition: boolean = false
// 暂停遮罩
private pauseOverlay: Phaser.GameObjects.Graphics | null = null
private pauseText: Phaser.GameObjects.Text | null = null
constructor() { super({ key: 'GameScene' }) } constructor() { super({ key: 'GameScene' }) }
preload(): void { preload(): void {
@@ -124,19 +128,34 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
audio.startBGM() audio.startBGM()
}) })
// 注册 PUA buff 接口 + HC 查询/扣除接口供 React 层调用 // 注册 PUA buff 接口 + HC 查询/扣除接口 + 暂停/恢复接口
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff = ( ;(window as any).__gamePuaBuff = (
effect: string, score: number, title: string effect: string, score: number, title: string
) => { ) => { this.applyPuaBuff(effect, score, title) }
this.applyPuaBuff(effect, score, title)
}
// 查询当前 HC
;(window as any).__gameGetHC = () => this.manager.hc ;(window as any).__gameGetHC = () => this.manager.hc
// 尝试扣除 HC成功返回 true不足返回 false ;(window as any).__gameSpendHC = (amount: number): boolean =>
;(window as any).__gameSpendHC = (amount: number): boolean => { this.manager.spendHC(amount)
return this.manager.spendHC(amount)
;(window as any).__gamePause = (): boolean => {
if (!this.manager.pause()) return false
this.showPauseOverlay()
// 暂停 Phaser 物理与 Tween
this.physics.pause()
this.tweens.pauseAll()
return true
} }
;(window as any).__gameResume = (): boolean => {
if (!this.manager.resume()) return false
this.hidePauseOverlay()
this.physics.resume()
this.tweens.resumeAll()
return true
}
;(window as any).__gameIsPaused = (): boolean => this.manager.isPaused
} }
this.setupInteraction() this.setupInteraction()
@@ -145,10 +164,34 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
} }
private createMuteButton(_audio: AudioEngine): void { private createMuteButton(_audio: AudioEngine): void {
// 静音按钮已移除,音乐默认开启
void _audio void _audio
} }
private showPauseOverlay(): void {
const { width, height } = this.scale
// 半透明深色遮罩
this.pauseOverlay = this.add.graphics().setDepth(90)
this.pauseOverlay.fillStyle(0x000000, 0.6)
this.pauseOverlay.fillRect(0, 0, width, height)
// "开会中" 提示卡片
this.pauseText = this.add.text(width / 2, height / 2, '📋 开会中...\n游戏已暂停', {
fontFamily: 'VT323, monospace',
fontSize: '32px',
color: '#FCD34D',
backgroundColor: '#1e3a5f',
padding: { x: 32, y: 18 },
align: 'center',
}).setOrigin(0.5, 0.5).setDepth(91)
}
private hidePauseOverlay(): void {
this.pauseOverlay?.destroy()
this.pauseOverlay = null
this.pauseText?.destroy()
this.pauseText = null
}
/** /**
* 应用 PUA buff 效果到游戏 * 应用 PUA buff 效果到游戏
* effect: attack_boost | speed_boost | money_rain | rage_mode | backfire * effect: attack_boost | speed_boost | money_rain | rage_mode | backfire
@@ -321,9 +364,10 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
// 自动下一波倒计时ms-1 表示未激活 // 自动下一波倒计时ms-1 表示未激活
private autoNextWaveTimer: number = -1 private autoNextWaveTimer: number = -1
private readonly AUTO_WAVE_DELAY = 3000 // 3 秒后自动开始 private readonly AUTO_WAVE_DELAY = 2000 // 自动开始等待3s→2s缩短喘息时间
update(_time: number, delta: number): void { update(_time: number, delta: number): void {
if (this.manager.gameState === 'paused') return // 暂停时完全跳过
if (this.manager.gameState !== 'playing' && this.manager.gameState !== 'idle') return if (this.manager.gameState !== 'playing' && this.manager.gameState !== 'idle') return
if (this.mapInTransition) return if (this.mapInTransition) return
@@ -399,6 +443,8 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
this.waveManager.clearAllEnemies() this.waveManager.clearAllEnemies()
// 清除上一关所有防御塔 // 清除上一关所有防御塔
this.towerManager.clearAllTowers() this.towerManager.clearAllTowers()
// 重置 HC每关重新开始不带入上关余量
this.manager.resetHC()
this.loadMap(ALL_MAPS[nextIndex]) this.loadMap(ALL_MAPS[nextIndex])
this.hud.enableWaveButton() this.hud.enableWaveButton()
this.hud.setWaveButtonText('▶ 召唤下一波') this.hud.setWaveButtonText('▶ 召唤下一波')

View File

@@ -12,10 +12,10 @@ export const HUD_HEIGHT = 60
export const WAVES_PER_MAP = 5 export const WAVES_PER_MAP = 5
// 游戏初始数值 // 游戏初始数值
export const INITIAL_HC = 200 export const INITIAL_HC = 170 // 初始资源150→170
export const INITIAL_KPI = 100 export const INITIAL_KPI = 100
export const STAMINA_MAX = 100 export const STAMINA_MAX = 100
export const STAMINA_REGEN = 5 // 每秒恢复量 export const STAMINA_REGEN = 8 // 摸鱼恢复速度5→8/s减少卡顿感
export const COFFEE_COST = 10 // 瑞幸咖啡 HC 成本 export const COFFEE_COST = 10 // 瑞幸咖啡 HC 成本
// 颜色常量Phaser 使用 0x 十六进制) // 颜色常量Phaser 使用 0x 十六进制)

View File

@@ -33,9 +33,9 @@ export const DIFFICULTY_MULTIPLIER: Record<DifficultyLevel, {
bossHp: number bossHp: number
hcReward: number hcReward: number
}> = { }> = {
easy: { enemyCount: 0.7, enemySpeed: 0.8, bossHp: 0.7, hcReward: 1.2 }, easy: { enemyCount: 0.7, enemySpeed: 0.85, bossHp: 0.7, hcReward: 1.1 },
normal: { enemyCount: 1.0, enemySpeed: 1.0, bossHp: 1.0, hcReward: 1.0 }, normal: { enemyCount: 1.0, enemySpeed: 1.0, bossHp: 1.0, hcReward: 1.0 },
hard: { enemyCount: 1.4, enemySpeed: 1.3, bossHp: 1.5, hcReward: 0.8 }, hard: { enemyCount: 1.5, enemySpeed: 1.35, bossHp: 1.8, hcReward: 0.75 },
} }
// 地图1人力资源部S型路径 // 地图1人力资源部S型路径
@@ -68,11 +68,11 @@ const MAP1: MapConfig = {
], ],
waveCount: 5, waveCount: 5,
waves: [ waves: [
{ enemies: [{ type: 'FreshGraduate', count: 10, interval: 800 }] }, { enemies: [{ type: 'FreshGraduate', count: 14, interval: 650 }] },
{ enemies: [{ type: 'FreshGraduate', count: 8, interval: 700 }, { type: 'OldEmployee', count: 2, interval: 2000 }] }, { enemies: [{ type: 'FreshGraduate', count: 10, interval: 600 }, { type: 'OldEmployee', count: 3, interval: 1800 }] },
{ enemies: [{ type: 'OldEmployee', count: 4, interval: 1800 }, { type: 'TroubleMaker', count: 3, interval: 1500 }] }, { enemies: [{ type: 'OldEmployee', count: 5, interval: 1500 }, { type: 'TroubleMaker', count: 4, interval: 1200 }] },
{ enemies: [{ type: 'FreshGraduate', count: 12, interval: 600 }, { type: 'TroubleMaker', count: 3, interval: 1200 }] }, { enemies: [{ type: 'FreshGraduate', count: 16, interval: 500 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 3, interval: 2000 }, { type: 'FreshGraduate', count: 5, interval: 800 }] }, { enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 4, interval: 1800 }, { type: 'FreshGraduate', count: 8, interval: 700 }] },
], ],
} }
@@ -111,11 +111,11 @@ const MAP2: MapConfig = {
], ],
waveCount: 5, waveCount: 5,
waves: [ waves: [
{ enemies: [{ type: 'FreshGraduate', count: 12, interval: 700 }, { type: 'OldEmployee', count: 2, interval: 2000 }] }, { enemies: [{ type: 'FreshGraduate', count: 15, interval: 600 }, { type: 'OldEmployee', count: 3, interval: 1800 }] },
{ enemies: [{ type: 'OldEmployee', count: 5, interval: 1500 }, { type: 'TroubleMaker', count: 3, interval: 1200 }] }, { enemies: [{ type: 'OldEmployee', count: 6, interval: 1300 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] },
{ enemies: [{ type: 'FreshGraduate', count: 15, interval: 500 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] }, { enemies: [{ type: 'FreshGraduate', count: 18, interval: 450 }, { type: 'TroubleMaker', count: 5, interval: 900 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 4, interval: 1500 }] }, { enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 5, interval: 1300 }, { type: 'TroubleMaker', count: 3, interval: 1000 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 10, interval: 600 }, { type: 'TroubleMaker', count: 5, interval: 1000 }] }, { enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 14, interval: 500 }, { type: 'TroubleMaker', count: 6, interval: 850 }] },
], ],
} }
@@ -154,11 +154,11 @@ const MAP3: MapConfig = {
], ],
waveCount: 5, waveCount: 5,
waves: [ waves: [
{ enemies: [{ type: 'OldEmployee', count: 6, interval: 1500 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] }, { enemies: [{ type: 'OldEmployee', count: 7, interval: 1300 }, { type: 'TroubleMaker', count: 5, interval: 900 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 12, interval: 600 }, { type: 'OldEmployee', count: 3, interval: 1500 }] }, { enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 16, interval: 500 }, { type: 'OldEmployee', count: 4, interval: 1300 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'TroubleMaker', count: 6, interval: 800 }, { type: 'OldEmployee', count: 4, interval: 1500 }] }, { enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'TroubleMaker', count: 8, interval: 700 }, { type: 'OldEmployee', count: 5, interval: 1200 }] },
{ enemies: [{ type: 'BossVP', count: 2, interval: 5000 }, { type: 'OldEmployee', count: 5, interval: 1200 }, { type: 'FreshGraduate', count: 8, interval: 600 }] }, { enemies: [{ type: 'BossVP', count: 2, interval: 4000 }, { type: 'OldEmployee', count: 6, interval: 1100 }, { type: 'FreshGraduate', count: 12, interval: 500 }] },
{ enemies: [{ type: 'BossVP', count: 2, interval: 3000 }, { type: 'TroubleMaker', count: 8, interval: 600 }, { type: 'OldEmployee', count: 6, interval: 1000 }] }, { enemies: [{ type: 'BossVP', count: 2, interval: 2500 }, { type: 'TroubleMaker', count: 10, interval: 550 }, { type: 'OldEmployee', count: 7, interval: 900 }] },
], ],
} }

View File

@@ -4,7 +4,7 @@ import { getRandomQuote } from '../data/quotes'
import { AudioEngine } from '../AudioEngine' import { AudioEngine } from '../AudioEngine'
export class BossVP extends EnemyBase { export class BossVP extends EnemyBase {
private skillTimer: number = 20000 private skillTimer: number = 12000 // 技能间隔从20s压缩到12s
private onDestroyTower?: () => void private onDestroyTower?: () => void
private bossLabel!: Phaser.GameObjects.Text private bossLabel!: Phaser.GameObjects.Text
@@ -15,7 +15,8 @@ export class BossVP extends EnemyBase {
speedMultiplier: number = 1.0, speedMultiplier: number = 1.0,
hpMultiplier: number = 1.0 hpMultiplier: number = 1.0
) { ) {
super(scene, pathPoints, 800, 40, 30, 150, 'enemy-boss', speedMultiplier, hpMultiplier) // HP: 900, speed: 45, kpiDamage: 35, hcReward: 120
super(scene, pathPoints, 900, 45, 35, 120, 'enemy-boss', speedMultiplier, hpMultiplier)
this.onDestroyTower = onDestroyTower this.onDestroyTower = onDestroyTower
const bossSize = this.cellW * 1.3 const bossSize = this.cellW * 1.3
this.imageSprite.setDisplaySize(bossSize, bossSize) this.imageSprite.setDisplaySize(bossSize, bossSize)
@@ -47,7 +48,7 @@ export class BossVP extends EnemyBase {
super.update(delta) super.update(delta)
this.skillTimer -= delta this.skillTimer -= delta
if (this.skillTimer <= 0) { if (this.skillTimer <= 0) {
this.skillTimer = 20000 this.skillTimer = 12000 // 重置技能冷却
this.triggerOrgRestructure() this.triggerOrgRestructure()
} }
if (this.bossLabel) { if (this.bossLabel) {

View File

@@ -9,7 +9,9 @@ export class FreshGraduate extends EnemyBase {
speedMultiplier: number = 1.0, speedMultiplier: number = 1.0,
hpMultiplier: number = 1.0 hpMultiplier: number = 1.0
) { ) {
super(scene, pathPoints, 30, 120, 2, 10, 'enemy-fresh', speedMultiplier, hpMultiplier) // HP: 40, speed: 130, kpiDamage: 2.5→取整3不行用整数2, hcReward: 9
// kpiDamage 用浮点:在 GameManager.reduceKPI 里用 Math.max(0, kpi - amount)
super(scene, pathPoints, 40, 130, 2, 9, 'enemy-fresh', speedMultiplier, hpMultiplier)
} }
getQuote(): string { return getRandomQuote('FreshGraduate') } getQuote(): string { return getRandomQuote('FreshGraduate') }
} }

View File

@@ -9,7 +9,8 @@ export class OldEmployee extends EnemyBase {
speedMultiplier: number = 1.0, speedMultiplier: number = 1.0,
hpMultiplier: number = 1.0 hpMultiplier: number = 1.0
) { ) {
super(scene, pathPoints, 150, 50, 8, 30, 'enemy-old', speedMultiplier, hpMultiplier) // HP: 180, speed: 55, kpiDamage: 10, hcReward: 25, shield: 3
super(scene, pathPoints, 180, 55, 10, 25, 'enemy-old', speedMultiplier, hpMultiplier)
this.shieldCount = 3 this.shieldCount = 3
this.imageSprite.setDisplaySize(this.cellW * 0.85, this.cellW * 0.85) this.imageSprite.setDisplaySize(this.cellW * 0.85, this.cellW * 0.85)
} }

View File

@@ -10,7 +10,8 @@ export class TroubleMaker extends EnemyBase {
speedMultiplier: number = 1.0, speedMultiplier: number = 1.0,
hpMultiplier: number = 1.0 hpMultiplier: number = 1.0
) { ) {
super(scene, pathPoints, 80, 80, 5, 20, 'enemy-trouble', speedMultiplier, hpMultiplier) // HP: 100, speed: 85, kpiDamage: 6, hcReward: 18
super(scene, pathPoints, 100, 85, 6, 18, 'enemy-trouble', speedMultiplier, hpMultiplier)
} }
protected override onDeath(): void { protected override onDeath(): void {

View File

@@ -5,6 +5,7 @@ import {
GAME_WIDTH, GAME_WIDTH,
GAME_HEIGHT, GAME_HEIGHT,
HUD_HEIGHT, HUD_HEIGHT,
INITIAL_HC,
INITIAL_KPI, INITIAL_KPI,
} from './constants' } from './constants'
import type { MapConfig } from './data/mapConfigs' import type { MapConfig } from './data/mapConfigs'
@@ -222,7 +223,7 @@ export function renderHUD(scene: Phaser.Scene): {
const hcText = scene.add.text( const hcText = scene.add.text(
GAME_WIDTH - 16, HUD_HEIGHT / 2, GAME_WIDTH - 16, HUD_HEIGHT / 2,
`HC: 200`, `HC: ${INITIAL_HC}`,
{ {
fontFamily: "'Press Start 2P', monospace", fontFamily: "'Press Start 2P', monospace",
fontSize: '10px', fontSize: '10px',

View File

@@ -98,7 +98,7 @@ export abstract class TowerBase {
const target = this.findTarget(enemies) const target = this.findTarget(enemies)
if (target && this.attackCooldown <= 0) { if (target && this.attackCooldown <= 0) {
this.attack(target) this.attack(target)
this.stamina -= 5 this.stamina -= 4 // 精力消耗8→4持续战斗时间翻倍
// 应用 PUA 攻速倍率(倍率越高冷却越短) // 应用 PUA 攻速倍率(倍率越高冷却越短)
this.attackCooldown = 1000 / (this.attackSpeed * this._puaSpeedMult) this.attackCooldown = 1000 / (this.attackSpeed * this._puaSpeedMult)
this.updateStaminaBar() this.updateStaminaBar()

View File

@@ -33,7 +33,6 @@ export class MapTransitionModal {
): void { ): void {
const manager = GameManager.getInstance() const manager = GameManager.getInstance()
const kpi = manager.kpi const kpi = manager.kpi
const hcReward = 100
const overlay = document.createElement('div') const overlay = document.createElement('div')
overlay.id = 'map-transition-overlay' overlay.id = 'map-transition-overlay'
@@ -92,9 +91,9 @@ export class MapTransitionModal {
` `
card.appendChild(hr) card.appendChild(hr)
// KPI & 奖励 // KPI & 说明
const statsEl = document.createElement('div') const statsEl = document.createElement('div')
statsEl.innerHTML = `KPI: <span style="color:#34d399">${kpi}%</span> &nbsp;|&nbsp; 获得奖励: <span style="color:#fbbf24">+${hcReward} HC</span>` statsEl.innerHTML = `最终KPI: <span style="color:#34d399">${kpi}%</span> &nbsp;|&nbsp; <span style="color:#94a3b8">HC 将重置为 170</span>`
statsEl.style.cssText = ` statsEl.style.cssText = `
font-size: 20px; font-size: 20px;
color: #e2e8f0; color: #e2e8f0;
@@ -129,10 +128,10 @@ export class MapTransitionModal {
} }
card.appendChild(nextEl) card.appendChild(nextEl)
// 倒计时提示 // 倒计时提示HC 重置提醒)
const countdownEl = document.createElement('div') const countdownEl = document.createElement('div')
countdownEl.id = 'mt-countdown' countdownEl.id = 'mt-countdown'
countdownEl.textContent = isFinalMap ? '3秒后返回主菜单...' : '3秒后自动进入下一关...' countdownEl.textContent = isFinalMap ? '3秒后返回主菜单...' : '3秒后进入下一关HC重置为170...'
countdownEl.style.cssText = ` countdownEl.style.cssText = `
font-size: 16px; font-size: 16px;
color: #6b7280; color: #6b7280;
@@ -151,9 +150,6 @@ export class MapTransitionModal {
document.body.appendChild(overlay) document.body.appendChild(overlay)
this.container = overlay this.container = overlay
// 给 HC 奖励
manager.addHC(hcReward)
// 3秒自动触发 // 3秒自动触发
let remaining = 3 let remaining = 3
const tick = () => { const tick = () => {
@@ -161,7 +157,7 @@ export class MapTransitionModal {
const el = document.getElementById('mt-countdown') const el = document.getElementById('mt-countdown')
if (el) el.textContent = isFinalMap if (el) el.textContent = isFinalMap
? `${remaining}秒后返回主菜单...` ? `${remaining}秒后返回主菜单...`
: `${remaining}秒后自动进入下一关...` : `${remaining}秒后进入下一关HC重置为170...`
} }
const interval = setInterval(tick, 1000) const interval = setInterval(tick, 1000)
this.autoCloseTimer = setTimeout(() => { this.autoCloseTimer = setTimeout(() => {