首领,动画 #41

Merged
Ghost merged 1 commits from why_ga into main 2026-09-16 15:10:44 +08:00
8 changed files with 290 additions and 115 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+51 -16
View File
@@ -33,6 +33,21 @@ async function loadRaces() {
work_comm: rc.sprite_work,
};
});
// 荒漠蝎(流沙荒漠 boss):Odoo 模板缺失时前端兜底注入。
// ⚠️ 正式上线应改为在 Odoo yt.world.race 建 desert_scorpion 模板(前端不写死数据)。
if (!map.desert_scorpion) {
map.desert_scorpion = {
name: '荒漠蝎', icon: '🦂', category: 'boss',
speed: 42, attack: 30, defense: 10, hpMax: 900, energyMax: 100,
attackRange: 76, regen: false, enrage: false, shieldRatio: 0,
trait: '流沙荒漠的霸主,甲壳坚硬的巨型蝎王',
fw: 96, // 帧宽(默认 48,boss 用更宽帧横展体型)
scale: 1.9, // 整体缩放:个头很大
walk: 'assets/characters/desert_scorpion/desert_scorpion_walk_1x6.png',
idle_attack: 'assets/characters/desert_scorpion/desert_scorpion_idle_attack_1x6.png',
work_comm: null,
};
}
RACE_CONFIG = map;
window.RACE_CONFIG = RACE_CONFIG;
return true;
@@ -301,7 +316,7 @@ async function loadMicroChars() {
updateCharPanel();
}
function createChar(race, x, y) {
function createChar(race, x, y, opts) {
const id = 'char_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 5);
const stats = raceStats(race);
const rt = RACE_CONFIG[race] || {};
@@ -340,15 +355,25 @@ function createChar(race, x, y) {
wanderInterval: 2 + Math.random() * 4, // 2~6 秒
_userMove: false, // 是否用户指令移动
freeMove: charPlaceFreeMove, // 自由移动开关(默认开)
boss: !!(opts && opts.boss), // 是否首领(荒漠蝎等 boss)
};
microChars.push(ch);
selectChar(id);
if (!(opts && opts.noSelect)) selectChar(id);
scheduleSaveChars(100);
// 任务:首位居民(ch4_char)。createChar 是种族角色唯一创建入口,
// 直接发 char_recruited 推进任务(不经由 microState.units,避免与角色 AI 渲染叠加)。
if (typeof emitTaskEvent === 'function') emitTaskEvent('char_recruited', { race: race });
if (!(opts && opts.skipTask) && typeof emitTaskEvent === 'function') emitTaskEvent('char_recruited', { race: race });
return ch;
}
// 首领挑战·流沙荒漠:自动生成荒漠蝎 boss(仅当当前没有时才生成,避免重复)
function ensureBossSpawn() {
if (microChars && microChars.some(c => c.race === 'desert_scorpion')) return;
if (typeof microCanvas === 'undefined' || !microCanvas) return;
const x = microCanvas.width / 2;
const y = microCanvas.height * 0.52;
createChar('desert_scorpion', x, y, { skipTask: true, noSelect: true, boss: true });
if (typeof updateCharPanel === 'function') updateCharPanel();
}
function getChar(id) { return microChars.find(c => c.id === id); }
function selectChar(id) {
microChars.forEach(c => {
@@ -386,8 +411,8 @@ function triggerCharAction(charId, action) {
const ch = getChar(charId);
if (!ch || !CHAR_STATES[action]) return false;
const raceCfg = RACE_CONFIG[ch.race];
// 非智慧生物(动物)无工作/交流动作,禁止触发
if (raceCfg && raceCfg.category === 'animal' && (action === 'work' || action === 'communicate')) return false;
// 非智慧生物(动物)/ boss 无工作/交流动作,禁止触发
if (raceCfg && (raceCfg.category === 'animal' || raceCfg.category === 'boss') && (action === 'work' || action === 'communicate')) return false;
ch.state = action;
ch.frame = 0;
ch.frameT = 0;
@@ -539,7 +564,7 @@ function updateChars(dt) {
if (ch.freeMove !== false && !ch.selected && ch.state === 'idle') {
// 非智慧生物(动物)不捡宝箱,直接走漫游逻辑
const raceCfg = RACE_CONFIG[ch.race];
const isAnimal = raceCfg && raceCfg.category === 'animal';
const isAnimal = raceCfg && (raceCfg.category === 'animal' || raceCfg.category === 'boss');
let goToChest = false;
if (!isAnimal) {
@@ -750,6 +775,10 @@ function updateChars(dt) {
function renderChars(ctx, time, mW, mH) {
visibleMicroChars().forEach(ch => {
const st = CHAR_STATES[ch.state];
// 每种族帧宽:默认 CHAR_FW(48);荒漠蝎等 boss 用更宽帧(fw:96)横展体型
const rc = RACE_CONFIG[ch.race] || {};
const FW = rc.fw || CHAR_FW;
const rscale = rc.scale || 1;
// 死亡动画:两阶段——先倒地(前50%,旋转90°、不透明),再消失(后50%,躺平渐隐)
if (ch.dying) {
@@ -772,7 +801,7 @@ function renderChars(ctx, time, mW, mH) {
ctx.translate(ch.x, ch.y);
ctx.rotate(fallP * Math.PI / 2); // 前 50% 旋转到底,后 50% 保持躺平
ctx.globalAlpha = 1 - fadeP; // 后 50% 渐隐
ctx.drawImage(img, fi * CHAR_FW, 0, CHAR_FW, CHAR_FH, -CHAR_FW / 2, -CHAR_FH * 0.9, CHAR_FW, CHAR_FH);
ctx.drawImage(img, fi * FW, 0, FW, CHAR_FH, -FW / 2, -CHAR_FH * 0.9, FW, CHAR_FH);
ctx.restore();
}
return; // 跳过正常渲染
@@ -781,7 +810,7 @@ function renderChars(ctx, time, mW, mH) {
const img = loadCharSheet(ch.race, st.sheet);
if (!img || !img.complete || img.naturalWidth === 0) return;
const fi = st.base + ch.frame;
const drawX = ch.x - CHAR_FW * 0.5;
const drawX = ch.x - FW * 0.5;
const drawY = ch.y - CHAR_FH * 0.90;
// 路径拖尾(仅选中角色)
@@ -815,12 +844,11 @@ function renderChars(ctx, time, mW, mH) {
ctx.fill();
ctx.restore();
// 角色精灵
// 角色精灵(以脚底 ch.x,ch.y 为锚点缩放,支持 boss 大体型 + 朝向翻转)
ctx.save();
ctx.translate(ch.x, 0);
ctx.scale(ch.facing, 1);
ctx.translate(-ch.x, 0);
ctx.drawImage(img, fi * CHAR_FW, 0, CHAR_FW, CHAR_FH, drawX, drawY, CHAR_FW, CHAR_FH);
ctx.translate(ch.x, ch.y);
ctx.scale(ch.facing * rscale, rscale);
ctx.drawImage(img, fi * FW, 0, FW, CHAR_FH, -FW * 0.5, -CHAR_FH * 0.9, FW, CHAR_FH);
ctx.restore();
// 受伤闪白(叠加半透明白)
@@ -828,7 +856,9 @@ function renderChars(ctx, time, mW, mH) {
ctx.save();
ctx.globalAlpha = Math.min(1, ch.hurtFlash * 2.5) * 0.55;
ctx.fillStyle = '#ffffff';
ctx.fillRect(drawX, drawY, CHAR_FW, CHAR_FH);
ctx.translate(ch.x, ch.y);
ctx.scale(ch.facing * rscale, rscale);
ctx.fillRect(-FW * 0.5, -CHAR_FH * 0.9, FW, CHAR_FH);
ctx.restore();
}
@@ -964,8 +994,12 @@ function updateCharPanel() {
const shieldHtml = ch.shieldMax > 0
? `<div style="margin-top:5px;color:#8a9aaa;font-size:11px;">🛡 护盾 <b style="color:#37c7f0;">${ch.shield}</b>/${ch.shieldMax}<span style="color:#667;">${ch.shield > 0 ? '' : '(未触发)'}</span></div>`
: '';
const isAnimal = rc && rc.category === 'animal';
const actions = isAnimal ? ['idle', 'attack'] : ['idle', 'attack', 'work', 'communicate'];
const cat = rc && rc.category;
const actions = cat === 'boss'
? ['idle', 'attack', 'walk']
: (cat === 'animal')
? ['idle', 'attack']
: ['idle', 'attack', 'work', 'communicate'];
const btns = actions.map(a => {
const active = ch.state === a ? 'background:#2a4a6a;border-color:#5a9acc;' : '';
return `<button class="charActBtn" data-act="${a}" style="${active}background:#1d2b4a;color:#cde;border:1px solid #3a5a8e;padding:5px 10px;border-radius:5px;cursor:pointer;font-size:12px;transition:background .15s;">${CHAR_CN[a]}</button>`;
@@ -1029,6 +1063,7 @@ window.renderChars = renderChars;
window.hitChar = hitChar;
window.clampCharToIsland = clampCharToIsland;
window.updateCharPanel = updateCharPanel;
window.ensureBossSpawn = ensureBossSpawn;
window.allSheetsLoaded = allSheetsLoaded;
window.loadCharSheet = loadCharSheet;
window.preloadAllSheets = preloadAllSheets;
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

+6 -44
View File
@@ -88,7 +88,7 @@
.hero .enter-btn{font-size:15px;padding:11px 30px;}
}
/* 进游戏开场动画(yusen 科技感)—— 总时长约 5.2s */
/* 进游戏开场动画(yuthon 科技感)—— 总时长约 5.2s */
#introSplash {
position: fixed; inset: 0; z-index: 600; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
@@ -150,36 +150,18 @@
letter-spacing: .4em; text-indent: .4em; color: rgba(150,200,255,.55); opacity: 0;
}
#introSplash.show .intro-sub { animation: introFade 1.2s ease 2.6s forwards; }
#introSplash .intro-skip {
position: absolute; bottom: 26px; z-index: 4; font-size: 12px; letter-spacing: .15em;
color: rgba(180,210,255,.4); opacity: 0;
}
#introSplash.show .intro-skip { animation: introFade 1s ease 3.6s forwards; }
#introSplash .intro-audio-unlock {
position: absolute; bottom: 58px; z-index: 5; cursor: pointer;
padding: 9px 20px; border-radius: 999px; font-size: 13px; letter-spacing: .12em;
color: #eaf6ff; background: rgba(74,175,255,.12);
border: 1px solid rgba(74,175,255,.5); box-shadow: 0 0 18px rgba(74,175,255,.35);
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
opacity: 0; transition: opacity .4s ease, transform .2s ease;
}
#introSplash.show .intro-audio-unlock { opacity: 1; }
#introSplash .intro-audio-unlock:hover { transform: scale(1.05); }
#introSplash.hide .intro-audio-unlock { opacity: 0; }
@keyframes introFade { to { opacity: 1; } }
</style>
<link rel="stylesheet" href="assets/story-player.css" />
</head>
<body>
<!-- 进游戏开场动画(yusen 科技感) -->
<!-- 进游戏开场动画(yuthon 科技感) -->
<div id="introSplash">
<div class="intro-grid"></div>
<div class="intro-sweep"></div>
<div class="intro-logo">yusen</div>
<div class="intro-logo">yuthon</div>
<div class="intro-line"></div>
<div class="intro-sub">宇森游戏</div>
<button id="introAudioUnlock" class="intro-audio-unlock" style="display:none;">🔊 点击开启音效</button>
<div class="intro-skip">点击任意位置跳过</div>
</div>
<div class="app">
<header class="topbar">
@@ -340,7 +322,7 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{
</script>
<script src="assets/story-player.js"></script>
<script>
// 进游戏开场动画(yusen 科技感):首次访问播放,约 5.2s,可点击跳过,本会话只播一次
// 进游戏开场动画(yuthon 科技感):首次访问播放,约 5.2s 后自动结束,本会话只播一次
let _introAudioCtx = null;
function getIntroAudioCtx() {
try {
@@ -405,23 +387,6 @@ function playIntro(done) {
el.classList.add('show');
let soundMaster = null;
try { soundMaster = playIntroSound(); } catch (e) { console.warn('intro音效异常', e); }
// 浏览器自动播放策略:无用户手势时 AudioContext 被挂起,音效静音 → 显示解锁按钮
const unlockBtn = document.getElementById('introAudioUnlock');
let needsUnlock = false;
try { const ctx = getIntroAudioCtx(); needsUnlock = !!ctx && ctx.state !== 'running'; } catch (e) {}
if (needsUnlock && unlockBtn) unlockBtn.style.display = '';
const unlockAudio = async () => {
try {
const ctx = getIntroAudioCtx();
if (ctx && ctx.state === 'suspended') await ctx.resume();
if (ctx && ctx.state === 'running') {
try { stopIntroSound(soundMaster); } catch (e) {}
try { soundMaster = playIntroSound(); } catch (e) {}
}
} catch (e) {}
if (unlockBtn) unlockBtn.style.display = 'none';
};
if (unlockBtn) unlockBtn.addEventListener('click', (e) => { e.stopPropagation(); unlockAudio(); });
let finished = false;
const finish = () => {
if (finished) return; finished = true;
@@ -430,13 +395,10 @@ function playIntro(done) {
el.classList.remove('show'); el.classList.add('hide');
setTimeout(() => { el.remove(); done && done(); }, 480);
};
const timer = setTimeout(finish, 5200);
const onSkip = () => { clearTimeout(timer); finish(); };
el.addEventListener('click', onSkip);
window.addEventListener('keydown', onSkip, { once: true });
setTimeout(finish, 5200);
}
// 首页首次访问:先播 yusen 开场动画,再播剧情漫画
// 首页首次访问:先播 yuthon 开场动画,再播剧情漫画
(function() {
if (localStorage.getItem('seen_intro_part1')) return;
const btn = document.getElementById('enterGame');
+227 -49
View File
@@ -552,6 +552,143 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
opacity: 1; visibility: visible; pointer-events: auto;
}
/* ===== 首领挑战地图(独立覆盖层:六边形区域选择;z=150 低于微观世界 200,退出战场后自动露出) ===== */
#bossMapView {
position: fixed; inset: 0; z-index: 150; display: none;
align-items: center; justify-content: center; overflow: hidden;
--bm-bg: url('assets/ui/boss_map_bg_b_final.png');
}
#bossMapView.active { display: flex; }
#bossMapView .bm-bg {
position: absolute; inset: 0;
background: var(--bm-bg) center / cover no-repeat;
transform: scale(1.15);
animation: bmSeaDrift 30s ease-in-out infinite alternate;
will-change: transform;
}
/* 水面流光:缓慢漂移的柔光,营造活水感 */
#bossMapView .bm-bg::before {
content: ''; position: absolute; inset: -12%;
background:
radial-gradient(58% 38% at 28% 34%, rgba(190,242,255,.20), rgba(190,242,255,0) 62%),
radial-gradient(50% 34% at 74% 62%, rgba(150,226,255,.15), rgba(150,226,255,0) 62%);
mix-blend-mode: screen;
filter: blur(8px);
animation: bmShimmer 17s ease-in-out infinite alternate;
will-change: transform, opacity;
}
#bossMapView .bm-bg::after {
content: ''; position: absolute; inset: 0;
background:
radial-gradient(ellipse at 50% 48%, rgba(3,12,22,0) 50%, rgba(3,12,22,.22) 80%, rgba(2,8,16,.52) 100%),
linear-gradient(180deg, rgba(3,12,22,.20), rgba(3,12,22,0) 32%, rgba(2,8,16,.22));
}
@keyframes bmSeaDrift {
0% { transform: scale(1.15) translate3d(-1.3%, -0.8%, 0) rotate(-0.25deg); }
50% { transform: scale(1.18) translate3d(0.6%, 0.4%, 0) rotate(0deg); }
100% { transform: scale(1.15) translate3d(1.4%, 1.0%, 0) rotate(0.25deg); }
}
@keyframes bmShimmer {
0% { transform: translate3d(-4%, -3%, 0); opacity: .65; }
100% { transform: translate3d(5%, 4%, 0); opacity: 1; }
}
#bossMapView .bm-head {
position: absolute; top: 0; left: 0; right: 0; z-index: 3;
padding: 20px 26px; display: flex; align-items: center; gap: 16px;
}
#bossMapView .bm-back {
width: 44px; height: 44px; border-radius: 12px; cursor: pointer;
border: 1px solid rgba(255,214,150,.3); background: rgba(30,20,10,.55);
color: #ffd9a0; font-size: 20px; line-height: 1; backdrop-filter: blur(8px);
transition: background .2s, transform .2s;
}
#bossMapView .bm-back:hover { background: rgba(70,46,18,.8); transform: scale(1.06); }
#bossMapView .bm-title h1 {
margin: 0; font-size: 24px; letter-spacing: .16em; color: #ffe6bd;
text-shadow: 0 2px 16px rgba(0,0,0,.8);
}
#bossMapView .bm-title p { margin: 3px 0 0; font-size: 11px; letter-spacing: .26em; color: rgba(255,214,160,.62); }
#bossMapView .bm-stage { position: relative; width: 520px; height: 560px; z-index: 3; }
/* —— 未开放区域:暗礁浮台(非六边形,柔和 3D 质感)—— */
#bossMapView .bm-lock {
position: absolute; left: 50%; top: 50%; width: 148px; height: 104px;
margin: 0; padding: 0; border: 0; cursor: not-allowed;
transform: translate(-50%,-50%) translate(var(--dx,0px), var(--dy,0px));
border-radius: 48% 52% 50% 50% / 42% 44% 56% 58%;
background:
radial-gradient(ellipse at 50% 32%, rgba(76,92,120,.5), rgba(20,28,44,.72) 68%, rgba(10,14,24,.82) 100%);
box-shadow:
inset 0 10px 20px rgba(150,180,230,.14),
inset 0 -16px 24px rgba(0,0,0,.6),
0 16px 30px rgba(0,0,0,.5);
color: rgba(196,210,236,.5); font-size: 14px; font-weight: 700; letter-spacing: .22em;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
opacity: .82; transition: opacity .25s, transform .25s;
}
#bossMapView .bm-lock:hover { opacity: .95; }
#bossMapView .bm-lock small { font-size: 9px; font-weight: 600; letter-spacing: .2em; color: rgba(180,196,226,.4); }
/* —— 可挑战区域:海上立体沙岛(点击进入)—— */
#bossMapView .bm-region {
position: absolute; left: 50%; top: 50%; width: 340px; height: 232px;
margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer;
transform: translate(-50%,-50%) translate(var(--dx,0px), var(--dy,0px));
transition: filter .3s ease;
filter: drop-shadow(0 0 14px rgba(255,178,84,.2));
}
/* 岛底水花环:让岛“坐”在水面上 */
#bossMapView .bm-region-glow {
position: absolute; left: 50%; bottom: -7%; width: 86%; height: 27%;
transform: translateX(-50%); pointer-events: none;
background: radial-gradient(ellipse at 50% 50%, rgba(228,252,255,.62), rgba(140,214,240,.24) 46%, rgba(80,170,210,0) 76%);
filter: blur(8px);
}
/* 水线泡沫:岛与水面交界处的浪花 */
#bossMapView .bm-region-foam {
position: absolute; left: 50%; bottom: -1.5%; width: 66%; height: 6.5%;
transform: translateX(-50%); pointer-events: none; border-radius: 50%;
background: radial-gradient(ellipse at 50% 50%, rgba(255,255,255,0) 30%, rgba(230,252,255,.42) 62%, rgba(230,252,255,0) 84%);
filter: blur(6px);
}
/* 水面倒影 */
#bossMapView .bm-region-refl {
position: absolute; left: 0; top: 100%; width: 100%; height: 100%;
overflow: hidden; pointer-events: none;
-webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.5), rgba(0,0,0,0) 56%);
mask-image: linear-gradient(180deg, rgba(0,0,0,.5), rgba(0,0,0,0) 56%);
}
#bossMapView .bm-region-refl i {
display: block; width: 100%; height: 100%;
transform: scaleY(-1);
background: url('assets/ui/boss_region_quicksand_v2.png') center / contain no-repeat;
filter: blur(3px) saturate(.8) brightness(.85);
}
#bossMapView .bm-region-img {
position: relative; width: 100%; height: 100%; object-fit: contain; display: block;
pointer-events: none; user-select: none;
filter: drop-shadow(0 0 5px rgba(255,236,196,.38)) drop-shadow(0 12px 22px rgba(0,0,0,.45));
}
#bossMapView .bm-region:hover {
filter: drop-shadow(0 20px 34px rgba(0,0,0,.5)) drop-shadow(0 0 34px rgba(255,206,118,.85)) brightness(1.05);
}
#bossMapView .bm-region-cap {
position: absolute; left: 50%; bottom: -14px; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 5px;
pointer-events: none; white-space: nowrap;
}
#bossMapView .bm-name { font-size: 20px; font-weight: 700; letter-spacing: .18em; color: #fff3de; text-shadow: 0 2px 14px rgba(0,0,0,.95); }
#bossMapView .bm-sub { font-size: 10px; letter-spacing: .2em; color: rgba(255,232,196,.82); text-shadow: 0 1px 8px rgba(0,0,0,.95); }
#bossMapView .bm-tag {
font-size: 10px; font-weight: 700; letter-spacing: .2em; padding: 3px 12px;
border-radius: 20px; background: rgba(255,152,42,.95); color: #2a1600;
box-shadow: 0 2px 12px rgba(0,0,0,.55), 0 0 16px rgba(255,160,50,.5);
}
@media (max-width: 640px) {
#bossMapView .bm-stage { transform: scale(.66); }
#bossMapView .bm-title h1 { font-size: 19px; }
#bossMapView .bm-head { padding: 14px 16px; gap: 12px; }
}
#microHeader {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
display: flex; align-items: center; padding: 0 16px;
@@ -867,7 +1004,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
#loadingScreen .loadFill { height: 100%; width: 0%; background: linear-gradient(90deg, #4af, #a8e6cf); border-radius: 6px; transition: width 0.3s; }
#loadingScreen .loadPct { font-size: 20px; color: #4af; font-variant-numeric: tabular-nums; letter-spacing: 1px; }
/* 进游戏开场动画(yusen 科技感)—— 总时长约 5.2s */
/* 进游戏开场动画(yuthon 科技感)—— 总时长约 5.2s */
#introSplash {
position: fixed; inset: 0; z-index: 600; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
@@ -929,22 +1066,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
letter-spacing: .4em; text-indent: .4em; color: rgba(150,200,255,.55); opacity: 0;
}
#introSplash.show .intro-sub { animation: introFade 1.2s ease 2.6s forwards; }
#introSplash .intro-skip {
position: absolute; bottom: 26px; z-index: 4; font-size: 12px; letter-spacing: .15em;
color: rgba(180,210,255,.4); opacity: 0;
}
#introSplash.show .intro-skip { animation: introFade 1s ease 3.6s forwards; }
#introSplash .intro-audio-unlock {
position: absolute; bottom: 58px; z-index: 5; cursor: pointer;
padding: 9px 20px; border-radius: 999px; font-size: 13px; letter-spacing: .12em;
color: #eaf6ff; background: rgba(74,175,255,.12);
border: 1px solid rgba(74,175,255,.5); box-shadow: 0 0 18px rgba(74,175,255,.35);
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
opacity: 0; transition: opacity .4s ease, transform .2s ease;
}
#introSplash.show .intro-audio-unlock { opacity: 1; }
#introSplash .intro-audio-unlock:hover { transform: scale(1.05); }
#introSplash.hide .intro-audio-unlock { opacity: 0; }
@keyframes introFade { to { opacity: 1; } }
/* 单位详情弹窗 */
@@ -1399,15 +1520,13 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
</div>
</div>
<!-- 进游戏开场动画(yusen 科技感) -->
<!-- 进游戏开场动画(yuthon 科技感) -->
<div id="introSplash">
<div class="intro-grid"></div>
<div class="intro-sweep"></div>
<div class="intro-logo">yusen</div>
<div class="intro-logo">yuthon</div>
<div class="intro-line"></div>
<div class="intro-sub">宇森游戏</div>
<button id="introAudioUnlock" class="intro-audio-unlock" style="display:none;">🔊 点击开启音效</button>
<div class="intro-skip">点击任意位置跳过</div>
</div>
<!-- 主视图 -->
@@ -1541,6 +1660,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
<img src="assets/icons/website/website.png?v=2" alt="官网">
<span class="top-icon-label">官网</span>
</button>
<button class="top-icon" id="btnBoss" type="button" title="首领挑战">
<span style="font-size:24px;line-height:1;">👹</span>
<span class="top-icon-label">首领挑战</span>
</button>
<button class="top-icon toolbar-toggle" id="btnToolbarToggle" type="button" title="展开/收起菜单">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
@@ -1620,6 +1743,37 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
</div>
<!-- 首领挑战地图(独立覆盖层:六边形区域选择,点击进入对应战场) -->
<div id="bossMapView">
<div class="bm-bg"></div>
<div class="bm-head">
<button class="bm-back" id="bossMapBack" type="button" title="返回世界">←</button>
<div class="bm-title">
<h1>首领挑战</h1>
<p>讨伐盘踞各界的强大首领</p>
</div>
</div>
<div class="bm-stage">
<button class="bm-lock" type="button" style="--dx:0px; --dy:-168px"><span>? ? ?</span><small>未开放</small></button>
<button class="bm-lock" type="button" style="--dx:-190px; --dy:-84px"><span>? ? ?</span><small>未开放</small></button>
<button class="bm-lock" type="button" style="--dx:190px; --dy:-84px"><span>? ? ?</span><small>未开放</small></button>
<button class="bm-region" id="bmHexQuicksand" type="button" style="--dx:0px; --dy:0px">
<span class="bm-region-glow"></span>
<span class="bm-region-refl"><i></i></span>
<img class="bm-region-img" src="assets/ui/boss_region_quicksand_v2.png" alt="流沙荒漠">
<span class="bm-region-foam"></span>
<span class="bm-region-cap">
<span class="bm-name">流沙荒漠</span>
<span class="bm-sub">首领未现身</span>
<span class="bm-tag">可挑战</span>
</span>
</button>
<button class="bm-lock" type="button" style="--dx:-190px; --dy:84px"><span>? ? ?</span><small>未开放</small></button>
<button class="bm-lock" type="button" style="--dx:190px; --dy:84px"><span>? ? ?</span><small>未开放</small></button>
<button class="bm-lock" type="button" style="--dx:0px; --dy:168px"><span>? ? ?</span><small>未开放</small></button>
</div>
</div>
<!-- 飞入过渡 -->
<div id="flyInOverlay">
<img class="flyBg" id="flyBg" alt="">
@@ -2437,6 +2591,7 @@ let forestBgImg = null; // 森林微观背景图
let mountainBgImg = null; // 山地微观背景图
let desertBgImg = null; // 荒漠微观背景图
let snowBgImg = null; // 雪地微观背景图
let quicksandBgImg = null; // 首领挑战·流沙荒漠场景底图
let waterBgImg = null; // 水域微观背景图
let _lastPaletteKey = ''; // 调色板指纹:时间色调明显变化时才重建基础缓存
const _cachedCam = { x: 0, y: 0, zoom: 1 }; // 快照对应的相机状态,交互期按相机增量平移/缩放 blit
@@ -4417,7 +4572,7 @@ function microTitleText(tile) {
}
// ============ 进入微观世界 ============
function enterMicroWorld(tile) {
function enterMicroWorld(tile, opts) {
if (!tile) return;
const overlay = document.getElementById('flyInOverlay');
const microView = document.getElementById('microView');
@@ -4456,28 +4611,39 @@ function enterMicroWorld(tile) {
microZoom = 1; microPanX = 0; microPanY = 0;
microViewOpen = true; // 暂停主世界重绘
microView.classList.add('active'); // 入场动画(缩放+去模糊)在遮罩掩护下播放,揭幕时早已归位
if (opts && opts.boss) {
document.getElementById('microTitle').textContent = '👹 首领挑战 · 流沙荒漠';
document.getElementById('microSubtitle').textContent = '独立战场 · 拖拽放置你的队伍,讨伐盘踞此地的首领';
} else {
document.getElementById('microTitle').textContent = microTitleText(tile);
document.getElementById('microSubtitle').textContent = `坐标(${tile.q}, ${tile.r}) · ${microState.units.length}位居民`;
}
applyMicroAccent(tile.terrain);
updateSidebar();
microLoadIcons(); // 进入微观视图即预加载建造贴图,避免已放置建筑先显示文字
microLoadCatalog(); // 拉取 Odoo 后台建造目录(未登录时保留前端兜底常量),hover/弹窗即时生效
microLoadGame(); // 拉取角色 / 生物 / 订单(Odoo 后端),覆盖单位状态
if (!(opts && opts.boss)) {
microLoadChests(tile.q, tile.r); // 拉取本地块已生成的宝箱
microStartChestGen(); // 启动测试期 10s 生成定时器
}
fetchTasks(); // 拉取任务定义 + 角色进度(新手主线)
// 资源系统:登录时一次性结算离线产出并兜底回库,再启动在线自动进账
// 资源系统:登录时一次性结算离线产出并兜底回库,再启动在线自动进账(首领场为独立战场,跳过)
if (!(opts && opts.boss)) {
microSettle(true, true).then(() => microRefreshResources());
startResourceLoop();
}
// 初始化角色动画系统:先动态加载种族模板(后端单一事实来源),再预加载精灵图与角色
if (typeof loadRaces === 'function') {
loadRaces().then(() => {
loadRaces().then(async () => {
if (typeof preloadAllSheets === 'function') preloadAllSheets();
if (typeof loadMicroChars === 'function') loadMicroChars(); // 从后端拉取该用户放置的角色(刷新/重进不丢)
if (typeof loadMicroChars === 'function') await loadMicroChars(); // 从后端拉取该用户放置的角色(刷新/重进不丢)
if (opts && opts.boss && typeof ensureBossSpawn === 'function') ensureBossSpawn(); // 首领场:自动生成荒漠蝎 boss(仅一次)
});
} else {
if (typeof preloadAllSheets === 'function') preloadAllSheets();
if (typeof loadMicroChars === 'function') loadMicroChars();
if (opts && opts.boss && typeof ensureBossSpawn === 'function') ensureBossSpawn();
}
if (typeof updateCharPanel === 'function') updateCharPanel();
// 注册角色障碍避让:随机游走 / 用户点击都被吸附到非建筑空地
@@ -4520,6 +4686,18 @@ function enterMicroWorld(tile) {
}, MICRO_ENTER_HOLD);
}
// 首领挑战:复用微观沙盘渲染器(网格 + 放置 + 拖拽),背景固定为流沙荒漠底图。
// 合成独立战场 tile(不参与真实地块宝箱/资源结算),渲染器与常规沙盘完全一致。
function enterBossArena() {
const bossTile = {
q: -1, r: -1, terrain: 'desert', layer: 'earth',
name: '流沙荒漠', hasBuilding: false, units: [], bossBg: true
};
const bb = document.getElementById('backBtn');
if (bb) bb.textContent = '← 返回首领地图';
enterMicroWorld(bossTile, { boss: true });
}
// ============ 资源系统:库存拉取 / 结算 / 回收 ============
function fmtNum(v) {
v = Number(v) || 0;
@@ -5415,6 +5593,8 @@ function microHexDirOfVector(dx, dy) {
// 朝某方向拖动 → 若该方向有相邻地块则切入它(像平移显微镜);否则不切换(边缘即墙)
function navigateMicro(dx, dy) {
const t = microState.tile;
// 首领战场(bossBg)为独立场景:拖拽不切换到相邻地块,与其他沙盘完全隔离
if (t && t.bossBg) return false;
const di = microHexDirOfVector(dx, dy);
const [dq, dr] = MICRO_DIRS[di];
const nb = getTile(t.q + dq, t.r + dr);
@@ -5507,7 +5687,8 @@ function buildMicroGridCache() {
plain: grassBgImg, forest: forestBgImg, mountain: mountainBgImg,
desert: desertBgImg, snow: snowBgImg, water: waterBgImg
};
const bgImg = microBgMap[tile.terrain];
let bgImg = microBgMap[tile.terrain];
if (tile && tile.bossBg && quicksandBgImg && quicksandBgImg.naturalWidth > 0) bgImg = quicksandBgImg;
if (bgImg && bgImg.naturalWidth > 0) {
// 微观视图:直接用对应地形背景图铺满浮岛,去掉默认树/石/建筑等装饰
const iw = bgImg.naturalWidth, ih = bgImg.naturalHeight;
@@ -7114,6 +7295,8 @@ window.wasdKeys = wasdKeys;
function exitMicroWorld() {
document.getElementById('microView').classList.remove('active');
const _bb = document.getElementById('backBtn');
if (_bb) _bb.textContent = '← 返回世界'; // 首领战场退出后复位按钮文案
if (microAnimId) { cancelAnimationFrame(microAnimId); microAnimId = null; }
if (typeof saveMicroCharsNow === 'function') saveMicroCharsNow(); // 退出前落库角色(删除/移动)
if (microResourceTimer) { clearInterval(microResourceTimer); microResourceTimer = null; }
@@ -8160,6 +8343,7 @@ function init() {
desertBgImg = preloadMicroBg('assets/backgrounds/desert_bg.png?v=20260803');
snowBgImg = preloadMicroBg('assets/backgrounds/snow_bg.png?v=20260803');
waterBgImg = preloadMicroBg('assets/backgrounds/water_bg.png?v=20260803');
quicksandBgImg = preloadMicroBg('assets/backgrounds/quicksand_bg.png?v=20260915g');
// 注意:初始 clampCamera 不在本函数内调用——clampCamera 定义在后续 <script> 块中,
// 跨 script 块的函数声明不会提升,若在此调用会抛 ReferenceError 导致加载屏永不关闭(页面"卡住")。
// 初始钳制改在 block1 末尾(clampCamera 定义之后)统一执行。
@@ -8192,7 +8376,7 @@ function init() {
});
}
// 进游戏开场动画(yusen 科技感):加载完成后覆盖播放,约 5.2s,可点击跳过,本会话只播一次
// 进游戏开场动画(yuthon 科技感):加载完成后覆盖播放,约 5.2s 后自动结束,本会话只播一次
let _introAudioCtx = null;
function getIntroAudioCtx() {
try {
@@ -8257,23 +8441,6 @@ function playIntro(done) {
el.classList.add('show');
let soundMaster = null;
try { soundMaster = playIntroSound(); } catch (e) { console.warn('intro音效异常', e); }
// 浏览器自动播放策略:无用户手势时 AudioContext 被挂起,音效静音 → 显示解锁按钮
const unlockBtn = document.getElementById('introAudioUnlock');
let needsUnlock = false;
try { const ctx = getIntroAudioCtx(); needsUnlock = !!ctx && ctx.state !== 'running'; } catch (e) {}
if (needsUnlock && unlockBtn) unlockBtn.style.display = '';
const unlockAudio = async () => {
try {
const ctx = getIntroAudioCtx();
if (ctx && ctx.state === 'suspended') await ctx.resume();
if (ctx && ctx.state === 'running') {
try { stopIntroSound(soundMaster); } catch (e) {}
try { soundMaster = playIntroSound(); } catch (e) {}
}
} catch (e) {}
if (unlockBtn) unlockBtn.style.display = 'none';
};
if (unlockBtn) unlockBtn.addEventListener('click', (e) => { e.stopPropagation(); unlockAudio(); });
let finished = false;
const finish = () => {
if (finished) return; finished = true;
@@ -8282,13 +8449,10 @@ function playIntro(done) {
el.classList.remove('show'); el.classList.add('hide');
setTimeout(() => { el.remove(); done && done(); }, 480);
};
const timer = setTimeout(finish, 5200);
const onSkip = () => { clearTimeout(timer); finish(); };
el.addEventListener('click', onSkip);
window.addEventListener('keydown', onSkip, { once: true });
setTimeout(finish, 5200);
}
// 首次进入剧情:在 yusen 开场动画之后播放,再进入登录/游戏
// 首次进入剧情:在 yuthon 开场动画之后播放,再进入登录/游戏
async function maybePlayIntroStory() {
if (localStorage.getItem('seen_intro_part1')) return;
try {
@@ -9031,6 +9195,20 @@ if (typeof fitWorld === 'function') fitWorld();
if (btnAuction) btnAuction.addEventListener('click', () => {
openAppModal('拍卖行', '<div style="text-align:center;padding:24px;color:#8899aa;font-size:13px;">拍卖行即将开放<br><br>玩家之间自由交易道具、材料与稀有素材。</div>');
});
// 首领挑战:先进入「首领挑战地图」(独立覆盖层),点区域再进战场
const btnBoss = document.getElementById('btnBoss');
if (btnBoss) btnBoss.addEventListener('click', () => {
const bm = document.getElementById('bossMapView');
if (bm) bm.classList.add('active');
});
const bossMapBack = document.getElementById('bossMapBack');
if (bossMapBack) bossMapBack.addEventListener('click', () => {
const bm = document.getElementById('bossMapView');
if (bm) bm.classList.remove('active');
});
// 流沙荒漠区域:进入战场(首领地图保持 active,位于微观世界之下,退出战场后自动露出)
const bmHexQuicksand = document.getElementById('bmHexQuicksand');
if (bmHexQuicksand) bmHexQuicksand.addEventListener('click', () => enterBossArena());
// 通用弹窗
const appModal = document.getElementById('appModal');