From 35ef321ff3f904c623bee70c3a1dd5477767c8d6 Mon Sep 17 00:00:00 2001 From: soul Date: Fri, 23 Oct 2026 02:45:36 +0200 Subject: [PATCH] A plain InstantWeapon is semi-automatic; AutoWeapon repeats while held; the shortest semi-auto cooldown is a named constant with a catalog check; auto-guns command Co-Authored-By: Claude Sonnet 5.5 --- .../ServerShared/Behaviors/AutoWeapon.cs | 5 ++- .../ServerShared/Behaviors/InstantWeapon.cs | 24 +++++++++--- tools/AetherDb/AutoGunsCatalog.cs | 37 +++++++++++++++++++ tools/AetherDb/Program.cs | 3 +- 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tools/AetherDb/AutoGunsCatalog.cs diff --git a/Assets/Scripts/ServerShared/Behaviors/AutoWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/AutoWeapon.cs index b408f8079..398a1bbe1 100644 --- a/Assets/Scripts/ServerShared/Behaviors/AutoWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/AutoWeapon.cs @@ -15,9 +15,12 @@ public class AutoWeaponData : InstantWeaponData } } +// A fully automatic gun: it fires every round its cooldown allows while its trigger is held, from the press (Activate) +// to the let-go (Deactivate). A plain InstantWeapon is semi-automatic. public class AutoWeapon : InstantWeapon { - + protected override bool Repeats => _firing; + public AutoWeapon(InstantWeaponData data, EquippedItem item) : base(data, item) { } public AutoWeapon(InstantWeaponData data, ConsumableItemEffect item) : base(data, item) { } } diff --git a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs index 47b61ddf4..c58783097 100644 --- a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs @@ -31,6 +31,21 @@ public class InstantWeaponData : WeaponData return new InstantWeapon(this, item); } + // The shortest cooldown a plain (semi-automatic) gun may be authored with, 0.1 s: about ten presses a second. A faster one + // is a fire-as-fast-as-you-can-press mechanic, unfriendly to accessibility and a special case for the AI (ruling + // fire-mode-is-the-design); a gun that cycles faster is an AutoWeapon. + public const float ShortestSemiAutoCooldown = .1f; + + // The plain guns of a catalog that cycle faster than a finger should have to: one fault line per gun. + public static IEnumerable SemiAutoFaults(IEnumerable catalog) + { + foreach (var gear in catalog) + foreach (var behavior in gear.Behaviors) + if (behavior.GetType() == typeof(InstantWeaponData) && behavior is InstantWeaponData plain && + Math.Min(plain.Cooldown.Min, plain.Cooldown.Max) < ShortestSemiAutoCooldown) + yield return $"{gear.Name}: a plain gun with a {Math.Min(plain.Cooldown.Min, plain.Cooldown.Max):0.###} s cooldown, below {ShortestSemiAutoCooldown} s; make it an AutoWeapon or slow it"; + } + // The one formula of an instant weapon's damage per second at a range; the live weapon calls it with its evaluated // stats, the design with its rated ones. A burst's round carries its share of the totals: PerRound, the one divisor // UpdateStats and the rated read both use. @@ -92,11 +107,10 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo // manager can bind its presentation to the one shot it belongs to instead of applying damage itself. public event Action OnFire; - // Whether the trigger is down, so a cooldown that ends inside a step starts the next burst inside it. The trigger is the - // weapon's own state, set by Activate and cleared by Deactivate (a player's press and let-go, an agent's pull and - // release), never inferred from how often it was pulled: a tap fires one burst, and a held trigger fires every round - // its cooldown allows, at any step length. - private bool Repeats => _firing; + // Whether a down trigger keeps firing: a plain gun is semi-automatic, one burst per press (a held key does not repeat, and + // an agent presses it again each time it is ready); an AutoWeapon repeats while its trigger is held, every round its + // cooldown allows at any step length. Fire mode is authored by the kind of the weapon (ruling fire-mode-is-the-design). + protected virtual bool Repeats => false; public virtual void ResetEvents() { diff --git a/tools/AetherDb/AutoGunsCatalog.cs b/tools/AetherDb/AutoGunsCatalog.cs new file mode 100644 index 000000000..4292d745b --- /dev/null +++ b/tools/AetherDb/AutoGunsCatalog.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; +using System.Linq; +using GameCult.Caching; +using MessagePack; + +// Makes the named plain guns fully automatic (ruling fire-mode-is-the-design): each one's InstantWeaponData becomes an +// AutoWeaponData carrying every authored stat unchanged, so cadence and damage do not move. A gun that is already an +// AutoWeapon (or any other kind) is left alone, so a rerun writes nothing. Dry run unless apply. +public static class AutoGunsCatalog +{ + public static readonly string[] Guns = { "Slug Rifle", "Long Slug Rifle" }; + + public static int Run(CultCache cache, bool apply, TextWriter output) + { + var changed = new System.Collections.Generic.List(); + foreach (var name in Guns) + { + var gear = cache.GetAll().SingleOrDefault(g => g.Name == name); + if (gear == null) { output.WriteLine($"{name}: no such gear, skipped"); continue; } + var index = gear.Behaviors.FindIndex(b => b.GetType() == typeof(InstantWeaponData)); + if (index < 0) { output.WriteLine($"{name}: no plain gun, kept"); continue; } + var plain = (InstantWeaponData) gear.Behaviors[index]; + var bytes = MessagePackSerializer.Serialize(typeof(InstantWeaponData), plain); + gear.Behaviors[index] = MessagePackSerializer.Deserialize(bytes); + output.WriteLine($"{name}: InstantWeaponData -> AutoWeaponData (cooldown {plain.Cooldown.Max:0.###} s)"); + changed.Add(gear); + } + + if (!apply) { output.WriteLine("\nDry run. Pass \"apply\" to author them."); return 0; } + if (changed.Count == 0) { output.WriteLine("Nothing to author."); return 0; } + foreach (var gear in changed) CultRecordRefs.Validate(gear); + cache.Commit(batch => { foreach (var gear in changed) batch.Upsert(typeof(GearData), gear, cache.RefOf(gear).Key); }); + output.WriteLine($"\nAuthored {changed.Count} guns in Aetheria.cc"); + return 0; + } +} diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 572d6f06f..6f289b537 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -76,9 +76,10 @@ public static class Program if (apply) db.Cache.FlushAsync().GetAwaiter().GetResult(); return code; } + case "auto-guns": return AutoGunsCatalog.Run(AetherDb.Open(catalogWritable: args.Contains("apply")).Cache, args.Contains("apply"), Console.Out); case "doctrine-catalog": return DoctrineCatalog.Run(AetherDb.Open(catalogWritable: args.Contains("apply")).Cache, args.Contains("apply"), Console.Out); default: - Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], loadout-commit [,,,[@]]... [--group ,...]... [--replace] [--boss-of ], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective ] [--end ], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], minor-power-factions [apply], place-names [apply], product-lines weapons|minor-powers|gear|tiers [apply], doctrine-catalog [apply], assembly-from-maker [apply], station-kinds [hull \"\" ] [apply]"); + Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], loadout-commit [,,,[@]]... [--group ,...]... [--replace] [--boss-of ], save, settings, settings-dump, dangling [clear ]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective ] [--end ], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], minor-power-factions [apply], place-names [apply], product-lines weapons|minor-powers|gear|tiers [apply], doctrine-catalog [apply], auto-guns [apply], assembly-from-maker [apply], station-kinds [hull \"\" ] [apply]"); return 1; } }