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 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-23 02:45:36 +02:00
commit 35ef321ff3
4 changed files with 62 additions and 7 deletions

View file

@ -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) { }
}

View file

@ -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<string> SemiAutoFaults(IEnumerable<GearData> 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<int> 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()
{

View file

@ -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<GearData>();
foreach (var name in Guns)
{
var gear = cache.GetAll<GearData>().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<AutoWeaponData>(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;
}
}

View file

@ -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 <name> <hull product> [<x>,<y>,<rotation>,<design>[@<product>]]... [--group <i>,<j>...]... [--replace] [--boss-of <faction>], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], 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 \"<name>\" <kind>] [apply]");
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], loadout-commit <name> <hull product> [<x>,<y>,<rotation>,<design>[@<product>]]... [--group <i>,<j>...]... [--replace] [--boss-of <faction>], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], 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 \"<name>\" <kind>] [apply]");
return 1;
}
}