Files
Nyaruru_modifier_public/tool/lib/trainer.js
T

149 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Injected into the game. Uses NYA's actor inventory/battle values, not RPG Maker HP/gold.
function installTrainer() {
if (window.__nyaruruTrainer) return window.__nyaruruTrainer;
const fields = { coin: '_coin', hp: '_hp', sp: 'sp', zp: 'zp' };
const locks = new Map();
const ids = new WeakMap();
let serial = 0, touched = Date.now(), actors = $gameActors, party = $gameParty;
const id = o => { if (!ids.has(o)) ids.set(o, ++serial); return ids.get(o); };
const active = () => {
const actor = $gamePlayer.actor(), battle = $gamePlayer.battleObject();
if (!actor || !battle) throw new Error('当前角色数据尚未就绪');
return { actor, battle, subject: `${id(actor)}:${id(battle)}:${!!$gamePlayer.isLilyMode()}` };
};
const maximum = (field, b) => field === 'coin' ? 99999 : Math.max(0, Math.floor(b['m' + field]));
const integer = (value, max) => {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0 || value > max) {
throw new Error(`请输入 0~${max} 的整数`);
}
return value;
};
// Accessor intercepts the actual storage slot, so even lethal damage cannot reach zero
// before a later polling refill. Descriptor flags and the current value survive unlock.
function detach(lock) {
if (!lock.object) return;
const value = lock.value();
Object.defineProperty(lock.object, fields[lock.field], { ...lock.descriptor, value });
lock.object = null;
}
function clear() {
for (const lock of locks.values()) detach(lock);
locks.clear();
}
function sync() {
if (actors !== $gameActors || party !== $gameParty || Date.now() - touched > 120000) {
clear(); actors = $gameActors; party = $gameParty;
}
if (!locks.size) return;
const { actor, battle } = active();
for (const lock of locks.values()) {
const object = lock.field === 'coin' ? actor : battle;
if (lock.object === object) continue;
detach(lock);
const key = fields[lock.field], descriptor = Object.getOwnPropertyDescriptor(object, key);
if (!descriptor?.configurable || !descriptor.writable || !('value' in descriptor)) {
throw new Error('当前游戏的数值结构不支持锁定');
}
lock.object = object; lock.descriptor = descriptor;
lock.value = () => Math.min(lock.target, maximum(lock.field, battle));
Object.defineProperty(object, key, {
configurable: true, enumerable: descriptor.enumerable,
get: () => lock.value(), set: () => {},
});
}
}
function touch() { sync(); touched = Date.now(); }
// Only one timer per game page; neither hooks nor lock settings enter save data.
setInterval(() => { try { sync(); } catch { clear(); } }, 100);
const categories = ['consumables', 'weapons', 'stones', 'ornaments'];
const specials = {
joycoin: ['joyCoinData', 'joyCoinNum', '_joyCoin'],
hpst: ['hpStrengthData', 'hpStrengthNum', '_hpStrengthItem'],
skillst: ['skillStrengthData', 'skillStrengthNum', '_skillStrengthItem'],
};
const localized = key => String(TextManager[key] ?? key ?? '');
function read() {
touch();
const { actor, battle, subject } = active();
const stats = Object.fromEntries(Object.keys(fields).map(field => [field, {
value: field === 'coin' ? actor.coin() : battle[field],
max: maximum(field, battle), locked: locks.has(field), target: locks.get(field)?.target ?? null,
}]));
const items = [];
for (const type of categories) {
const container = actor['_' + type];
for (const [key, data] of Object.entries(container.data())) {
const itemId = Number(key);
if (!Number.isSafeInteger(itemId) || itemId < 0 || !data) continue;
items.push({ type, id: itemId, name: localized(data.name), desc: localized(data.desc),
count: type === 'consumables' ? container.itemNum(itemId) : Number(container.isUnlock(itemId)),
max: type === 'consumables' ? container.maxNum(itemId) : 1,
equipped: type === 'weapons' ? container.currentWeapon() === itemId : !!container.isEquip?.(itemId),
});
}
}
for (const [type, [dataMethod, countMethod]] of Object.entries(specials)) {
const data = actor[dataMethod]();
items.push({ type, id: 0, name: localized(data.name), desc: localized(data.desc), count: actor[countMethod](), max: 99999, equipped: false });
}
return { ok: true, subject, role: $gamePlayer.isLilyMode() ? '莉莉娅' : actor.name(), stats, items };
}
function change(body) {
touch();
const { actor, battle, subject } = active();
if (body.subject !== subject) throw new Error('操作角色已变化,请刷新角色与背包后重试');
if (body.action === 'stat') {
const field = body.field;
if (!Object.prototype.hasOwnProperty.call(fields, field)) throw new Error('未知数值');
if (body.locked !== undefined && typeof body.locked !== 'boolean') throw new Error('锁定参数无效');
if (body.locked === false) {
const old = locks.get(field);
if (old) { detach(old); locks.delete(field); }
return read();
}
const value = integer(body.value, maximum(field, battle));
if (field === 'hp' && battle.hp === 0 && value > 0) throw new Error('角色已倒下,请先在游戏里复活或读档,再修改血量');
const old = locks.get(field), keepLocked = body.locked === true || !!old;
if (old) { detach(old); locks.delete(field); }
if (field === 'coin') {
const delta = value - actor.coin();
if (delta >= 0) actor.addCoin(delta); else actor.deductCoin(-delta);
} else battle['add' + field[0].toUpperCase() + field.slice(1)](value - battle[field]);
if (keepLocked) {
locks.set(field, { field, target: value, object: null });
try { sync(); } catch (e) { clear(); throw e; }
}
} else if (body.action === 'item') {
const { type } = body;
const itemId = integer(body.id, 99999);
if (Object.prototype.hasOwnProperty.call(specials, type)) {
if (itemId !== 0) throw new Error('物品编号无效');
actor[specials[type][2]] = integer(body.count, 99999);
} else {
if (!categories.includes(type)) throw new Error('物品分类无效');
const container = actor['_' + type], data = container.data();
if (!Object.prototype.hasOwnProperty.call(data, itemId) || !data[itemId]) throw new Error('物品不存在');
const max = type === 'consumables' ? (body.overCap === true ? 99999 : container.maxNum(itemId)) : 1;
const count = integer(body.count, max);
if (type === 'consumables') container.add(itemId, count - container.itemNum(itemId), body.overCap === true);
else if (count) container.unlock(itemId);
else if (type === 'weapons') {
const remaining = container._unlock.filter(n => n !== itemId);
if (!remaining.length) throw new Error('至少需要保留一件武器,避免游戏攻击时找不到武器');
if (container.currentWeapon() === itemId) { actor.changeWeapon(remaining[0]); $gamePlayer.setWeaponChange(); }
container._unlock = remaining;
} else if (type === 'ornaments') container.remove(itemId);
else {
if (container.isEquip(itemId)) container.equip(itemId);
container._unlock = container._unlock.filter(n => n !== itemId);
}
}
} else throw new Error('未知修改操作');
return read();
}
return window.__nyaruruTrainer = { read, change, touch, clear };
}
module.exports = { installTrainer };