Files
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

139 lines
5.2 KiB
Go

package actionobject
import (
"testing"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
func parseTestScript(t *testing.T, path, text string) Script {
t.Helper()
document, err := dnfpvf.Parse(path, text)
if err != nil {
t.Fatalf("parse %s: %v", path, err)
}
script, err := Parse(document)
if err != nil {
t.Fatalf("decode %s: %v", path, err)
}
return script
}
// The gate object of 能源储藏室: it closes its own boss door while
// `storage gate open` is short of 2 and reopens it once both cylinders have
// counted in. Verbatim from
// passiveobject/actionobject/map/luke_raid/phase2/enegy_save/action/gate_control_gate.act.
const lukeEnergySaveGateScript = "" +
"[MOTION]\n[BASE ANI]\n`../`\n[/MOTION]\n" +
"[TRIGGER]\n[LIMIT]\n1\n[ENABLE]\n[ON]\n" +
"[DUNGEON SYMBOL COMPARE]\n`storage gate open`\n[<]\n2\n" +
"[DO BEHAVIOR]\n[ME]\n0\n[/TRIGGER]\n" +
"[BEHAVIOR]\n" +
"[NOTICE]\n`gate closed` 255 255 0 `None` 4000 1000\n[/NOTICE]\n" +
"[GATE ENABLE]\n-1 -1 0\n[DOWN]\n[/GATE ENABLE]\n" +
"[TRIGGER]\n1\n[ON]\n[TRIGGER]\n0\n[OFF]\n[/BEHAVIOR]\n" +
"[TRIGGER]\n[LIMIT]\n1\n[ENABLE]\n[OFF]\n" +
"[DUNGEON SYMBOL COMPARE]\n`storage gate open`\n[>=]\n2\n" +
"[DO BEHAVIOR]\n[ME]\n1\n[/TRIGGER]\n" +
"[BEHAVIOR]\n" +
"[NOTICE]\n`gate open` 255 255 0 `None` 4000 1000\n[/NOTICE]\n" +
"[GATE ENABLE]\n-1 -1 1\n[DOWN]\n[/GATE ENABLE]\n[/BEHAVIOR]\n"
// The left cylinder: it adds to the symbol once its room's monster is gone.
const lukeEnergySaveCylinderScript = "" +
"[MOTION]\n[BASE ANI]\n`../`\n[/MOTION]\n" +
"[TRIGGER]\n[LIMIT]\n1\n[WHICH]\n[MONSTER]\n[CHECKUP]\n" +
"[IS INDEX]\n57156\n[/IS INDEX]\n[CHECKED NO]\n[<]\n1\n[/CHECKUP]\n" +
"[DO BEHAVIOR]\n[ME]\n0\n[/TRIGGER]\n" +
"[BEHAVIOR]\n[DUNGEON SYMBOL CALC]\n`storage gate open`\n[+]\n1\n[/BEHAVIOR]\n"
func TestParseGateScriptDecodesBothBranches(t *testing.T) {
script := parseTestScript(t, "gate_control_gate.act", lukeEnergySaveGateScript)
if len(script.Triggers) != 2 || len(script.Behaviors) != 2 {
t.Fatalf("triggers=%d behaviors=%d want 2/2 (%+v)", len(script.Triggers), len(script.Behaviors), script)
}
if !script.Drives() {
t.Fatal("gate script does not drive anything")
}
closing := script.Triggers[0]
if closing.Limit != 1 || !closing.Enabled || closing.Behavior != 0 {
t.Fatalf("closing trigger=%+v", closing)
}
if len(closing.Conditions) != 1 ||
closing.Conditions[0] != (Condition{
Kind: ConditionDungeonSymbol, Symbol: "storage gate open", Op: CompareLess, Value: 2,
}) {
t.Fatalf("closing conditions=%+v", closing.Conditions)
}
if len(closing.Unsupported) != 0 {
t.Fatalf("closing trigger dropped %v", closing.Unsupported)
}
// The reopening trigger ships disabled and is switched on by behavior 0.
opening := script.Triggers[1]
if opening.Enabled || opening.Behavior != 1 {
t.Fatalf("opening trigger=%+v", opening)
}
if len(opening.Conditions) != 1 || opening.Conditions[0].Op != CompareGreaterEqual {
t.Fatalf("opening conditions=%+v", opening.Conditions)
}
closeBehavior := script.Behaviors[0]
if len(closeBehavior.Operations) != 4 {
t.Fatalf("close behavior operations=%+v", closeBehavior.Operations)
}
if got := closeBehavior.Operations[0]; got.Kind != OperationNotice ||
got.Text != "gate closed" || got.Color != 0xFFFFFF00 || got.DurationMS != 4000 {
t.Fatalf("notice=%+v", got)
}
if got := closeBehavior.Operations[1]; got.Kind != OperationGateEnable || got.GateEnabled ||
len(got.GateDirections) != 1 || got.GateDirections[0] != "down" {
t.Fatalf("gate close=%+v", got)
}
if got := closeBehavior.Operations[2]; got.Kind != OperationTriggerSwitch ||
got.TriggerIndex != 1 || !got.TriggerOn {
t.Fatalf("trigger switch on=%+v", got)
}
if got := closeBehavior.Operations[3]; got.Kind != OperationTriggerSwitch ||
got.TriggerIndex != 0 || got.TriggerOn {
t.Fatalf("trigger switch off=%+v", got)
}
openBehavior := script.Behaviors[1]
if len(openBehavior.Operations) != 2 {
t.Fatalf("open behavior operations=%+v", openBehavior.Operations)
}
if got := openBehavior.Operations[1]; got.Kind != OperationGateEnable || !got.GateEnabled {
t.Fatalf("gate open=%+v", got)
}
}
func TestParseCylinderScriptCountsTheRoomMonster(t *testing.T) {
script := parseTestScript(t, "gate_control_left.act", lukeEnergySaveCylinderScript)
if len(script.Triggers) != 1 || len(script.Behaviors) != 1 {
t.Fatalf("triggers=%d behaviors=%d want 1/1", len(script.Triggers), len(script.Behaviors))
}
trigger := script.Triggers[0]
if len(trigger.Conditions) != 1 ||
trigger.Conditions[0] != (Condition{
Kind: ConditionMonsterCount, MonsterID: 57156, Op: CompareLess, Value: 1,
}) {
t.Fatalf("conditions=%+v unsupported=%v", trigger.Conditions, trigger.Unsupported)
}
operations := script.Behaviors[0].Operations
if len(operations) != 1 ||
operations[0].Kind != OperationSymbolCalc ||
operations[0].Symbol != "storage gate open" ||
operations[0].Calc != "+" || operations[0].Value != 1 {
t.Fatalf("operations=%+v", operations)
}
}
func TestParseAnimationOnlyScriptDrivesNothing(t *testing.T) {
script := parseTestScript(t, "idle.act", "[MOTION]\n[BASE ANI]\n`../a.ani`\n[/MOTION]\n")
if script.Drives() {
t.Fatalf("animation-only script reported as driving: %+v", script)
}
}