fixed # 修复手机端地图滑动异常的问题

fixed # 重构更新框架,支持选择是否下载梦三模式
fixed # 补充部分意图的资源图片与逻辑
This commit is contained in:
2026-09-29 17:44:21 +08:00
parent c9c09582c1
commit dd562ae627
32 changed files with 1156 additions and 135 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

+2 -1
View File
@@ -6,7 +6,7 @@ const mengsanConfig = {
saveKey: "mengsan_run_shuying",
profileKey: "mengsan_profile_shuying",
schemaVersion: 1,
mapLayoutVersion: 7,
mapLayoutVersion: 8,
routeCount: 4,
characters: ["mengsan_liubei_shuying"],
acts: contentRegistry.acts,
@@ -16,6 +16,7 @@ const mengsanConfig = {
elite: "精英",
event: "事件",
story: "剧情",
chest: "宝箱",
rest: "休息",
shop: "商店",
boss: "Boss",
+10 -2
View File
@@ -1,9 +1,17 @@
const act1Map = {
id: "act1",
name: "第一关·初梦",
floorNodes: [1, 4, 4, 4, 1],
minFloorNodes: [1, 3, 3, 3, 1],
floorNodes: [
1, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 1,
],
minFloorNodes: [
1, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 1,
],
nodeWeights: { battle: 65, event: 15, rest: 10, shop: 10 },
requiredFloors: { 2: "battle", 10: "chest", 16: "rest" },
chestRewardPool: "act1.pool.chest",
fixedNodes: [
{
floor: 0,
+5
View File
@@ -1,4 +1,9 @@
export const rewardPools = {
"act1.pool.chest": [
"shared.reward.item.handCharm",
"shared.reward.maxHp",
"shared.reward.support.scout",
],
"act1.pool.story.scoutGift": [
"shared.reward.support.scout",
"shared.reward.card.sha",
+8
View File
@@ -10,6 +10,8 @@ const iconPaths = {
elite: "M16 3l10 5v9c0 6-10 12-10 12S6 23 6 17V8zM10 13l6 4 6-4M16 17v7M11 5l5-3 5 3",
event: "M10 10c0-9 15-8 15 0 0 5-9 5-9 10M16 25v1",
story: "M8 5h18v20H10c-5 0-5-6 0-6h16M8 5c-5 0-5 6 0 6h3V5M12 10h10M12 14h8",
chest: "M4 13h24v15H4zM2 8h28v6H2zM16 8v20" +
"M9 8c-4-3-1-7 3-5l4 5M23 8c4-3 1-7-3-5l-4 5",
rest: "M3 26L16 5l13 21zM10 26l6-11 6 11M16 5V2M7 28h19",
shop: "M4 12l3-7h18l3 7M4 12v4h24v-4M7 16v12h18V16M12 28v-8h8v8M3 28h26M11 6v6M21 6v6",
boss: "M6 28V5M6 6c8-8 12 6 22-1v16c-10 7-14-7-22 1M11 10l11 7M22 10l-11 7M3 28h9",
@@ -60,6 +62,7 @@ const nodeColor = {
elite: "#7e3f79",
event: "#477aa6",
story: "#8c6a34",
chest: "#a27b35",
rest: "#3f8560",
shop: "#a27b35",
boss: "#a62727",
@@ -353,6 +356,7 @@ export const showMap = run => new Promise(resolve => {
viewport.addEventListener("pointerdown", event => {
if (!event.isPrimary || event.button !== 0) return;
suppressClick = false;
if (event.pointerType !== "mouse") return;
drag = { id: event.pointerId, y: event.clientY, top: viewport.scrollTop, moved: false };
});
viewport.addEventListener("pointermove", event => {
@@ -386,6 +390,10 @@ export const showMap = run => new Promise(resolve => {
}
}, true);
const map = ui.create.div(".mengsan-map-shuying", viewport);
const floorCount = Math.max(...run.map.nodes.map(node =>
Math.floor(node.floor))) + 1;
map.style.setProperty("--mengsan-map-min-height",
`${floorCount * 100}px`);
const landscape = document.createElementNS("http://www.w3.org/2000/svg", "svg");
landscape.classList.add("mengsan-ink-landscape-shuying");
landscape.setAttribute("viewBox", "0 0 800 1200");
+144 -13
View File
@@ -4,6 +4,21 @@ import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport }
import { installPersonalPiles } from "./runtime/personal-piles.js";
import { createMonster } from "./runtime/monster.js";
import { mountEnemyIntent } from "./runtime/intent-display.js";
import { attackHitCount, outgoingAttackDamage } from "./runtime/intent-damage.js";
import {
beginDeathBlow,
finishDeathBlow,
hasPendingDeathBlow,
isDeathBlowIntent,
} from "./runtime/death-blow.js";
import {
STUN_INTENT,
applyStun,
clearStun,
finishStunnedTurn,
isStunned,
skipStunnedAction,
} from "./runtime/stun-intent.js";
import { selectFlyconidMove, recordFlyconidAction } from "./runtime/flyconid-intent.js";
import { PLAYER_ENERGY, PLAYER_HAND_LIMIT, canPayCard, payCard, isActiveCardUse } from "./runtime/combat-rules.js";
import { mountBattlePiles } from "./runtime/card-library.js";
@@ -172,6 +187,13 @@ const finishUtilityNode = async (run, node) => {
]);
applyReward(run, choice);
}
else if (node.type == "chest") {
await chooseReward(run, {
rewardPool: config.acts[run.actIndex].chestRewardPool ||
"shared.pool.boss.premium",
rewardTitle: "宝箱奖励(三选一)",
});
}
else if (node.type == "shop") {
const price = 20;
const choice = await chooseButtons("商店 Demo", [
@@ -186,12 +208,18 @@ const finishUtilityNode = async (run, node) => {
}
}
else {
const choice = await chooseButtons("随机事件", [
{ id: "branch", name: "调查异象", description: "在当前路线后插入一个额外剧情战斗节点" },
const choices = [
{ id: "gold", name: "收下钱袋", description: "获得 15 金币" },
]);
];
if (run.actIndex !== 0) {
choices.unshift({
id: "branch", name: "调查异象",
description: "在当前路线后插入一个额外剧情战斗节点",
});
}
const choice = await chooseButtons("随机事件", choices);
if (choice == "branch") insertStoryNode(run, node.id);
else {
else if (choice == "gold") {
run.player.gold += 15;
run.statistics.goldEarned += 15;
}
@@ -286,10 +314,17 @@ const executeFlyconidIntent = async player => {
game.log(player, "消耗1费用发动", move.name);
const target = game.me;
if (!target?.isAlive() || !player.isAlive()) return;
if (isDeathBlowIntent(move)) {
await beginDeathBlow(player, target, move);
return;
}
if (move.damage != null) {
const hit = target.damage(move.damage, player);
hit.mengsanAttack_shuying = true;
await hit;
for (let index = 0; index < attackHitCount(move); index++) {
if (!target.isAlive() || !player.isAlive()) break;
const hit = target.damage(outgoingAttackDamage(move, player), player);
hit.mengsanAttack_shuying = true;
await hit;
}
}
if (target.isAlive() && player.isAlive()) applyFlyconidDebuff(target, move);
};
@@ -316,7 +351,8 @@ const createScenario = (plan, current) => {
installMonsterPile(player, monster, current);
if (monster) {
current.energyUI.set(player, mountEnergy(player, current.session));
current.intentUI.set(player, mountEnemyIntent(player, current.session, STYLE_PATH, document));
current.intentUI.set(player, mountEnemyIntent(player,
current.session, STYLE_PATH, document, () => game.me));
}
for (const participant of [...game.players, ...game.dead]) participant.setSeatNum(Number(participant.dataset.position) + 1);
await equipBattleUnit(player, spec, current.resources);
@@ -398,7 +434,9 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
const monsters = plan.units.map((spec, index) => initBattleUnit(participants[index], spec));
plan.units.forEach((spec, index) => installMonsterPile(participants[index], monsters[index], currentBattle));
participants.forEach((player, index) => {
if (monsters[index]) currentBattle.intentUI.set(player, mountEnemyIntent(player, session, STYLE_PATH, document));
if (monsters[index]) currentBattle.intentUI.set(player,
mountEnemyIntent(player, session, STYLE_PATH, document,
() => game.me));
});
activeBattle.director = createScenario(plan, currentBattle);
activeBattle.director.bind("player", me);
@@ -418,7 +456,15 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
run.player.permanentSkills.forEach(skill => {
if (lib.skill[skill] && !me.hasSkill(skill)) me.addSkill(skill);
});
for (const skill of ["mengsan_draw_shuying", "mengsan_card_use_shuying", "mengsan_card_affixes_shuying", "mengsan_scenario_shuying", "mengsan_card_payment_shuying", "mengsan_monster_draw_shuying", "mengsan_flyconid_action_shuying"]) {
for (const skill of [
"mengsan_draw_shuying", "mengsan_card_use_shuying",
"mengsan_card_affixes_shuying", "mengsan_scenario_shuying",
"mengsan_card_payment_shuying", "mengsan_monster_draw_shuying",
"mengsan_flyconid_action_shuying",
"mengsan_death_blow_finish_shuying",
"mengsan_stun_skip_shuying", "mengsan_stun_recover_shuying",
"mengsan_stun_clear_shuying",
]) {
if (!lib.skill.global.includes(skill)) {
game.addGlobalSkill(skill);
session.ownResource({}, () => game.removeGlobalSkill(skill));
@@ -552,7 +598,10 @@ const routeRun = async (run, selected = null, supplied = false) => {
const node = supplied ? selected : await showMap(run);
supplied = false;
if (!node) { await openModeSelection(); return; }
if (["event", "rest", "shop"].includes(node.type)) { await finishUtilityNode(run, node); continue; }
if (["event", "rest", "shop", "chest"].includes(node.type)) {
await finishUtilityNode(run, node);
continue;
}
if (node.type === "story" && config.nodeContents?.[node.contentId]?.kind === "dialogue") {
if (await finishStoryNode(run, node)) continue;
return;
@@ -650,6 +699,42 @@ const createMode = identityMode => {
update(intent);
return true;
},
mengsanStunEnemy_shuying(player, options = {}) {
const update = activeBattle?.intentUI.get(player);
if (!activeBattle?.session.active || !update ||
!player?.isAlive() ||
player.storage?.mengsanCamp_shuying !== "enemy") {
return false;
}
if (isStunned(player)) return false;
const { resume = "advance", recover } = options;
const flyconid = isFlyconid(player);
const intent = options.intent ?? (flyconid ?
player.storage.mengsanFlyconidIntent_shuying : null);
const restore = recover || (flyconid ?
(choice, original) => {
const state =
player.storage.mengsanFlyconidState_shuying || {};
player.storage.mengsanFlyconidState_shuying =
recordFlyconidAction(state, null, false);
const next = choice === "retry" ? original : null;
player.storage.mengsanFlyconidIntent_shuying = next;
return next;
} : null);
const onRecover = async (choice, original) => {
const next = await restore(choice, original);
update(next === undefined ?
(choice === "retry" ? original : null) : next);
};
if (typeof restore !== "function") {
throw new TypeError("此怪物需要击晕恢复回调");
}
if (!applyStun(player, intent, { resume, recover: onRecover })) {
return false;
}
update(STUN_INTENT);
return true;
},
async mengsanPersistRun_shuying(run) {
await saveRun(run);
},
@@ -720,14 +805,60 @@ const createMode = identityMode => {
trigger: { player: "phaseDrawBegin2" },
forced: true, silent: true, popup: false,
filter(event, player) { return Boolean(player.storage?.mengsanMonster_shuying); },
async content(event, trigger, player) { trigger.num = player.storage.mengsanMonster_shuying.draw; },
async content(event, trigger, player) {
trigger.num = isStunned(player) ? 0 :
player.storage.mengsanMonster_shuying.draw;
},
},
mengsan_flyconid_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active && isFlyconid(event.player) && event.player.storage.mengsanFlyconidIntent_shuying); },
filter(event) { return Boolean(activeBattle?.session.active &&
!isStunned(event.player) && isFlyconid(event.player) &&
event.player.storage.mengsanFlyconidIntent_shuying); },
async content(event, trigger) { await executeFlyconidIntent(trigger.player); },
},
mengsan_stun_skip_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 200,
filter(event) {
return Boolean(activeBattle?.session.active &&
isStunned(event.player));
},
async content(event, trigger) {
skipStunnedAction(trigger.player, trigger);
},
},
mengsan_stun_recover_shuying: {
trigger: { global: "phaseAfter" },
forced: true, silent: true, popup: false, priority: 200,
filter(event) {
return Boolean(activeBattle?.session.active &&
isStunned(event.player));
},
async content(event, trigger) {
await finishStunnedTurn(trigger.player);
},
},
mengsan_stun_clear_shuying: {
trigger: { global: "dieAfter" },
forced: true, silent: true, popup: false,
filter(event) { return isStunned(event.player); },
async content(event, trigger) {
clearStun(trigger.player);
},
},
mengsan_death_blow_finish_shuying: {
trigger: { global: "phaseUseAfter" },
forced: true, silent: true, popup: false,
filter(event) {
return Boolean(activeBattle?.session.active &&
hasPendingDeathBlow(event.player));
},
async content(event, trigger) {
await finishDeathBlow(trigger.player);
},
},
mengsan_frail_shuying: {
mark: true, marktext: "脆",
onremove(player) { delete player.storage.mengsanFrail_shuying; },
+38
View File
@@ -0,0 +1,38 @@
import {
attackHitCount,
outgoingAttackDamage,
} from "./intent-damage.js";
const pendingDeathBlows = new WeakSet();
export function isDeathBlowIntent(intent) {
return intent?.intentType === "death_blow";
}
export function hasPendingDeathBlow(source) {
return pendingDeathBlows.has(source);
}
export async function beginDeathBlow(source, target, intent) {
if (!isDeathBlowIntent(intent)) {
throw new TypeError("濒死一击需要 death_blow 意图类型");
}
if (attackHitCount(intent) !== 1) {
throw new RangeError("濒死一击只能造成一次伤害");
}
const damage = outgoingAttackDamage(intent, source);
if (!source?.isAlive() || !target?.isAlive()) return false;
const hit = target.damage(damage, source);
hit.mengsanAttack_shuying = true;
await hit;
if (source.isAlive()) pendingDeathBlows.add(source);
return true;
}
export async function finishDeathBlow(source) {
if (!hasPendingDeathBlow(source)) return false;
pendingDeathBlows.delete(source);
if (source.isAlive()) await source.die();
return true;
}
+28
View File
@@ -0,0 +1,28 @@
export function attackHitCount(intent) {
const hits = intent.hits ?? 1;
if (!Number.isInteger(hits) || hits < 1) {
throw new RangeError("攻击段数必须是正整数");
}
return hits;
}
export function outgoingAttackDamage(intent, source) {
if (!Number.isInteger(intent.damage) || intent.damage < 0) {
throw new RangeError("预测伤害必须是非负整数");
}
const storedStrength = source?.storage?.mengsanStrength_shuying;
const strength = Number.isInteger(storedStrength) ? storedStrength : 0;
let damage = Math.max(0, intent.damage + strength);
if (source?.storage?.mengsanWeak_shuying > 0) {
damage = Math.floor(damage * 0.75);
}
return damage;
}
export function previewAttackDamage(intent, source, target) {
let damage = outgoingAttackDamage(intent, source);
if (target?.storage?.mengsanVulnerable_shuying > 0) {
damage = Math.ceil(damage * 1.5);
}
return damage;
}
+85 -24
View File
@@ -1,6 +1,12 @@
// 只呈现已经选定的预测技能;不从手牌或原生技能猜测意图。
import { attackHitCount, previewAttackDamage } from "./intent-damage.js";
import { isDeathBlowIntent } from "./death-blow.js";
import { isStunIntent } from "./stun-intent.js";
export function attackIconForDamage(damage) {
if (!Number.isInteger(damage) || damage < 0) throw new RangeError("预测伤害必须是非负整数");
if (!Number.isInteger(damage) || damage < 0) {
throw new RangeError("预测伤害必须是非负整数");
}
if (damage < 5) return 1;
if (damage < 10) return 2;
if (damage < 20) return 3;
@@ -8,56 +14,111 @@ export function attackIconForDamage(damage) {
return 5;
}
export function mountEnemyIntent(player, session, assetBase, document) {
export function mountEnemyIntent(player, session, assetBase, document,
getTarget = () => null) {
const badge = document.createElement("div");
badge.className = "mengsan-intent-shuying";
badge.hidden = true;
badge.setAttribute("aria-live", "polite");
const name = document.createElement("span");
name.className = "mengsan-intent-name-shuying";
badge.setAttribute("aria-atomic", "true");
const damageLine = document.createElement("span");
damageLine.className = "mengsan-intent-damage-shuying";
const amount = document.createElement("span");
amount.className = "mengsan-intent-amount-shuying";
const icon = document.createElement("img");
icon.className = "mengsan-intent-icon-shuying";
icon.alt = "预计伤害";
icon.alt = "Attack";
icon.decoding = "async";
damageLine.append(amount, icon);
damageLine.append(icon, amount);
const debuffLine = document.createElement("span");
debuffLine.className = "mengsan-intent-debuff-shuying";
const debuffIcon = document.createElement("img");
debuffIcon.className = "mengsan-intent-icon-shuying";
debuffIcon.alt = "预计施加负面状态";
debuffIcon.alt = "Debuff";
debuffIcon.decoding = "async";
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.webp`;
const debuffAmount = document.createElement("span");
debuffLine.append(debuffIcon, debuffAmount);
badge.append(name, damageLine, debuffLine);
badge.append(damageLine, debuffLine);
player.appendChild(badge);
session.ownResource(badge, () => badge.remove());
return intent => {
let currentIntent = null;
let timer;
session.ownResource(badge, () => {
clearInterval(timer);
badge.remove();
});
const refresh = () => {
const intent = currentIntent;
if (!intent) {
badge.hidden = true;
badge.removeAttribute("title");
return;
}
if (typeof intent.name !== "string" || !intent.name.trim()) throw new TypeError("预测技能必须有名称");
name.textContent = intent.name;
const hasDamage = intent.damage != null;
damageLine.hidden = !hasDamage;
const hasDebuff = typeof intent.debuff === "string" && Number.isInteger(intent.stacks) && intent.stacks > 0;
debuffLine.hidden = !hasDebuff;
if (hasDebuff) debuffAmount.textContent = `${intent.debuff}×${intent.stacks}`;
if (hasDamage) {
const tier = attackIconForDamage(intent.damage);
amount.textContent = String(intent.damage);
const src = `${assetBase}/assets/intent/Intent_attack_${tier}.webp`;
if (isStunIntent(intent)) {
damageLine.hidden = false;
debuffLine.hidden = true;
amount.textContent = "";
const src = `${assetBase}/assets/intent/Intent_stun.png`;
if (icon.getAttribute("src") !== src) icon.src = src;
badge.title = `${intent.name}:预计造成 ${intent.damage} 点伤害(非最终伤害)${hasDebuff ? `,施加${intent.stacks}层${intent.debuff}` : ""}`;
icon.alt = "Stunned";
if (badge.getAttribute("aria-label") !== "Stunned") {
badge.setAttribute("aria-label", "Stunned");
}
badge.hidden = false;
return;
}
const hasDamage = intent.damage != null;
const deathBlow = isDeathBlowIntent(intent);
if (deathBlow && !hasDamage) {
throw new TypeError("濒死一击必须有伤害数值");
}
damageLine.hidden = !hasDamage;
const hasDebuff = typeof intent.debuff === "string" &&
Number.isInteger(intent.stacks) && intent.stacks > 0;
debuffLine.hidden = !hasDebuff;
if (hasDebuff) {
const label = intent.id === "frail" ? "Frail" :
intent.id === "vulnerable" ? "Vulnerable" : "Debuff";
const debuffText = `${label} ×${intent.stacks}`;
if (debuffAmount.textContent !== debuffText) {
debuffAmount.textContent = debuffText;
}
}
if (hasDamage) {
const hits = attackHitCount(intent);
if (deathBlow && hits !== 1) {
throw new RangeError("濒死一击只能造成一次伤害");
}
const damage = previewAttackDamage(intent, player, getTarget());
const tier = deathBlow ? null :
attackIconForDamage(damage * hits);
const damageText = hits > 1 ? `${damage}×${hits}` :
String(damage);
if (amount.textContent !== damageText) {
amount.textContent = damageText;
}
const src = deathBlow ?
`${assetBase}/assets/intent/Intent_death_blow.png` :
`${assetBase}/assets/intent/Intent_attack_${tier}.webp`;
if (icon.getAttribute("src") !== src) icon.src = src;
icon.alt = deathBlow ? "Death Blow" : "Attack";
const label = `${deathBlow ? "Death Blow" : "Attack"}: ` +
`${damage} damage${hits > 1 ?
` × ${hits} hits` : ""}${hasDebuff ?
`, ${debuffAmount.textContent}` : ""}`;
if (badge.getAttribute("aria-label") !== label) {
badge.setAttribute("aria-label", label);
}
} else {
badge.title = hasDebuff ? `${intent.name}:施加${intent.stacks}层${intent.debuff}` : intent.name;
const label = debuffAmount.textContent;
if (badge.getAttribute("aria-label") !== label) {
badge.setAttribute("aria-label", label);
}
}
badge.hidden = false;
};
timer = setInterval(refresh, 100);
return intent => {
currentIntent = intent;
refresh();
};
}
+45
View File
@@ -0,0 +1,45 @@
const stunned = new WeakMap();
export const STUN_INTENT = Object.freeze({ intentType: "stun" });
export function isStunIntent(intent) {
return intent?.intentType === "stun";
}
export function isStunned(player) {
return stunned.has(player);
}
export function applyStun(player, intent, options = {}) {
if (stunned.has(player)) return false;
const { resume = "advance", recover } = options;
if (resume !== "advance" && resume !== "retry") {
throw new RangeError("击晕恢复方式无效");
}
if (typeof recover !== "function") {
throw new TypeError("击晕需要恢复回调");
}
if (!player?.isAlive()) return false;
stunned.set(player, { intent, resume, recover, skipped: false });
return true;
}
export function skipStunnedAction(player, phaseUse) {
const state = stunned.get(player);
if (!state || state.skipped) return false;
phaseUse.cancel();
state.skipped = true;
return true;
}
export async function finishStunnedTurn(player) {
const state = stunned.get(player);
if (!state?.skipped) return false;
stunned.delete(player);
if (player.isAlive()) await state.recover(state.resume, state.intent);
return true;
}
export function clearStun(player) {
return stunned.delete(player);
}
+3
View File
@@ -110,6 +110,9 @@ export const generateActMap = (run, actIndex) => {
let type;
if (floor == lastFloor) type = "boss";
else if (fixedNode?.type) type = fixedNode.type;
else if (act.requiredFloors?.[floor + 1]) {
type = act.requiredFloors[floor + 1];
}
else if (floor == 0) type = "battle";
else type = weightedType(run, act.nodeWeights);
const id = `${act.id}_f${floor}_n${index}_${Math.floor(nextRandom(run) * 1e6)}`;
+4 -8
View File
@@ -188,7 +188,7 @@
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: none;
touch-action: pan-y;
cursor: grab;
user-select: none;
scrollbar-width: none;
@@ -198,7 +198,8 @@
position: relative;
display: block !important;
width: min(100%, 860px);
height: max(1080px, 160vh) !important;
height: max(1080px, 160vh,
var(--mengsan-map-min-height, 1080px)) !important;
margin: 0 auto;
overflow: hidden;
isolation: isolate;
@@ -520,7 +521,6 @@
.mengsan-map-viewport-shuying::-webkit-scrollbar { display: none; width: 0; height: 0; }
.mengsan-map-viewport-shuying.dragging, .mengsan-map-viewport-shuying.dragging * { cursor: grabbing !important; }
.mengsan-map-shuying .mengsan-node-shuying { touch-action: none; }
/* 梦三行军图视觉系统:深墨舞台、宣纸路线、朱砂交互、铜金层级 */
.mengsan-map-overlay-shuying {
--ms-ink-950: #111713;
@@ -1301,7 +1301,7 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
text-align: center;
pointer-events: none;
}
/* The selected skill appears below an enemy's frame; number precedes the weapon icon. */
/* The selected intent appears below an enemy's frame. */
.player > .mengsan-intent-shuying {
position: absolute !important;
bottom: -31px !important;
@@ -1327,10 +1327,6 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
.player > .mengsan-intent-shuying[hidden],
.mengsan-intent-damage-shuying[hidden],
.mengsan-intent-debuff-shuying[hidden] { display: none !important; }
.mengsan-intent-name-shuying {
overflow: hidden;
text-overflow: ellipsis;
}
.mengsan-intent-damage-shuying {
display: inline-flex;
flex: none;