Files
90CNServer/pvf/frontend/tests/item-skill-effects.test.ts
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

106 lines
8.8 KiB
TypeScript
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.
import { describe,expect,it } from 'vitest';
import { applySkillDataSelection, defaultItemSkillEffect, editItemSkillEffect, itemSkillEffectSummary, readItemSkillEffects, type ItemSkillEffect } from '../src/gui/itemSkillEffects';
const base='#PVF_File\r\n[name]\r\n`装备😀`\r\n[if]\r\n[cooltime]\r\n3000\r\n[/if]\r\n[then]\r\n[skill levelup]\r\n`[swordman]` 99 5\r\n[/skill levelup]\r\n[/then]\r\n[unknown]\r\n{5=1234} # 未知字段\r\n';
const level:ItemSkillEffect={kind:'level',job:'[swordman]',skill:'12',value:'2'};
const data:ItemSkillEffect={kind:'data',job:'[at fighter]',skill:'233',scope:'[all]',data:'[cooltime]',index:'0',operator:'%',value:'-6'};
const range=defaultItemSkillEffect('range');
const parsed=(text:string)=>{const result=readItemSkillEffects(text);expect(result.issues).toEqual([]);return result;};
describe('装备技能增强精确编辑',()=>{
it('三个格式可新增解析,合并同类段落,忽略触发器中的嵌套效果',()=>{
let text=editItemSkillEffect(base,{kind:'add',effect:level});text=editItemSkillEffect(text,{kind:'add',effect:data});text=editItemSkillEffect(text,{kind:'add',effect:range});
expect(parsed(text).entries.map(e=>e.effect)).toEqual([level,data,range]);expect(text).toContain(base);
text=editItemSkillEffect(text,{kind:'add',effect:{...data,index:'1',value:'1.5'}});
text=editItemSkillEffect(text,{kind:'add',effect:{...level,skill:'14',value:'-1'}});
text=editItemSkillEffect(text,{kind:'add',effect:{...range,lower:'20',upper:'30'}});
expect(parsed(text).entries).toHaveLength(6);expect(text.match(/\[skill data up\]/g)).toHaveLength(1);expect(text.match(/\[all skill item container\]/g)).toHaveLength(1);
});
it('等级加成支持连续记录、重复段落及字符串类型标记,未改时原文往返',()=>{
const text=base+'[skill levelup]\r\n{7=`[swordman]`} 12 2 # 第一项\r\n`[mage]` 20 1\r\n[/skill levelup]\r\n[skill levelup]\r\n`[swordman]` 14 3\r\n[/skill levelup]\r\n';
const doc=parsed(text);expect(doc.entries).toHaveLength(3);
expect(editItemSkillEffect(text,{kind:'edit',entry:0,effect:doc.entries[0]!.effect})).toBe(text);
expect(editItemSkillEffect(text,{kind:'edit',entry:0,effect:{...level,job:'[mage]',value:'4'}})).toBe(text.replace('{7=`[swordman]`} 12 2','{7=`[mage]`} 12 4'));
});
it('数据增强精确修改序号、数值、作用范围,保留未知字段和 CRLF',()=>{
const text=editItemSkillEffect(base,{kind:'add',effect:data}),next={...data,scope:'[dungeon type]',index:'1',value:'-1.5'};
const output=editItemSkillEffect(text,{kind:'edit',entry:0,effect:next});
expect(parsed(output).entries[0]!.effect).toEqual(next);expect(output).toContain(base);
expect(output.replaceAll('\r\n','')).not.toContain('\n');
});
it('删除连续记录只删除目标参数,删除最后记录移除整段',()=>{
let text=editItemSkillEffect('',{kind:'add',effect:level});text=editItemSkillEffect(text,{kind:'add',effect:{...level,skill:'14'}});
text=editItemSkillEffect(text,{kind:'delete',entry:0});expect(parsed(text).entries[0]!.effect).toMatchObject({skill:'14'});
text=editItemSkillEffect(text,{kind:'delete',entry:0});expect(parsed(text).entries).toEqual([]);expect(text).not.toContain('[skill levelup]');
});
it('区间修改按字段定位,保留注释及已有额外条件,未改原文往返',()=>{
const text=editItemSkillEffect(base,{kind:'add',effect:range}).replace('85\r\n','85 # 等级上限\r\n');
const doc=parsed(text);expect(editItemSkillEffect(text,{kind:'edit',entry:0,effect:doc.entries[0]!.effect})).toBe(text);
const next={...range,job:'[swordman]',upper:'90',value:'2'};
const result=editItemSkillEffect(text,{kind:'edit',entry:0,effect:next});
expect(parsed(result).entries[0]!.effect).toEqual(next);expect(result).toContain('90 # 等级上限');expect(result).toContain(base);
});
it('区间额外条件可增删改,支持多个区间独立修改与删除',()=>{
let text=editItemSkillEffect('',{kind:'add',effect:{...range,extra:[]}});
text=editItemSkillEffect(text,{kind:'edit',entry:0,effect:{...range,extra:['[normal skill]','[ex skill]']}});
expect(parsed(text).entries[0]!.effect).toMatchObject({extra:['[normal skill]','[ex skill]']});
text=editItemSkillEffect(text,{kind:'add',effect:{...range,lower:'30'}});
text=editItemSkillEffect(text,{kind:'edit',entry:0,effect:{...range,extra:[]}});
expect(parsed(text).entries[0]!.effect).toMatchObject({extra:[]});expect(parsed(text).entries).toHaveLength(2);
text=editItemSkillEffect(text,{kind:'delete',entry:0});expect(parsed(text).entries[0]!.effect).toMatchObject({lower:'30'});
text=editItemSkillEffect(text,{kind:'delete',entry:0});expect(text).not.toContain('[all skill item container]');
});
it('特殊结构分段只读,不影响其他有效增强;不修改未知条件字段',()=>{
const text='[skill data up]\n`[mage]` 1 `custom`\n[/skill data up]\n'+editItemSkillEffect(base,{kind:'add',effect:level});
const doc=readItemSkillEffects(text);expect(doc.issues.length).toBeGreaterThan(0);expect(doc.entries).toHaveLength(1);
const next=editItemSkillEffect(text,{kind:'edit',entry:0,effect:{...level,value:'4'}});expect(next).toContain(text.slice(0,text.indexOf('#PVF_File')));
const special=editItemSkillEffect('',{kind:'add',effect:range}).replace('[skill phase]','[special field]\n1\n[skill phase]');
expect(readItemSkillEffects(special).issues.length).toBeGreaterThan(0);
});
it('校验职业、技能 ID、数值、等级范围和操作符,拒绝代码注入及过期定位',()=>{
for(const effect of [
{...level,job:'swordman'},{...level,job:'[mage]\n[name]'},{...level,skill:'-1'},{...level,value:'1.5'},
{...data,index:'-1'},{...data,value:'NaN'},{...data,value:'3.5e38'},{...data,operator:'eval'},
{...range,lower:'90',upper:'1'},{...range,extra:['[/all skill item]']},
])expect(()=>editItemSkillEffect(base,{kind:'add',effect})).toThrow();
const text=editItemSkillEffect(base,{kind:'add',effect:level});
expect(()=>editItemSkillEffect(text,{kind:'edit',entry:0,effect:data})).toThrow('类型');
expect(()=>editItemSkillEffect(text,{kind:'delete',entry:9})).toThrow('条目已变化');
});
it('新增效果不追加到参数损坏的同名段落,坏记录保留且新记录可见',()=>{
const text='[skill data up]\n`mage` 1 `[all]` `[cooltime]` 0 `%` 2\n[/skill data up]\n';
const next=editItemSkillEffect(text,{kind:'add',effect:data}),doc=readItemSkillEffects(next);
expect(next).toContain(text);expect(doc.issues.length).toBeGreaterThan(0);expect(doc.entries.map(e=>e.effect)).toEqual([data]);
expect(next.match(/\[skill data up\]/g)).toHaveLength(2);
});
it('区间标签大小写与空格不因无修改应用而重写',()=>{
const text=editItemSkillEffect('',{kind:'add',effect:range}).replace('[all]','[ ALL ]');
const entry=parsed(text).entries[0]!;expect(editItemSkillEffect(text,{kind:'edit',entry:0,effect:entry.effect})).toBe(text);
});
it('缺失闭合或损坏的文本不能通过新增增强绕过解析检查',()=>{
expect(()=>editItemSkillEffect('[if]\n[probability]\n10',{kind:'add',effect:level})).toThrow();
expect(()=>editItemSkillEffect('[name]\n`未闭合',{kind:'add',effect:range})).toThrow();
});
it('摘要区分技能等级、数据修改和等级区间,不替数据序号猜测语义',()=>{
expect(itemSkillEffectSummary(level)).toBe('技能等级 +2');
expect(itemSkillEffectSummary(data)).toContain('冷却时间 [0] % -6');
expect(itemSkillEffectSummary(range)).toContain('Lv1–85');
});
});
describe('从技能 GUI 选择增强数据', () => {
it('使用原始动态索引并保留装备作用范围、运算符与小数操作数', () => {
const source = { ...data, value: '-7.5' } as Extract<ItemSkillEffect, {kind:'data'}>;
const result = applySkillDataSelection(source, { job: '[mage]', skill: '14', dynamic: true, index: 2, mode: 'dungeon' });
expect(result).toEqual({ ...source, job: '[mage]', skill: '14', data: '[level]', index: '2' });
expect(source).toMatchObject({ job: '[at fighter]', data: '[cooltime]', index: '0' });
expect(parsed(editItemSkillEffect('', {kind:'add',effect:result})).entries[0]!.effect).toEqual(result);
});
it('静态数据对应 static,索引零有效,决斗场选择不猜测装备作用范围', () => {
expect(applySkillDataSelection(data as Extract<ItemSkillEffect,{kind:'data'}>, {job:'[mage]',skill:'14',dynamic:false,index:0,mode:'pvp'})).toMatchObject({data:'[static]',index:'0',scope:'[all]'});
});
it('拒绝失效索引和非法技能身份', () => {
const selection={job:'[mage]',skill:'14',dynamic:true,index:0,mode:'dungeon'};
for(const index of [-1, 1.5, NaN, Infinity, 2147483648]) expect(()=>applySkillDataSelection(data as Extract<ItemSkillEffect,{kind:'data'}>, {...selection,index})).toThrow();
expect(()=>applySkillDataSelection(data as Extract<ItemSkillEffect,{kind:'data'}>, {...selection,skill:'bad'})).toThrow();
});
});