Aetheria/Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs

185 lines
8.4 KiB
C#
Raw Normal View History

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
using GameCult.Caching;
using MessagePack;
using Newtonsoft.Json;
using CultMath;
using static CultMath.math;
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), EntityTypeRestriction(HullType.Ship), RuntimeInspectable]
public class AetherDriveData : BehaviorData
{
[Inspectable, JsonProperty("diameters"), Key(1)]
public float3 RotorDiameter;
[Inspectable, JsonProperty("masses"), Key(2)]
public float3 RotorMass;
[Inspectable, JsonProperty("rpm"), Key(3), RuntimeInspectable]
public PerformanceStat MaximumRpm;
[Inspectable, JsonProperty("couplingLambdas"), Key(4)]
public float3 CouplingLambda;
[Inspectable, JsonProperty("lambdaMultiplier"), Key(5)]
public PerformanceStat LambdaMultiplier;
[Inspectable, JsonProperty("couplingEfficiency"), Key(6), RuntimeInspectable]
public PerformanceStat CouplingEfficiency;
[Inspectable, JsonProperty("torque"), Key(7), RuntimeInspectable]
public PerformanceStat Torque;
[Inspectable, JsonProperty("torqueProfile"), Key(8), RuntimeInspectable]
public BezierCurve TorqueProfile;
[Inspectable, JsonProperty("draw"), Key(9), RuntimeInspectable]
public PerformanceStat EnergyDraw;
[Inspectable, JsonProperty("passiveCoupling"), Key(10), RuntimeInspectable]
public PerformanceStat PassiveCoupling;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Inspectable, JsonProperty("rpmAudio"), Key(11), RuntimeInspectable]
public uint RpmAudioParameter;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Inspectable, JsonProperty("torqueAudio"), Key(12), RuntimeInspectable]
public uint TorqueRatioAudioParameter;
Switch engine asset references from Resources paths to Addressables GUIDs Cut 2 of docs/addressables-cut.md. The asset move (git mv Resources/{Prefabs,Schematics,Sprites/Icons} -> Content/...) and the UnityHelpers.cs deletion already landed in 1fdf8151, picked up by a concurrent session sharing this working tree; this commit is the rest of Cut 2 landing on top of that shared state, atomically: - Bump CultLib to cultlib-unity-v1.0.60 / caching-unity-v1.4.0 (both 45c2f40) in Packages/manifest.json, packages-lock.json and Directory.Build.props, together with the 8 ServerShared [CultInspectorAssetPath] usages becoming [CultInspectorAssetGuid] - the .NET and Unity sides move together or nothing compiles. - Assets/Scripts/EngineAssets.cs replaces UnityHelpers: loads an Addressables key (bare GUID or guid[subAsset]) synchronously, loading the GameObject and taking the component for the four sites that load a component. ActionBarSlot, EntityInstance, ShipInstance and ZoneRenderer call it instead of Resources.Load/UnityHelpers; the inline "Assets/Resources/" prefix strips are gone with them. CultCacheDrawers' schematic preview goes through AssetDatabase.GUIDToAssetPath first. - Assets/Scripts/Editor/EngineAssetCheck.cs validates every stored [CultInspectorAssetGuid] value: 32 lowercase hex, resolves to a file, is addressable, and satisfies its member's consumer-type contract (Texture2D for icons/logos/schematics, a GameObject with the right component for prefabs) - table-driven, so an attributed member with no row fails loudly instead of being skipped. - GameData/Aetheria.cc: the scratch migration (Aetheria.Shared + the pinned CultLib revision, a throwaway console app, never landed) walked all 138 attributed field slots across 180 documents, rewriting the 30 non-empty Resources paths to their .meta GUIDs - 108 stayed empty, 0 unresolved, matching the map's census exactly. Verification: dotnet test tests/Aetheria.Shared.Tests 61/61 green against CultLib 45c2f40; Unity 6000.3.24f1 batchmode compiles clean; EngineAssetCheck.Run passes all 30 stored references; the map's negative greps are clean; tests/mutation_tests.py's 5 mutations (wrong type, non-addressable, random guid, an old path, and an untabled attributed member) are all caught and named, the control case and the post-revert recompile stay clean, and the catalog/source are restored byte-identical afterward. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:04:50 +02:00
[Inspectable, CultInspectorAssetGuid, JsonProperty("particles"), Key(13)]
public string Particles;
public override Behavior CreateInstance(EquippedItem item)
{
return new AetherDrive(this, item);
}
public override Behavior CreateInstance(ConsumableItemEffect item)
{
return new AetherDrive(this, item);
}
}
public class AetherDrive : Behavior, IPowerConsumer
{
private AetherDriveData _data;
private float3 _axis;
public float3 Thrust { get; private set; }
public float3 Rpm { get; private set; }
public float MaximumRpm { get; private set; }
public float2 ThrustDirection { get; private set; }
public AetherDriveData DriveData => _data;
// Cut 8 (operator ask 2026-09-19): the equivalent quantity to Thruster.Condition -- Torque is the stat that
// actually governs this behaviour's rotor spin-up (PowerRequest/Execute above both read it), so it is what
// "broken" means for a drive. 1f for the item-less ConsumableItemEffect case, same reasoning as Thruster.
public float Condition => Item?.ConditionRatio(_data.Torque) ?? 1f;
public float3 Axis
{
get => _axis;
set => _axis = clamp(value, -1, 1);
}
public AetherDrive(AetherDriveData data, EquippedItem item) : base(data, item)
{
_data = data;
}
public AetherDrive(AetherDriveData data, ConsumableItemEffect item) : base(data, item)
{
_data = data;
}
// Cut 3 (docs/stats-and-power-cut.md): the request PowerBus needs before Execute runs. Only the rotor
// spin-up (torque accelerating Rpm toward MaximumRpm) costs power -- spending existing spin into thrust
// (Execute's decay-to-thrust arithmetic below) is free, same as before. Pure and side-effect-free: it reads
// Rpm but does not write it, so Execute's own identical arithmetic a few lines later -- which does perform
// the real decay -- produces the same numbers Rpm actually moves by. Accepts the recompute (Q3's ruling).
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
//
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Torque, LambdaMultiplier,
// MaximumRpm, PassiveCoupling and EnergyDraw are all registered request fields (StatValidation.
// PowerRequestFields) -- read nominally (what full power would produce) so Torque carrying its own
// PowerSupply term (Cut 7's brownout curve) no longer makes this tick's request depend on this tick's own
// grant. Execute below is unchanged: it calls the real, curved Evaluate, so the actual spin-up still
// degrades with whatever the bus actually grants.
public float PowerRequest(float dt)
{
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
var couplingLambda = _data.CouplingLambda * Item.EvaluateNominalPower(_data.LambdaMultiplier) * max(abs(_axis), EvaluateNominalPower(_data.PassiveCoupling));
var rpmAfterDecay = decay(Rpm, couplingLambda, dt);
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
var maximumRpm = EvaluateNominalPower(_data.MaximumRpm);
var torqueProfile = float3(
_data.TorqueProfile.Evaluate(rpmAfterDecay.x / maximumRpm),
_data.TorqueProfile.Evaluate(rpmAfterDecay.y / maximumRpm),
_data.TorqueProfile.Evaluate(rpmAfterDecay.z / maximumRpm));
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
var potentialTorque = EvaluateNominalPower(_data.Torque) * torqueProfile;
var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt;
var actualRpmDelta = min(maximumRpm - rpmAfterDecay, potentialRpmDelta);
var torqueRatio = actualRpmDelta / potentialRpmDelta;
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
var draw = torqueRatio * EvaluateNominalPower(_data.EnergyDraw) / 3;
return (draw.x + draw.y + draw.z) * dt;
}
Cut 5: priority tiers replace PowerBus's single shared GrantRatio PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their per-behaviour-kind defaults: Radiator is Critical (the closest thing this game has to life support -- a starved radiator cascades into wear and shutdown across the whole hull), Shield is High, Thruster/AetherDrive are Medium, ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility. IPowerConsumer.DefaultPowerTier lets each behaviour own its own default. EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the unit rather than the design; EquippedItem's constructor seeds a kind default the first time a unit is equipped, only when the field is still PowerTiers.Unassigned, and never again -- a tier is a stored choice, not something the bus re-derives. PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in full before the next tier sees anything left, an empty tier passes its whole share down, and within a tier every consumer rations by the same grant/demand fraction so unequal requests still divide proportionally. A new TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to read starvation from directly, instead of reconstructing it from every item's PowerSupply. Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its only implementer): the bus already steps before any equipment's Execute, so equip/behaviour order never decided who got fed even before this cut, and nothing else needed the interface (subject to the map's O3 operator check). docs/stats-and-power-cut.md Cut 5 / §1.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Medium -- mobility, same as Thruster.
public int DefaultPowerTier => PowerTiers.Medium;
public override bool Execute(float dt)
{
var rotorSpeed = Rpm * _data.RotorDiameter / 100;
var forward = normalize(Entity.Direction);
var right = forward.Rotate(ItemRotation.Clockwise);
var speed = float2(dot(Entity.Velocity, forward), dot(Entity.Velocity, right));
var couplingEfficiency = Evaluate(_data.CouplingEfficiency);
var efficiency = float3(saturate(1 - speed / max(rotorSpeed.xy, 1) * sign(_axis.xy)) * couplingEfficiency, 1);
Thrust = (Rpm - decay(Rpm, _data.CouplingLambda, dt)) * _data.RotorMass * efficiency;
var couplingLambda = _data.CouplingLambda * Item.Evaluate(_data.LambdaMultiplier) * max(abs(_axis), Evaluate(_data.PassiveCoupling));
var previousRpm = Rpm;
Rpm = decay(Rpm, couplingLambda, dt);
var rpmLoss = previousRpm - Rpm;
var force = rpmLoss * _data.RotorMass * efficiency;
var heat = rpmLoss * _data.RotorMass * (1 - couplingEfficiency);
AddHeat((heat.x + heat.y + heat.z)*ItemManager.GameplaySettings.AetherHeatMultiplier);
2021-04-30 15:37:39 +01:00
ThrustDirection = forward * (_axis.x * force.x / Entity.Mass) + right * (_axis.y * force.y / Entity.Mass);
Entity.Velocity += ThrustDirection;
Entity.Direction = mul(Entity.Direction,
CultMath.float2x2.Rotate(force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass));
if(float.IsNaN(Entity.Velocity.x))
ItemManager.Log("FUCK FUCK FUCK FUCK");
MaximumRpm = Evaluate(_data.MaximumRpm);
var torqueProfile = float3(
_data.TorqueProfile.Evaluate(Rpm.x / MaximumRpm),
_data.TorqueProfile.Evaluate(Rpm.y / MaximumRpm),
_data.TorqueProfile.Evaluate(Rpm.z / MaximumRpm));
var potentialTorque = Evaluate(_data.Torque) * torqueProfile;
var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt;
var actualRpmDelta = min(MaximumRpm - Rpm, potentialRpmDelta);
var torqueRatio = actualRpmDelta / potentialRpmDelta;
Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(max(abs(_axis.x), abs(_axis.y)), abs(_axis.z)));
Item.SetAudioParameter(_data.RpmAudioParameter, (Rpm.x + Rpm.y + Rpm.z) / 3 / MaximumRpm);
Item.SetAudioParameter(_data.TorqueRatioAudioParameter, max(max(torqueRatio.x, torqueRatio.y), torqueRatio.z));
// Cut 7 (docs/stats-and-power-target.md): actualRpmDelta already derives from Evaluate(_data.Torque)
// above, so a Torque stat carrying a PowerSupply term already shrinks the rotor's spin-up under a
// partial grant -- this no longer demands a full one, only that supply has not been cut to true zero.
if (Item.PowerSupply > 1e-4f)
{
Rpm += actualRpmDelta;
return true;
}
return false;
}
}