632 lines
35 KiB
HTML
632 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>《狱国争霸》深度拆解与研发赋能报告</title>
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
body {
|
|
font-family: 'Inter', 'Noto Sans SC', sans-serif;
|
|
background-color: #ffffff;
|
|
color: #1e293b;
|
|
}
|
|
.section-card {
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 1rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
|
}
|
|
.nav-active {
|
|
background-color: #f1f5f9;
|
|
color: #4f46e5;
|
|
font-weight: 600;
|
|
border-left: 4px solid #4f46e5;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen flex flex-col overflow-x-hidden antialiased">
|
|
|
|
<!-- Header Banner -->
|
|
<header class="border-b border-slate-200 bg-white sticky top-0 z-50 shadow-sm">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center font-bold text-white shadow-md">
|
|
狱
|
|
</div>
|
|
<div>
|
|
<h1 class="text-base font-bold text-slate-900 tracking-wide">《狱国争霸》小游戏深度拆解报告</h1>
|
|
<p class="text-xs text-slate-500">多线节点导流底座与混合变现研发赋能</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center space-x-3">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700 border border-slate-200">
|
|
<span class="w-1.5 h-1.5 mr-1.5 rounded-full bg-indigo-600"></span>
|
|
战略研究部 内部文档
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Grid Content -->
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 flex-1 grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
|
|
<!-- Sidebar Navigation (Sticky) -->
|
|
<aside class="lg:col-span-1 hidden lg:block">
|
|
<div class="bg-slate-50 border border-slate-200 rounded-2xl p-4 sticky top-24 space-y-6">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 px-2">报告目录导航</p>
|
|
<nav class="space-y-1" id="sidebar-nav">
|
|
<a href="#overview" id="nav-overview" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition nav-active">1. 项目定位与画像</a>
|
|
<a href="#interaction" id="nav-interaction" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition">2. 核心交互底座仿真</a>
|
|
<a href="#onboarding" id="nav-onboarding" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition">3. 玩家实测摩擦卡点</a>
|
|
<a href="#monetization" id="nav-monetization" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition">4. 真实 IAA+IAP 变现链条</a>
|
|
<a href="#asynchronous" id="nav-asynchronous" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition">5. 异步匹配与拟人行为</a>
|
|
<a href="#recommendation" id="nav-recommendation" class="block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition">6. 研发超车提议</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="pt-4 border-t border-slate-200 text-xs text-slate-400 space-y-1 px-2">
|
|
<p>编写团队:战略研究部</p>
|
|
<p>分析方法:逆向拆解 / 行为观察</p>
|
|
<p>发布时间:2026年6月</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content Area -->
|
|
<main class="lg:col-span-3 space-y-8">
|
|
|
|
<!-- Toast UI Alert -->
|
|
<div id="toast" class="fixed bottom-5 right-5 bg-slate-900 text-white px-4 py-3 rounded-xl shadow-xl transition-all duration-300 opacity-0 pointer-events-none translate-y-2 flex items-center space-x-2 z-50 text-xs">
|
|
<span id="toast-text">提示信息</span>
|
|
</div>
|
|
|
|
<!-- SECTION 1: OVERVIEW -->
|
|
<section id="overview" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 01</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">产品定位与受众画像评估</h2>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|
<span class="text-xs text-slate-500 block mb-1">技术核心底座</span>
|
|
<span class="text-sm font-bold text-slate-800">多线节点导流 (Node-Link Flow)</span>
|
|
</div>
|
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|
<span class="text-xs text-slate-500 block mb-1">经济变现模型</span>
|
|
<span class="text-sm font-bold text-slate-800">IAA + IAP 混合变现</span>
|
|
</div>
|
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|
<span class="text-xs text-slate-500 block mb-1">研发核心定位</span>
|
|
<span class="text-sm font-bold text-indigo-600">外圈数值驱动 / 异步轻度SLG</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-slate-600 text-sm leading-relaxed space-y-4">
|
|
<p>
|
|
《狱国争霸》是一款在微信、抖音小游戏平台运营的**“轻操作、强数值反弹”**策略产品。在玩法底座上,其剥离了传统 RTS(实时战略)繁琐的单兵微操,将其高度抽象为「节点间的兵力数值流动与对冲」。通过包装东亚受众认知成本极低的三国叙事,大幅降低了用户的进入门槛。
|
|
</p>
|
|
<p>
|
|
与海外同类产品(如《State.io》等纯依靠高频局内广告打断的IAA模式不同),《狱国争霸》在立项之初就引入了深度的**外圈武将养成系统**。玩家的实际单局胜率不仅取决于局内连线调度,更取决于局外武将卡牌的面板数值(等级、突破星级)。这种设计成功将一款超休闲游戏升级为具备长线留存和IAP(内购)深度的策略小游戏。
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION 2: INTERACTION BASE & SIMULATOR -->
|
|
<section id="interaction" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 02</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">交互底座提取:多线节点导流实时仿真</h2>
|
|
</div>
|
|
|
|
<p class="text-xs text-slate-500">
|
|
💡 <strong>交互测试说明:</strong>点击选中左侧的<b>己方节点(绿色-建业)</b>,然后再点击中部的<b>中立节点(灰色)</b>或右侧的<b>敌方节点(红色-许昌)</b>,即可建立正确的划线出兵路径。
|
|
</p>
|
|
|
|
<!-- Simulator Container -->
|
|
<div class="bg-slate-950 rounded-xl p-4 flex flex-col justify-between">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<span class="text-[10px] font-mono tracking-widest text-slate-400 uppercase">Real-Time Simulation Node-Link Flow</span>
|
|
<button onclick="resetSimulator()" class="px-2 py-0.5 text-[10px] bg-slate-800 text-slate-300 hover:bg-indigo-600 hover:text-white rounded transition">
|
|
重置模拟
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Canvas Box -->
|
|
<div class="relative bg-slate-900 rounded-lg overflow-hidden" style="height: 280px;">
|
|
<canvas id="simCanvas" class="w-full h-full block cursor-pointer"></canvas>
|
|
<div class="absolute bottom-2 left-2 bg-slate-950/80 px-2 py-1 rounded text-[10px] text-slate-400 border border-slate-800">
|
|
状态: <span id="linkState" class="text-indigo-400 font-mono">无选中</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mathematical Formulas -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs text-slate-600">
|
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200/60">
|
|
<span class="font-bold text-slate-800 block mb-1">① 节点自增长溢出模型</span>
|
|
<p>节点随时间自动生产兵力,达到上限后产出冻结。该设计在数值上自然形成了“溢出焦虑”,驱动玩家必须保持高频的连线和调度行为,杜绝消极防守。</p>
|
|
</div>
|
|
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200/60">
|
|
<span class="font-bold text-slate-800 block mb-1">② 空间距离时延与对冲</span>
|
|
<p>连线长度(物理像素距离)决定了兵力抵达的时间。对冲公式为:相同归属则累加,不同归属则扣减(并受城防衰减系数影响),策略深度天然产生于出兵时机差。</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION 3: ONBOARDING RETENTION -->
|
|
<section id="onboarding" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 03</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">玩家核心摩擦点解构(基于实测观察修正版)</h2>
|
|
</div>
|
|
|
|
<div class="text-slate-600 text-sm leading-relaxed">
|
|
<p>
|
|
基于实际游戏表现和逆向对冲机制,玩家在前期(尤其是前30分钟)遭遇的核心摩擦卡点,本质上是由<b>“局内战局地图结构的复杂化”</b>与<b>“局外武将数值成长迟滞”</b>共同导致的。系统并没有采用激进的局内强打断式机制,而是通过局外循环释放压力:
|
|
</p>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-xs text-left text-slate-600 border-collapse">
|
|
<thead class="bg-slate-100 text-slate-700 font-bold uppercase">
|
|
<tr>
|
|
<th class="p-3 rounded-l-lg">体验节点</th>
|
|
<th class="p-3">系统核心摩擦表现</th>
|
|
<th class="p-3">玩家面临的实质性障碍</th>
|
|
<th class="p-3 rounded-r-lg">系统压力释放路径(真实的变现映射)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-200">
|
|
<tr>
|
|
<td class="p-3 font-medium text-slate-900">关卡地形变轨</td>
|
|
<td class="p-3">中立城市间距拉长,敌方初始节点增加。</td>
|
|
<td class="p-3">玩家单兵输送时延变大,难以在AI合围前攻占樊城等枢纽。</td>
|
|
<td class="p-3">引导提升局外科技树、利用战斗结算累积的金币强化“初始兵力”或“产兵速度”面板。</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="p-3 font-medium text-slate-900">武将数值卡点</td>
|
|
<td class="p-3">关卡敌军武将战力属性提升,连线对冲效率降低。</td>
|
|
<td class="p-3">玩家在局部战场遭遇数值碾压,己方小兵对冲损耗率异常增高。</td>
|
|
<td class="p-3">引流至武将招募系统。利用<b>“看广告刷新免费招募CD”</b>或<b>“元宝直购十连抽”</b>获取高稀有度武将卡牌。</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="p-3 font-medium text-slate-900">养成资源匮乏</td>
|
|
<td class="p-3">随着武将等级提升,升级所需的金币/经验石指数级上涨。</td>
|
|
<td class="p-3">金币和碎片的自然挂机获取速度,无法跟上主线关卡的数值陡峭度。</td>
|
|
<td class="p-3">提供<b>“战斗结算看广告翻倍/三倍金币”</b>,以及通过<b>“军旅增强”</b>看视频补充体力或领取救济资源。</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION 4: MONETIZATION -->
|
|
<section id="monetization" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 04</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">IAA + IAP 变现架构精细化拆解</h2>
|
|
</div>
|
|
|
|
<div class="text-slate-600 text-sm leading-relaxed mb-4">
|
|
<p>
|
|
《狱国争霸》的变现逻辑极为克制,其遵循**“外圈重度化,局内无打断”**的原则。广告主要服务于非氪用户的养成加速,而内购则针对深度付费玩家的特权和长线数值深度进行收割。
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- IAA -->
|
|
<div class="bg-amber-50/40 p-5 rounded-xl border border-amber-200/50 space-y-3">
|
|
<h4 class="text-sm font-bold text-amber-800 flex items-center gap-1.5">
|
|
<span class="w-2 h-2 bg-amber-500 rounded-full"></span>
|
|
真实 IAA 广告点布局(服务于局外养成加速)
|
|
</h4>
|
|
<ul class="text-xs text-slate-600 space-y-4 list-none pl-0">
|
|
<li>
|
|
<strong class="text-slate-800 block">● 贪婪放大:战后翻倍金币结算</strong>
|
|
战胜关卡时,提供“观看视频获得3倍/双倍金币结算”选项。利用胜利后的正面反馈,引导玩家用时间换取升级资金,这是游戏内广告变现的大盘基石。
|
|
</li>
|
|
<li>
|
|
<strong class="text-slate-800 block">● 赌徒心理:免费抽卡CD重置</strong>
|
|
武将招募系统拥有24小时免费单抽限制。系统允许玩家“通过观看激励广告直接刷新CD”(每日限次数),将高价值武将获取概率与零金钱门槛的广告挂钩。
|
|
</li>
|
|
<li>
|
|
<strong class="text-slate-800 block">● 救济机制:体力与基础物资补充</strong>
|
|
当关卡遭遇瓶颈或单日挑战次数(体力)耗尽时,允许看广告补充挑战资格,解决当前进程受阻的焦虑。
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- IAP -->
|
|
<div class="bg-indigo-50/40 p-5 rounded-xl border border-indigo-200/50 space-y-3">
|
|
<h4 class="text-sm font-bold text-indigo-800 flex items-center gap-1.5">
|
|
<span class="w-2 h-2 bg-indigo-500 rounded-full"></span>
|
|
IAP 深度变现(提升 ARPU 的特权过滤)
|
|
</h4>
|
|
<ul class="text-xs text-slate-600 space-y-4 list-none pl-0">
|
|
<li>
|
|
<strong class="text-slate-800 block">● 物理摩擦豁免:18元月卡 / 68元终身免广卡</strong>
|
|
小游戏的核心流失点在于频繁的广告体验。免广卡允许玩家在不看广告的前提下,直接白嫖所有的“翻倍结算金币”和“免费抽卡刷新”,价值感知极高,是筛选大R和留存核心玩家的黄金漏斗装置。
|
|
</li>
|
|
<li>
|
|
<strong class="text-slate-800 block">● 武将卡池内购:元宝直购招募包</strong>
|
|
核心的长线内购项。通过产出高稀有度神将及其专属碎片(用于武将突破升星、技能升级),构建了深度不亚于重度SLG游戏的数值付费深渊。
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION 5: ASYNCHRONOUS COMPETITION -->
|
|
<section id="asynchronous" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 05</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">异步匹配机制与拟人化行为逻辑</h2>
|
|
</div>
|
|
|
|
<div class="text-slate-600 text-sm leading-relaxed space-y-3">
|
|
<p>
|
|
经技术观察,该作的“竞技场”与“同省同屏大乱斗”实质上均基于**异步人机(Asynchronous Bot)**构建,而非成本极高的实时网络同步。系统拉取真实玩家的武将搭配与战力快照,交由本地 AI 托管。
|
|
</p>
|
|
<p>
|
|
为了规避人机的僵硬感,其 AI 在运行时具备了高拟人度的「决策权重模型」:
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Visual Logic Diagram -->
|
|
<div class="bg-slate-50 border border-slate-200 rounded-xl p-5 space-y-4">
|
|
<span class="text-xs font-bold text-slate-700 block">拟人化 AI 决策行为权重图 (Behavioral Weights)</span>
|
|
|
|
<div class="space-y-3 text-xs">
|
|
<div>
|
|
<div class="flex justify-between text-slate-500 mb-1">
|
|
<span>1. 战术慌乱度模拟 (Bluffing Delay / 随连败率动态调整)</span>
|
|
<span class="font-bold text-slate-700">15% - 40%</span>
|
|
</div>
|
|
<div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden">
|
|
<div class="bg-indigo-600 h-full rounded-full" style="width: 35%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex justify-between text-slate-500 mb-1">
|
|
<span>2. 战术截断倾向 (主动识别并连接玩家薄弱后方的概率)</span>
|
|
<span class="font-bold text-slate-700">65%</span>
|
|
</div>
|
|
<div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden">
|
|
<div class="bg-amber-500 h-full rounded-full" style="width: 65%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex justify-between text-slate-500 mb-1">
|
|
<span>3. 战术容错差率 (故意将微量数值投射至高防重镇的失误表现)</span>
|
|
<span class="font-bold text-slate-700">15% 恒定</span>
|
|
</div>
|
|
<div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden">
|
|
<div class="bg-emerald-500 h-full rounded-full" style="width: 15%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- SECTION 6: RECOMMENDATIONS -->
|
|
<section id="recommendation" class="section-card p-6 lg:p-8 space-y-6">
|
|
<div class="border-b border-slate-100 pb-4">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Module 06</span>
|
|
<h2 class="text-xl font-bold text-slate-900 mt-0.5">研发超车提议与商业化改良方案</h2>
|
|
</div>
|
|
|
|
<div class="space-y-4 text-sm text-slate-600">
|
|
<div class="p-4 bg-slate-50 rounded-xl border border-slate-150">
|
|
<h4 class="font-bold text-slate-900 mb-1 text-xs">提议一:引入“物理折射与障碍物力场”要素(大幅拉高买量 CTR)</h4>
|
|
<p class="text-xs text-slate-500 leading-relaxed">
|
|
《狱国争霸》的连线出兵轨迹均为生硬的直线。若我们在克隆立项中,引入可破坏的战场障碍、加速风道或引力折射点,出行的士兵流会呈现出抛物线或曲线偏折。这种微小的改动能大幅提升游戏在短视频平台买量广告的点击率(CTR),降低买量成本(CAC)。
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-4 bg-slate-50 rounded-xl border border-slate-150">
|
|
<h4 class="font-bold text-slate-900 mb-1 text-xs">提议二:构建真正的“双人同屏肉鸽(Roguelike)合作”裂变特质</h4>
|
|
<p class="text-xs text-slate-500 leading-relaxed">
|
|
依托微信强大的熟人社交链,设计“双人联手抗曹”的异步或同步 PVE 模式。一人主导防御与增益,另一人主导战线切断,配合局内随机天赐技能(如:出兵路线带冰冻减速、士兵到达发生范围爆炸)。这将产生极强的“邀请好友共同破局”裂变自传播。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="bg-slate-50 border-t border-slate-200 py-6 text-center text-xs text-slate-400">
|
|
<div class="max-w-7xl mx-auto px-4">
|
|
<p>© 2026 战略研究部 · 小游戏混合变现与底座克隆课题组. 基于高保真物理节点仿真系统.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Canvas Simulator & Interactive JavaScript Core -->
|
|
<script>
|
|
// --- Tab Highlight Synchronization on Scroll ---
|
|
window.addEventListener('scroll', () => {
|
|
const sections = ['overview', 'interaction', 'onboarding', 'monetization', 'asynchronous', 'recommendation'];
|
|
let currentActive = 'overview';
|
|
|
|
for (let section of sections) {
|
|
const el = document.getElementById(section);
|
|
if (el) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.top <= 120) {
|
|
currentActive = section;
|
|
}
|
|
}
|
|
}
|
|
|
|
sections.forEach(s => {
|
|
const navBtn = document.getElementById('nav-' + s);
|
|
if (navBtn) {
|
|
if (s === currentActive) {
|
|
navBtn.className = "block px-3 py-2 text-sm text-indigo-600 rounded-lg bg-slate-100 font-semibold border-left-4 border-indigo-600 transition nav-active";
|
|
} else {
|
|
navBtn.className = "block px-3 py-2 text-sm text-slate-600 rounded-lg hover:bg-slate-100 transition";
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Core Dynamic Canvas Simulator ---
|
|
let canvas, ctx;
|
|
let nodes = [];
|
|
let links = [];
|
|
let particles = [];
|
|
let selectedNode = null;
|
|
let animationId = null;
|
|
|
|
function initSimulator() {
|
|
canvas = document.getElementById('simCanvas');
|
|
if (!canvas) return;
|
|
ctx = canvas.getContext('2d');
|
|
|
|
// Handle retina displays
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = rect.width * window.devicePixelRatio;
|
|
canvas.height = 280 * window.devicePixelRatio;
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
|
|
resetSimulatorState();
|
|
|
|
// Click listener
|
|
canvas.removeEventListener('click', handleCanvasClick);
|
|
canvas.addEventListener('click', handleCanvasClick);
|
|
|
|
// Start animation loop
|
|
if (animationId) cancelAnimationFrame(animationId);
|
|
gameLoop();
|
|
}
|
|
|
|
function resetSimulatorState() {
|
|
const w = canvas.width / window.devicePixelRatio;
|
|
const h = canvas.height / window.devicePixelRatio;
|
|
|
|
// Segregated locations. Left side is player, right side is enemy.
|
|
nodes = [
|
|
{ id: 1, x: 60, y: h/2, r: 22, team: 'player', troops: 40, maxTroops: 100, rate: 3, label: '建业' },
|
|
{ id: 2, x: w/2, y: 55, r: 18, team: 'neutral', troops: 10, maxTroops: 40, rate: 0, label: '樊城' },
|
|
{ id: 3, x: w/2, y: h - 55, r: 18, team: 'neutral', troops: 15, maxTroops: 40, rate: 0, label: '合肥' },
|
|
{ id: 4, x: w - 60, y: h/2, r: 22, team: 'enemy', troops: 35, maxTroops: 100, rate: 2.5, label: '许昌' }
|
|
];
|
|
links = [];
|
|
particles = [];
|
|
selectedNode = null;
|
|
document.getElementById('linkState').innerText = "无选中";
|
|
}
|
|
|
|
function resetSimulator() {
|
|
resetSimulatorState();
|
|
showToast("仿真模拟器已重置为初始战局");
|
|
}
|
|
|
|
function handleCanvasClick(e) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const mouseX = e.clientX - rect.left;
|
|
const mouseY = e.clientY - rect.top;
|
|
|
|
let clickedNode = null;
|
|
for (let n of nodes) {
|
|
if (Math.hypot(n.x - mouseX, n.y - mouseY) < n.r + 10) {
|
|
clickedNode = n;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (clickedNode) {
|
|
if (selectedNode) {
|
|
if (selectedNode.id !== clickedNode.id) {
|
|
// Establish direction link from source node
|
|
links = links.filter(l => l.from !== selectedNode.id || l.to !== clickedNode.id);
|
|
links.push({
|
|
from: selectedNode.id,
|
|
to: clickedNode.id,
|
|
timer: 0
|
|
});
|
|
showToast(`已建立兵力导流线: ${selectedNode.label} ➔ ${clickedNode.label}`);
|
|
selectedNode = null;
|
|
document.getElementById('linkState').innerText = "无选中";
|
|
} else {
|
|
selectedNode = null;
|
|
document.getElementById('linkState').innerText = "无选中";
|
|
}
|
|
} else {
|
|
if (clickedNode.team === 'player') {
|
|
selectedNode = clickedNode;
|
|
document.getElementById('linkState').innerText = `已选中起点: ${clickedNode.label}`;
|
|
} else {
|
|
showToast("只能选择绿色的己方城市作为出兵源节点!");
|
|
}
|
|
}
|
|
} else {
|
|
selectedNode = null;
|
|
document.getElementById('linkState').innerText = "无选中";
|
|
}
|
|
}
|
|
|
|
let lastTime = Date.now();
|
|
function gameLoop() {
|
|
const now = Date.now();
|
|
const dt = (now - lastTime) / 1000;
|
|
lastTime = now;
|
|
|
|
// Update nodes base value
|
|
nodes.forEach(n => {
|
|
if (n.team !== 'neutral') {
|
|
n.troops = Math.min(n.troops + n.rate * dt, n.maxTroops);
|
|
}
|
|
});
|
|
|
|
// Update links and spawn soldiers
|
|
links.forEach(l => {
|
|
l.timer += dt;
|
|
if (l.timer >= 0.45) { // Spawn particle every 450ms
|
|
l.timer = 0;
|
|
const fromN = nodes.find(n => n.id === l.from);
|
|
const toN = nodes.find(n => n.id === l.to);
|
|
|
|
if (fromN && toN && fromN.troops > 1) {
|
|
fromN.troops -= 1;
|
|
particles.push({
|
|
fromId: fromN.id,
|
|
targetNode: toN,
|
|
team: fromN.team,
|
|
speed: 110,
|
|
progress: 0,
|
|
distance: Math.hypot(toN.x - fromN.x, toN.y - fromN.y)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update flying troops positions
|
|
particles.forEach((p, idx) => {
|
|
p.progress += (p.speed * dt) / p.distance;
|
|
const fromNode = nodes.find(n => n.id === p.fromId);
|
|
const startX = fromNode ? fromNode.x : p.x;
|
|
const startY = fromNode ? fromNode.y : p.y;
|
|
|
|
p.curX = startX + (p.targetNode.x - startX) * p.progress;
|
|
p.curY = startY + (p.targetNode.y - startY) * p.progress;
|
|
|
|
if (p.progress >= 1.0) {
|
|
// Encounter target node
|
|
resolveArrival(p);
|
|
particles.splice(idx, 1);
|
|
}
|
|
});
|
|
|
|
// Basic Dynamic Enemy AI Bot simulation
|
|
const enemyNode = nodes.find(n => n.team === 'enemy');
|
|
const neutrals = nodes.filter(n => n.team === 'neutral');
|
|
if (enemyNode && enemyNode.troops > 35 && neutrals.length > 0 && links.filter(l => l.from === enemyNode.id).length === 0) {
|
|
// Target first neutral node
|
|
links.push({ from: enemyNode.id, to: neutrals[0].id, timer: 0 });
|
|
}
|
|
|
|
drawSimulator();
|
|
animationId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function resolveArrival(p) {
|
|
const target = p.targetNode;
|
|
if (p.team === target.team) {
|
|
target.troops = Math.min(target.troops + 1, target.maxTroops);
|
|
} else {
|
|
target.troops -= 1;
|
|
if (target.troops <= 0) {
|
|
target.team = p.team;
|
|
target.troops = 1;
|
|
target.rate = p.team === 'player' ? 2 : 1.8; // Change regeneration owner
|
|
links = links.filter(l => l.from !== target.id); // Sever severed lines
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawSimulator() {
|
|
const w = canvas.width / window.devicePixelRatio;
|
|
const h = canvas.height / window.devicePixelRatio;
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// Render Link lines
|
|
links.forEach(l => {
|
|
const f = nodes.find(n => n.id === l.from);
|
|
const t = nodes.find(n => n.id === l.to);
|
|
if (f && t) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(f.x, f.y);
|
|
ctx.lineTo(t.x, t.y);
|
|
ctx.strokeStyle = f.team === 'player' ? 'rgba(34, 197, 94, 0.4)' : 'rgba(239, 68, 68, 0.4)';
|
|
ctx.lineWidth = 2.5;
|
|
ctx.setLineDash([4, 4]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
});
|
|
|
|
// Render particles
|
|
particles.forEach(p => {
|
|
ctx.beginPath();
|
|
ctx.arc(p.curX, p.curY, 4, 0, Math.PI * 2);
|
|
ctx.fillStyle = p.team === 'player' ? '#22c55e' : '#ef4444';
|
|
ctx.fill();
|
|
});
|
|
|
|
// Render city nodes
|
|
nodes.forEach(n => {
|
|
let color, stroke;
|
|
if (n.team === 'player') { color = '#16a34a'; stroke = '#22c55e'; }
|
|
else if (n.team === 'enemy') { color = '#dc2626'; stroke = '#ef4444'; }
|
|
else { color = '#475569'; stroke = '#64748b'; }
|
|
|
|
const isSelected = selectedNode && selectedNode.id === n.id;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(n.x, n.y, n.r + (isSelected ? 3 : 0), 0, Math.PI * 2);
|
|
ctx.fillStyle = color;
|
|
ctx.strokeStyle = isSelected ? '#000000' : stroke;
|
|
ctx.lineWidth = isSelected ? 3 : 1.5;
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// Text labels
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.font = 'bold 10px Inter, Noto Sans SC';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(n.label, n.x, n.y - 2);
|
|
|
|
ctx.fillStyle = '#f1f5f9';
|
|
ctx.font = '9px monospace';
|
|
ctx.fillText(Math.floor(n.troops).toString(), n.x, n.y + 9);
|
|
});
|
|
}
|
|
|
|
// Custom UI Toast
|
|
function showToast(msg) {
|
|
const toast = document.getElementById('toast');
|
|
document.getElementById('toast-text').innerText = msg;
|
|
toast.className = toast.className.replace('opacity-0 pointer-events-none translate-y-2', 'opacity-100 pointer-events-auto translate-y-0');
|
|
setTimeout(() => {
|
|
toast.className = toast.className.replace('opacity-100 pointer-events-auto translate-y-0', 'opacity-0 pointer-events-none translate-y-2');
|
|
}, 2500);
|
|
}
|
|
|
|
window.onload = function() {
|
|
initSimulator();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |