首领,动画 #41
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
+51
-16
@@ -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
@@ -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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user