diff --git a/assets/backgrounds/quicksand_bg.png b/assets/backgrounds/quicksand_bg.png new file mode 100644 index 0000000..1f150d3 Binary files /dev/null and b/assets/backgrounds/quicksand_bg.png differ diff --git a/assets/characters.js b/assets/characters.js index 1a776d5..a199ca4 100644 --- a/assets/characters.js +++ b/assets/characters.js @@ -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 ? `
🛡 护盾 ${ch.shield}/${ch.shieldMax}${ch.shield > 0 ? '' : '(未触发)'}
` : ''; - 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 ``; @@ -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; diff --git a/assets/characters/desert_scorpion/desert_scorpion_idle_attack_1x6.png b/assets/characters/desert_scorpion/desert_scorpion_idle_attack_1x6.png new file mode 100644 index 0000000..beff568 Binary files /dev/null and b/assets/characters/desert_scorpion/desert_scorpion_idle_attack_1x6.png differ diff --git a/assets/characters/desert_scorpion/desert_scorpion_walk_1x6.png b/assets/characters/desert_scorpion/desert_scorpion_walk_1x6.png new file mode 100644 index 0000000..3a4f594 Binary files /dev/null and b/assets/characters/desert_scorpion/desert_scorpion_walk_1x6.png differ diff --git a/assets/ui/boss_map_bg_b_final.png b/assets/ui/boss_map_bg_b_final.png new file mode 100644 index 0000000..2f3c06a Binary files /dev/null and b/assets/ui/boss_map_bg_b_final.png differ diff --git a/assets/ui/boss_region_quicksand_v2.png b/assets/ui/boss_region_quicksand_v2.png new file mode 100644 index 0000000..af21eab Binary files /dev/null and b/assets/ui/boss_region_quicksand_v2.png differ diff --git a/index.html b/index.html index bdb1818..3a87e07 100644 --- a/index.html +++ b/index.html @@ -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; } } - +
- +
宇森游戏
- -
点击任意位置跳过
@@ -340,7 +322,7 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{