25Result<Value> nullRuntime() {
27 "combat carrier runtime must not be null",
"runtime", {},
28 "weapon.carrier.squirrel"));
31class ScriptTargetProvider final :
public IProjectileTargetProvider {
33 [[nodiscard]] Result<ProjectilePoint>
position(ecs::EntityHandle
target)
const override {
35 auto it = positions_.find(
key);
36 if (it == positions_.end())
39 "weapon.carrier.squirrel"));
44 void clear() { positions_.clear(); }
45 [[nodiscard]]
bool empty() const noexcept {
return positions_.empty(); }
48 [[nodiscard]]
static std::uint64_t keyOf(ecs::EntityHandle
target) {
49 return (
static_cast<std::uint64_t
>(
target.generation) << 32) |
target.id;
52 std::map<std::uint64_t, ProjectilePoint> positions_;
55class ScriptCircleHitProbe final :
public ICarrierHitProbe {
58 ecs::EntityHandle
id{};
63 [[nodiscard]] Result<std::vector<Contact>>
query(CarrierHandle,
const CarrierMotion&
previous,
64 const CarrierMotion&
current)
const override {
65 std::vector<Contact> contacts;
66 for (
const auto&
target : targets_) {
73 const double abx =
bx -
ax;
74 const double aby =
by -
ay;
75 const double abz =
bz -
az;
76 const double ab2 = abx * abx + aby * aby + abz * abz;
79 t = -(
ax * abx +
ay * aby +
az * abz) / ab2;
83 const double cx =
ax + abx *
t;
84 const double cy =
ay + aby *
t;
85 const double cz =
az + abz *
t;
86 const double dist2 =
cx *
cx +
cy *
cy + cz * cz;
89 contact.target =
target.id;
91 const double len = std::sqrt(dist2);
93 contact.normal = {
cx / len,
cy / len, cz / len};
95 contact.normal = {-1.0, 0.0, 0.0};
97 contacts.push_back(contact);
100 return Result<std::vector<Contact>>::success(std::move(contacts));
104 void clear() { targets_.clear(); }
105 [[nodiscard]]
bool empty() const noexcept {
return targets_.empty(); }
108 std::vector<Target> targets_;
113 object.emplace(
"slot",
Value(
static_cast<std::int64_t
>(
handle.slot)));
114 object.emplace(
"generation",
Value(
static_cast<std::int64_t
>(
handle.generation)));
115 return Value(std::move(
object));
141Value encodeEvent(
const CarrierEvent& event) {
143 object.emplace(
"slot",
Value(
static_cast<std::int64_t
>(event.carrier.slot)));
144 object.emplace(
"generation",
Value(
static_cast<std::int64_t
>(event.carrier.generation)));
145 object.emplace(
"recipeId",
Value(event.recipeId.format()));
146 object.emplace(
"trigger",
Value(std::string(triggerKindName(event.trigger))));
147 object.emplace(
"impact",
Value(std::string(impactKindName(event.impact))));
148 object.emplace(
"damage",
Value(event.damage));
149 object.emplace(
"splashRadius",
Value(event.splashRadius));
150 object.emplace(
"damageType",
Value(event.damageType));
151 object.emplace(
"element",
Value(event.element));
152 object.emplace(
"x",
Value(event.position.x));
153 object.emplace(
"y",
Value(event.position.y));
154 object.emplace(
"z",
Value(event.position.z));
155 object.emplace(
"nx",
Value(event.normal.x));
156 object.emplace(
"ny",
Value(event.normal.y));
157 object.emplace(
"nz",
Value(event.normal.z));
158 object.emplace(
"sourceId",
Value(
static_cast<std::int64_t
>(event.source.id)));
159 object.emplace(
"sourceGeneration",
Value(
static_cast<std::int64_t
>(event.source.generation)));
160 object.emplace(
"targetId",
Value(
static_cast<std::int64_t
>(event.target.id)));
161 object.emplace(
"targetGeneration",
Value(
static_cast<std::int64_t
>(event.target.generation)));
162 return Value(std::move(
object));
165Value encodeFrame(
const CarrierFrame& frame) {
168 for (
const auto&
handle : frame.advanced) advanced.push_back(encodeHandle(
handle));
170 for (
const auto&
handle : frame.released) released.push_back(encodeHandle(
handle));
172 for (
const auto& event : frame.
events)
events.push_back(encodeEvent(event));
173 object.emplace(
"advanced",
Value(std::move(advanced)));
174 object.emplace(
"released",
Value(std::move(released)));
175 object.emplace(
"events",
Value(std::move(
events)));
176 object.emplace(
"eventCount",
Value(
static_cast<std::int64_t
>(frame.events.size())));
177 object.emplace(
"activeAdvanced",
Value(
static_cast<std::int64_t
>(frame.advanced.size())));
178 return Value(std::move(
object));
181Value encodePlan(
const SpellCastPlan& plan) {
183 object.emplace(
"recipeId",
Value(plan.recipe.id.format()));
184 object.emplace(
"hasVolley",
Value(plan.volley.has_value()));
185 if (plan.volley.has_value()) {
186 object.emplace(
"volleyCount",
Value(
static_cast<std::int64_t
>(plan.volley->count)));
187 object.emplace(
"volleyPattern",
189 object.emplace(
"volleySpread",
Value(plan.volley->spreadDegrees));
191 return Value(std::move(
object));
194class ScriptCombatCarrierRuntime {
196 ScriptCombatCarrierRuntime() =
default;
198 [[nodiscard]] Result<Value> configurePool(std::int64_t
capacity) {
199 if (capacity < 1 || capacity > 1048576)
201 "pool capacity must be in 1..1048576",
"capacity", {},
202 "weapon.carrier.squirrel"));
203 auto configured = runtime_.configurePool(
static_cast<std::uint32_t
>(
capacity));
206 object.emplace(
"capacity",
Value(
static_cast<std::int64_t
>(runtime_.capacity())));
210 [[nodiscard]] Result<Value> registerRecipeValue(
const Value&
value) {
213 CarrierRecipe
owned = std::move(recipe).takeValue();
214 for (
const auto& op :
owned.motionOps) {
218 "SteerAvoidBody recipes are not supported by the script carrier runtime; omit avoidBody or use C++",
219 "motion", {},
"weapon.carrier.squirrel"));
222 const std::string
id =
owned.id.format();
223 auto registered = runtime_.registerRecipe(
owned);
226 object.emplace(
"recipeId",
Value(
id));
230 [[nodiscard]] Result<Value> registerRecipeJson(
const std::string& json) {
233 return registerRecipeValue(parsed.value());
236 [[nodiscard]] Result<Value> compileFragmentsValue(
const Value& fragments) {
239 return registerPlan(std::move(plan).takeValue());
242 [[nodiscard]] Result<Value> compileFragmentsJson(
const std::string& json) {
245 return registerPlan(std::move(plan).takeValue());
248 [[nodiscard]] Result<Value> castValue(
const Value& fragments,
double x,
double y,
double z,
double dx,
double dy,
249 double dz, std::int64_t
targetId, std::int64_t targetGeneration) {
252 SpellCastPlan
owned = std::move(plan).takeValue();
253 auto uniquified = uniquifyRecipeId(
owned.recipe.id);
255 owned.recipe.id = uniquified.value();
257 bool needsTarget =
false;
258 for (
const auto& op :
owned.recipe.motionOps) {
261 for (
const auto& trigger :
owned.recipe.triggers) {
267 "Homing/proximity casts require castAtTarget with a positive target id",
"target", {},
268 "weapon.carrier.squirrel"));
271 auto registered = registerPlan(
owned);
272 if (!registered.ok())
return registered;
276 [[nodiscard]] Result<Value> castJson(
const std::string& json,
double x,
double y,
double z,
double dx,
double dy,
277 double dz, std::int64_t
targetId, std::int64_t targetGeneration) {
280 return castValue(parsed.value(),
x,
y,
z,
dx,
dy,
dz,
targetId, targetGeneration);
283 [[nodiscard]]
static Result<void> validateHandleParts(std::int64_t
id, std::int64_t
generation,
284 std::string_view
path) {
285 if (
id < 0 ||
generation < 0 ||
static_cast<std::uint64_t
>(
id) > std::numeric_limits<std::uint32_t>::max() ||
286 static_cast<std::uint64_t
>(
generation) > std::numeric_limits<std::uint32_t>::max()) {
288 "handle id/generation must fit in uint32", std::string(
path),
289 {},
"weapon.carrier.squirrel"));
294 [[nodiscard]]
static Result<void> validatePoint(
double x,
double y,
double z, std::string_view
path) {
295 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z)) {
297 "coordinates must be finite", std::string(
path), {},
298 "weapon.carrier.squirrel"));
303 [[nodiscard]] Result<Value> spawn(
const std::string& recipeId,
double x,
double y,
double z,
double dx,
double dy,
304 double dz, std::int64_t
targetId, std::int64_t targetGeneration) {
308 "recipe id must be namespace:name",
"recipeId", {},
309 "weapon.carrier.squirrel"));
316 auto handle = validateHandleParts(
targetId, targetGeneration,
"target");
320 target.generation =
static_cast<std::uint32_t
>(targetGeneration);
326 object.emplace(
"handle", encodeHandle(
spawned.value()));
327 object.emplace(
"activeCount",
Value(
static_cast<std::int64_t
>(runtime_.activeCount())));
331 [[nodiscard]] Result<Value> spawnVolley(
const std::string& recipeId,
double x,
double y,
double z,
double dx,
333 double spread, std::int64_t
targetId, std::int64_t targetGeneration) {
337 "recipe id must be namespace:name",
"recipeId", {},
338 "weapon.carrier.squirrel"));
343 volley.spreadDegrees = spread;
350 auto handle = validateHandleParts(
targetId, targetGeneration,
"target");
354 target.generation =
static_cast<std::uint32_t
>(targetGeneration);
362 object.emplace(
"handles",
Value(std::move(handles)));
363 object.emplace(
"count",
Value(
static_cast<std::int64_t
>(
spawned.value().size())));
364 object.emplace(
"activeCount",
Value(
static_cast<std::int64_t
>(runtime_.activeCount())));
368 [[nodiscard]] Result<Value> update(
double seconds) {
373 auto frame = runtime_.update(delta.value(), &targets_, &hits_,
nullptr);
375 lastFrame_ = frame.value();
376 Value encoded = encodeFrame(lastFrame_);
378 object->emplace(
"activeCount",
Value(
static_cast<std::int64_t
>(runtime_.activeCount())));
383 [[nodiscard]] Result<Value> addHitTarget(std::int64_t
id, std::int64_t
generation,
double x,
double y,
double z,
387 auto point = validatePoint(
x,
y,
z,
"target");
391 "hit target requires positive finite radius",
"radius", {},
392 "weapon.carrier.squirrel"));
393 ScriptCircleHitProbe::Target
target;
394 target.id.id =
static_cast<std::uint32_t
>(
id);
400 object.emplace(
"count",
Value(
static_cast<std::int64_t
>(1)));
404 [[nodiscard]] Result<Value> clearHitTargets() {
409 [[nodiscard]] Result<Value> setTargetPosition(std::int64_t
id, std::int64_t
generation,
double x,
double y,
413 auto point = validatePoint(
x,
y,
z,
"target");
416 target.id =
static_cast<std::uint32_t
>(
id);
422 [[nodiscard]] Result<Value> clearTargets() {
427 [[nodiscard]] std::int64_t activeCount() const noexcept {
428 return static_cast<std::int64_t
>(runtime_.activeCount());
431 [[nodiscard]] std::int64_t
capacity() const noexcept {
return static_cast<std::int64_t
>(runtime_.capacity()); }
434 [[nodiscard]] Result<LogicalId> uniquifyRecipeId(
const LogicalId& base) {
436 const std::string uniqueName = std::string(base.name()) +
"-c" + std::to_string(castSeq_);
438 if (!parsed.has_value())
440 "unable to allocate unique cast recipe id",
"recipeId",
441 {},
"weapon.carrier.squirrel"));
445 [[nodiscard]] Result<Value> registerPlan(SpellCastPlan plan) {
446 for (
const auto& op : plan.recipe.motionOps) {
450 "SteerAvoidBody recipes are not supported by the script carrier runtime",
451 "motion", {},
"weapon.carrier.squirrel"));
454 for (
const auto& dependent : plan.dependentRecipes) {
455 auto registered = runtime_.registerRecipe(dependent);
458 auto registered = runtime_.registerRecipe(plan.recipe);
463 [[nodiscard]] Result<Value> spawnPlan(
const SpellCastPlan& plan,
double x,
double y,
double z,
double dx,
double dy,
464 double dz, std::int64_t
targetId, std::int64_t targetGeneration) {
465 auto spawned = plan.volley.has_value()
466 ? spawnVolley(plan.recipe.id.format(),
x,
y,
z,
dx,
dy,
dz, plan.volley->count,
473 object->emplace(
"recipeId",
Value(plan.recipe.id.format()));
478 CombatCarrierRuntime runtime_;
479 ScriptTargetProvider targets_;
480 ScriptCircleHitProbe hits_;
481 CarrierFrame lastFrame_{};
482 std::uint64_t castSeq_ = 0;
491 auto runtime = std::make_unique<ScriptCombatCarrierRuntime>();
492 auto configured = runtime->configurePool(
capacity);
494 auto object = script::makeOwnedSquirrelInstance<ScriptCombatCarrierRuntime>(
vm, std::move(runtime));
497 result.set(
"value", std::move(
object).takeValue());
498 result.set(
"ownership", std::string(
"owned"));
502Result<Value> valueFromObject(
const ssq::Object&
object) {
511 auto runtime = table.addClass<ScriptCombatCarrierRuntime>(
512 "CombatCarrierRuntime", std::function<ScriptCombatCarrierRuntime*()>([] {
return nullptr; }),
true);
513 runtime.addFunc(
"ownership", [](ScriptCombatCarrierRuntime*) {
return std::string(
"owned"); });
514 runtime.addFunc(
"activeCount", [](ScriptCombatCarrierRuntime* self) {
515 return self ? self->activeCount() :
static_cast<std::int64_t
>(0);
517 runtime.addFunc(
"capacity", [](ScriptCombatCarrierRuntime* self) {
518 return self ? self->capacity() :
static_cast<std::int64_t
>(0);
520 runtime.addFunc(
"configurePool", [
vm](ScriptCombatCarrierRuntime* self, std::int64_t
capacity) {
521 return project(
vm, self ? self->configurePool(
capacity) : nullRuntime());
523 runtime.addFunc(
"registerRecipe", [
vm](ScriptCombatCarrierRuntime* self, ssq::Object recipe) {
524 if (!self)
return project(
vm, nullRuntime());
525 auto value = valueFromObject(recipe);
527 return project(
vm, self->registerRecipeValue(
value.value()));
529 runtime.addFunc(
"registerRecipeJson", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& json) {
530 return project(
vm, self ? self->registerRecipeJson(json) : nullRuntime());
532 runtime.addFunc(
"compileFragments", [
vm](ScriptCombatCarrierRuntime* self, ssq::Object fragments) {
533 if (!self)
return project(
vm, nullRuntime());
534 auto value = valueFromObject(fragments);
536 return project(
vm, self->compileFragmentsValue(
value.value()));
538 runtime.addFunc(
"compileFragmentsJson", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& json) {
539 return project(
vm, self ? self->compileFragmentsJson(json) : nullRuntime());
541 runtime.addFunc(
"cast", [
vm](ScriptCombatCarrierRuntime* self, ssq::Object fragments,
float x,
float y,
float z,
542 float dx,
float dy,
float dz) {
543 if (!self)
return project(
vm, nullRuntime());
544 auto value = valueFromObject(fragments);
546 return project(
vm, self->castValue(
value.value(),
x,
y,
z,
dx,
dy,
dz, 0, 0));
548 runtime.addFunc(
"castAtTarget", [
vm](ScriptCombatCarrierRuntime* self, ssq::Object fragments,
float x,
float y,
550 std::int64_t targetGeneration) {
551 if (!self)
return project(
vm, nullRuntime());
552 auto value = valueFromObject(fragments);
556 runtime.addFunc(
"castJson", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& json,
float x,
float y,
557 float z,
float dx,
float dy,
float dz) {
558 return project(
vm, self ? self->castJson(json,
x,
y,
z,
dx,
dy,
dz, 0, 0) : nullRuntime());
560 runtime.addFunc(
"spawn", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& recipeId,
float x,
float y,
561 float z,
float dx,
float dy,
float dz) {
562 return project(
vm, self ? self->spawn(recipeId,
x,
y,
z,
dx,
dy,
dz, 0, 0) : nullRuntime());
564 runtime.addFunc(
"spawnAtTarget", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& recipeId,
float x,
566 std::int64_t targetGeneration) {
567 return project(
vm, self ? self->spawn(recipeId,
x,
y,
z,
dx,
dy,
dz,
targetId, targetGeneration)
570 runtime.addFunc(
"spawnVolley", [
vm](ScriptCombatCarrierRuntime* self,
const std::string& recipeId,
float x,
float y,
571 float z,
float dx,
float dy,
float dz, std::int64_t
count,
572 const std::string&
pattern,
float spread) {
573 return project(
vm, self ? self->spawnVolley(recipeId,
x,
y,
z,
dx,
dy,
dz,
count,
pattern, spread, 0, 0)
576 runtime.addFunc(
"update", [
vm](ScriptCombatCarrierRuntime* self,
float seconds) {
577 return project(
vm, self ? self->update(seconds) : nullRuntime());
579 runtime.addFunc(
"addHitTarget", [
vm](ScriptCombatCarrierRuntime* self, std::int64_t
id, std::int64_t
generation,
580 float x,
float y,
float z,
float radius) {
583 runtime.addFunc(
"clearHitTargets", [
vm](ScriptCombatCarrierRuntime* self) {
584 return project(
vm, self ? self->clearHitTargets() : nullRuntime());
586 runtime.addFunc(
"setTargetPosition", [
vm](ScriptCombatCarrierRuntime* self, std::int64_t
id,
588 return project(
vm, self ? self->setTargetPosition(
id,
generation,
x,
y,
z) : nullRuntime());
590 runtime.addFunc(
"clearTargets", [
vm](ScriptCombatCarrierRuntime* self) {
591 return project(
vm, self ? self->clearTargets() : nullRuntime());
601 cls.addFunc(
"newCarrierRuntimeDefault", [
vm](
Weapon*) {
return newCarrierRuntime(
vm, 256); });
Decode/encode CombatCarrier recipes from canonical eve::Value trees.
Composable combat-carrier runtime: motion ops, triggers, and impacts.
struct SQVM * HSQUIRRELVM
Stable, structured diagnostics shared by engine modules.
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::array< float, 3 > position
graphics::Canvas * previous
std::optional< CarrierVolleySpec > volley
Compile Noita-style spell/modifier fragment sequences into carrier recipes.
The single Squirrel projection for common Result, Status and Value.
Explicit Value/Owned/Borrowed semantics at the Squirrel boundary.
Common time values and an injectable deterministic simulation clock.
Owning, renderer-independent dynamic values.
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 std::optional< LogicalId > fromParts(std::string_view namespaceName, std::string_view name)
Builds a logical ID from separate namespace and name components.
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Move-only operation result carrying either a value or Status.
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 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
static Value object(Object value)
Compatibility factory for an object value.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
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.
Result< Value > valueFromSquirrel(HSQUIRRELVM vm, SQInteger index, const SquirrelValueOptions &options)
Convert one Squirrel value into the canonical owning Value tree.
载具模块入口(eve.Vehicle):定义注册 / 实体工厂 / RTS 命令 / 帧调度。 设计文档:docs/dev/通用载具系统设计.md
CarrierImpactKind
What happens when a matching trigger fires.
void exposeCombatCarrierModuleMethods(ssq::Class &cls)
Register Weapon module methods that create owned carrier runtimes.
Result< CarrierRecipe > decodeCarrierRecipe(const Value &value)
Decode one CarrierRecipe from an owning Value object.
CarrierTriggerKind
When an impact may fire.
void exposeCombatCarrierBindings(ssq::Table &table)
Expose CombatCarrierRuntime class bindings under the shared eve table.
Result< SpellCastPlan > compileSpellFragmentsJson(const std::string &json)
Compile a JSON array string into one cast plan.
Result< SpellCastPlan > compileSpellFragments(const Value &fragments)
Compile a fragment array Value into one cast plan.
CarrierVolleyPattern
Multi-projectile aim pattern for one salvo.