Signature, ActiveEmission and Reveal carry Claimed (the class the ship's EW claims for the item, the true item's data by default); Entity gains the claim slots and one write port; Contact gains Profile, derived from the ship's claimed hull once Identified; Entity.RevealedWeapons is the one classifying read for weapons. Combat and the enemy schematic read it; their reads of the target's live gear are cut. Weapons rate by their claimed design (ew-claim-weapon-measure-ruled, rated-design): WeaponData.RatedRangeDamagePerSecond runs the same per-kind formula as the live weapon. Aim, damage and physics keep the truth. Not yet compiled or run at this commit; the Unity-only SchematicDisplay is unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
No EOL
9.3 KiB
C#
221 lines
No EOL
9.3 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 MessagePack;
|
|
using Newtonsoft.Json;
|
|
using CultMath;
|
|
using static CultMath.math;
|
|
|
|
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
|
public class ConstantWeaponData : WeaponData
|
|
{
|
|
[Inspectable, JsonProperty("ammoInterval"), Key(17)]
|
|
public float AmmoInterval = 1;
|
|
|
|
public override Behavior CreateInstance(EquippedItem item)
|
|
{
|
|
return new ConstantWeapon(this, item);
|
|
}
|
|
|
|
public override Behavior CreateInstance(ConsumableItemEffect item)
|
|
{
|
|
return new ConstantWeapon(this, item);
|
|
}
|
|
|
|
// The one formula of a beam's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond).
|
|
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float minRange, float range, float atRange) =>
|
|
damage * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange)));
|
|
|
|
public override float RatedRangeDamagePerSecond(float range) =>
|
|
RangeDamagePerSecond(DamageCurve, Damage.Max, MinRange.Max, Range.Max, range);
|
|
}
|
|
|
|
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
|
|
{
|
|
private ConstantWeaponData _data;
|
|
private int _ammo = 1;
|
|
private float _ammoInterval;
|
|
private float _reload;
|
|
private bool _reloading;
|
|
// Cut 4 (docs/fire-control-cut.md): accumulates while firing; rolled off every time it reaches
|
|
// GameplaySettings.BeamResolveInterval, through FireControl.Fire/Step exactly like a discrete shot.
|
|
private float _beamTimer;
|
|
|
|
public override int Ammo
|
|
{
|
|
get => _ammo;
|
|
}
|
|
|
|
public float Progress
|
|
{
|
|
get { return saturate(_reload); }
|
|
}
|
|
|
|
public override float DamagePerSecond => Damage;
|
|
public override float RangeDamagePerSecond(float range)
|
|
{
|
|
return ConstantWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, MinRange, Range, range);
|
|
}
|
|
|
|
public event Action OnReloadBegin;
|
|
public event Action OnReloadComplete;
|
|
public event Action OnStartFiring;
|
|
public event Action OnStopFiring;
|
|
// Cut 4 (docs/fire-control-cut.md): carries the ShotId each beam roll's FireControl.Fire assigned, the
|
|
// same handle InstantWeapon.OnFire gives discrete shots. No presentation binds to it yet -- kept for a
|
|
// future pass, same as Laser.ShotId.
|
|
public event Action<int> OnBeamShot;
|
|
|
|
public void ResetEvents()
|
|
{
|
|
OnReloadBegin = null;
|
|
OnReloadComplete = null;
|
|
OnStartFiring = null;
|
|
OnStopFiring = null;
|
|
OnBeamShot = null;
|
|
}
|
|
|
|
public ConstantWeapon(ConstantWeaponData data, EquippedItem item) : base(data, item)
|
|
{
|
|
_data = data;
|
|
}
|
|
|
|
public ConstantWeapon(ConstantWeaponData data, ConsumableItemEffect item) : base(data, item)
|
|
{
|
|
_data = data;
|
|
}
|
|
|
|
// Cut 3 (docs/stats-and-power-cut.md): the request the bus needs before Execute runs -- what continuing to
|
|
// fire this tick would cost, or nothing when not firing or safed. _firing is set externally (Activate/
|
|
// Deactivate) before Entity.Update calls PowerBus.Step, so it is already current when this runs.
|
|
//
|
|
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Energy is a registered
|
|
// request field (StatValidation.PowerRequestFields) -- read nominally, same reasoning as every other
|
|
// IPowerConsumer in this cut. Damage (the field Cut 7 curves) is a separate stat base.Execute reads with the
|
|
// real Evaluate.
|
|
// Cut 5 (docs/fire-control-cut.md, 5.3, Soul finding 9): StanceAllowsFire alone let a side-mounted beam
|
|
// fire forward -- ArcAllowsFire joins it here, matching InstantWeapon.cs's player arc gate (Q2: manual and
|
|
// programmatic are one truth, and a continuous weapon is no exception).
|
|
// Operator ruling 2026-09-30: a refused round is free, and a beam is a stream of rounds, so a fused beam whose
|
|
// arming distance exceeds its Range fires nothing and draws nothing -- no power, ammo, wear, heat or
|
|
// visibility. FireControl.Refuses is the one arming test; it reads the Range Execute last refreshed, the same
|
|
// one-tick lag every PowerRequest reads its stats with.
|
|
private bool MayFire => StanceAllowsFire && ArcAllowsFire && !FireControl.Refuses(this, Entity);
|
|
|
|
public float PowerRequest(float dt) => _firing && MayFire ? EvaluateNominalPower(_data.Energy) * dt : 0f;
|
|
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Low -- offense, same as InstantWeapon.
|
|
public int DefaultPowerTier => PowerTiers.Low;
|
|
|
|
public override bool Execute(float dt)
|
|
{
|
|
base.Execute(dt);
|
|
if (_firing && !MayFire)
|
|
{
|
|
// Safed: shooter has a target and hasn't declared hostility toward it, or the target no longer
|
|
// bears (5.3: a beam obeys its arc exactly as InstantWeapon's trigger does), or a fused round is
|
|
// refused.
|
|
_firing = false;
|
|
OnStopFiring?.Invoke();
|
|
return false;
|
|
}
|
|
if (_firing)
|
|
{
|
|
// Cut 7 (docs/stats-and-power-target.md): base.Execute(dt) above already re-read Damage through
|
|
// Evaluate(_data.Damage), so a Damage stat carrying a PowerSupply term already fires this tick for
|
|
// less under a partial grant -- the weapon no longer safes itself off (the "flicking off gear" bug
|
|
// the ruling names) just because the grant fell short of 1. Only true zero supply still stops it.
|
|
if (Item != null && Item.PowerSupply <= 1e-4f)
|
|
{
|
|
_firing = false;
|
|
OnStopFiring?.Invoke();
|
|
return false;
|
|
}
|
|
if (_data.AmmoType.IsSet())
|
|
{
|
|
if (_reloading)
|
|
{
|
|
_reload -= dt / _data.ReloadTime;
|
|
if (_reload < 0)
|
|
{
|
|
_reloading = false;
|
|
OnReloadComplete?.Invoke();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
_ammoInterval -= dt / _data.AmmoInterval;
|
|
if (_ammoInterval < 0)
|
|
{
|
|
_ammoInterval = 1;
|
|
if (_data.MagazineSize > 1 && _ammo > 0) _ammo--;
|
|
else
|
|
{
|
|
var cargo = Entity.FindItemInCargo(_data.AmmoType.Key);
|
|
if (cargo != null)
|
|
{
|
|
var item = cargo.ItemsOfType[_data.AmmoType.Key][0];
|
|
if (item is SimpleCommodity simpleCommodity)
|
|
cargo.Remove(simpleCommodity, 1);
|
|
|
|
if(_data.MagazineSize > 1)
|
|
{
|
|
_reloading = true;
|
|
_reload = 1;
|
|
OnReloadBegin?.Invoke();
|
|
|
|
_firing = false;
|
|
OnStopFiring?.Invoke();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
CauseWearDamage(dt);
|
|
AddHeat(Evaluate(_data.Heat) * dt);
|
|
Entity.SetSignature(this, SignatureChannel.LaunchFlash, Evaluate(_data.Visibility), Item);
|
|
|
|
// Cut 4 (docs/fire-control-cut.md): a beam is a sequence of rolls, not a continuous truth -- one
|
|
// FireControl.Fire per BeamResolveInterval, for that interval's worth of Damage, through the same
|
|
// freeze-and-queue path a discrete shot uses (a beam's authored Velocity is 0, so the flight
|
|
// commits and resolves in this same tick, R4's short-flight degradation).
|
|
var interval = Entity.ItemManager.GameplaySettings.BeamResolveInterval;
|
|
_beamTimer += dt;
|
|
while (_beamTimer >= interval)
|
|
{
|
|
_beamTimer -= interval;
|
|
// FireControl.Fire must run whether or not anything is listening: `OnBeamShot?.Invoke(FireControl.Fire(...))`
|
|
// looks equivalent but is not -- C#'s null-conditional short-circuits the whole expression,
|
|
// argument included, when OnBeamShot has no subscriber (true today, R9's "no design uses it
|
|
// yet"), so the roll would silently never happen. Evaluate it into a local first.
|
|
var shotId = FireControl.Fire(this, Item, Entity, Damage * interval);
|
|
OnBeamShot?.Invoke(shotId);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public override void Activate()
|
|
{
|
|
// A refused beam never starts (operator ruling 2026-09-30): OnStartFiring drives the beam's visuals and
|
|
// audio, so starting it only for Execute to stop it a tick later would flash a beam that fires nothing.
|
|
if(!_firing && !_reloading && !FireControl.Refuses(this, Entity))
|
|
{
|
|
_firing = true;
|
|
OnStartFiring?.Invoke();
|
|
}
|
|
}
|
|
|
|
public override void Deactivate()
|
|
{
|
|
if (_firing)
|
|
{
|
|
_firing = false;
|
|
OnStopFiring?.Invoke();
|
|
}
|
|
}
|
|
} |