服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
139 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|