11#include <simplesquirrel/simplesquirrel.hpp>
25Result<SubjectRef> parseSubject(
const std::string&
text, std::string
path) {
35 result[
"x"] =
value.x;
36 result[
"z"] =
value.z;
37 return Value(std::move(result));
51class ScriptCombatRuntime {
53 ScriptCombatRuntime() : locomotion_(navigation_) {}
55 Result<void> initializeAbilities() {
59 const std::string definitionId = definition.id.format();
60 abilityActions_[definitionId] = definition.action.id;
61 auto registered = abilities_.registerDefinition(std::move(definition));
62 if (!registered)
return registered;
67 Result<Value> registerFighter(
const std::string& subjectText,
const std::string& ownerId,
68 double x,
double z,
double health,
double poise,
69 double maximumSpeed,
double acceleration) {
70 auto subject = parseSubject(subjectText,
"subject");
72 CombatState combatState{
subject.value(), health, health, poise, poise};
73 auto valid = combatState.validate();
75 if (states_.contains(subjectText))
78 auto registered = locomotion_.registerSubject(
79 {
subject.value(), ownerId, {
x,
z}, maximumSpeed, acceleration});
81 states_.emplace(subjectText, std::move(combatState));
82 return state(subjectText);
85 Result<Value> setMoveIntent(
const std::string& subjectText,
double x,
double z,
86 double speedFraction) {
87 auto subject = parseSubject(subjectText,
"subject");
89 auto changed = locomotion_.setMoveIntent(
subject.value(), {x, z}, speedFraction);
91 return state(subjectText);
94 Result<Value> navigateTo(
const std::string& subjectText,
double x,
double z,
95 double acceptanceRadius) {
96 auto subject = parseSubject(subjectText,
"subject");
98 auto changed = locomotion_.navigateTo(
subject.value(), {{x, z}, acceptanceRadius});
100 return state(subjectText);
103 Result<Value> stop(
const std::string& subjectText) {
104 auto subject = parseSubject(subjectText,
"subject");
108 return state(subjectText);
111 Result<Value> advance(std::int64_t
tick,
double seconds) {
117 auto advanced = locomotion_.
advance(
121 events.reserve(advanced.value().events.size());
122 for (
const auto& event : advanced.
value().
events) {
124 value[
"subject"] =
event.subject.format();
126 value[
"position"] = vectorValue(event.position);
130 result[
"tick"] =
tick;
131 result[
"events"] = std::move(
events);
135 Result<Value> applyDamage(
const std::string& sourceText,
const std::string& targetText,
136 const std::string&
damageType,
double healthDamage,
137 double poiseDamage,
double knockbackX,
double knockbackY,
139 auto source = parseSubject(sourceText,
"source");
141 auto target = parseSubject(targetText,
"target");
143 const auto found = states_.find(targetText);
144 if (
found == states_.end())
146 targetText, {},
"combat.squirrel"));
151 request.healthDamage = healthDamage;
152 request.poiseDamage = poiseDamage;
153 request.knockback = {knockbackX, knockbackY, knockbackZ};
157 result[
"source"] = outcome.value().source.format();
158 result[
"target"] = outcome.value().target.format();
159 result[
"health"] = outcome.value().health;
160 result[
"poise"] = outcome.value().poise;
161 result[
"appliedHealthDamage"] = outcome.value().appliedHealthDamage;
162 result[
"appliedPoiseDamage"] = outcome.value().appliedPoiseDamage;
163 result[
"reaction"] = reactionName(outcome.value().reaction);
167 Result<Value> applyTimelineDamage(
const std::string& sourceText,
const std::string& targetText,
168 const std::string& payloadJson) {
174 "combat timeline damage payload must be an object",
175 "payload", {},
"combat.squirrel"));
178 return applyDamage(sourceText, targetText,
binding.value().damageType,
binding.value().amount,
183 Result<Value> grantAbility(
const std::string& ownerId,
const std::string& definitionText) {
187 "ability definition id is invalid",
"definitionId", {},
189 const auto action = abilityActions_.find(definitionText);
190 if (action == abilityActions_.end())
192 "ability definition is not registered", definitionText, {},
194 auto granted = abilities_.grant(ownerId, *definitionId);
196 return abilityGrantValue(granted.value());
199 Result<Value> registerTimelineAbility(
const std::string& definitionText,
200 const std::string& timelineJson,
double cooldownSeconds,
201 const std::string& instancingText,
202 const std::string& groupText,
203 const std::string& triggerTag) {
204 return setTimelineAbility(definitionText, timelineJson, cooldownSeconds, instancingText,
205 groupText, triggerTag,
false);
208 Result<Value> replaceTimelineAbility(
const std::string& definitionText,
209 const std::string& timelineJson,
double cooldownSeconds,
210 const std::string& instancingText,
211 const std::string& groupText,
212 const std::string& triggerTag) {
213 return setTimelineAbility(definitionText, timelineJson, cooldownSeconds, instancingText,
214 groupText, triggerTag,
true);
217 Result<Value> setTimelineAbility(
const std::string& definitionText,
218 const std::string& timelineJson,
double cooldownSeconds,
219 const std::string& instancingText,
220 const std::string& groupText,
221 const std::string& triggerTag,
bool replace) {
225 "ability definition id is invalid",
"definitionId", {},
234 action::AbilityDefinition definition;
235 definition.id = *definitionId;
236 definition.action.id = timeline.value().actionId;
237 definition.action.timeline = timeline.value();
238 definition.cooldown = cooldown.value();
239 if (instancingText ==
"non-instanced")
241 else if (instancingText ==
"per-owner")
243 else if (instancingText ==
"per-execution")
247 "ability instancing policy is invalid",
"instancing", {},
249 if (groupText ==
"independent")
251 else if (groupText ==
"exclusive-replaceable")
253 else if (groupText ==
"exclusive-blocking")
257 "ability activation group is invalid",
"activationGroup",
258 {},
"combat.squirrel"));
259 if (!triggerTag.empty()) {
262 "ability trigger tag is invalid",
"triggerTag", {},
267 const auto& splits = timeline.value().splitTimestamps;
268 if (splits.empty()) {
269 definition.action.timing.active = timeline.value().duration;
270 }
else if (splits.size() == 1) {
271 definition.action.timing.windup = splits[0];
273 timeline.value().duration.nanoseconds() - splits[0].nanoseconds());
275 definition.action.timing.windup = splits[0];
277 splits[1].nanoseconds() - splits[0].nanoseconds());
279 timeline.value().duration.nanoseconds() - splits[1].nanoseconds());
281 auto changed = replace ? abilities_.replaceDefinition(definition)
282 : abilities_.registerDefinition(definition);
284 abilityActions_[definitionText] = definition.action.id;
286 result[
"definitionId"] = definitionText;
287 result[
"actionId"] = definition.action.id.format();
288 result[
"durationSeconds"] = timeline.value().duration.seconds();
289 result[
"sectionCount"] =
static_cast<std::int64_t
>(timeline.value().sectionCount());
293 Result<Value> activateAbility(std::int64_t grantValue, std::int64_t tickValue) {
294 auto grant = parseGrant(grantValue);
299 auto grantState = abilities_.findGrant(grant.value());
301 const auto action = abilityActions_.find(grantState.value().definitionId.format());
302 if (action == abilityActions_.end())
304 "ability definition action is unavailable",
"definitionId",
305 {},
"combat.squirrel"));
310 auto activated = abilities_.activate(grant.value(), std::move(
request),
tick);
316 Result<Value> advanceAbilities(std::int64_t tickValue,
double seconds) {
323 for (
const auto& activation : abilities_.activeActivations()) {
324 auto advanced = actions_.advance(activation.executionId,
328 value[
"activationId"] =
static_cast<std::int64_t
>(activation.id.value());
329 value[
"executionId"] =
static_cast<std::int64_t
>(activation.executionId.value());
331 value[
"elapsedSeconds"] = advanced.value().totalElapsed.seconds();
332 advances.emplace_back(std::move(
value));
334 auto cooled = abilities_.advanceCooldowns(delta.value());
336 auto synchronized = abilities_.synchronize();
339 result[
"advances"] = std::move(advances);
340 result[
"completedCount"] =
static_cast<std::int64_t
>(
synchronized.value());
341 result[
"activeCount"] =
static_cast<std::int64_t
>(abilities_.activeActivations().size());
345 Result<Value> cancelAbility(std::int64_t activationValue, std::int64_t tickValue) {
346 if (activationValue <= 0 || tickValue < 0)
348 "ability activation or tick is invalid",
"activation", {},
351 const auto active = abilities_.activeActivations();
353 [&](
const auto& item) { return item.id == activationId; });
357 auto cancelled = actions_.cancel(
found->executionId,
360 auto synchronized = abilities_.synchronize();
363 result[
"activationId"] = activationValue;
364 result[
"phase"] = std::string(
"cancelled");
368 Result<Value> abilityGrant(std::int64_t grantValue)
const {
369 auto grant = parseGrant(grantValue);
371 return abilityGrantValue(grant.value());
374 Result<Value> matchingAbilities(
const std::string& ownerId,
const std::string& eventTag)
const {
379 for (
const auto grant : abilities_.matchingGrants(ownerId, eventTag))
380 result.emplace_back(static_cast<
std::int64_t>(grant.
value()));
384 Result<Value>
state(
const std::string& subjectText)
const {
385 auto subject = parseSubject(subjectText,
"subject");
387 const auto found = states_.find(subjectText);
388 if (
found == states_.end())
390 subjectText, {},
"combat.squirrel"));
394 result[
"subject"] = subjectText;
395 result[
"ownerId"] = movement.value().ownerId;
396 result[
"position"] = vectorValue(movement.value().position);
397 result[
"velocity"] = vectorValue(movement.value().velocity);
398 result[
"facing"] = vectorValue(movement.value().facing);
399 result[
"health"] =
found->second.health;
400 result[
"maxHealth"] =
found->second.maxHealth;
401 result[
"poise"] =
found->second.poise;
402 result[
"maxPoise"] =
found->second.maxPoise;
403 result[
"navigating"] = movement.value().navigationGoal.has_value();
408 static Result<action::AbilityGrantId> parseGrant(std::int64_t
value) {
411 "ability grant id must be positive",
412 "grantId", {},
"combat.squirrel"));
418 auto state = abilities_.findGrant(grantId);
421 result[
"grantId"] =
static_cast<std::int64_t
>(
state.value().grantId.value());
422 result[
"ownerId"] =
state.value().ownerId;
423 result[
"definitionId"] =
state.value().definitionId.format();
424 result[
"cooldownSeconds"] =
state.value().cooldownRemaining.seconds();
428 static Value abilityActivationValue(
const action::AbilityActivation& activation) {
430 result[
"activationId"] =
static_cast<std::int64_t
>(activation.id.value());
431 result[
"grantId"] =
static_cast<std::int64_t
>(activation.grantId.value());
432 result[
"instanceId"] =
static_cast<std::int64_t
>(activation.instanceId.value());
433 result[
"executionId"] =
static_cast<std::int64_t
>(activation.executionId.value());
434 result[
"ownerId"] = activation.ownerId;
435 return Value(std::move(result));
438 DirectCombatNavigationProvider navigation_;
439 CombatLocomotionRuntime locomotion_;
440 DamageRuntime damage_;
441 std::map<std::string, CombatState, std::less<>> states_;
442 action::ActionRuntime actions_;
443 action::AbilityRuntime abilities_{actions_};
444 std::map<std::string, LogicalId, std::less<>> abilityActions_;
452 auto runtime = std::make_unique<ScriptCombatRuntime>();
453 auto initialized = runtime->initializeAbilities();
455 auto object = script::makeOwnedSquirrelInstance<ScriptCombatRuntime>(
vm, std::move(runtime));
458 result.set(
"value", std::move(
object).takeValue());
459 result.set(
"ownership", std::string(
"owned"));
467void Combat::expose(ssq::Table& table) {
469 auto runtime = table.addClass<ScriptCombatRuntime>(
470 "CombatRuntime", std::function<ScriptCombatRuntime*()>([] {
return nullptr; }),
true);
471 runtime.addFunc(
"ownership", [](ScriptCombatRuntime*) {
return std::string(
"owned"); });
473 "registerFighter", [
vm](ScriptCombatRuntime* self,
const std::string&
subject,
const std::string& owner,
474 float x,
float z,
float health,
float poise,
float speed,
float acceleration) {
475 return project(
vm, self ? self->registerFighter(
subject, owner,
x,
z, health, poise, speed, acceleration)
476 : Result<Value>::failure(Diagnostic::
error(DiagnosticCode::InvalidArgument,
477 "combat runtime must not be null",
478 "runtime", {},
"combat.squirrel")));
481 "setMoveIntent", [
vm](ScriptCombatRuntime* self,
const std::string&
subject,
float x,
float z,
float speed) {
484 "combat runtime must not be null",
485 "runtime", {},
"combat.squirrel")));
488 "navigateTo", [
vm](ScriptCombatRuntime* self,
const std::string&
subject,
float x,
float z,
float radius) {
491 "combat runtime must not be null",
492 "runtime", {},
"combat.squirrel")));
494 runtime.addFunc(
"stop", [
vm](ScriptCombatRuntime* self,
const std::string&
subject) {
497 "combat runtime must not be null",
"runtime",
498 {},
"combat.squirrel")));
500 runtime.addFunc(
"advance", [
vm](ScriptCombatRuntime* self, std::int64_t
tick,
float seconds) {
503 "combat runtime must not be null",
"runtime",
504 {},
"combat.squirrel")));
506 runtime.addFunc(
"applyDamage", [
vm](ScriptCombatRuntime* self,
const std::string&
source,
const std::string&
target,
507 const std::string&
type,
float health,
float poise,
float x,
float y,
float z) {
510 "combat runtime must not be null",
"runtime",
511 {},
"combat.squirrel")));
513 runtime.addFunc(
"applyTimelineDamage", [
vm](ScriptCombatRuntime* self,
const std::string&
source,
514 const std::string&
target,
const std::string& payloadJson) {
517 "combat runtime must not be null",
"runtime",
518 {},
"combat.squirrel")));
521 "grantAbility", [
vm](ScriptCombatRuntime* self,
const std::string& owner,
const std::string& definitionId) {
522 return project(
vm, self ? self->grantAbility(owner, definitionId)
524 "combat runtime must not be null",
525 "runtime", {},
"combat.squirrel")));
527 runtime.addFunc(
"registerTimelineAbility", [
vm](ScriptCombatRuntime* self,
const std::string& definitionId,
528 const std::string& timelineJson,
float cooldownSeconds,
529 const std::string&
instancing,
const std::string& activationGroup,
530 const std::string& triggerTag) {
531 return project(
vm, self ? self->registerTimelineAbility(definitionId, timelineJson, cooldownSeconds,
instancing,
532 activationGroup, triggerTag)
534 "combat runtime must not be null",
"runtime",
535 {},
"combat.squirrel")));
537 runtime.addFunc(
"replaceTimelineAbility", [
vm](ScriptCombatRuntime* self,
const std::string& definitionId,
538 const std::string& timelineJson,
float cooldownSeconds,
539 const std::string&
instancing,
const std::string& activationGroup,
540 const std::string& triggerTag) {
541 return project(
vm, self ? self->replaceTimelineAbility(definitionId, timelineJson, cooldownSeconds,
instancing,
542 activationGroup, triggerTag)
544 "combat runtime must not be null",
"runtime",
545 {},
"combat.squirrel")));
547 runtime.addFunc(
"activateAbility", [
vm](ScriptCombatRuntime* self, std::int64_t grantId, std::int64_t
tick) {
548 return project(
vm, self ? self->activateAbility(grantId,
tick)
550 "combat runtime must not be null",
"runtime",
551 {},
"combat.squirrel")));
553 runtime.addFunc(
"advanceAbilities", [
vm](ScriptCombatRuntime* self, std::int64_t
tick,
float seconds) {
554 return project(
vm, self ? self->advanceAbilities(
tick, seconds)
556 "combat runtime must not be null",
"runtime",
557 {},
"combat.squirrel")));
559 runtime.addFunc(
"cancelAbility", [
vm](ScriptCombatRuntime* self, std::int64_t activationId, std::int64_t
tick) {
560 return project(
vm, self ? self->cancelAbility(activationId,
tick)
562 "combat runtime must not be null",
"runtime",
563 {},
"combat.squirrel")));
565 runtime.addFunc(
"abilityGrant", [
vm](ScriptCombatRuntime* self, std::int64_t grantId) {
566 return project(
vm, self ? self->abilityGrant(grantId)
568 "combat runtime must not be null",
"runtime",
569 {},
"combat.squirrel")));
572 "matchingAbilities", [
vm](ScriptCombatRuntime* self,
const std::string& owner,
const std::string& eventTag) {
573 return project(
vm, self ? self->matchingAbilities(owner, eventTag)
575 "combat runtime must not be null",
576 "runtime", {},
"combat.squirrel")));
578 runtime.addFunc(
"state", [
vm](ScriptCombatRuntime* self,
const std::string&
subject) {
581 "combat runtime must not be null",
"runtime",
582 {},
"combat.squirrel")));
585 auto module = table.addClass(name, Combat::create, false);
586 module.addFunc("getName", &Combat::getName);
587 module.addFunc("newRuntime", [vm](Combat*) { return newRuntime(vm); });
590void Combat::expose(ssq::Class&
cls) {
591 cls.addFunc(
"getName", &Combat::getName);
592 cls.addFunc(
"newRuntime", [
vm =
cls.getHandle()](Combat*) { return newRuntime(vm); });
Grants, cooldowns, instancing, activation groups and gameplay-event triggers.
Typed instantaneous combat-damage action contract.
Deterministic player/navigation locomotion adapter for combat subjects.
Script-facing combat composition module.
struct SQVM * HSQUIRRELVM
Deterministic damage, poise, hit-reaction and knockback resolution.
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
#define Module_IMPL(ModuleName, newExpr)
eve::action::ActionVfxBinding binding
The single Squirrel projection for common Result, Status and Value.
Explicit Value/Owned/Borrowed semantics at the Squirrel boundary.
AbilityInstancingPolicy instancing
Registerable standard action-combat ability archetypes.
const UnitySourceAsset & source
const std::map< std::string, Value > & definitions
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
static SubjectRef fromPersistentId(PersistentId id) noexcept
Wrap a persistent identity without changing its bytes.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
std::vector< Value > Array
Result< CombatLocomotionAdvance > advance(const SimulationStep &step)
Atomically advance every subject using only the supplied deterministic step.
Result< CombatLocomotionState > state(SubjectRef subject) const
Return an owning state snapshot, or NotFound for a stale subject.
Result< void > stop(SubjectRef subject)
Clear a navigation goal and desired movement for one subject.
Factory module for script-owned deterministic combat runtimes.
Result< DamageOutcome > apply(CombatState &target, const DamageRequest &request) const
Resolve and atomically commit one damage request.
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
const char * actionPhaseName(ActionPhase phase) noexcept
Return the stable lowercase spelling of an action phase.
eve::detail::StrongUint64< AbilityActivationIdTag > AbilityActivationId
Opaque identity of one activation attempt accepted by ActionRuntime.
eve::detail::StrongUint64< AbilityGrantIdTag > AbilityGrantId
Opaque identity of one owner-to-definition grant.
Result< std::vector< action::AbilityDefinition > > standardCombatAbilities()
Build twenty validated, registerable standard combat ability definitions.
HitReaction
Typed reaction selected after health and poise are resolved.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
eve::Diagnostic Diagnostic
ssq::Table project(HSQUIRRELVM vm, const eve::Result< void > &result)
Project a completed editing result that has no payload.
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
DiagnosticCode
Stable machine-readable diagnostic codes.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
static Result< ActionDamageBinding > fromPayload(const Value::Object &payload)
Decode and validate a damage payload without mutating combat state.
static Result< ActionTimeline > fromValue(const Value &value)
Decode and validate one canonical schema value transactionally.