Imported from mattemoore/MonoGame_Learning (
.kilo/skills/create-weapon/SKILL.md). Install upstream withnpx skills add mattemoore/MonoGame_Learning --skill create-weapon. Copyright stays with the author.
Create Weapon Wizard
Walk the user through adding a melee weapon to this MonoGame project, OR replacing an existing weapon's placeholder art with real art. Drive the wizard interactively — one question at a time, recommending defaults (like the grill-me style). Explore the codebase before proposing so the wizard matches current structure; the bat weapon (BatWeapon.cs, BatSprite.cs, bat-texture.png) is the reference implementation to mirror.
0. Identify the goal
Ask the user to pick one scenario (recommend defaults):
- New weapon, no art yet — generate placeholder art now (assistant runs the companion
placeholder_gen.py), user swaps real art in later. - New weapon, user has real art — user places art files, assistant wires the rest.
- Replace an existing weapon's placeholder/art (e.g. the bat) — user supplies the new files; helper keeps frame count/naming or updates code and JSON accordingly.
Then gather the weapon spec before touching anything:
- Display name and asset slug (kebab-case), e.g.
Pipe→pipe. Class names derive from the PascalCase name (PipeSprite,PipeWeapon). - Swing frame count (
FrameCount). Default 4 — must equal both the player attack animation frame count used bySwingMove.AnimationKey(checkPlayerSprite.AnimationAttack1etc.) and the JSON frame run, and theSwingAnchors/hitbox frames must stay inside it. - Damage,
AttackStrength(Light/Medium/Heavy),AttackSfx/ImpactSfxkeys (seeAudioManifest.cs/SfxId). - Which
PlayerSprite.AnimationAttack*the swing overlays (defaultAnimationAttack1). SwingAnchorsper frame +CarryAnchor(defaults fromBatWeapon.cs:33as a starting point; fine-tune after in-game check).FrameHitboxes— frames +Offset/Sizeper frame (bat: frames 2–3 only at swing apex).- Whether it drops as a pickup, spawns on minions, or both (drives
LevelContentkey +Level1.csspawns +LevelEntityFactory.CreatePickup). - Cross-check every choice against the ORIGINAL: after wiring, run the game (see Phase 6) and tune anchors, hitboxes, and frame timing visually.
1. Art intake
Pick one branch based on Phase 0.
1a. User supplies real art
Instruct the user to drop these files into MonoGameLearning.Game/Content/images/ (relative to that folder):
<slug>-texture.png— the swing animation strip/atlas texture.<slug>.json— TexturePacker JSON atlas inmonogame-extendeddataformat, with frames named<slug>-NN(zero-padded,NNstarting at00) in swing order, one named frame per sheet region. Copy region shapes fromimages/bat.jsonexactly.<slug>-pickup.png— a single static pickup icon texture (likebat-pickup.png).
Optional but encouraged: keep original sources under MonoGameLearning.Game/Sources/<Name>/ and pack the atlas with TexturePacker (see Sources/ examples). The JSON must be generated with dataformat: monogame-extended or the pipeline import will fail.
If the user has a .achj animation-chain file from their sprite tool instead of a hand-made atlas, run Utils/achj_to_monogame_extended.py to produce the <slug>.json from it — it maps each chain to a <slug>-<chain>-NN frame run, emits the matching SpriteAnimationDef lines, and notes any flips/offsets the atlas format can't hold. Remind the user to place the source texture next to the JSON under the referenced name (<slug>-texture.png).
WAIT for the user to place the files before continuing. Then validate as the assistant:
- Every filename has no path prefix or spaces — filenames ARE the content keys (
images/<slug>). - The JSON parses, lists exactly
FrameCountframes named<slug>-<NN>in swing order, and the texture exists at the referenced width/height. - Note: if you are replacing an existing weapon's placeholder (e.g. the bat), the user's JSON may change the sheet region layout but MUST keep frame names
<slug>-NNand the sameFrameCount(or you updateFrameCount+SwingAnchors+FrameHitboxesto match).
1b. No art available (default for a new weapon)
Run the companion script (kept in the repo's Utils/ tools folder) to synthesize a placeholder sheet, JSON atlas, and pickup icon straight into the content folder:
python3 Utils/placeholder_gen.py <slug> MonoGameLearning.Game/Content/images [frames=N] [frame_w=W] [frame_h=H]
Defaults match the bat: 4 frames of 12x40. Tell the user these are temporary placeholders they can replace later (re-run Phase 1/2/6 after swapping in real files, keeping frame count/naming to avoid code churn).
2. Register assets in the content pipeline
Add the three build entries to MonoGameLearning.Game/Content/Content.mgcb, mirroring the bat block at lines 77–104:
#begin images/<slug>.json
/importer:TexturePackerJsonImporter
/processor:TexturePackerProcessor
/build:images/<slug>.json
#begin images/<slug>-texture.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/<slug>-texture.png
#begin images/<slug>-pickup.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/<slug>-pickup.png
A normal dotnet build rebuilds these through MonoGame.Content.Builder.Task.
3. Create the sprite asset class
Create MonoGameLearning.Game/AnimatedSprites/<Name>Sprite.cs mirroring BatSprite.cs (single non-looping "swing" animation):
using Microsoft.Xna.Framework.Content;
using MonoGame.Extended.Graphics;
using MonoGameLearning.Core.Rendering;
namespace MonoGameLearning.Game.AnimatedSprites;
public static class PipeSprite
{
public const string AnimationSwing = "swing";
private const int FrameCount = 4;
private static readonly SpriteSheetAsset Asset = new(
"pipe", "images/pipe",
new SpriteAnimationDef(AnimationSwing, "pipe", FrameCount, false));
public static SpriteSheet Sheet => Asset.Sheet;
public static void Load(ContentManager content) => Asset.Load(content);
public static AnimatedSprite Create() => Asset.Create(AnimationSwing);
}
The SpriteSheetAsset first argument is the SpriteSheet display name (used in load error messages); the Prefix ("pipe") must match the JSON frame-name prefix, and FrameCount must match the JSON frame run exactly. Never fewer SwingAnchors than there are swing frames.
4. Create the weapon definition class
Create MonoGameLearning.Game/Weapons/<Name>Weapon.cs mirroring BatWeapon.cs:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using MonoGameLearning.Core.Audio;
using MonoGameLearning.Core.Combat;
using MonoGameLearning.Core.Rendering;
using MonoGameLearning.Game.AnimatedSprites;
using MonoGameLearning.Game.Levels;
namespace MonoGameLearning.Game.Weapons;
public static class PipeWeapon
{
private static readonly StaticTextureAsset PickupTexture = new("images/pipe-pickup");
public static readonly MeleeWeaponDef Pipe = new()
{
Name = "Pipe",
SwingMove = new()
{
AnimationKey = PlayerSprite.AnimationAttack1,
Damage = 6,
Strength = AttackStrength.Light,
AttackSfx = SfxId.AttackSwing1,
ImpactSfx = SfxId.HitHeavy,
FrameHitboxes = new()
{
[2] = [new() { Offset = new Vector2(45, 0), Size = new Point(70, 45) }],
[3] = [new() { Offset = new Vector2(45, 0), Size = new Point(70, 45) }],
}
},
SwingAnimation = PipeSprite.AnimationSwing,
CarryAnchor = new Vector2(20, 0),
SwingAnchors = [new Vector2(12, -15), new Vector2(25, -4), new Vector2(34, 0), new Vector2(30, -2)],
};
public static MeleeWeaponDef Get(string key) => key switch
{
LevelContent.Pipe => Pipe,
_ => throw new ArgumentException($"Unknown weapon: {key}", nameof(key)),
};
public static void Load(ContentManager content)
{
PickupTexture.Load(content);
PipeSprite.Load(content);
Pipe.Texture = PickupTexture.Texture;
Pipe.Sheet = PipeSprite.Sheet;
}
}
Key rules from the existing implementation:
SwingMove.AnimationKeymust be one ofPlayerSprite.AnimationAttack*— the swing overlay is frame-stepped against that animation, andFrameCountmust equal that animation's frame count (attack1= 4).BatWeapon.cs:33holds the reference anchors; reuse them as a starting point then re-tune in-game.- Hitboxes fire at swing apex (frames 2–3) only — see the bat pattern.
MeleeWeaponDefTODO item 4 (AGENTS.md): assertSwingAnchors.Length <= Sheetframe count in Debug withDebug.Assert.- Naming: one static
MeleeWeaponDefper weapon, referenced via aGet(string key)dispatcher andLevelContentconstant.
5. Wire it into the game
Add the LevelContent key (so pools/levels can reference it):
// MonoGameLearning.Game/Levels/LevelContent.cs
public const string Pipe = "Pipe";
Then wire the pieces in this order:
-
GameLoop.cs:- In
LoadContent(), afterBatWeapon.Load(Content);:PipeWeapon.Load(Content); - Pass the def to the
LevelEntityFactoryconstructor (line ~140) as an extra argument.
- In
-
LevelEntityFactory.cs:-
Add the constructor parameter (e.g.
MeleeWeaponDef pipeWeapon) and store it. -
Add a case in
CreatePickup:LevelContent.Pipe => new WeaponPickupEntity(def.Type, def.Position, pipeWeapon),
-
-
Weapon resolution dispatcher — extend the existing
getWeapondelegate. InGameLoop.InitLevelSystemsit isBatWeapon.Get; switch it to a combined dispatcher that resolves every registered weapon key (bat + your new one), OR renameBatWeapon.Getto a generalWeaponCatalog.Getrespected by bothGameLoopandTestLevelContent. Whichever you pick, keep ONE switch that keys onLevelContent.*so wrenching in future weapons is a single case add. -
Level1.cs(only if the weapon should appear in the level): add aPickupSpawnDef(LevelContent.Pipe, ...)to thePickupslist, and/or aWeapon: LevelContent.Pipeon a waveEnemySpawnDef, and/or aDropsentry on a prop. -
Tests — mirror the bat coverage:
LevelDirectorTests.TestLevelContent: addLevelContent.Pipe => PipeWeapon.PipetoGetWeaponand aPipecase toCreatePickup.- Add weapon-specific tests modeled on
BatSwingSyncTests.csandMeleeWeaponTests.cs(equip/unequip with and without a Sheet, anchor/frame resolution at rest vs attacking, hitbox registration on the swing frames). - If you changed the dispatcher shape, update existing tests to the new
Get.
6. Verify the wizard close
Nothing is done until the game actually shows it:
dotnet build --warnaserrordotnet test- Run
dotnet run --project MonoGameLearning.Game/MonoGameLearning.Game.csproj, equip the weapon from the pickup, and attack. Confirm: the overlay sprite tracks the player arm sweep (no lag/desync), swings apex around attack frames 2–3, hitboxes appear at the right frames, the dropped pickup icon is the-pickup.pngtexture, and facing-left flips correctly. - Update
MANUAL_TESTING.mdrows (combat/pickups sections) to include the new weapon so the manual run covers it.
Pitfalls to watch
- The swing overlay is FRAME-STEPPED (
CombatActorBase.RenderWeaponOverlaycallsSetFrame), NOT time-driven — soFrameCountandSwingAnchorsMUST match the player attack animation's frame count, or the overlay desyncs from the arm.SetFramealone does not refreshTextureRegion(see AGENTS.md pitfall) — this is handled inCombatActorBase, don't add a fix for it. - The JSON frame names are content keys:
images/<slug>content path,<slug>-NNframe names, and theSpriteAnimationDefprefix must all line up, or Content loading throws. - Do not add a new abstraction for weapon resolution when one switch keyed on
LevelContent.*in a singleGetsuffices. - Replacing placeholder art later: keep frame count + naming so only the two PNGs and the JSON change; if the new art's frames differ in count, update
FrameCount,SwingAnchors, andFrameHitboxesin the same change and re-run Phase 6. - The pickup icon is a plain texture (
StaticTextureAsset), never part of the animation atlas.