Compare commits
11 Commits
1473542f65
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd9c70473b | ||
|
|
7253a882b0 | ||
|
|
ee7222929a | ||
|
|
dc9904b3f9 | ||
|
|
de17de46e1 | ||
|
|
b8ba572ffb | ||
|
|
6843d2b74c | ||
|
|
4fec1ebe9d | ||
|
|
e460d90b3a | ||
|
|
361a1bed5d | ||
|
|
a36c8af344 |
9
.env
Normal file
9
.env
Normal 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
|
||||||
@@ -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
1
.project
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"project_id": "371a88b57cff44999c4cf61b4708638a", "user_id": 1009}
|
||||||
@@ -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 ./
|
||||||
|
|||||||
@@ -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: 话太废了反而debuff(score 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_boost,speed_boost→attack_boost,etc.
|
||||||
|
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
430
app/game/TutorialModal.tsx
Normal 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 />外包程序员凑数堵路口,00后实习生建2-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 />转弯处密集建塔 > 直线段稀疏建;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 }
|
||||||
|
}
|
||||||
@@ -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: '主力输出,射程超长。优先打护盾老员工和Boss,DOT能绕过护盾持续掉血。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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('▶ 召唤下一波')
|
||||||
|
|||||||
@@ -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 十六进制)
|
||||||
|
|||||||
@@ -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 }] },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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') }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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> | 获得奖励: <span style="color:#fbbf24">+${hcReward} HC</span>`
|
statsEl.innerHTML = `最终KPI: <span style="color:#34d399">${kpi}%</span> | <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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user