三国群英传游戏构建技能 (SG Heroes Builder)
This skill builds a complete turn-based Three Kingdoms strategy game in a single HTML file. It covers all systems: map rendering, 3D cities, factions, heroes, battles, internal affairs, diplomacy, wild general recruitment, custom factions, and AI behavior.
Architecture Overview
Single-file approach: All HTML, CSS, and JavaScript live in one sg-heroes.html file. No external dependencies, no build tools, no frameworks. Pure vanilla JS + CSS 3D transforms + SVG.
File Structure
sg-heroes.html
├── <style> — All CSS (theme variables, 3D city styles, animations, layout)
├── <body> — Game screens (menu, game, battle, modal)
├── <script> — All game logic (data, systems, rendering, AI, events)
└── manifest.json — PWA manifest (optional)
Step 1: HTML Structure & Game Screens
Required DOM Elements
<!-- Main menu screen -->
<div id="menu-screen" class="screen active">
<h1>三国群英传 - 群雄逐鹿</h1>
<button id="start-game-btn" onclick="showFactionSelect()">开始游戏</button>
<button onclick="loadGame()">读取存档</button>
</div>
<!-- Game screen with map + panels + action bar -->
<div id="game-screen" class="screen">
<div id="top-bar">
<span id="turn-info"></span>
<span id="faction-info"></span>
<span id="resource-info"></span>
</div>
<div id="map-container">
<svg id="map-svg"></svg>
<div id="cities-layer"></div>
<!-- Zoom controls -->
<div style="position:absolute;right:10px;top:10px;">
<button onclick="zoomMap(0.2)">+</button>
<button onclick="zoomMap(-0.2)">−</button>
</div>
<!-- Faction legend -->
<div id="map-legend">...</div>
</div>
<div id="city-panel"></div>
<div id="action-bar">
<button onclick="centerOnCapital()">🏛 回主城</button>
<button onclick="resetMapView()">全图</button>
<button onclick="showGeneralsList()">武将名册</button>
<button onclick="showTreasures()">宝物图鉴</button>
<button onclick="showDiplomacy()">外交谋略</button>
<button onclick="showFactionInfo()">天下大势</button>
<button onclick="saveGame()">保存进度</button>
<button onclick="confirmEndTurn()">结束回合</button>
</div>
</div>
<!-- Battle screen -->
<div id="battle-screen" class="screen">...</div>
<!-- Modal overlay -->
<div id="modal-overlay"><div id="modal-content"></div></div>
Step 2: CSS Theme & 3D City Styles
Theme Variables
:root {
--bg-dark: #0a0a0a;
--bg-panel: #1a1410;
--gold: #d4af37;
--gold-dim: #b8860b;
--text-main: #e8d5b7;
--text-dim: #a0896a;
--border: #5a3a10;
--wei-color: #4169e1;
--shu-color: #228b22;
--wu-color: #dc143c;
--qun-color: #daa520;
--custom-color: #9b59b6;
--neutral-color: #666;
}
3D City CSS (Isometric Projection)
.city-node {
position: absolute;
transform: translate(-50%, -80%);
width: 40px; height: 40px;
cursor: pointer;
transition: transform 0.3s;
}
.city-node:hover { transform: translate(-50%, -80%) scale(1.15); z-index: 100; }
.city-3d {
position: relative;
width: 100%; height: 100%;
transform-style: preserve-3d;
transform: rotateX(55deg) rotateZ(-45deg);
}
/* City walls (two visible faces) */
.city-wall-left, .city-wall-right {
position: absolute;
bottom: 0;
width: 50%; height: 60%;
border: 1px solid #5a4520;
}
.city-wall-left { left: 0; }
.city-wall-right { right: 0; border-left: none; }
/* Corner towers */
.corner-tower {
position: absolute;
width: 35%; height: 50%;
transform-style: preserve-3d;
}
.ct-tl { top: 0; left: 0; }
.ct-tr { top: 0; right: 0; }
.ct-bl { bottom: 0; left: 0; }
.ct-br { bottom: 0; right: 0; }
/* Tower roofs (Chinese style curved eaves using border triangles) */
.ct-roof, .tower-roof {
position: absolute;
top: -4px;
left: 50%;
transform: translateX(-50%);
}
/* Central tower */
.city-tower {
position: absolute;
top: 15%; left: 25%;
width: 50%; height: 70%;
transform-style: preserve-3d;
}
/* Flag */
.city-flag {
position: absolute;
top: -12px; left: 50%;
transform: translateX(-50%) rotateZ(45deg);
}
.flag-pole { width: 2px; height: 16px; background: #444; }
.flag-cloth {
width: 10px; height: 7px;
animation: flagWave 1.5s ease-in-out infinite;
}
@keyframes flagWave { 0%,100%{transform:skewX(0);} 50%{transform:skewX(-8deg);} }
City Size Classes
.size-s { width: 32px; height: 32px; }
.size-m { width: 40px; height: 40px; }
.size-l { width: 48px; height: 48px; }
.capital { width: 56px; height: 56px; }
Special City Types
.water-city .city-base { background: #4a6a8a; }
.border-city .city-wall-left { border-color: #8b0000; }
.sky-city { animation: skyFloat 4s ease-in-out infinite; }
@keyframes skyFloat { 0%,100%{transform:translate(-50%,-80%);} 50%{transform:translate(-50%,-84%);} }
Map SVG & Layer
#map-container {
position: relative;
width: 100%; flex: 1;
overflow: hidden;
background: #f5e6c8;
background-image:
radial-gradient(ellipse at center, rgba(245,230,200,1) 0%, rgba(230,210,170,0.9) 100%);
}
#map-svg {
position: absolute; top:0; left:0; width:100%; height:100%;
transition: transform 0.5s cubic-bezier(0.22,1,0.36,1);
transform-origin: center center;
}
#cities-layer {
position: absolute; top:0; left:0; width:100%; height:100%;
pointer-events: none;
transition: transform 0.5s cubic-bezier(0.22,1,0.36,1);
transform-origin: center center;
}
Step 3: Game Data Structures
Factions
const FACTIONS = {
wei: { name:'曹魏', color:'#4169e1', leader:'曹操', capital:'xuchang', personality:'cautious' },
shu: { name:'蜀汉', color:'#228b22', leader:'刘备', capital:'xinye', personality:'righteous' },
wu: { name:'东吴', color:'#dc143c', leader:'孙权', capital:'jianye', personality:'defensive' },
qun: { name:'群雄', color:'#daa520', leader:'董卓', capital:'luoyang', personality:'aggressive' },
custom: { name:'自立', color:'#9b59b6', leader:'自定义', capital:'longcheng', personality:'balanced' }
};
const COLORS = { wei:'#4169e1', shu:'#228b22', wu:'#dc143c', qun:'#daa520', custom:'#9b59b6', neutral:'#666' };
Cities (50+ cities based on real Three Kingdoms geography)
const CITY_DATA = [
// Central plains
{ id:'luoyang', name:'洛阳', x:48, y:40, faction:'qun', pop:80000, gold:200, food:200, defense:9, garrison:5000, isCapital:true, desc:'东都洛阳,天下之中' },
{ id:'xuchang', name:'许昌', x:52, y:45, faction:'wei', pop:60000, gold:150, food:150, defense:7, garrison:4000, isCapital:true, desc:'曹魏都城' },
{ id:'xinye', name:'新野', x:45, y:55, faction:'shu', pop:30000, gold:80, food:80, defense:5, garrison:2000, isCapital:true, desc:'刘备驻扎之地' },
{ id:'jianye', name:'建业', x:72, y:65, faction:'wu', pop:70000, gold:180, food:160, defense:8, garrison:4500, isCapital:true, desc:'吴国都城' },
// ... 50+ more cities covering all of China
{ id:'longcheng', name:'悬空城', x:90, y:88, faction:'custom', pop:35000, gold:100, food:80, defense:7, garrison:2000, isCapital:true, desc:'云海之上的悬空之城' }
];
City Adjacency (for roads and movement)
const ADJACENCY = {
luoyang: ['xuchang','tongguan','hulaoguan'],
xuchang: ['luoyang','xinye','runan'],
xinye: ['xuchang','xiangyang','wancheng'],
// ... all connections
longcheng: ['kuaiji'] // 悬空城通过海路连接会稽
};
Generals (60+ famous characters from Romance of the Three Kingdoms)
const GENERAL_DATA = [
// Wei
{ id:'g_caocao', name:'曹操', faction:'wei', star:5, force:72, int:92, command:95, skill:'奸雄', skillDesc:'战斗胜率+15%', troop:'cavalry', bio:'治世之能臣,乱世之奸雄' },
{ id:'g_xiahou', name:'夏侯惇', faction:'wei', star:4, force:88, int:55, command:78, skill:'刚烈', skillDesc:'受伤后攻击+20%', troop:'spearman', bio:'独眼猛将' },
// Shu
{ id:'g_liubei', name:'刘备', faction:'shu', star:5, force:65, int:85, command:88, skill:'仁德', skillDesc:'招募成功率+15%', troop:'spearman', bio:'中山靖王之后' },
{ id:'g_guanyu', name:'关羽', faction:'shu', star:5, force:98, int:78, command:85, skill:'武圣', skillDesc:'攻击+25%,必杀率+15%', troop:'cavalry', bio:'武财神,义薄云天' },
{ id:'g_zhangfei', name:'张飞', faction:'shu', star:5, force:97, int:50, command:72, skill:'咆哮', skillDesc:'敌军士气-20%', troop:'spearman', bio:'燕人张翼德' },
// Wu
{ id:'g_sunquan', name:'孙权', faction:'wu', star:4, force:60, int:88, command:82, skill:'制衡', skillDesc:'防御+20%', troop:'archer', bio:'生子当如孙仲谋' },
{ id:'g_zhouyu', name:'周瑜', faction:'wu', star:5, force:72, int:95, command:90, skill:'火攻', skillDesc:'火攻伤害+50%', troop:'navy', bio:'美周郎' },
// Qun
{ id:'g_dongzhuo', name:'董卓', faction:'qun', star:4, force:85, int:50, command:70, skill:'暴虐', skillDesc:'掠夺资源+30%', troop:'cavalry', bio:'权倾朝野的暴君' },
// ... 60+ more
];
Wild Generals ( recruitable hidden characters)
const WILD_GENERALS = [
// Legend tier (rare, powerful, expensive)
{ id:'w_lvbu', name:'吕布(野)', star:5, force:100, int:42, command:78, skill:'无双', skillDesc:'攻击+30%,必杀率+20%', troop:'cavalry', cost:3000, bio:'人中吕布', rarity:'legend' },
{ id:'w_zhaoyun', name:'赵云(野)', star:5, force:96, int:82, command:90, skill:'龙胆', skillDesc:'攻防各+15%', troop:'spearman', cost:2500, bio:'常山赵子龙', rarity:'legend' },
// Epic tier
{ id:'w_zhugeliang', name:'诸葛亮(野)', star:5, force:40, int:100, command:88, skill:'卧龙', skillDesc:'计谋成功率+30%', troop:'archer', cost:3000, bio:'卧龙先生', rarity:'legend' },
// Female generals
{ id:'w_diaochan', name:'貂蝉(野)', star:4, force:35, int:88, command:50, skill:'闭月', skillDesc:'敌将倒戈概率+25%', troop:'archer', cost:1500, bio:'四大美人之一', rarity:'epic', isFemale:true },
// ... 40+ more wild generals
];
Treasures
const TREASURES = [
{ id:'t_qinglong', name:'青龙偃月刀', owner:'guanyu', forceBonus:8, desc:'重八十二斤,刀中之王' },
{ id:'t_zhangba', name:'丈八蛇矛', owner:'zhangfei', forceBonus:7, desc:'全用镔铁点钢' },
{ id:'t_chitu', name:'赤兔马', owner:null, speedBonus:5, desc:'日行千里,夜走八百' },
// ... more treasures
];
Historical Events
const HISTORICAL_EVENTS = [
{
id:'e_taoyuan', name:'桃园结义', turn:1,
condition: (g) => g.generals['g_liubei'] && g.generals['g_guanyu'] && g.generals['g_zhangfei'],
effect: (g) => {
// Add sworn brother relationship
addRelationship('g_liubei','g_guanyu','sworn_brother');
addRelationship('g_liubei','g_zhangfei','sworn_brother');
addLog('【桃园结义】刘关张桃园三结义,兄弟同心!');
}
},
{
id:'e_sangu', name:'三顾茅庐', turn:10,
condition: (g) => g.factions.shu.cities.length >= 2 && !g.generals['g_zhugeliang'],
effect: (g) => {
// Add Zhuge Liang to Shu faction
addGeneralToFaction('g_zhugeliang','shu','xinye');
addLog('【三顾茅庐】刘备三顾茅庐,诸葛亮出山!');
}
},
// ... more events
];
Step 4: Map Rendering System
Coordinate Transformation (City spacing scale)
const MAP_SCALE = 1.75;
const MAP_CX = 50, MAP_CY = 50;
function mapX(x) { return MAP_CX + (x - MAP_CX) * MAP_SCALE; }
function mapY(y) { return MAP_CY + (y - MAP_CY) * MAP_SCALE; }
Map View Control (Pan/Zoom)
let mapView = { offsetX: 0, offsetY: 0, zoom: 1 };
function centerOnCapital() {
const capitalId = game.factions[game.playerFaction].capital;
const capital = game.cities[capitalId];
const cx = mapX(capital.x), cy = mapY(capital.y);
mapView.offsetX = 50 - cx;
mapView.offsetY = 50 - cy;
mapView.zoom = 1.6;
renderMap();
game.selectedCity = capitalId;
showCityPanel(capitalId);
showToast(`已回到主城:${capital.name}`);
}
function resetMapView() {
mapView = { offsetX: 0, offsetY: 0, zoom: 1 };
renderMap();
}
function zoomMap(delta) {
mapView.zoom = Math.max(0.5, Math.min(2.5, mapView.zoom + delta));
renderMap();
}
renderMap() Function (Complete)
function renderMap() {
const svg = document.getElementById('map-svg');
const layer = document.getElementById('cities-layer');
svg.innerHTML = '';
layer.innerHTML = '';
svg.setAttribute('viewBox', '-40 -40 180 180');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
// 1. Draw terrain decorations (mountains, forests)
// Mountains (small hill symbols)
const mountains = [[20,18],[25,22],[30,15],/*...*/];
for (const [mx,my] of mountains) {
const m = document.createElementNS('http://www.w3.org/2000/svg','path');
const tx = mapX(mx), ty = mapY(my);
m.setAttribute('d',`M ${tx-2} ${ty+1} L ${tx} ${ty-2} L ${tx+2} ${ty+1} Z`);
m.setAttribute('fill','rgba(120,100,70,0.25)');
svg.appendChild(m);
}
// 2. Draw faction territory color blocks
// 2a. Territory bands between same-faction adjacent cities
const territoryDrawn = new Set();
for (const cityId in ADJACENCY) {
const city = game.cities[cityId];
if (!city || city.faction === 'neutral') continue;
for (const adjId of ADJACENCY[cityId]) {
const adj = game.cities[adjId];
if (!adj || adj.faction !== city.faction) continue;
const key = [cityId, adjId].sort().join('-');
if (territoryDrawn.has(key)) continue;
territoryDrawn.add(key);
// Draw thick translucent line in faction color
const x1=mapX(city.x), y1=mapY(city.y), x2=mapX(adj.x), y2=mapY(adj.y);
const terrLine = document.createElementNS('http://www.w3.org/2000/svg','line');
terrLine.setAttribute('x1',x1); terrLine.setAttribute('y1',y1);
terrLine.setAttribute('x2',x2); terrLine.setAttribute('y2',y2);
terrLine.setAttribute('stroke', COLORS[city.faction]);
terrLine.setAttribute('stroke-width', '8');
terrLine.setAttribute('stroke-linecap', 'round');
terrLine.setAttribute('opacity', city.faction === game.playerFaction ? '0.18' : '0.12');
svg.appendChild(terrLine);
}
}
// 2b. Faction glow circles around each city
for (const cityId in game.cities) {
const city = game.cities[cityId];
if (city.faction === 'neutral') continue;
const cx = mapX(city.x), cy = mapY(city.y);
const color = COLORS[city.faction];
const isPlayer = city.faction === game.playerFaction;
const isCapital = city.isCapital;
const radius = isCapital ? 7 : 5;
// Outer glow
const glow = document.createElementNS('http://www.w3.org/2000/svg','circle');
glow.setAttribute('cx',cx); glow.setAttribute('cy',cy);
glow.setAttribute('r',radius+3);
glow.setAttribute('fill',color);
glow.setAttribute('opacity', isPlayer ? '0.08' : '0.05');
svg.appendChild(glow);
// Core circle
const core = document.createElementNS('http://www.w3.org/2000/svg','circle');
core.setAttribute('cx',cx); core.setAttribute('cy',cy);
core.setAttribute('r',radius);
core.setAttribute('fill',color);
core.setAttribute('opacity', isPlayer ? '0.15' : '0.1');
svg.appendChild(core);
// Capital ring
if (isCapital) {
const ring = document.createElementNS('http://www.w3.org/2000/svg','circle');
ring.setAttribute('cx',cx); ring.setAttribute('cy',cy);
ring.setAttribute('r',radius+1.5);
ring.setAttribute('fill','none');
ring.setAttribute('stroke',color);
ring.setAttribute('stroke-width','0.5');
ring.setAttribute('opacity','0.5');
ring.setAttribute('stroke-dasharray','1,0.5');
svg.appendChild(ring);
}
}
// 3. Draw roads (passable paths between cities)
const drawn = new Set();
for (const cityId in ADJACENCY) {
const city = game.cities[cityId];
if (!city) continue;
for (const adjId of ADJACENCY[cityId]) {
const adj = game.cities[adjId];
if (!adj) continue;
const key = [cityId, adjId].sort().join('-');
if (drawn.has(key)) continue;
drawn.add(key);
const x1=mapX(city.x), y1=mapY(city.y), x2=mapX(adj.x), y2=mapY(adj.y);
const sameFaction = city.faction === adj.faction && city.faction !== 'neutral';
// Road shadow (dark)
const shadow = document.createElementNS('http://www.w3.org/2000/svg','line');
shadow.setAttribute('x1',x1); shadow.setAttribute('y1',y1);
shadow.setAttribute('x2',x2); shadow.setAttribute('y2',y2);
shadow.setAttribute('stroke','rgba(120,90,50,0.4)');
shadow.setAttribute('stroke-width','1.2');
svg.appendChild(shadow);
// Road main (earthen yellow)
const road = document.createElementNS('http://www.w3.org/2000/svg','line');
road.setAttribute('x1',x1); road.setAttribute('y1',y1);
road.setAttribute('x2',x2); road.setAttribute('y2',y2);
road.setAttribute('stroke', sameFaction ? 'rgba(180,140,80,0.7)' : 'rgba(160,130,80,0.5)');
road.setAttribute('stroke-width','0.7');
svg.appendChild(road);
// Dashed line for same-faction official roads
if (sameFaction) {
const dash = document.createElementNS('http://www.w3.org/2000/svg','line');
dash.setAttribute('x1',x1); dash.setAttribute('y1',y1);
dash.setAttribute('x2',x2); dash.setAttribute('y2',y2);
dash.setAttribute('stroke','rgba(200,170,100,0.4)');
dash.setAttribute('stroke-width','0.2');
dash.setAttribute('stroke-dasharray','1.5,1.5');
svg.appendChild(dash);
}
}
}
// 4. Draw rivers (Yangtze and Yellow River)
// Draw as SVG paths with blue translucent strokes
// 5. Draw cities as 3D DOM elements
for (const cityId in game.cities) {
const city = game.cities[cityId];
const color = city.faction === 'neutral' ? COLORS.neutral : COLORS[city.faction];
const node = document.createElement('div');
node.className = `city-node ${sizeClass}${specialClass}`;
node.style.left = mapX(city.x) + '%';
node.style.top = mapY(city.y) + '%';
node.onclick = () => onCityClick(cityId);
// Build 3D city HTML with faction-specific roof colors
const city3d = document.createElement('div');
city3d.className = 'city-3d';
city3d.innerHTML = buildCity3DHTML(color, roofColor, wallColors);
node.appendChild(city3d);
// Wild general indicator badge
const wildGens = getWildGeneralsInCity(cityId);
if (wildGens.length > 0) {
const badge = document.createElement('div');
badge.style.cssText = 'position:absolute;top:-2px;right:-8px;width:14px;height:14px;background:linear-gradient(135deg,#ff6b35,#ff8c42);color:#fff;font-size:0.6em;font-weight:bold;border-radius:50%;display:flex;align-items:center;justify-content:center;animation:wildPulse 1.5s infinite;z-index:10;';
badge.textContent = '贤';
city3d.appendChild(badge);
}
// City name label
const nameLabel = document.createElement('div');
nameLabel.className = 'city-name';
nameLabel.textContent = city.name + (city.isCapital ? '★' : '');
node.appendChild(nameLabel);
// Garrison count
const garLabel = document.createElement('div');
garLabel.className = 'city-gar';
garLabel.textContent = city.garrison;
node.appendChild(garLabel);
layer.appendChild(node);
}
// 6. Apply map view transform (SVG and layer sync)
const tx = mapView.offsetX, ty = mapView.offsetY, s = mapView.zoom;
const cssTransform = `translate(${tx}%, ${ty}%) scale(${s})`;
svg.style.transform = cssTransform;
layer.style.transform = cssTransform;
}
3D City HTML Builder
function buildCity3DHTML(color, roofColor, wallColors) {
const { wallBase, wallLight, wallDark } = wallColors;
return `
<div class="city-base" style="background:${wallBase};"></div>
<div class="city-wall-left" style="background:linear-gradient(180deg,${wallLight} 0%,${wallBase} 100%);">
<div class="crenellations"></div>
</div>
<div class="city-wall-right" style="background:linear-gradient(180deg,${wallDark} 0%,${wallDark} 100%);">
<div class="crenellations"></div>
</div>
<div class="city-gate"></div>
<!-- 4 corner towers with roofs -->
<div class="corner-tower ct-tl">...</div>
<div class="corner-tower ct-tr">...</div>
<div class="corner-tower ct-bl">...</div>
<div class="corner-tower ct-br">...</div>
<!-- Central tower -->
<div class="city-tower">...</div>
<!-- Flag -->
<div class="city-flag">
<div class="flag-pole"></div>
<div class="flag-cloth" style="background:${color};"></div>
</div>
`;
}
Step 5: Game Initialization
function initGame(playerFactionId) {
mapView = { offsetX: 0, offsetY: 0, zoom: 1 };
game = {
turn: 1,
playerFaction: playerFactionId,
cities: {},
factions: {},
generals: {},
wildGenerals: {}, // cityId -> [generalId, ...]
treasures: {},
relationships: {},
log: [],
selectedCity: null,
pendingAction: null
};
// Initialize cities from CITY_DATA
for (const cd of CITY_DATA) {
game.cities[cd.id] = { ...cd, garrison: cd.garrison, originalFaction: cd.faction };
}
// Initialize factions
for (const f in FACTIONS) {
const fdata = FACTIONS[f];
let capitalCity = null;
for (const cid in game.cities) {
if (game.cities[cid].faction === f && game.cities[cid].isCapital) {
capitalCity = cid; break;
}
}
game.factions[f] = {
name: fdata.name, color: fdata.color, leader: fdata.leader,
capital: capitalCity, gold: 2000, food: 2000,
personality: fdata.personality, atWar: {}, allies: []
};
}
// Initialize generals from GENERAL_DATA
for (const gd of GENERAL_DATA) {
game.generals[gd.id] = {
...gd, troops: 1000+gd.star*200, maxTroops: 800+gd.star*200,
level: 1, exp: 0, loyalty: 90, wounded: false, treasures: []
};
// Assign to city
for (const cid in game.cities) {
if (game.cities[cid].faction === gd.faction) {
game.generals[gd.id].cityId = cid; break;
}
}
}
// Initialize wild generals (random distribution)
distributeWildGenerals();
// Initialize treasures
for (const t of TREASURES) {
game.treasures[t.id] = { ...t };
}
// Initialize relationships
initRelationships();
// Trigger turn-1 events
checkHistoricalEvents();
renderAll();
}
Step 6: Custom Faction System
Custom Lord Setup
let customLordName = '新君主';
let customFemaleName = '红颜';
function showCustomLordDialog() {
let html = `<div class="modal-header"><span class="modal-title">自立势力 - 君主设定</span></div>`;
html += `<div class="modal-body">
<div style="margin:20px 0;">
<label>君主姓名:</label>
<input id="custom_lord_name" type="text" value="${customLordName}" maxlength="6"
style="background:var(--bg-dark);border:2px solid var(--gold);color:var(--gold);padding:8px;font-size:1.2em;">
</div>
<div style="margin:20px 0;">
<label>女武将姓名:</label>
<input id="custom_female_name" type="text" value="${customFemaleName}" maxlength="6"
style="background:var(--bg-dark);border:2px solid var(--gold);color:var(--gold);padding:8px;font-size:1.2em;">
</div>
<div style="color:var(--text-dim);font-size:0.85em;">
◆ 起始城池:悬空城(云海之上,右下角秘境)<br>
◆ 配备:自定义女武将(弓将)+ 天威将军(猛将)<br>
◆ 初始资源:金币3500、粮草3500
</div>
</div>`;
html += `<div class="modal-footer">
<button class="btn btn-success" onclick="confirmCustomLord()">确认起兵</button>
<button class="btn" onclick="closeModal()">取消</button>
</div>`;
showModal(html);
}
function confirmCustomLord() {
const ln = document.getElementById('custom_lord_name').value.trim() || '新君主';
const fn = document.getElementById('custom_female_name').value.trim() || '红颜';
customLordName = ln;
customFemaleName = fn;
closeModal();
selectFactionMenu('custom');
}
function initCustomFaction() {
game.factions.custom = {
name: '自立', color: COLORS.custom, leader: customLordName,
capital: 'longcheng', gold: 3500, food: 3500,
personality: 'balanced', atWar: {}, allies: []
};
// Custom lord general
game.generals['g_custom_lord'] = {
id: 'g_custom_lord', name: customLordName, faction: 'custom',
star: 5, force: 88, int: 85, command: 90,
skill: '雄主', skillDesc: '攻防+15%,招募率+20%',
troop: 'cavalry', troops: 2000, maxTroops: 2500,
loyalty: 100, level: 1, exp: 0, wounded: false, treasures: [],
isLord: true, cityId: 'longcheng',
biography: customLordName + ',于悬空城起兵,志在天下。'
};
// Custom female general
game.generals['g_custom_female'] = {
id: 'g_custom_female', name: customFemaleName, faction: 'custom',
star: 5, force: 85, int: 92, command: 86,
skill: '巾帼', skillDesc: '弓术+20%,士气+15%',
troop: 'archer', troops: 1500, maxTroops: 2000,
loyalty: 95, level: 1, exp: 0, wounded: false, treasures: [],
isFemale: true, cityId: 'longcheng',
biography: customFemaleName + ',文武双全的巾帼英雄。'
};
// Third powerful general: 天威将军
game.generals['g_tianwei'] = {
id: 'g_tianwei', name: '天威将军', faction: 'custom',
star: 5, force: 96, int: 70, command: 88,
skill: '天威', skillDesc: '攻击+25%,冲锋+20%',
troop: 'cavalry', troops: 1800, maxTroops: 2300,
loyalty: 95, level: 1, exp: 0, wounded: false, treasures: [],
isLordGeneral: true, cityId: 'longcheng',
biography: '天生神力,万夫不当之勇,随主公起兵悬空城。'
};
// Assign to 悬空城
game.cities['longcheng'].garrison = 2000;
addLog(`【悬空起兵】${customLordName}于悬空城起兵,${customFemaleName}与天威将军相随,霸业自此始!`);
}
Step 7: Wild General Recruitment System
Distribution
function distributeWildGenerals() {
game.wildGenerals = {};
const shuffled = [...WILD_GENERALS].sort(() => Math.random() - 0.5);
const cities = Object.keys(game.cities);
// Distribute to ~20 cities, 1-2 generals each
let idx = 0;
for (const wg of shuffled) {
if (idx >= 25) break;
const cityId = cities[Math.floor(Math.random() * cities.length)];
if (!game.wildGenerals[cityId]) game.wildGenerals[cityId] = [];
if (game.wildGenerals[cityId].length < 2) {
game.wildGenerals[cityId].push(wg.id);
game.generals[wg.id] = {
...wg, faction: null, cityId: cityId, troops: 0,
maxTroops: 800+wg.star*200, level: 1, exp: 0,
loyalty: 0, wounded: false, treasures: [], isWild: true
};
idx++;
}
}
}
function getWildGeneralsInCity(cityId) {
return game.wildGenerals[cityId] || [];
}
Search & Recruit
function searchWildGeneral(cityId) {
const city = game.cities[cityId];
if (city.faction !== game.playerFaction) { showToast('只能在己方城池寻访!'); return; }
if (game.factions[game.playerFaction].gold < 200) { showToast('寻访需要200金!'); return; }
const wildGens = getWildGeneralsInCity(cityId);
if (wildGens.length === 0) {
// Search with 25% chance to discover new general
game.factions[game.playerFaction].gold -= 200;
if (Math.random() < 0.25) {
const remaining = WILD_GENERALS.filter(w => !game.generals[w.id]);
if (remaining.length > 0) {
const newWild = remaining[Math.floor(Math.random()*remaining.length)];
const wildGen = { ...newWild, faction:null, cityId:cityId, troops:0,
maxTroops: 800+newWild.star*200, level:1, exp:0, loyalty:0,
wounded:false, treasures:[], isWild:true };
game.generals[newWild.id] = wildGen;
if (!game.wildGenerals[cityId]) game.wildGenerals[cityId] = [];
game.wildGenerals[cityId].push(newWild.id);
addLog(`【寻访】在${city.name}发现了在野武将${newWild.name}!`);
showToast(`发现武将:${newWild.name}!`);
} else { showToast('未发现贤才'); }
} else { showToast('未发现贤才'); }
renderAll();
return;
}
// Show recruitment UI
let html = `<div class="modal-header"><span class="modal-title">在野武将 - ${city.name}</span></div>`;
html += `<div class="modal-body">`;
for (const wid of wildGens) {
const g = game.generals[wid];
if (!g) continue;
// Success rate by rarity + faction leader bonuses
let successRate = g.rarity==='legend' ? 0.35 : g.rarity==='epic' ? 0.5 : g.rarity==='rare' ? 0.7 : 0.85;
const lord = game.generals['g_custom_lord'] || game.generals['g_liubei'];
if (lord && lord.skill === '雄主') successRate += 0.2;
if (lord && lord.skill === '仁德') successRate += 0.15;
successRate = Math.min(0.95, successRate);
// Build recruitment card with stats, cost, success rate, recruit button
html += buildRecruitCard(g, successRate, cityId);
}
html += `</div>`;
showModal(html);
}
function recruitGeneral(wid, cityId) {
const g = game.generals[wid];
const city = game.cities[cityId];
const faction = game.factions[game.playerFaction];
if (faction.gold < g.cost) { showToast('金币不足!'); return; }
let successRate = g.rarity==='legend' ? 0.35 : g.rarity==='epic' ? 0.5 : g.rarity==='rare' ? 0.7 : 0.85;
const lord = game.generals['g_custom_lord'] || game.generals['g_liubei'];
if (lord && lord.skill === '雄主') successRate += 0.2;
if (lord && lord.skill === '仁德') successRate += 0.15;
successRate = Math.min(0.95, successRate);
faction.gold -= g.cost;
if (Math.random() < successRate) {
// Success
g.faction = game.playerFaction;
g.cityId = cityId;
g.troops = 500;
g.maxTroops = 800 + g.star * 200;
g.loyalty = 70;
g.isWild = false;
game.wildGenerals[cityId] = game.wildGenerals[cityId].filter(id => id !== wid);
addLog(`【招募】成功招募${g.name}!`);
showToast(`${g.name}已加入麾下!`);
} else {
addLog(`【招募】${g.name}拒绝了招募...`);
showToast(`${g.name}拒绝了招募`);
}
renderAll();
// Reopen panel
searchWildGeneral(cityId);
}
Wild General AI Movement (End of Turn)
function moveWildGenerals() {
for (const cityId in game.wildGenerals) {
const gens = [...game.wildGenerals[cityId]];
for (const wid of gens) {
if (Math.random() < 0.15) { // 15% chance to move
const neighbors = ADJACENCY[cityId] || [];
if (neighbors.length > 0) {
const newCity = neighbors[Math.floor(Math.random()*neighbors.length)];
// Remove from old city
game.wildGenerals[cityId] = game.wildGenerals[cityId].filter(id => id !== wid);
// Add to new city
if (!game.wildGenerals[newCity]) game.wildGenerals[newCity] = [];
game.wildGenerals[newCity].push(wid);
if (game.generals[wid]) game.generals[wid].cityId = newCity;
}
}
}
}
}
Step 8: Battle System
Battle Calculation
function calculateBattle(attacker, defender, attackerCity, defenderCity) {
let atkPower = attacker.force + attacker.troops * 0.1;
let defPower = defender.force + defender.troops * 0.1;
// Formation bonuses
atkPower *= getFormationBonus(currentFormation, 'attack');
defPower *= getFormationBonus(defenderFormation, 'defense');
// Unit counter system (cavalry > archer > spearman > cavalry)
atkPower *= getUnitCounterMultiplier(attacker.troop, defender.troop);
// Skill effects
atkPower *= getSkillMultiplier(attacker, 'attack');
defPower *= getSkillMultiplier(defender, 'defense');
// Relationship bonuses (sworn brothers fight together: +10%)
if (hasRelationship(attacker.id, defender.id, 'sworn_brother')) {
atkPower *= 1.1;
}
// Treasure bonuses
atkPower += getTreasureBonus(attacker, 'force');
defPower += getTreasureBonus(defender, 'force');
// Random factor
atkPower *= (0.8 + Math.random() * 0.4);
defPower *= (0.8 + Math.random() * 0.4);
const attackerWins = atkPower > defPower;
const casualtyRate = Math.abs(atkPower - defPower) / Math.max(atkPower, defPower);
return {
winner: attackerWins ? 'attacker' : 'defender',
attackerLosses: Math.floor(attacker.troops * casualtyRate * 0.5),
defenderLosses: Math.floor(defender.troops * casualtyRate * 0.5),
attackerPower: atkPower,
defenderPower: defPower
};
}
Unit Counter System
const UNIT_COUNTERS = {
cavalry: 'archer', // Cavalry beats Archer
archer: 'spearman', // Archer beats Spearman
spearman: 'cavalry', // Spearman beats Cavalry
navy: 'none' // Navy is neutral on water
};
function getUnitCounterMultiplier(atkTroop, defTroop) {
if (UNIT_COUNTERS[atkTroop] === defTroop) return 1.3; // Counter advantage
if (UNIT_COUNTERS[defTroop] === atkTroop) return 0.7; // Counter disadvantage
return 1.0;
}
Step 9: Internal Affairs System
function developCity(cityId, action) {
const city = game.cities[cityId];
const faction = game.factions[city.faction];
switch(action) {
case 'farm': // +Food
if (faction.gold < 100) { showToast('金币不足'); return; }
faction.gold -= 100;
city.food += 50 + Math.floor(Math.random()*50);
addLog(`【内政】${city.name}农业发展,粮食+${50}`);
break;
case 'commerce': // +Gold
if (faction.gold < 100) { showToast('金币不足'); return; }
faction.gold -= 100;
faction.gold += 200 + Math.floor(Math.random()*100);
addLog(`【内政】${city.name}商业发展,金币+200`);
break;
case 'fortify': // +Defense
if (faction.gold < 150) { showToast('金币不足'); return; }
faction.gold -= 150;
city.defense += 1;
addLog(`【内政】${city.name}城防加固,防御+1`);
break;
case 'recruit': // +Garrison
if (faction.gold < 100 || faction.food < 100) { showToast('资源不足'); return; }
faction.gold -= 100; faction.food -= 100;
city.garrison += 500;
addLog(`【内政】${city.name}招募士兵+500`);
break;
}
renderAll();
}
Step 10: Diplomacy System
function diplomacyAction(targetFaction, action) {
const player = game.factions[game.playerFaction];
const target = game.factions[targetFaction];
switch(action) {
case 'alliance':
if (player.gold < 500) { showToast('金币不足'); return; }
player.gold -= 500;
if (Math.random() < 0.5) {
player.allies.push(targetFaction);
target.allies.push(game.playerFaction);
addLog(`【外交】与${target.name}结成同盟!`);
} else { addLog(`【外交】${target.name}拒绝了同盟提议`); }
break;
case 'sowDiscord':
if (player.gold < 800) { showToast('金币不足'); return; }
player.gold -= 800;
// Reduce target's relationship with its allies
addLog(`【外交】对${target.name}实施离间计`);
break;
case 'surrender':
// Persuade target city to surrender
addLog(`【外交】劝降${target.name}`);
break;
}
renderAll();
}
Step 11: AI Behavior System
function runAITurn(factionId) {
const faction = game.factions[factionId];
const personality = faction.personality;
// 1. Internal affairs
for (const cityId in game.cities) {
const city = game.cities[cityId];
if (city.faction !== factionId) continue;
// AI develops based on personality
if (personality === 'cautious' && city.defense < 8) developCity(cityId, 'fortify');
else if (personality === 'aggressive' && city.garrison < 3000) developCity(cityId, 'recruit');
else developCity(cityId, Math.random() < 0.5 ? 'farm' : 'commerce');
}
// 2. Military decisions
const myCities = Object.values(game.cities).filter(c => c.faction === factionId);
const enemyCities = Object.values(game.cities).filter(c => c.faction !== factionId && c.faction !== 'neutral');
// Aggression level by personality and turn
const baseAggression = personality === 'aggressive' ? 0.7 : personality === 'cautious' ? 0.2 : 0.4;
const turnBonus = Math.min(0.3, game.turn * 0.01);
const aggression = baseAggression + turnBonus;
if (Math.random() < aggression) {
// Find weakest adjacent enemy city
for (const myCity of myCities) {
const adjacent = ADJACENCY[myCity.id] || [];
for (const adjId of adjacent) {
const enemy = game.cities[adjId];
if (!enemy || enemy.faction === factionId || enemy.faction === 'neutral') continue;
if (myCity.garrison > enemy.garrison * 1.5) {
// Attack!
aiAttackCity(factionId, myCity.id, adjId);
break;
}
}
}
}
// 3. Wild general movement
moveWildGenerals();
}
Step 12: Turn Management
function confirmEndTurn() {
let html = `<div class="modal-header"><span class="modal-title">结束回合?</span></div>`;
html += `<div class="modal-body"><p>当前回合:第${game.turn}回合</p></div>`;
html += `<div class="modal-footer"><button class="btn btn-success" onclick="endTurn()">确认结束</button><button class="btn" onclick="closeModal()">取消</button></div>`;
showModal(html);
}
function endTurn() {
closeModal();
// AI turns
for (const f in game.factions) {
if (f !== game.playerFaction) runAITurn(f);
}
game.turn++;
// Income
for (const fid in game.factions) {
const faction = game.factions[fid];
for (const cityId in game.cities) {
const city = game.cities[cityId];
if (city.faction === fid) {
faction.gold += 50 + Math.floor(city.pop / 1000);
faction.food += 30 + Math.floor(city.pop / 2000);
}
}
}
// Check events
checkHistoricalEvents();
// Check victory/defeat
checkGameOver();
addLog(`--- 第${game.turn}回合 ---`);
renderAll();
}
Step 13: Save/Load System
function saveGame() {
const data = JSON.stringify(game);
localStorage.setItem('sgHeroesSave', data);
showToast('进度已保存');
}
function loadGame() {
const data = localStorage.getItem('sgHeroesSave');
if (!data) { showToast('无存档'); return; }
game = JSON.parse(data);
showScreen('game-screen');
renderAll();
showToast('存档已读取');
}
Step 14: Visual Effects System (FX Class)
class FX {
static flash(element, color = 'gold', duration = 500) {
const colors = { gold: '#d4af37', red: '#dc143c', green: '#228b22', blue: '#4169e1' };
const c = colors[color] || color;
element.style.transition = 'box-shadow 0.3s';
element.style.boxShadow = `0 0 20px ${c}, 0 0 40px ${c}`;
setTimeout(() => { element.style.boxShadow = ''; }, duration);
}
static particleBurst(x, y, count = 12) {
for (let i = 0; i < count; i++) {
const p = document.createElement('div');
p.style.cssText = `position:fixed;left:${x}px;top:${y}px;width:6px;height:6px;background:#d4af37;border-radius:50%;pointer-events:none;z-index:9999;`;
const angle = (i / count) * Math.PI * 2;
const dist = 50 + Math.random() * 50;
p.animate([
{ transform: 'translate(0,0) scale(1)', opacity: 1 },
{ transform: `translate(${Math.cos(angle)*dist}px,${Math.sin(angle)*dist}px) scale(0)`, opacity: 0 }
], { duration: 600, easing: 'cubic-bezier(0.22,1,0.36,1)' });
document.body.appendChild(p);
setTimeout(() => p.remove(), 600);
}
}
static screenShake(intensity = 5, duration = 300) {
const container = document.body;
const startTime = Date.now();
function shake() {
const elapsed = Date.now() - startTime;
if (elapsed > duration) { container.style.transform = ''; return; }
const x = (Math.random() - 0.5) * intensity;
const y = (Math.random() - 0.5) * intensity;
container.style.transform = `translate(${x}px, ${y}px)`;
requestAnimationFrame(shake);
}
shake();
}
}
Step 15: Key CSS Animations
@keyframes wildPulse { 0%,100%{transform:scale(1);} 50%{transform:scale(1.15);} }
@keyframes pulse { 0%,100%{box-shadow:0 0 5px rgba(34,139,34,0.5);} 50%{box-shadow:0 0 15px rgba(34,139,34,0.8);} }
@keyframes flagWave { 0%,100%{transform:skewX(0);} 50%{transform:skewX(-8deg);} }
@keyframes skyFloat { 0%,100%{transform:translate(-50%,-80%) translateY(0px);} 50%{transform:translate(-50%,-80%) translateY(-4px);} }
@keyframes cloudFloat { 0%,100%{opacity:0.6;transform:scaleX(1);} 50%{opacity:0.9;transform:scaleX(1.1);} }
@keyframes skyGlow { 0%{opacity:0.4;transform:scale(1);} 100%{opacity:0.8;transform:scale(1.3);} }
Step 16: Helper Functions
function showToast(msg) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
function showModal(html) {
document.getElementById('modal-content').innerHTML = html;
document.getElementById('modal-overlay').classList.add('active');
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('active');
}
function addLog(msg) {
game.log.push({ turn: game.turn, msg });
}
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function renderAll() {
renderMap();
renderTopBar();
if (game.selectedCity) showCityPanel(game.selectedCity);
}
Build Order (Implementation Sequence)
- HTML structure — game screens, panels, modal
- CSS theme — variables, layout, city styles, animations
- Data definitions — CITY_DATA, GENERAL_DATA, WILD_GENERALS, TREASURES, ADJACENCY
- initGame() — initialize all game state
- renderMap() — SVG terrain + territory + roads + 3D city nodes
- City panel — show city info, generals, internal affairs, recruit, attack
- Battle system — calculateBattle, formation, unit counters, FX
- Internal affairs — developCity (farm, commerce, fortify, recruit)
- Diplomacy — alliance, sow discord, surrender
- AI behavior — runAITurn with personality-based decisions
- Turn management — endTurn, income, events, victory check
- Wild generals — distributeWildGenerals, searchWildGeneral, recruitGeneral, moveWildGenerals
- Custom faction — showCustomLordDialog, initCustomFaction, 悬空城 setup
- Map features — centerOnCapital, resetMapView, zoomMap, wheel zoom
- Save/Load — localStorage persistence
- Polish — FX effects, toast, modal, log, legend