mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-24 00:30:07 +08:00
fixed # 补全部分卡牌
fixed # 修复部分已知BUG
This commit is contained in:
+174
-20
@@ -1,6 +1,11 @@
|
||||
// Mode-owned piles use real Card nodes so native searches/gain/insert operations agree.
|
||||
// DIY must access ui piles inside the acting player's engine event, never cache global nodes.
|
||||
import { AFFIX_INFO } from "./affixes.js";
|
||||
import { snapshotRampage } from "./rampage.js";
|
||||
import { cardDefinitions } from "./card-definitions.js";
|
||||
import { hasCardAffix } from "./intrinsic-affixes.js";
|
||||
import { sharedHooks } from "./shared-hooks.js";
|
||||
import { queueIroncladExhaust, flushIroncladExhaust, ironcladSkillExhausts, ironcladHooks } from "./ironclad-hooks.js";
|
||||
|
||||
|
||||
export function installPersonalPiles(session, owner, battle, run, resources, env) {
|
||||
@@ -15,18 +20,31 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
draw.id = "cardPile"; discard.id = "discardPile";
|
||||
const owned = new Set(), topPlaying = new Set(), restores = [];
|
||||
const strict = env.strict !== false;
|
||||
let released = false, forcedOwner = false;
|
||||
let released = false, forcedOwner = false, forcedPublic = false, exhaustTotal = 0;
|
||||
function recordExhaust(card) {
|
||||
if (card.storage?.mengsanExhausted_shuying) return false;
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card)); exhaustTotal++;
|
||||
queueIroncladExhaust(owner, card); refresh(); return true;
|
||||
}
|
||||
const personal = () => {
|
||||
if (released) return false;
|
||||
if (released || forcedPublic) return false;
|
||||
if (forcedOwner) return true;
|
||||
const seen = new Set();
|
||||
for (let event = _status.event; event && !seen.has(event); event = event.parent) {
|
||||
seen.add(event);
|
||||
// 判定及其公共牌清理沿用原生牌堆;判定触发的独立摸牌/弃牌事件
|
||||
// 会先遇到自己的 player,仍按个人牌堆处理。
|
||||
if (event.name === "judge" || event.name === "judgeCallback" ||
|
||||
event.name === "orderingDiscard" && event.relatedEvent?.name === "judge") return false;
|
||||
if (event.player) return event.player === owner;
|
||||
}
|
||||
return false; // Never infer ownership from whose turn it is.
|
||||
};
|
||||
const refresh = () => { if (!released) env.refresh(); };
|
||||
const canReceiveTransfer = (card, sourceSession) => !released && session.active && sourceSession===session &&
|
||||
owner.isAlive() && !owned.has(card) && typeof card?.storage?.mengsanCard_shuying?.id==="string" &&
|
||||
![...owned].some(c=>c.storage?.mengsanCard_shuying?.id===card.storage.mengsanCard_shuying.id);
|
||||
function adopt(card, data) {
|
||||
if (owned.has(card)) return;
|
||||
owned.add(card); resources.card(card);
|
||||
@@ -34,6 +52,9 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
card.storage.mengsanOwnerId_shuying = owner.playerid;
|
||||
if (data) {
|
||||
card.storage.mengsanCard_shuying = JSON.parse(JSON.stringify(data));
|
||||
const classification = cardDefinitions[card.name] || env.lib?.card?.[card.name];
|
||||
card.storage.mengsanCard_shuying.cardType = classification?.cardType ?? null;
|
||||
card.storage.mengsanCard_shuying.rarity = classification?.rarity ?? "common";
|
||||
// Retired affixes from old saves must not acquire new battle behaviour.
|
||||
card.storage.mengsanCard_shuying.affixes = (card.storage.mengsanCard_shuying.affixes || []).filter(key => key !== "annihilate");
|
||||
}
|
||||
@@ -44,29 +65,33 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
if (released || !["discardPile", "equip", "judge"].includes(position)) return false;
|
||||
if (position === "discardPile" && topPlaying.has(current)) {
|
||||
if (!current.storage.mengsanExhausted_shuying) {
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
refresh();
|
||||
recordExhaust(current);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const exhaust = env.lib?.card?.[current.name]?.mengsanExhaust_shuying === true ||
|
||||
current.storage.mengsanCard_shuying.affixes.includes("exhaust");
|
||||
// 能力牌退出战斗牌堆,不进入消耗牌堆,也不触发消耗收益。
|
||||
if (env.lib?.card?.[current.name]?.mengsanPower_shuying && current.storage.mengsanConsumed_shuying) {
|
||||
current.storage.mengsanPowerRemoved_shuying = true;
|
||||
return true;
|
||||
}
|
||||
const exhaustRule = env.lib?.card?.[current.name]?.mengsanExhaust_shuying;
|
||||
const exhaust = (typeof exhaustRule === "function" ? exhaustRule(current) : exhaustRule === true) ||
|
||||
current.storage.mengsanCard_shuying.affixes.includes("exhaust") ||
|
||||
ironcladSkillExhausts(owner,current);
|
||||
if (exhaust && current.storage.mengsanConsumed_shuying) {
|
||||
if (current.storage.mengsanExhausted_shuying) return true;
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
refresh();
|
||||
recordExhaust(current);
|
||||
return true;
|
||||
}
|
||||
return false; // Actual discarded cards remain available to native skills.
|
||||
};
|
||||
// Only explicit affixes are native gaintags; intrinsic keywords can change on upgrade.
|
||||
for (const key of card.storage.mengsanCard_shuying.affixes) card.addGaintag(AFFIX_INFO[key]?.name || key);
|
||||
// 强化仅用手牌 +X 角标显示;数值仍保存在 mengsanCard_shuying.upgrade。
|
||||
}
|
||||
function snapshot(card) {
|
||||
const data = card.storage?.mengsanCard_shuying || {};
|
||||
return { ...data, name: card.name, suit: card.suit, number: card.number,
|
||||
return { ...data, ...snapshotRampage(card), ...ironcladHooks(owner)?.snapshotCard(card), ...sharedHooks(owner)?.snapshotCard?.(card), name: card.name, suit: card.suit, number: card.number,
|
||||
nature: card.nature || null, affixes: data.affixes?.slice() || [], upgrade: data.upgrade || 0 };
|
||||
}
|
||||
for (const data of battle.drawPile) {
|
||||
@@ -121,15 +146,26 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
}
|
||||
refresh(); return cards;
|
||||
}
|
||||
function wrap(object, key, replacement) {
|
||||
function wrap(object, key, replacement, shouldReplace = personal) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(object, key), original = object[key];
|
||||
const wrapped = function (...args) { return personal() ? replacement(...args) : original.apply(this, args); };
|
||||
const wrapped = function (...args) { return shouldReplace() ? replacement.apply(this, args) : original.apply(this, args); };
|
||||
object[key] = wrapped;
|
||||
restores.push(() => { if (object[key] === wrapped) { if (descriptor) Object.defineProperty(object, key, descriptor); else delete object[key]; } });
|
||||
}
|
||||
wrap(get, "cards", (number, putBack) => take(typeof number === "number" ? number : 1, putBack));
|
||||
wrap(get, "bottomCards", (number, putBack) => take(typeof number === "number" ? number : 1, putBack, true));
|
||||
wrap(game, "washCard", wash);
|
||||
// 原生 player.judge 在事件创建时就读取默认弃牌位置,此时尚未进入 judge。
|
||||
// 仅在同步创建期间读取公共节点,不把此状态延续到异步判定或子技能。
|
||||
// 敌人或助战也可能在玩家事件中创建判定,须统一默认位置。
|
||||
const judgeHost = env.lib?.element?.Player?.prototype || owner;
|
||||
const nativeJudge = judgeHost.judge;
|
||||
wrap(judgeHost, "judge", function (...args) {
|
||||
const before = forcedPublic;
|
||||
forcedPublic = true;
|
||||
try { return nativeJudge.apply(this, args); }
|
||||
finally { forcedPublic = before; }
|
||||
}, () => !released);
|
||||
session.ownResource(draw, () => {
|
||||
if (released) return;
|
||||
observer.disconnect(); released = true;
|
||||
@@ -139,6 +175,42 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
});
|
||||
return {
|
||||
take,
|
||||
canReceiveTransfer,
|
||||
receiveTransfer(card,sourceSession) {
|
||||
if(!canReceiveTransfer(card,sourceSession))return null;
|
||||
const originalData=card.storage.mengsanCard_shuying;
|
||||
adopt(card,originalData);
|
||||
return {
|
||||
gain:()=>owner.gain(card,"gain2"),
|
||||
received:()=>!released && session.active && owner.isAlive() && owned.has(card) && owner.getCards("h").includes(card),
|
||||
rollback:()=>{owned.delete(card);refresh();},
|
||||
};
|
||||
},
|
||||
async transferTo(card,recipient) {
|
||||
if(released || !session.active || !owner.isAlive() || !owned.has(card) ||
|
||||
card.storage?.mengsanExhausted_shuying || card.storage?.mengsanPowerRemoved_shuying ||
|
||||
!recipient?.canReceiveTransfer?.(card,session))return false;
|
||||
const previous={data:card.storage.mengsanCard_shuying,ownerId:card.storage.mengsanOwnerId_shuying,
|
||||
destroyed:card.destroyed,parent:card.parentNode,next:card.nextSibling};
|
||||
owned.delete(card);
|
||||
let receipt,committed=false;
|
||||
try {
|
||||
receipt=recipient.receiveTransfer(card,session);
|
||||
if(!receipt)return false;
|
||||
await receipt.gain();
|
||||
if(receipt.received()){committed=true;refresh();return true;}
|
||||
return false;
|
||||
} finally {
|
||||
if(!committed)receipt?.rollback();
|
||||
if(!committed && !released && session.active){
|
||||
owned.add(card);card.storage.mengsanCard_shuying=previous.data;
|
||||
card.storage.mengsanOwnerId_shuying=previous.ownerId;card.destroyed=previous.destroyed;
|
||||
if(previous.parent)previous.parent.insertBefore(card,previous.next?.parentNode===previous.parent?previous.next:null);
|
||||
else discard.appendChild(card);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 取出当前堆顶,不算抽牌、不洗牌,也不触发不足牌堆失败。
|
||||
beginTopPlay() {
|
||||
if (released || !session.active || !owner.isAlive() || !draw.firstChild) return null;
|
||||
@@ -151,7 +223,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
if (!topPlaying.has(card)) return null;
|
||||
try {
|
||||
if (released || !session.active) return null;
|
||||
if (card.storage?.mengsanExhausted_shuying) { card.remove(); return card; }
|
||||
if (card.storage?.mengsanExhausted_shuying) { card.remove(); await flushIroncladExhaust(owner); return card; }
|
||||
if (owner.getCards("hej").includes(card)) {
|
||||
const loss = owner.lose(card, exhausted); loss.type = "mengsanExhaust";
|
||||
await loss;
|
||||
@@ -159,8 +231,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
}
|
||||
// 被使用后在排序区/弃牌堆,或无法使用而仍在暂存区,均消耗原牌一次。
|
||||
if (!owned.has(card)) return null;
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card)); card.remove(); refresh();
|
||||
recordExhaust(card); card.remove(); refresh(); await flushIroncladExhaust(owner);
|
||||
return card;
|
||||
} finally { topPlaying.delete(card); }
|
||||
},
|
||||
@@ -175,13 +246,88 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
// A cancelled/redirection event must not falsely consume its original.
|
||||
if (released || !session.active || card.parentNode !== exhausted) return null;
|
||||
if (!card.storage.mengsanExhausted_shuying) {
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card));
|
||||
recordExhaust(card);
|
||||
}
|
||||
card.remove();
|
||||
refresh();
|
||||
await flushIroncladExhaust(owner);
|
||||
return card;
|
||||
},
|
||||
exhaustTotal() { return exhaustTotal; },
|
||||
exhaustedCards() { return [...owned].filter(card => card.storage?.mengsanExhausted_shuying); },
|
||||
syncExhaustSnapshots() {
|
||||
if(released || !session.active)return;
|
||||
const current=new Map([...owned].filter(card=>card.storage?.mengsanExhausted_shuying)
|
||||
.map(card=>[card.storage.mengsanCard_shuying?.id,card]));
|
||||
for(let index=0;index<battle.exhaustPile.length;index++){
|
||||
const card=current.get(battle.exhaustPile[index].id);
|
||||
if(card)battle.exhaustPile[index]=snapshot(card);
|
||||
}
|
||||
refresh();
|
||||
},
|
||||
// 自动打出不会触发抽牌、洗牌或堆顶牌不足的失败规则。
|
||||
beginAutoPlay(card, fromExhaust = false) {
|
||||
if (released || !session.active || !owned.has(card)) return false;
|
||||
if (fromExhaust) {
|
||||
if (!card.storage?.mengsanExhausted_shuying) return false;
|
||||
const id = card.storage?.mengsanCard_shuying?.id;
|
||||
const index = battle.exhaustPile.findIndex(data => data.id === id);
|
||||
if (index >= 0) battle.exhaustPile.splice(index,1);
|
||||
delete card.storage.mengsanExhausted_shuying;
|
||||
delete card._selfDestroyed;
|
||||
} else if (card.parentNode !== draw && card.parentNode !== discard) return false;
|
||||
delete card.storage.mengsanConsumed_shuying;
|
||||
card.fix?.(); exhausted.appendChild(card); refresh(); return true;
|
||||
},
|
||||
finishAutoPlay(card) {
|
||||
if (released || !session.active || !owned.has(card)) return;
|
||||
// 已由原生用牌处理过的牌保留真实去向;没有合法目标的牌进入弃牌堆。
|
||||
if (card.parentNode === exhausted && !card.storage?.mengsanExhausted_shuying &&
|
||||
!card.storage?.mengsanPowerRemoved_shuying) discard.appendChild(card);
|
||||
refresh();
|
||||
},
|
||||
drawTop() { return released || !session.active ? null : draw.firstChild; },
|
||||
drawCards() { return released || !session.active ? [] : Array.from(draw.childNodes); },
|
||||
async gainFromDraw(card) {
|
||||
if(released || !session.active || !owned.has(card) || card.parentNode!==draw)return false;
|
||||
await owner.gain(card,"gain2");refresh();return owner.getCards("h").includes(card);
|
||||
},
|
||||
async moveHandToTop(card) {
|
||||
if(released || !session.active || !owned.has(card) || !owner.getCards("h").includes(card))return false;
|
||||
await owner.lose(card,draw);
|
||||
if(released || !session.active || card.parentNode!==draw)return false;
|
||||
card.fix?.();draw.insertBefore(card,draw.firstChild);refresh();return true;
|
||||
},
|
||||
addToDraw(data,index=0) {
|
||||
if(!Number.isSafeInteger(index) || index<0)throw new RangeError("插入牌堆的位置无效");
|
||||
const card=this.addToDiscard(data);if(!card)return null;
|
||||
draw.insertBefore(card,draw.childNodes[Math.min(index,draw.childNodes.length)] || null);refresh();return card;
|
||||
},
|
||||
async gainToHand(card) {
|
||||
if (released || !session.active || !owned.has(card) || card.storage?.mengsanExhausted_shuying ||
|
||||
card.parentNode !== discard) return false;
|
||||
await owner.gain(card,"gain2"); refresh(); return owner.getCards("h").includes(card);
|
||||
},
|
||||
async returnToHand(card) {
|
||||
if(released || !session.active || !owner.isAlive() || !owned.has(card))return false;
|
||||
if(owner.getCards("h").includes(card))return true;
|
||||
if(card.storage?.mengsanExhausted_shuying){
|
||||
const id=card.storage.mengsanCard_shuying?.id;
|
||||
const index=battle.exhaustPile.findIndex(data=>data.id===id);
|
||||
if(index>=0)battle.exhaustPile.splice(index,1);
|
||||
delete card.storage.mengsanExhausted_shuying;delete card._selfDestroyed;
|
||||
}
|
||||
delete card.storage.mengsanConsumed_shuying;
|
||||
card.fix?.();await owner.gain(card,"gain2");refresh();return owner.getCards("h").includes(card);
|
||||
},
|
||||
transformHand(card, data) {
|
||||
if (released || !session.active || !owned.has(card) || !owner.getCards("h").includes(card) ||
|
||||
hasCardAffix(card,"eternal")) return false;
|
||||
card.init([data.suit,data.number,data.name,data.nature]);
|
||||
card.storage ||= {}; card.storage.mengsanCard_shuying = {...data,affixes:data.affixes.slice()};
|
||||
card.storage.mengsanOwnerId_shuying = owner.playerid;
|
||||
refresh(); return true;
|
||||
},
|
||||
// Current physical combat cards only. Exhausted originals remain in owned;
|
||||
// never add the serialized exhaust snapshots a second time.
|
||||
battleCards(playing = []) {
|
||||
@@ -210,6 +356,11 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
refresh();
|
||||
return card;
|
||||
},
|
||||
async addToHand(data) {
|
||||
const card = this.addToDiscard(data);
|
||||
if (!card) return null;
|
||||
await owner.gain(card,"gain2"); refresh(); return card;
|
||||
},
|
||||
// Return references only to cards currently in this actor's discard node.
|
||||
discardCards() {
|
||||
if (released || !session.active) return [];
|
||||
@@ -242,14 +393,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
refresh();
|
||||
},
|
||||
trimOpeningHand(limit = 4) {
|
||||
for (const card of owner.getCards("h").filter(card => !card.storage?.mengsanCard_shuying?.affixes?.includes("innate")).slice(limit)) {
|
||||
for (const card of owner.getCards("h").filter(card => !card.storage?.mengsanCard_shuying?.affixes?.includes("innate") &&
|
||||
!env.lib?.card?.[card.name]?.mengsanInnate_shuying?.(card)).slice(limit)) {
|
||||
if (owned.has(card)) draw.insertBefore(card, draw.firstChild);
|
||||
else card.remove();
|
||||
}
|
||||
refresh();
|
||||
},
|
||||
drawInnate() {
|
||||
const cards = Array.from(draw.childNodes).filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("innate") && !owner.getCards("h").includes(card));
|
||||
const cards = Array.from(draw.childNodes).filter(card =>
|
||||
(card.storage?.mengsanCard_shuying?.affixes?.includes("innate") ||
|
||||
env.lib?.card?.[card.name]?.mengsanInnate_shuying?.(card)) && !owner.getCards("h").includes(card));
|
||||
if (cards.length) owner.directgain(cards);
|
||||
refresh();
|
||||
return cards;
|
||||
|
||||
Reference in New Issue
Block a user