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>
207 lines
No EOL
6.7 KiB
C#
207 lines
No EOL
6.7 KiB
C#
/* 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/. */
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using MessagePack;
|
|
using Newtonsoft.Json;
|
|
using CultMath;
|
|
using static CultMath.math;
|
|
|
|
public abstract class Behavior
|
|
{
|
|
public BehaviorData Data { get; }
|
|
public EquippedItem Item { get; }
|
|
private ConsumableItemEffect Consumable { get; }
|
|
protected ItemManager ItemManager { get; }
|
|
|
|
protected Entity Entity => Item?.Entity ?? Consumable.Entity;
|
|
public float Temperature => Item?.Temperature ?? Consumable.Entity.MaxTemp;
|
|
|
|
public float3 Direction
|
|
{
|
|
get
|
|
{
|
|
if(Item != null)
|
|
{
|
|
var hardpoint = Entity.Hardpoints[Item.Position.x, Item.Position.y];
|
|
if (hardpoint != null && Entity.HardpointTransforms.ContainsKey(hardpoint))
|
|
{
|
|
return normalize(Entity.HardpointTransforms[hardpoint].direction);
|
|
}
|
|
else
|
|
{
|
|
var itemDirection = Entity.Direction.Rotate(Item.EquippableItem.Rotation);
|
|
return float3(itemDirection.x, 0, itemDirection.y);
|
|
}
|
|
}
|
|
|
|
return float3(Entity.Direction.x, 0, Entity.Direction.y);
|
|
}
|
|
}
|
|
|
|
protected Behavior(BehaviorData data, EquippedItem item)
|
|
{
|
|
Data = data;
|
|
Item = item;
|
|
ItemManager = Item.ItemManager;
|
|
}
|
|
|
|
protected Behavior(BehaviorData data, ConsumableItemEffect consumable)
|
|
{
|
|
Data = data;
|
|
Consumable = consumable;
|
|
ItemManager = consumable.Entity.ItemManager;
|
|
}
|
|
|
|
public float Evaluate(PerformanceStat stat) => Item?.Evaluate(stat) ?? Consumable.Evaluate(stat);
|
|
|
|
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): the read a PowerRequest
|
|
// implementation uses for a stat named in StatValidation.PowerRequestFields, in place of Evaluate above. A
|
|
// consumable-hosted instance (Item == null) never reaches PowerBus.Step at all (see e.g. EnergyDraw.Execute's
|
|
// own comment), and ConsumableItemEffect.PowerSupplyFactor is already the identity (1) unconditionally, so
|
|
// Consumable.Evaluate is already nominal with respect to power and needs no separate context.
|
|
public float EvaluateNominalPower(PerformanceStat stat) => Item?.EvaluateNominalPower(stat) ?? Consumable.Evaluate(stat);
|
|
protected void AddHeat(float heat) => Item?.AddHeat(heat); // TODO: Heat for Consumables
|
|
|
|
protected void CauseDamage(float damage)
|
|
{
|
|
if (Item != null)
|
|
{
|
|
Item.EquippableItem.Durability -= damage;
|
|
Entity.ItemDamage.OnNext((Item, damage));
|
|
}
|
|
else
|
|
{
|
|
Consumable.Entity.Hull.Durability -= damage;
|
|
Consumable.Entity.HullDamage.OnNext(damage);
|
|
}
|
|
}
|
|
|
|
protected void CauseWearDamage(float multiplier)
|
|
{
|
|
if (Item != null)
|
|
{
|
|
CauseDamage(Item.Wear * multiplier);
|
|
}
|
|
}
|
|
|
|
public virtual bool Execute(float dt)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public interface IActivatedBehavior
|
|
{
|
|
void Activate();
|
|
void Deactivate();
|
|
}
|
|
|
|
public interface IAnalogBehavior
|
|
{
|
|
float Axis { get; set; }
|
|
}
|
|
|
|
public interface IEventBehavior
|
|
{
|
|
void ResetEvents();
|
|
}
|
|
|
|
public interface IInitializableBehavior
|
|
{
|
|
void Initialize();
|
|
}
|
|
|
|
public interface IInteractiveBehavior
|
|
{
|
|
bool Exposed { get; }
|
|
}
|
|
|
|
public interface IAlwaysUpdatedBehavior
|
|
{
|
|
void Update(float delta);
|
|
}
|
|
|
|
public interface IProgressBehavior
|
|
{
|
|
float Progress { get; }
|
|
}
|
|
|
|
public interface IPopulationAssignment
|
|
{
|
|
int AssignedPopulation { get; set; }
|
|
}
|
|
|
|
// Cut 3 (docs/stats-and-power-cut.md §1.2): a behaviour that wants to draw from its entity's PowerBus declares a
|
|
// request instead of spending energy itself. PowerBus.Step calls this on every consumer once per tick, before
|
|
// any behaviour's Execute runs, so PowerRequest must be computable from state that is already current at that
|
|
// point -- a resolved stat and, where the map calls for one, a behaviour-supplied scalar such as Thruster's
|
|
// throttle axis (set externally before the tick) -- never from work Execute itself would otherwise do. The
|
|
// behaviour reads back how much of its own request was granted from Item.PowerSupply during its own Execute.
|
|
//
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3): every kind of consumer also names the priority tier (PowerTiers.cs)
|
|
// it defaults to. EquippedItem reads this exactly once, to seed EquippableItem.PowerTier the first time an item
|
|
// without a player-chosen tier gets equipped -- after that the stored field is what PowerBus reads, never this.
|
|
public interface IPowerConsumer
|
|
{
|
|
float PowerRequest(float dt);
|
|
int DefaultPowerTier { get; }
|
|
}
|
|
|
|
[Inspectable,
|
|
Union(0, typeof(GuidedWeaponData)),
|
|
Union(1, typeof(LauncherData)),
|
|
Union(2, typeof(ReactorData)),
|
|
Union(3, typeof(RadiatorData)),
|
|
Union(4, typeof(StatModifierData)),
|
|
Union(5, typeof(SensorData)),
|
|
Union(6, typeof(ReflectorData)),
|
|
Union(7, typeof(ShieldData)),
|
|
Union(8, typeof(ThrusterData)),
|
|
Union(9, typeof(WearData)),
|
|
Union(10, typeof(VelocityConversionData)),
|
|
Union(11, typeof(VelocityLimitData)),
|
|
Union(12, typeof(AetherDriveData)),
|
|
// Union(14, typeof(TowingControllerData)),
|
|
Union(15, typeof(CooldownData)),
|
|
Union(16, typeof(HeatData)),
|
|
// Union(17, typeof(HitscanData)),
|
|
Union(18, typeof(ItemUsageData)),
|
|
//Union(19, typeof(RadianceData)),
|
|
Union(20, typeof(SwitchData)),
|
|
Union(21, typeof(TriggerData)),
|
|
Union(22, typeof(VisibilityData)),
|
|
Union(23, typeof(ThermotoggleData)),
|
|
Union(24, typeof(EnergyDrawData)),
|
|
// Union(25, typeof(MiningControllerData)),
|
|
Union(26, typeof(MiningToolData)),
|
|
// Union(27, typeof(SurveyControllerData)),
|
|
Union(28, typeof(ResourceScannerData)),
|
|
// Union(29, typeof(WanderControllerData)),
|
|
// Union(30, typeof(HaulingControllerData)),
|
|
Union(31, typeof(CapacitorData)),
|
|
Union(32, typeof(CockpitData)),
|
|
Union(33, typeof(HeatStorageData)),
|
|
Union(34, typeof(TurretControllerData)),
|
|
Union(35, typeof(InstantWeaponData)),
|
|
Union(36, typeof(ConstantWeaponData)),
|
|
Union(37, typeof(ChargedWeaponData)),
|
|
Union(38, typeof(AutoWeaponData)),
|
|
JsonObject(MemberSerialization.OptIn)]
|
|
public abstract class BehaviorData
|
|
{
|
|
[Inspectable, JsonProperty("group"), Key(0)]
|
|
public int Group;
|
|
|
|
public abstract Behavior CreateInstance(EquippedItem item);
|
|
public abstract Behavior CreateInstance(ConsumableItemEffect consumable);
|
|
|
|
public override string ToString()
|
|
{
|
|
return base.ToString().FormatTypeName();
|
|
}
|
|
} |