Defines DecorDef and DecorPiece for aquarium decor. DecorDef is a data class with many predefined static entries (props, backdrops, substrates), plus catalog and lookup helpers. DecorPiece represents an instance placed in a tank with position, velocity, held state and computed shortcuts to its Def and IsMoving state.
namespace NoChillquarium;
/// <summary>
/// Placeable tank ornaments. Sizes are tank pixels — props should dwarf fish
/// (goldfish ≈ 52×36). GroundSink buries the sprite base into gravel so plants
/// don't look like floating islands.
/// </summary>
public sealed class DecorDef
{
public string Id { get; init; }
public string Name { get; init; }
public string Note { get; init; }
public double Cost { get; init; }
public float Width { get; init; }
public float Height { get; init; }
public string CssClass { get; init; }
public string SpritePath { get; init; }
public float ComfortBonus { get; init; }
public float ChaosOnPlace { get; init; }
/// <summary>How many px of the sprite bottom sink into the gravel band.</summary>
public float GroundSink { get; init; }
/// <summary>Full-bleed tank back-glass poster. One per tank, not a gravel prop.</summary>
public bool IsBackdrop { get; init; }
/// <summary>Tank-floor gravel / sand skin. One per tank, not a prop.</summary>
public bool IsSubstrate { get; init; }
public bool HasSprite => SpriteCatalog.Has( Id );
public string SpriteId => Id;
public static readonly DecorDef Plant = new()
{
Id = "plant_green",
Name = "Plastic Plant",
Note = "chill greenery",
Cost = 10,
Width = 140,
Height = 320,
CssClass = "decor-plant",
SpritePath = "plant_green",
ComfortBonus = 0.05f,
// Dirt mound in the art must sit inside the gravel strip
GroundSink = 48f
};
public static readonly DecorDef Treasure = new()
{
Id = "treasure_chest",
Name = "Treasure Chest",
Note = "bubbles optional",
Cost = 45,
// Art ~91×96 — keep a solid grab box without a huge empty frame.
Width = 140,
Height = 120,
CssClass = "decor-chest",
SpritePath = "treasure_chest",
ComfortBonus = 0.06f,
GroundSink = 10f
};
public static readonly DecorDef BubbleWand = new()
{
Id = "bubble_wand",
Name = "Bubble Wand",
Note = "party mode",
Cost = 18,
Width = 90,
Height = 340,
CssClass = "decor-bubbler",
SpritePath = "bubble_wand",
ComfortBonus = 0.04f,
GroundSink = 16f
};
public static readonly DecorDef FortyOz = new()
{
Id = "forty_oz",
Name = "40oz Bottle",
Note = "no-chill centerpiece",
Cost = 22,
// Art is 96×96 — tall-ish tank prop, not a skyscraper hitbox.
Width = 96,
Height = 160,
CssClass = "decor-forty",
SpritePath = "forty_oz",
ComfortBonus = 0.03f,
ChaosOnPlace = 2f,
GroundSink = 12f
};
public static readonly DecorDef Handcuffs = new()
{
Id = "handcuffs_skeleton",
Name = "Cuffs + Bone Hand",
Note = "don't ask",
Cost = 35,
Width = 170,
Height = 120,
CssClass = "decor-cuffs",
SpritePath = "handcuffs_skeleton",
ComfortBonus = 0.02f,
ChaosOnPlace = 4f,
GroundSink = 12f
};
public static readonly DecorDef Boot = new()
{
Id = "old_boot",
Name = "Old Boot",
Note = "classic dumpster chic",
Cost = 10,
// Source art is 96×96 — keep hitbox tight so grab/throw feels right.
Width = 128,
Height = 128,
CssClass = "decor-boot",
SpritePath = "old_boot",
ComfortBonus = 0.02f,
ChaosOnPlace = 1f,
GroundSink = 10f
};
public static readonly DecorDef Meds = new()
{
Id = "medicine_bottles",
Name = "Medicine Bottles",
Note = "vibes only (for now)",
Cost = 16,
Width = 120,
Height = 120,
CssClass = "decor-meds",
SpritePath = "medicine_bottles",
ComfortBonus = 0.06f,
GroundSink = 10f
};
public static readonly DecorDef DogeStatue = new()
{
Id = "doge_statue",
Name = "Doge Statue",
Note = "much worship",
Cost = 150,
Width = 180,
Height = 240,
CssClass = "decor-doge",
SpritePath = "doge_statue",
ComfortBonus = 0.12f,
GroundSink = 14f
};
public static readonly DecorDef M80Crate = new()
{
Id = "m80_crate",
Name = "M80 Crate",
Note = "decorative. mostly.",
Cost = 30,
Width = 180,
Height = 140,
CssClass = "decor-m80crate",
SpritePath = "m80_crate",
ComfortBonus = 0.01f,
ChaosOnPlace = 3f,
GroundSink = 12f
};
// CSS-only props (no sprite) — variety for tank flex / hybrids later
public static readonly DecorDef Rock = new()
{
Id = "rock_pile",
Name = "Rock Pile",
Note = "boring on purpose",
Cost = 8,
Width = 110,
Height = 70,
CssClass = "decor-rock",
SpritePath = "rock_pile",
ComfortBonus = 0.02f,
GroundSink = 14f
};
public static readonly DecorDef Castle = new()
{
Id = "plastic_castle",
Name = "Plastic Castle",
Note = "every tank's lie",
Cost = 28,
Width = 130,
Height = 160,
CssClass = "decor-castle",
SpritePath = "plastic_castle",
ComfortBonus = 0.05f,
GroundSink = 12f
};
public static readonly DecorDef Anchor = new()
{
Id = "rusty_anchor",
Name = "Rusty Anchor",
Note = "salt flex",
Cost = 38,
Width = 90,
Height = 140,
CssClass = "decor-anchor",
SpritePath = "rusty_anchor",
ComfortBonus = 0.04f,
ChaosOnPlace = 1f,
GroundSink = 10f
};
public static readonly DecorDef RoadCone = new()
{
Id = "road_cone",
Name = "Road Cone",
Note = "construction chic",
Cost = 14,
Width = 70,
Height = 100,
CssClass = "decor-cone",
SpritePath = "road_cone",
ComfortBonus = 0.02f,
ChaosOnPlace = 1.5f,
GroundSink = 8f
};
public static readonly DecorDef LavaLamp = new()
{
Id = "lava_lamp",
Name = "Lava Lamp",
Note = "vibes only",
Cost = 55,
Width = 70,
Height = 150,
CssClass = "decor-lava",
SpritePath = "lava_lamp",
ComfortBonus = 0.07f,
GroundSink = 10f
};
public static readonly DecorDef PizzaBox = new()
{
Id = "pizza_box",
Name = "Pizza Box",
Note = "ecosystem",
Cost = 12,
Width = 120,
Height = 40,
CssClass = "decor-pizza",
SpritePath = "pizza_box",
ComfortBonus = 0.01f,
ChaosOnPlace = 2f,
GroundSink = 6f
};
public static readonly DecorDef Trophy = new()
{
Id = "cheap_trophy",
Name = "Cheap Trophy",
Note = "participation",
Cost = 70,
Width = 80,
Height = 110,
CssClass = "decor-trophy",
SpritePath = "cheap_trophy",
ComfortBonus = 0.08f,
GroundSink = 8f
};
public static readonly DecorDef FakeCoral = new()
{
Id = "fake_coral",
Name = "Fake Coral",
Note = "salt starter kit",
Cost = 32,
Width = 120,
Height = 100,
CssClass = "decor-coral",
SpritePath = "fake_coral",
ComfortBonus = 0.05f,
GroundSink = 12f
};
public static readonly DecorDef BackdropNone = new()
{
Id = "bg_none",
Name = "Plain Glass",
Note = "peel the poster",
Cost = 0,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_none",
IsBackdrop = true
};
public static readonly DecorDef BackdropNewspaper = new()
{
Id = "bg_newspaper",
Name = "Newspaper",
Note = "lot sale this weekend",
Cost = 8,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_newspaper",
ComfortBonus = 0.02f,
ChaosOnPlace = 0.5f,
IsBackdrop = true
};
public static readonly DecorDef BackdropMagazines = new()
{
Id = "bg_magazines",
Name = "Pinup Mags",
Note = "adults only · tanked babes",
Cost = 18,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_magazines",
ComfortBonus = 0.03f,
ChaosOnPlace = 2.5f,
IsBackdrop = true
};
public static readonly DecorDef BackdropAqua = new()
{
Id = "bg_aqua_poster",
Name = "Castle Poster",
Note = "pet store $3.99",
Cost = 16,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_aqua_poster",
ComfortBonus = 0.06f,
IsBackdrop = true
};
public static readonly DecorDef BackdropReef = new()
{
Id = "bg_reef_poster",
Name = "Reef Poster",
Note = "generic paradise",
Cost = 22,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_reef_poster",
ComfortBonus = 0.07f,
IsBackdrop = true
};
public static readonly DecorDef BackdropValueMenu = new()
{
Id = "bg_value_menu",
Name = "Value Meal Sign",
Note = "$1 · tax not included",
Cost = 14,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_value_menu",
ComfortBonus = 0.03f,
ChaosOnPlace = 1.5f,
IsBackdrop = true
};
public static readonly DecorDef BackdropGreek = new()
{
Id = "bg_greek_ruins",
Name = "Greek Ruins",
Note = "acropolis aquarium",
Cost = 28,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_greek_ruins",
ComfortBonus = 0.08f,
IsBackdrop = true
};
public static readonly DecorDef BackdropWood = new()
{
Id = "bg_wood_panel",
Name = "Wood Paneling",
Note = "trailer chic",
Cost = 12,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_wood_panel",
ComfortBonus = 0.04f,
IsBackdrop = true
};
public static readonly DecorDef BackdropLeopard = new()
{
Id = "bg_leopard",
Name = "Leopard Towel",
Note = "motel souvenir",
Cost = 20,
Width = 1280,
Height = 620,
CssClass = "decor-backdrop",
SpritePath = "bg_leopard",
ComfortBonus = 0.04f,
ChaosOnPlace = 1.2f,
IsBackdrop = true
};
public static readonly DecorDef GravelNone = new()
{
Id = "gravel_none",
Name = "Lot Dirt",
Note = "stock brown",
Cost = 0,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_none",
IsSubstrate = true
};
public static readonly DecorDef GravelBrown = new()
{
Id = "gravel_brown",
Name = "River Gravel",
Note = "pet-store classic",
Cost = 6,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_brown",
ComfortBonus = 0.02f,
IsSubstrate = true
};
public static readonly DecorDef GravelNeon = new()
{
Id = "gravel_neon",
Name = "Hot Pink Gravel",
Note = "mall neon",
Cost = 14,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_neon",
ComfortBonus = 0.03f,
ChaosOnPlace = 1.2f,
IsSubstrate = true
};
public static readonly DecorDef GravelBlack = new()
{
Id = "gravel_black",
Name = "Onyx Gravel",
Note = "funeral chic",
Cost = 12,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_black",
ComfortBonus = 0.03f,
IsSubstrate = true
};
public static readonly DecorDef GravelBlue = new()
{
Id = "gravel_blue",
Name = "Caribbean Sand",
Note = "crushed glass",
Cost = 16,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_blue",
ComfortBonus = 0.04f,
IsSubstrate = true
};
public static readonly DecorDef GravelGold = new()
{
Id = "gravel_gold",
Name = "Gold Gravel",
Note = "much wow floor",
Cost = 22,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_gold",
ComfortBonus = 0.05f,
IsSubstrate = true
};
public static readonly DecorDef GravelWhite = new()
{
Id = "gravel_white",
Name = "Sugar Sand",
Note = "bingo-hall pale",
Cost = 10,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_white",
ComfortBonus = 0.03f,
IsSubstrate = true
};
public static readonly DecorDef GravelRainbow = new()
{
Id = "gravel_rainbow",
Name = "Rainbow Gravel",
Note = "party mix",
Cost = 18,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_rainbow",
ComfortBonus = 0.04f,
ChaosOnPlace = 0.8f,
IsSubstrate = true
};
public static readonly DecorDef GravelSlime = new()
{
Id = "gravel_slime",
Name = "Slime Gravel",
Note = "pipe runoff",
Cost = 15,
Width = 1280,
Height = 42,
CssClass = "decor-gravel",
SpritePath = "gravel_slime",
ComfortBonus = 0.02f,
ChaosOnPlace = 1.4f,
IsSubstrate = true
};
public static IReadOnlyList<DecorDef> Catalog { get; } = new[]
{
Rock,
Boot,
Plant,
PizzaBox,
RoadCone,
BubbleWand,
Meds,
FortyOz,
Castle,
FakeCoral,
Treasure,
M80Crate,
Anchor,
Handcuffs,
LavaLamp,
Trophy,
DogeStatue,
BackdropNone,
BackdropNewspaper,
BackdropMagazines,
BackdropAqua,
BackdropReef,
BackdropValueMenu,
BackdropGreek,
BackdropWood,
BackdropLeopard,
GravelNone,
GravelBrown,
GravelNeon,
GravelBlack,
GravelBlue,
GravelGold,
GravelWhite,
GravelRainbow,
GravelSlime
};
public static IEnumerable<DecorDef> Props =>
Catalog.Where( d => !d.IsBackdrop && !d.IsSubstrate );
public static IEnumerable<DecorDef> Backdrops =>
Catalog.Where( d => d.IsBackdrop );
public static IEnumerable<DecorDef> Substrates =>
Catalog.Where( d => d.IsSubstrate );
public static DecorDef Find( string id ) =>
Catalog.FirstOrDefault( d => d.Id == id ) ?? Plant;
public static DecorDef FindBackdrop( string id )
{
if ( string.IsNullOrWhiteSpace( id ) || id == BackdropNone.Id )
return null;
return Catalog.FirstOrDefault( d => d.IsBackdrop && d.Id == id );
}
public static DecorDef FindGravel( string id )
{
if ( string.IsNullOrWhiteSpace( id ) || id == GravelNone.Id )
return null;
return Catalog.FirstOrDefault( d => d.IsSubstrate && d.Id == id );
}
}
public sealed class DecorPiece
{
public string DefId { get; set; }
public float X { get; set; }
public float Y { get; set; }
/// <summary>Throw / tumble velocity (tank px/s). Zeroed when resting on gravel.</summary>
public float Vx { get; set; }
public float Vy { get; set; }
/// <summary>True while the player is dragging this piece.</summary>
public bool Held { get; set; }
/// <summary>Seconds after release where floor stick is relaxed so throws launch.</summary>
public float ThrowGrace { get; set; }
public DecorDef Def => DecorDef.Find( DefId );
public bool IsMoving =>
Held || ThrowGrace > 0f || MathF.Abs( Vx ) > 2f || MathF.Abs( Vy ) > 2f;
}