From cfe341b753dab99b38febf6d413a69f666d7330a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sun, 13 Sep 2026 12:56:33 +0800 Subject: [PATCH] feat(editor): route montage events into combat playtest --- docs/usr/modules/combat.md | 4 +- docs/usr/modules/editor.md | 12 ++ examples/combat-action-editor/README.md | 8 +- examples/combat-action-editor/main.nut | 103 +++++++++++++++++- .../editor/ActionTimelineScriptBindings.cpp | 47 +++++++- src/modules/combat/Combat.cpp | 23 ++++ test/combat_script.cpp | 5 +- test/editor_action_timeline_script.cpp | 4 + 8 files changed, 196 insertions(+), 10 deletions(-) diff --git a/docs/usr/modules/combat.md b/docs/usr/modules/combat.md index dc11179f..8db89ab4 100644 --- a/docs/usr/modules/combat.md +++ b/docs/usr/modules/combat.md @@ -5,7 +5,9 @@ Combat 模块提供不依赖具体角色类的伤害、韧性、战斗属性、 脚本入口 `eve.Combat()` 提供 `getName`,并可通过 `newRuntime()` 创建一个 Squirrel-owned `CombatRuntime`。`registerFighter`、 -`setMoveIntent`、`navigateTo`、`stop`、`advance`、`applyDamage` 和 `state` 全部返回统一 Result 投影; +`setMoveIntent`、`navigateTo`、`stop`、`advance`、`applyDamage`、`applyTimelineDamage` 和 `state` +全部返回统一 Result 投影;`applyTimelineDamage` 直接消费 Montage runtime event 的 payload JSON,并复用 +`ActionDamageBinding` 校验,不会在示例脚本中复制伤害字段解析规则。 runtime 内部复用下述 C++ 权威实现,不在脚本层复制生命值或移动状态。runtime 随脚本对象 GC 销毁, `ownership()` 返回 `owned`。 diff --git a/docs/usr/modules/editor.md b/docs/usr/modules/editor.md index 84349108..442f6b32 100644 --- a/docs/usr/modules/editor.md +++ b/docs/usr/modules/editor.md @@ -594,6 +594,11 @@ local created = eve.ActionEditorModule().create("ability.light-attack", timeline if (!created.ok) throw created.status.summary; local actionEditor = created.value; // ownership == "owned" +// A project-owned target adapter may claim selected block types. The Montage +// still emits their events, but the preview runtime does not require a native +// sink for those types. +actionEditor.setRuntimeBlockExternallyHandled("combat:damage", true); + local ws = editor.newWorkspace("combat", "Combat Action Editor"); local composed = actionEditor.configureWorkspace(ws); // Assets/Preview/Inspector/Timeline actionEditor.setViewport(900.0, 36.0, 145.0); @@ -609,6 +614,13 @@ actionEditor.setItemEnabled("combat-state:hitbox", false); actionEditor.editItemDetails("combat-state:hitbox", "combat:hitbox-window", "{\"hitbox\":\"weapon.main\"}"); +// Runtime crossings preserve the canonical item identity and owning payload. +// Consumers can route the exact edited data into a target-domain adapter. +for (local i = 0; i < actionEditor.getRuntimeEventCount(); ++i) { + print(actionEditor.getRuntimeEventId(i) + " " + + actionEditor.getRuntimeEventPayloadJson(i) + "\n"); +} + // Build insertion pickers from the registered semantic notify types, then // insert through the same validation and transaction authority as dragging. local stateCount = actionEditor.getInsertableTypeCount(true); diff --git a/examples/combat-action-editor/README.md b/examples/combat-action-editor/README.md index 157fae9e..2ba0dcf2 100644 --- a/examples/combat-action-editor/README.md +++ b/examples/combat-action-editor/README.md @@ -6,7 +6,9 @@ native hit testing and drag preview, one-step transactions, undo/redo, and deterministic preview events. Its document toolbar opens two independently persisted montage assets, projects dirty state into tabs, and protects unsaved work when a tab is closed. The 3D viewport is driven by the same preview cursor -as the active timeline. +as the active timeline. The viewport also runs a two-fighter playtest: edited +`combat:damage` payloads flow from Montage runtime events into the canonical +Combat damage runtime, while movement and health stay owned by that runtime. Run on Windows: @@ -83,6 +85,10 @@ Controls: transaction history. - Hold the right mouse button over the preview to orbit; use the mouse wheel to zoom. +- Use WASD to move the player through `CombatLocomotionRuntime`. When playback + crosses the damage notify, target health and reaction update above the + viewport. Editing that block changes the next hit without a demo-only damage + model. The preview and timeline headers show the current selection, playback state, edit revision and the last emitted event so every interaction has visible diff --git a/examples/combat-action-editor/main.nut b/examples/combat-action-editor/main.nut index dc0546b1..bb72a46c 100644 --- a/examples/combat-action-editor/main.nut +++ b/examples/combat-action-editor/main.nut @@ -13,6 +13,11 @@ persist combatEditor = { selectedTrack = 0, selectedSplit = 0, newTrackOpen = false, newTrackKind = 0, nextTrackId = 0, clipEditor = null, boneMouseDown = false, clip = null, player = null, knight = null, knightParts = [], skins = [], + enemySkeleton = null, enemyPlayer = null, enemyParts = [], enemySkins = [], + combatRuntime = null, combatTick = 0, + playerSubject = "01020304-0506-4708-890a-0b0c0d0e0f10", + enemySubject = "11121314-1516-4718-991a-1b1c1d1e1f20", + playerHealth = 100.0, enemyHealth = 80.0, enemyReaction = "none", generalLibrary = null, meleeLibrary = null, swordParts = [], knightTexture = null, ground = null, mouseDown = false, keyLight = null, fillLight = null, @@ -30,6 +35,10 @@ local editorUiDefaults = { insertPanelOpen=false, newSectionOpen=false, nextSectionId=0, insertTrack=0, insertNotifyType=0, insertStateType=2, selectedTrack=0, selectedSplit=0, newTrackOpen=false, newTrackKind=0, nextTrackId=0, + enemySkeleton=null, enemyPlayer=null, enemyParts=[], enemySkins=[], combatRuntime=null, combatTick=0, + playerSubject="01020304-0506-4708-890a-0b0c0d0e0f10", + enemySubject="11121314-1516-4718-991a-1b1c1d1e1f20", + playerHealth=100.0, enemyHealth=80.0, enemyReaction="none", }; foreach (key, value in editorUiDefaults) if (!(key in combatEditor)) combatEditor[key] <- value; @@ -220,6 +229,14 @@ function buildCharacterPreview() { combatEditor.player.play(combatEditor.clip); combatEditor.player.setLoop(false); + combatEditor.enemySkeleton = anim.newSkeletonFromModel(combatEditor.knight); + local idleIndex = findAnimation(combatEditor.generalLibrary, "Idle_A"); + if (idleIndex < 0) throw "KayKit idle clip Idle_A was not found"; + local enemyIdle = anim.newClipFromModel(combatEditor.generalLibrary, combatEditor.enemySkeleton, idleIndex); + enemyIdle.setLoop(true); + combatEditor.enemyPlayer = anim.newPlayer(combatEditor.enemySkeleton); + combatEditor.enemyPlayer.play(enemyIdle); combatEditor.enemyPlayer.setLoop(true); + for (local i = 0; i < combatEditor.knight.getMeshCount(); ++i) { local part = model3d.createRenderable(gfx, combatEditor.knight, i); part.setPosition(0.0, 0.02, 0.0); part.setYaw(-0.25); @@ -228,6 +245,14 @@ function buildCharacterPreview() { if (combatEditor.knight.hasBones(i)) combatEditor.skins.push({ skin=anim.newSkinFromModel(combatEditor.knight, i, combatEditor.skeleton), part=part }); + + local enemyPart = model3d.createRenderable(gfx, combatEditor.knight, i); + enemyPart.setPosition(-1.65, 0.02, 0.0); enemyPart.setYaw(0.25); + enemyPart.setCastShadow(true); configurePreviewMaterial(enemyPart, combatEditor.knightTexture); + combatEditor.enemyParts.push(enemyPart); + if (combatEditor.knight.hasBones(i)) + combatEditor.enemySkins.push({ + skin=anim.newSkinFromModel(combatEditor.knight, i, combatEditor.enemySkeleton), part=enemyPart }); } // Import the weapon independently too; the same character + weapon + clip @@ -264,6 +289,53 @@ function buildCharacterPreview() { combatEditor.fillLight.setColor(0.55, 0.70, 1.0, 1.3); } +function buildCombatPlaytest() { + combatEditor.combatRuntime = requireResult(eve.Combat().newRuntime(), "Create combat playtest runtime"); + requireResult(combatEditor.combatRuntime.registerFighter( + combatEditor.playerSubject, "player", 0.0, 0.0, 100.0, 40.0, 3.5, 18.0), "Register player"); + requireResult(combatEditor.combatRuntime.registerFighter( + combatEditor.enemySubject, "enemy", -1.65, 0.0, 80.0, 30.0, 0.1, 0.1), "Register enemy"); +} + +function advanceCombatPlaytest(dt) { + local x = 0.0; local z = 0.0; + if (keyboard.isDown("a") || keyboard.isDown("A")) x -= 1.0; + if (keyboard.isDown("d") || keyboard.isDown("D")) x += 1.0; + if (keyboard.isDown("w") || keyboard.isDown("W")) z -= 1.0; + if (keyboard.isDown("s") || keyboard.isDown("S")) z += 1.0; + if (x != 0.0 || z != 0.0) + requireResult(combatEditor.combatRuntime.setMoveIntent(combatEditor.playerSubject, x, z, 1.0), + "Set playtest movement"); + else + requireResult(combatEditor.combatRuntime.stop(combatEditor.playerSubject), "Stop playtest movement"); + combatEditor.combatTick += 1; + requireResult(combatEditor.combatRuntime.advance(combatEditor.combatTick, dt), "Advance combat playtest"); + local playerState = requireResult(combatEditor.combatRuntime.state(combatEditor.playerSubject), "Read player"); + local enemyState = requireResult(combatEditor.combatRuntime.state(combatEditor.enemySubject), "Read enemy"); + combatEditor.playerHealth = playerState.health; combatEditor.enemyHealth = enemyState.health; + foreach (part in combatEditor.knightParts) + part.setPosition(playerState.position.x, 0.02, playerState.position.z); + foreach (part in combatEditor.enemyParts) + part.setPosition(enemyState.position.x, 0.02, enemyState.position.z); +} + +function routeRuntimeCombatEvents() { + for (local i = 0; i < combatEditor.timeline.getRuntimeEventCount(); ++i) { + if (combatEditor.timeline.getRuntimeEventKind(i) != "notify" || + combatEditor.timeline.getRuntimeEventType(i) != "combat:damage") continue; + local outcome = combatEditor.combatRuntime.applyTimelineDamage( + combatEditor.playerSubject, combatEditor.enemySubject, + combatEditor.timeline.getRuntimeEventPayloadJson(i)); + if (!outcome.ok) { combatEditor.status = outcome.status.summary; continue; } + combatEditor.enemyHealth = outcome.value.health; + combatEditor.enemyReaction = outcome.value.reaction; + combatEditor.lastEvent = combatEditor.timeline.getRuntimeEventId(i) + " · " + + outcome.value.appliedHealthDamage + " damage · " + outcome.value.reaction; + print("combat-action-editor: runtime damage=" + outcome.value.appliedHealthDamage + + " targetHealth=" + outcome.value.health + " reaction=" + outcome.value.reaction + "\n"); + } +} + function startMontageRuntime(timeline) { local idle = findAnimation(combatEditor.generalLibrary, "Idle_A"); local attack = findAnimation(combatEditor.meleeLibrary, "Melee_1H_Attack_Chop"); @@ -278,6 +350,16 @@ function startMontageRuntime(timeline) { requireResult(timeline.registerRuntimeClip( "asset://kaykit/Rig_Medium_CombatMelee.glb#Melee_Block", combatEditor.meleeLibrary, combatEditor.skeleton, block), "Register recovery clip"); + requireResult(timeline.setRuntimeBlockExternallyHandled("combat:damage", true), + "Route combat damage to playtest runtime"); + requireResult(timeline.setRuntimeBlockExternallyHandled("combat:hitbox-window", true), + "Route hitbox window to playtest UI"); + requireResult(timeline.setRuntimeBlockExternallyHandled("input:combo-window", true), + "Route combo window to playtest UI"); + requireResult(timeline.setRuntimeBlockExternallyHandled("presentation:audio", true), + "Route placeholder audio to playtest UI"); + requireResult(timeline.setRuntimeBlockExternallyHandled("presentation:vfx", true), + "Route placeholder VFX to playtest UI"); requireResult(timeline.beginRuntime(combatEditor.skeleton), "Start action montage runtime"); } @@ -440,8 +522,9 @@ function panelAssets() { } function panelPreview() { - ui.text("RMB orbit · wheel zoom · timeline drives pose", "preview-help"); + ui.text("WASD move · RMB orbit · wheel zoom", "preview-help"); ui.text("", "preview-status"); + ui.text("", "combat-status"); ui.viewport("combat-preview", combatEditor.workspace.getRegionW("center") - 20.0, combatEditor.workspace.getRegionH("center") - 92.0); } @@ -1223,11 +1306,15 @@ function updateKeyboardShortcuts() { combatEditor.undoWas = undo; combatEditor.redoWas = redo; combatEditor.saveWas = save; } -function updatePose() { +function updatePose(dt) { combatEditor.player.setTime(combatEditor.clipEditor.getPlayhead()); local pose = combatEditor.player.getPose(); pose.computeWorld(combatEditor.skeleton); foreach (binding in combatEditor.skins) binding.skin.applyToMesh(gfx, binding.part.getMesh(), pose); + combatEditor.enemyPlayer.update(dt); + local enemyPose = combatEditor.enemyPlayer.getPose(); enemyPose.computeWorld(combatEditor.enemySkeleton); + foreach (binding in combatEditor.enemySkins) + binding.skin.applyToMesh(gfx, binding.part.getMesh(), enemyPose); } function updateLabels() { @@ -1235,6 +1322,8 @@ function updateLabels() { ui.setText("preview-status", format("%s %.3f / %.3f s", combatEditor.timeline.isPlaying() ? "PLAYING" : "PAUSED", combatEditor.timeline.getPreviewTime(), combatEditor.timeline.getDuration())); + ui.setText("combat-status", format("Player %.0f HP · Target %.0f HP · %s", + combatEditor.playerHealth, combatEditor.enemyHealth, combatEditor.enemyReaction)); ui.select("action.inspector"); ui.setText("action-uri", "Clip: Melee_1H_Attack_Chop"); ui.setText("revision", "Revision " + combatEditor.clipEditor.getRevision()); if (combatEditor.inspectorMode == "action") { @@ -1499,7 +1588,7 @@ function drawBoneTimeline() { } eve_init = function() { - buildCharacterPreview(); buildWorkspace(); ui.setTheme("dark"); ui.setScale(0.75); + buildCharacterPreview(); buildCombatPlaytest(); buildWorkspace(); ui.setTheme("dark"); ui.setScale(0.75); ui.setNavKeyboard(true); mountPanels(); combatEditor.timeline.play(); combatEditor.status = "KayKit clip loaded · drag timeline items or use the inspector"; @@ -1515,7 +1604,10 @@ eve_update = function(dt) { if (!advanced.ok) combatEditor.status = advanced.status.summary; if (combatEditor.timeline.isRuntimePlaying()) { local runtimeAdvanced = combatEditor.timeline.advanceRuntime(dt); - if (!runtimeAdvanced.ok) combatEditor.status = runtimeAdvanced.status.summary; + if (!runtimeAdvanced.ok) { + combatEditor.status = runtimeAdvanced.status.summary; + print("combat-action-editor: runtime error=" + runtimeAdvanced.status.summary + "\n"); + } else { if (runtimeAdvanced.value.sectionId != "") combatEditor.status = "Runtime · " + runtimeAdvanced.value.sectionId; @@ -1526,6 +1618,7 @@ eve_update = function(dt) { combatEditor.timeline.getRuntimeEventType(eventIndex) + " @ " + format("%.3f s", combatEditor.timeline.getRuntimeEventSeconds(eventIndex)); } + routeRuntimeCombatEvents(); } } } @@ -1534,7 +1627,7 @@ eve_update = function(dt) { combatEditor.lastEvent = combatEditor.timeline.getEventKind(i) + " · " + combatEditor.timeline.getEventType(i) + " @ " + format("%.3f s", combatEditor.timeline.getEventTime(i)); } - updatePose(); updateLabels(); + advanceCombatPlaytest(dt); updatePose(dt); updateLabels(); }; eve_render = function() { diff --git a/src/modules/action/editor/ActionTimelineScriptBindings.cpp b/src/modules/action/editor/ActionTimelineScriptBindings.cpp index 33eadbd4..bed28ed3 100644 --- a/src/modules/action/editor/ActionTimelineScriptBindings.cpp +++ b/src/modules/action/editor/ActionTimelineScriptBindings.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -590,6 +591,23 @@ public: return Result::success(std::move(combined), Status::success(StatusCode::Pending)); } + [[nodiscard]] Result setRuntimeBlockExternallyHandled(const std::string& type, bool external) { + auto parsed = LogicalId::parse(type); + if (!parsed) + return Result::failure( + Diagnostic::error(DiagnosticCode::InvalidArgument, + "externally handled runtime block type is invalid", "type")); + if (!registry_.hasHandler(type)) + return Result::failure( + Diagnostic::error(DiagnosticCode::NotFound, + "runtime block type has no registered handler", type)); + if (external) + externallyHandledTypes_.insert(type); + else + externallyHandledTypes_.erase(type); + return Result::success(Status::success(StatusCode::Applied)); + } + [[nodiscard]] Result jumpRuntime(Duration target) { if (!runtimeTimeline_ || !montage_) return Result::failure( @@ -742,9 +760,13 @@ private: handled.phaseElapsed = advance.phaseElapsed; handled.totalElapsed = advance.totalElapsed; for (const auto& event : advance.timelineEvents) - if (registry_.hasHandler(event.type.format())) handled.timelineEvents.push_back(event); + if (registry_.hasHandler(event.type.format()) && + !externallyHandledTypes_.contains(event.type.format())) + handled.timelineEvents.push_back(event); for (const auto& block : advance.activeBlocks) - if (registry_.hasHandler(block.type.format())) handled.activeBlocks.push_back(block); + if (registry_.hasHandler(block.type.format()) && + !externallyHandledTypes_.contains(block.type.format())) + handled.activeBlocks.push_back(block); return blockRuntime_.apply(handled, runtimeContext(advance.totalElapsed)); } @@ -763,6 +785,7 @@ private: std::optional runtimeAdvance_; std::optional runtimeTimeline_; std::unordered_map sectionRates_; + std::set> externallyHandledTypes_; double runtimeRate_ = 1.0; bool runtimePaused_ = false; std::unique_ptr documentStore_; @@ -1265,6 +1288,13 @@ void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleCla actionEditor.addFunc("setRuntimePaused", [](ScriptActionTimelineEditor* self, bool paused) { if (self) self->setRuntimePaused(paused); }); + actionEditor.addFunc("setRuntimeBlockExternallyHandled", + [vm](ScriptActionTimelineEditor* self, const std::string& type, bool external) { + if (!self) + return bindingFailure(vm, DiagnosticCode::InvalidArgument, + "action timeline editor must not be null"); + return script::projectResult(vm, self->setRuntimeBlockExternallyHandled(type, external)); + }); actionEditor.addFunc("isRuntimePaused", [](ScriptActionTimelineEditor* self) { return self && self->runtimePaused(); }); actionEditor.addFunc("cancelRuntime", [vm](ScriptActionTimelineEditor* self) { @@ -1301,6 +1331,12 @@ void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleCla return std::string{}; return eventKind(self->runtimeAdvance()->events[static_cast(index)].kind); }); + actionEditor.addFunc("getRuntimeEventId", [](ScriptActionTimelineEditor* self, int index) { + if (!self || !self->runtimeAdvance() || index < 0 || + static_cast(index) >= self->runtimeAdvance()->events.size()) + return std::string{}; + return self->runtimeAdvance()->events[static_cast(index)].itemId.format(); + }); actionEditor.addFunc("getRuntimeEventType", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->events.size()) @@ -1313,6 +1349,13 @@ void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleCla return 0.0f; return static_cast(self->runtimeAdvance()->events[static_cast(index)].time.seconds()); }); + actionEditor.addFunc("getRuntimeEventPayloadJson", [](ScriptActionTimelineEditor* self, int index) { + if (!self || !self->runtimeAdvance() || index < 0 || + static_cast(index) >= self->runtimeAdvance()->events.size()) + return std::string("{}"); + auto encoded = Value(self->runtimeAdvance()->events[static_cast(index)].payload).toJson(); + return encoded ? encoded.value() : std::string("{}"); + }); actionEditor.addFunc("getRuntimeActiveBlockCount", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? static_cast(self->runtimeAdvance()->activeBlocks.size()) : 0; }); diff --git a/src/modules/combat/Combat.cpp b/src/modules/combat/Combat.cpp index ef100b33..8dfe7ce9 100644 --- a/src/modules/combat/Combat.cpp +++ b/src/modules/combat/Combat.cpp @@ -1,5 +1,6 @@ #include "combat/Combat.h" +#include "action/ActionDamageBlock.h" #include "combat/CombatLocomotion.h" #include "combat/Damage.h" #include "common/SquirrelBinding.h" @@ -151,6 +152,21 @@ public: return Result::success(Value(std::move(result)), Status::success(StatusCode::Applied)); } + Result applyTimelineDamage(const std::string& sourceText, const std::string& targetText, + const std::string& payloadJson) { + auto decoded = Value::fromJson(payloadJson); + if (!decoded) return Result::failure(decoded.status()); + const auto* payload = decoded.value().getIf(); + if (!payload) + return failure(DiagnosticCode::InvalidArgument, + "combat timeline damage payload must be an object", "payload"); + auto binding = action::ActionDamageBinding::fromPayload(*payload); + if (!binding) return Result::failure(binding.status()); + return applyDamage(sourceText, targetText, binding.value().damageType, binding.value().amount, + binding.value().poiseAmount, binding.value().knockback.x, + binding.value().knockback.y, binding.value().knockback.z); + } + Result state(const std::string& subjectText) const { auto subject = parseSubject(subjectText, "subject"); if (!subject) return Result::failure(subject.status()); @@ -239,6 +255,13 @@ void Combat::expose(ssq::Table& table) { : failure(DiagnosticCode::InvalidArgument, "combat runtime must not be null", "runtime")); }); + runtime.addFunc("applyTimelineDamage", + [vm](ScriptCombatRuntime* self, const std::string& source, + const std::string& target, const std::string& payloadJson) { + return project(vm, self ? self->applyTimelineDamage(source, target, payloadJson) + : failure(DiagnosticCode::InvalidArgument, + "combat runtime must not be null", "runtime")); + }); runtime.addFunc("state", [vm](ScriptCombatRuntime* self, const std::string& subject) { return project(vm, self ? self->state(subject) : failure(DiagnosticCode::InvalidArgument, diff --git a/test/combat_script.cpp b/test/combat_script.cpp index 5c891815..716b6762 100644 --- a/test/combat_script.cpp +++ b/test/combat_script.cpp @@ -24,6 +24,8 @@ TEST_CASE("combatScript.runtimeOwnsMovementAndDamageState") { advanced <- arena.advance(1, 0.25); playerState <- arena.state(player); damaged <- arena.applyDamage(player, enemy, "Damage.Physical.Slash", 18.0, 10.0, 0.0, 0.0, 0.0); + timelineDamage <- arena.applyTimelineDamage(player, enemy, + "{\"damageType\":\"Damage.Physical.Slash\",\"amount\":7,\"poiseAmount\":2}"); enemyState <- arena.state(enemy); invalid <- arena.applyDamage("bad", enemy, "Damage.Physical.Slash", 1.0, 0.0, 0.0, 0.0, 0.0); playerX <- playerState.value.position.x; @@ -37,7 +39,8 @@ TEST_CASE("combatScript.runtimeOwnsMovementAndDamageState") { CHECK(vm.find("moved").toTable().get("ok")); CHECK(vm.find("advanced").toTable().get("ok")); CHECK(vm.find("playerX").toFloat() > 0.0f); - CHECK_EQ(vm.find("enemyHealth").toFloat(), 62.0f); + CHECK_EQ(vm.find("enemyHealth").toFloat(), 55.0f); + CHECK(vm.find("timelineDamage").toTable().get("ok")); CHECK_EQ(vm.find("damageReaction").toString(), std::string("flinch")); CHECK(!vm.find("invalid").toTable().get("ok")); } diff --git a/test/editor_action_timeline_script.cpp b/test/editor_action_timeline_script.cpp index 71d35998..3a0f26e5 100644 --- a/test/editor_action_timeline_script.cpp +++ b/test/editor_action_timeline_script.cpp @@ -33,6 +33,8 @@ TEST_CASE("editor.actionTimeline.scriptUsesCanonicalTransactionsAndWorkspace") { }; created <- eve.ActionEditorModule().create("test.asset.light-attack", timelineAsset); actionEditor <- created.value; + externalDamage <- actionEditor.setRuntimeBlockExternallyHandled("combat:damage", true); + externalUnknown <- actionEditor.setRuntimeBlockExternallyHandled("test:unknown", true); previewHostAvailable <- actionEditor.hasPreviewHost(); previewRefresh <- actionEditor.refreshPreview(); configured <- actionEditor.configureWorkspace(workspace); @@ -160,6 +162,8 @@ TEST_CASE("editor.actionTimeline.scriptUsesCanonicalTransactionsAndWorkspace") { CHECK(vm.find("created").toTable().get("ok")); CHECK_EQ(vm.find("created").toTable().get("ownership"), std::string("owned")); + CHECK(vm.find("externalDamage").toTable().get("ok")); + CHECK(!vm.find("externalUnknown").toTable().get("ok")); CHECK(!vm.find("previewHostAvailable").toBool()); CHECK(vm.find("previewRefresh").toTable().get("ok")); CHECK(vm.find("configured").toTable().get("ok"));