服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
103 lines
8.2 KiB
TypeScript
103 lines
8.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { editQuest, parseQuest, questValues, questAnnotationText, questDisplayValue, questReference, questReferenceAnnotation } from '../src/gui/quest';
|
|
import type { EditorAnnotation } from '../bindings/pvfine/services/models';
|
|
import { getGUIProvider } from '../src/gui/registry';
|
|
const source = '#PVF_File\n[name]\n`任务😀` # 名称\n[type]\n`[seeking]`\n[sub type]\n-1\n[int data]\n100 2 # 第一组\n200 3\n[/int data]\n[custom]\n{8=`<1::text>`}\n';
|
|
describe('任务 GUI', () => {
|
|
it('识别 qst 并读取草稿,忽略字符串内标签', () => {
|
|
expect(getGUIProvider({ index: 1, path: 'quest/TEST.QST', text: source, editable: true })?.id).toBe('quest');
|
|
const doc = parseQuest(source);
|
|
expect(questValues(doc, 'name')).toEqual(['任务😀']);
|
|
expect(questValues(doc, 'type')).toEqual(['[seeking]']);
|
|
expect(questValues(doc, 'int data')).toEqual(['100', '2', '200', '3']);
|
|
expect(doc.sections).toHaveLength(5);
|
|
});
|
|
it('仅修改目标 token,保留未知字段、注释、空白及 CRLF', () => {
|
|
const text = source.replaceAll('\n', '\r\n');
|
|
expect(editQuest(text, [{ name: 'int data', values: ['100', '9', '200', '3'], kind: 'integer' }])).toBe(text.replace('100 2', '100 9'));
|
|
expect(editQuest(text, [{ name: 'name', values: ['新`任务😀\n对话'], kind: 'string' }])).toBe(text.replace('`任务😀`', '`新``任务😀\n对话`'));
|
|
});
|
|
it('增删列表参数保留闭合标签和注释,支持空字段', () => {
|
|
const added = editQuest(source, [{ name: 'int data', values: ['100', '2', '200', '3', '300', '4'], kind: 'integer' }]);
|
|
expect(added).toContain('200 3\t300\t4\n[/int data]');
|
|
const removed = editQuest(source, [{ name: 'int data', values: [], kind: 'integer' }]);
|
|
expect(removed).toContain('# 第一组');
|
|
expect(removed).toContain('[/int data]');
|
|
expect(questValues(parseQuest(removed), 'int data')).toEqual([]);
|
|
expect(questValues(parseQuest(editQuest('[int data]\n# keep\n[/int data]', [{ name: 'int data', values: ['1', '2'], kind: 'integer' }])), 'int data')).toEqual(['1', '2']);
|
|
});
|
|
it('新增字段追加到末尾,切换类型保留原始参数', () => {
|
|
const added = editQuest(source, [{ name: 'reward selection int data', values: ['100', '1'], kind: 'integer', paired: true }]);
|
|
expect(added.startsWith(source)).toBe(true);
|
|
expect(added).toContain('[reward selection int data]\n\t100\t1\n[/reward selection int data]');
|
|
const changed = editQuest(source, [{ name: 'type', values: ['[condition under clear]'], kind: 'string' }, { name: 'sub type', values: ['6'], kind: 'integer' }]);
|
|
expect(questValues(parseQuest(changed), 'sub type')).toEqual(['6']);
|
|
expect(questValues(parseQuest(changed), 'int data')).toEqual(['100', '2', '200', '3']);
|
|
});
|
|
it('重复字段、字符串引用和特殊类型禁止覆盖', () => {
|
|
expect(() => editQuest('[name]\n`a`\n[name]\n`b`', [{ name: 'name', values: ['c'], kind: 'string' }])).toThrow('重复字段');
|
|
const ref = '[name]\n{8=`<1::name>`}\n[level]\n1 120';
|
|
expect(() => editQuest(ref, [{ name: 'name', values: ['c'], kind: 'string' }])).toThrow('字符串引用');
|
|
expect(editQuest(ref, [{ name: 'level', values: ['10', '120'], kind: 'integer' }])).toContain('{8=`<1::name>`}');
|
|
expect(() => editQuest('[level]\n`1`', [{ name: 'level', values: ['2'], kind: 'integer' }])).toThrow('类型不匹配');
|
|
});
|
|
it('拒绝未闭合、交叉闭合、非整数和溢出', () => {
|
|
for (const text of ['[name]\n`oops', '[name', '[outer]\n[inner]\n1\n[/outer]\n[/inner]', '[name]\n{8=`x`']) expect(() => parseQuest(text)).toThrow();
|
|
for (const value of ['', '1.5', 'NaN', '2147483648', '-2147483649']) expect(() => editQuest(source, [{ name: 'sub type', values: [value], kind: 'integer' }])).toThrow();
|
|
});
|
|
it('嵌套区块只读保留,子字段不会覆盖顶层字段', () => {
|
|
const text = '[name]\n`root`\n[change depend on grow job]\n1\n[name]\n`nested`\n[solve message]\n`dialogue`\n[/change depend on grow job]\n[level]\n1 115';
|
|
const doc = parseQuest(text);
|
|
expect(questValues(doc, 'name')).toEqual(['root']);
|
|
expect(questValues(doc, 'solve message')).toEqual([]);
|
|
expect(doc.sections.find(s => s.name === 'change depend on grow job')?.issue).toContain('嵌套');
|
|
expect(() => editQuest(text, [{ name: 'change depend on grow job', kind: 'integer', values: ['2'] }])).toThrow('嵌套');
|
|
expect(editQuest(text, [{ name: 'level', kind: 'integer', values: ['2', '115'] }])).toBe(text.replace('1 115', '2 115'));
|
|
const paired = '[outer]\n[inner]\n1\n[/inner]\n[other]\n2\n[/outer]\n[level]\n3';
|
|
expect(parseQuest(paired).sections.map(s => s.name)).toEqual(['outer', 'level']);
|
|
expect(editQuest(paired, [{ name: 'level', kind: 'integer', values: ['4'] }])).toBe(paired.replace('[level]\n3', '[level]\n4'));
|
|
});
|
|
it('正确处理反引号转义、多行文本和注释中的标签', () => {
|
|
const text = '# [name]\n[name]\n`a``b\n[other]`\n[level]\n1 120';
|
|
expect(questValues(parseQuest(text), 'name')).toEqual(['a`b\n[other]']);
|
|
expect(editQuest(text, [{ name: 'level', values: ['5', '120'], kind: 'integer' }])).toBe(text.replace('1 120', '5 120'));
|
|
});
|
|
});
|
|
|
|
describe('110+ 任务字符串', () => {
|
|
const annotation: EditorAnnotation = { start: 0, end: 1, title: '冒险任务', content: '', type: 'placeholder', targetFileIndex: -1, rarity: -1, placeholder: { tableIndex: 31, key: 'quest_name' } };
|
|
it('解析 8/10 引用和字面文本,忽略引用内容中的标签及花括号', () => {
|
|
const text = '[name]\n{8=`<31::quest_name>`}\n[depend message]\n{10=`<31::quest_dialogue>`}\n[grade]\n{10=`[daily]`}\n[solve message]\n{8=`文字``与}\n[标签]`}';
|
|
const doc = parseQuest(text);
|
|
expect(questValues(doc, 'grade')).toEqual(['[daily]']);
|
|
expect(doc.sections[0]?.tokens[0]).toMatchObject({ markerType: 8, kind: 'string', reference: { tableIndex: 31, key: 'quest_name' } });
|
|
expect(doc.sections[1]?.tokens[0]?.markerType).toBe(10);
|
|
expect(questValues(doc, 'solve message')).toEqual(['文字`与}\n[标签]']);
|
|
expect(questReference('< 31 :: quest_name >')).toEqual({ tableIndex: 31, key: 'quest_name' });
|
|
});
|
|
it('编辑字面文本保留 8/10 类型、转义、注释和其他引用', () => {
|
|
const text = '[name]\r\n{8=`<31::quest_name>`}\r\n[grade]\r\n{10= `[daily]` } # keep\r\n[solve message]\r\n{8=`原文`}';
|
|
expect(editQuest(text, [{ name: 'grade', kind: 'string', values: ['[epic]'] }])).toBe(text.replace('{10= `[daily]` }', '{10=`[epic]`}'));
|
|
expect(editQuest(text, [{ name: 'grade', kind: 'string', values: ['[daily]'] }])).toBe(text);
|
|
expect(editQuest(text, [{ name: 'solve message', kind: 'string', values: ['新`文本\n下一行'] }])).toBe(text.replace('{8=`原文`}', '{8=`新``文本\n下一行`}'));
|
|
});
|
|
it('引用禁止改写为内联文本或删除,数值编辑保留原始标记', () => {
|
|
for (const marker of ['{8=`<31::quest_name>`}', '{10=`<31::quest_name>`}', '`<31::quest_name>`']) {
|
|
const text = `[name]\n${marker}\n[level]\n1 120`;
|
|
expect(() => editQuest(text, [{ name: 'name', kind: 'string', values: ['替换'] }])).toThrow('修改译文');
|
|
expect(() => editQuest(text, [{ name: 'name', kind: 'string', values: [] }])).toThrow('修改译文');
|
|
expect(editQuest(text, [{ name: 'level', kind: 'integer', values: ['5', '120'] }])).toBe(text.replace('1 120', '5 120'));
|
|
}
|
|
});
|
|
it('按表号和键解析译文,摆脱 DSL 改动后的过期位置', () => {
|
|
const doc = parseQuest('[name]\n{10=`<31::quest_name>`}');
|
|
expect(questDisplayValue(doc, 'name', [annotation])).toBe('冒险任务');
|
|
expect(questReferenceAnnotation({ tableIndex: 32, key: 'quest_name' }, [annotation])).toBeUndefined();
|
|
expect(questDisplayValue(doc, 'name', [])).toBe('<31::quest_name>');
|
|
const fallback = { ...annotation, title: '冒险任务(未翻译)', placeholder: { ...annotation.placeholder!, fallback: true } };
|
|
expect(questAnnotationText(fallback)).toBe('冒险任务');
|
|
expect(questAnnotationText({ ...annotation, title: '原文(未翻译)' })).toBe('原文(未翻译)');
|
|
expect(questAnnotationText({ ...annotation, placeholder: { ...annotation.placeholder!, missing: true } })).toBe('');
|
|
});
|
|
});
|