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 ? `