HUD pass, weapon and trinket rework, and font arrows

HUD:
- Hotbars fit a quarter of the screen, dropping to small then tiny slots
  (thinner outlines when tiny), with cooldown bars matching their size.
- HUD elements are drawn on their own layer and composited at one
  opacity: hotbar slots are see-through until hovered, the rest fade
  while hovered.
- Rival windows fit the right column in at most two rows, shrinking
  icons then font, then dropping icons for name, health and ping bars.
- Tooltips number equipped items' slots and say Recoil for weapons that
  only push back. Previews use arrows, spread uses degrees.
- Crosshair fills in and shakes with charge, and weapons shake after
  firing until their cooldown ends. DPS and speed meters off by default.

Gameplay:
- Dual Wield stacks: each copy fires another recently held weapon from
  a fanned off-hand, at its own level's cut.
- Flamethrower: no slowing, smaller fireball with some recoil, and an
  airblast that knocks others away.
- Iron Weights slam on any landing faster than a plain fall (platforms
  too), stacking radius and damage, and speeding fast falls.
- Stuck arrows are one-way ledges until they fade, matching their
  (slightly larger) hit size. Attacks now collide with slopes.
- Halved Tesla Coil, Recoil Coil, Diazepam, Shock Absorber, Feather and
  screenshake values.

Art and tools:
- Weapon and trinket icons merged into one sheet each (normal, small,
  tiny), cells in catalog order. Grimoire wisps stand upright.
- Fonts gain the degree sign and four arrows, decoded from UTF-8.
- Menu fighter preview walks at quarter speed.
- Unequip from either catalog tab by right click or gamepad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 16:43:45 -05:00
1 parent 6003175173
commit 6f112ab231
54 files changed
+1130 -3026

No files matched your search

+6 -1
View File
@@ -98,6 +98,11 @@
"{": {"x": 253, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 10}, "{": {"x": 253, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 10},
"|": {"x": 276, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 6}, "|": {"x": 276, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 6},
"}": {"x": 299, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 10}, "}": {"x": 299, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 10},
"~": {"x": 322, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14} "~": {"x": 322, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14},
"°": {"x": 345, "y": 145, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 10},
"←": {"x": 0, "y": 174, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14},
"↑": {"x": 23, "y": 174, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14},
"→": {"x": 46, "y": 174, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14},
"↓": {"x": 69, "y": 174, "w": 22, "h": 28, "dx": -2, "dy": 0, "advance": 14}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+6 -1
View File
@@ -98,6 +98,11 @@
"{": {"x": 374, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 15}, "{": {"x": 374, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 15},
"|": {"x": 408, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 9}, "|": {"x": 408, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 9},
"}": {"x": 442, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 15}, "}": {"x": 442, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 15},
"~": {"x": 476, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21} "~": {"x": 476, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21},
"°": {"x": 510, "y": 215, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 15},
"←": {"x": 0, "y": 258, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21},
"↑": {"x": 34, "y": 258, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21},
"→": {"x": 68, "y": 258, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21},
"↓": {"x": 102, "y": 258, "w": 33, "h": 42, "dx": -3, "dy": 0, "advance": 21}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+6 -1
View File
@@ -98,6 +98,11 @@
"{": {"x": 132, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 5}, "{": {"x": 132, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 5},
"|": {"x": 144, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 3}, "|": {"x": 144, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 3},
"}": {"x": 156, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 5}, "}": {"x": 156, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 5},
"~": {"x": 168, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7} "~": {"x": 168, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7},
"°": {"x": 180, "y": 75, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 5},
"←": {"x": 0, "y": 90, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7},
"↑": {"x": 12, "y": 90, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7},
"→": {"x": 24, "y": 90, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7},
"↓": {"x": 36, "y": 90, "w": 11, "h": 14, "dx": -1, "dy": 0, "advance": 7}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+6 -1
View File
@@ -97,6 +97,11 @@
"{": {"x": 66, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4}, "{": {"x": 66, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4},
"|": {"x": 72, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 2}, "|": {"x": 72, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 2},
"}": {"x": 78, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4}, "}": {"x": 78, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4},
"~": {"x": 84, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 5} "~": {"x": 84, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 5},
"°": {"x": 90, "y": 45, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 5},
"←": {"x": 0, "y": 54, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 5},
"↑": {"x": 6, "y": 54, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4},
"→": {"x": 12, "y": 54, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 5},
"↓": {"x": 18, "y": 54, "w": 5, "h": 8, "dx": 0, "dy": 0, "advance": 4}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 8.9 KiB

+23 -10
View File
@@ -6,13 +6,13 @@
"path": "effects.png", "path": "effects.png",
"regions": [ "regions": [
{"name": "Arrow", "crop": [5, 19], "size": [21, 9], "pivot": [10.5, 4.5], "origin": "center"}, {"name": "Arrow", "crop": [5, 19], "size": [21, 9], "pivot": [10.5, 4.5], "origin": "center"},
{"name": "Bullet", "crop": [43, 19], "size": [9, 9], "pivot": [4.5, 4.5], "origin": "center"}, {"name": "Bullet", "crop": [42, 19], "size": [16, 9], "pivot": [6.5, 4.5]},
{"name": "Chain", "crop": [75, 19], "size": [10, 10], "pivot": [5, 5], "origin": "center"}, {"name": "Chain", "crop": [75, 19], "size": [10, 10], "pivot": [5, 5], "origin": "center"},
{"name": "Flail Ball", "crop": [102, 14], "size": [19, 19], "pivot": [9.5, 9.5], "origin": "center"}, {"name": "Flail Ball", "crop": [102, 14], "size": [19, 19], "pivot": [9.5, 9.5], "origin": "center"},
{"name": "Flame 1", "crop": [9, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, {"name": "Flame 1", "crop": [10, 58], "size": [11, 21], "pivot": [5, 16]},
{"name": "Flame 2", "crop": [41, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, {"name": "Flame 2", "crop": [42, 58], "size": [11, 21], "pivot": [5, 16]},
{"name": "Flame 3", "crop": [73, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, {"name": "Flame 3", "crop": [74, 58], "size": [11, 21], "pivot": [5, 16]},
{"name": "Flame 4", "crop": [105, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, {"name": "Flame 4", "crop": [106, 58], "size": [11, 21], "pivot": [5, 16]},
{"name": "Mine", "crop": [7, 115], "size": [17, 10], "pivot": [8.5, 5], "origin": "center"}, {"name": "Mine", "crop": [7, 115], "size": [17, 10], "pivot": [8.5, 5], "origin": "center"},
{"name": "Rocket", "crop": [40, 115], "size": [15, 9], "pivot": [7.5, 4.5], "origin": "center"}, {"name": "Rocket", "crop": [40, 115], "size": [15, 9], "pivot": [7.5, 4.5], "origin": "center"},
{"name": "Slash", "crop": [65, 96], "size": [30, 48], "pivot": [15, 24], "origin": "center"}, {"name": "Slash", "crop": [65, 96], "size": [30, 48], "pivot": [15, 24], "origin": "center"},
@@ -20,10 +20,10 @@
{"name": "Whip Tip", "crop": [11, 163], "size": [9, 9], "pivot": [0, 4.5]}, {"name": "Whip Tip", "crop": [11, 163], "size": [9, 9], "pivot": [0, 4.5]},
{"name": "Punch", "crop": [34, 161], "size": [26, 14], "pivot": [19, 7]}, {"name": "Punch", "crop": [34, 161], "size": [26, 14], "pivot": [19, 7]},
{"name": "Uppercut", "crop": [64, 144], "size": [32, 48], "pivot": [17, 22]}, {"name": "Uppercut", "crop": [64, 144], "size": [32, 48], "pivot": [17, 22]},
{"name": "Wisp 1", "crop": [4, 193], "size": [24, 15], "pivot": [17, 7.5]}, {"name": "Wisp 1", "crop": [8, 196], "size": [15, 24], "pivot": [7.5, 17]},
{"name": "Wisp 2", "crop": [36, 193], "size": [24, 15], "pivot": [17, 7.5]}, {"name": "Wisp 2", "crop": [40, 196], "size": [15, 24], "pivot": [7.5, 17]},
{"name": "Wisp 3", "crop": [68, 193], "size": [24, 15], "pivot": [17, 7.5]}, {"name": "Wisp 3", "crop": [72, 196], "size": [15, 24], "pivot": [7.5, 17]},
{"name": "Wisp 4", "crop": [100, 193], "size": [24, 15], "pivot": [17, 7.5]} {"name": "Wisp 4", "crop": [104, 196], "size": [15, 24], "pivot": [7.5, 17]}
] ]
} }
], ],
@@ -57,6 +57,19 @@
"Whip Tip": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]}, "Whip Tip": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]},
"Punch": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]}, "Punch": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]},
"Uppercut": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]}, "Uppercut": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
"Wisp": {"length": 16, "layers": [{"layer": 0, "frames": [{"region": 15, "delay": 4}, {"region": 16, "delay": 4}, {"region": 17, "delay": 4}, {"region": 18, "delay": 4}]}]} "Wisp": {
"length": 16,
"layers": [
{
"layer": 0,
"frames": [
{"region": 15, "delay": 4},
{"region": 16, "delay": 4},
{"region": 17, "delay": 4},
{"region": 18, "delay": 4}
]
}
]
}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 8.9 KiB

+14
View File
@@ -58,6 +58,20 @@
} }
] ]
}, },
"Preview": {
"length": 16,
"layers": [
{
"layer": 0,
"frames": [
{"region": 1, "scale": [-100, 100], "delay": 4, "interpolation": "linear"},
{"region": 2, "scale": [-100, 100], "delay": 4, "interpolation": "linear"},
{"region": 3, "scale": [-100, 100], "delay": 4, "interpolation": "linear"},
{"region": 4, "scale": [-100, 100], "delay": 4, "interpolation": "linear"}
]
}
]
},
"Jump": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]}, "Jump": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]},
"Fall": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]}, "Fall": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]},
"Slide": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]}, "Slide": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 9.0 KiB

+130 -222
View File
@@ -7,26 +7,70 @@
"regions": [ "regions": [
{"name": "Gloves", "crop": [0, 0], "size": [24, 24], "origin": "center"}, {"name": "Gloves", "crop": [0, 0], "size": [24, 24], "origin": "center"},
{"name": "Energy Drink", "crop": [24, 0], "size": [24, 24], "origin": "center"}, {"name": "Energy Drink", "crop": [24, 0], "size": [24, 24], "origin": "center"},
{"name": "Accelerator", "crop": [48, 0], "size": [24, 24], "origin": "center"}, {"name": "Boxing Glove", "crop": [48, 0], "size": [24, 24], "origin": "center"},
{"name": "Enlarger", "crop": [72, 0], "size": [24, 24], "origin": "center"}, {"name": "Accelerator", "crop": [72, 0], "size": [24, 24], "origin": "center"},
{"name": "Boxing Glove", "crop": [96, 0], "size": [24, 24], "origin": "center"}, {"name": "Enlarger", "crop": [96, 0], "size": [24, 24], "origin": "center"},
{"name": "Splitter", "crop": [120, 0], "size": [24, 24], "origin": "center"}, {"name": "Diazepam", "crop": [120, 0], "size": [24, 24], "origin": "center"},
{"name": "Doubler", "crop": [0, 24], "size": [24, 24], "origin": "center"}, {"name": "Splitter", "crop": [0, 24], "size": [24, 24], "origin": "center"},
{"name": "Dual Wield", "crop": [24, 24], "size": [24, 24], "origin": "center"}, {"name": "Doubler", "crop": [24, 24], "size": [24, 24], "origin": "center"},
{"name": "Ricochet", "crop": [48, 24], "size": [24, 24], "origin": "center"}, {"name": "Dual Wield", "crop": [48, 24], "size": [24, 24], "origin": "center"},
{"name": "Tesla Coil", "crop": [72, 24], "size": [24, 24], "origin": "center"}, {"name": "Ricochet", "crop": [72, 24], "size": [24, 24], "origin": "center"},
{"name": "Boots", "crop": [96, 24], "size": [24, 24], "origin": "center"}, {"name": "Tesla Coil", "crop": [96, 24], "size": [24, 24], "origin": "center"},
{"name": "Speedometer", "crop": [120, 24], "size": [24, 24], "origin": "center"}, {"name": "Recoil Coil", "crop": [120, 24], "size": [24, 24], "origin": "center"},
{"name": "Shrinker", "crop": [0, 48], "size": [24, 24], "origin": "center"}, {"name": "Shock Absorber", "crop": [0, 48], "size": [24, 24], "origin": "center"},
{"name": "Wings", "crop": [24, 48], "size": [24, 24], "origin": "center"}, {"name": "Shrinker", "crop": [24, 48], "size": [24, 24], "origin": "center"},
{"name": "Feather", "crop": [48, 48], "size": [24, 24], "origin": "center"}, {"name": "Embiggener", "crop": [48, 48], "size": [24, 24], "origin": "center"},
{"name": "Iron Weights", "crop": [72, 48], "size": [24, 24], "origin": "center"}, {"name": "Pebble Helmet", "crop": [72, 48], "size": [24, 24], "origin": "center"},
{"name": "Climb Claw", "crop": [96, 48], "size": [24, 24], "origin": "center"}, {"name": "Boots", "crop": [96, 48], "size": [24, 24], "origin": "center"},
{"name": "Recoil Coil", "crop": [120, 48], "size": [24, 24], "origin": "center"}, {"name": "Speedometer", "crop": [120, 48], "size": [24, 24], "origin": "center"},
{"name": "Diazepam", "crop": [0, 72], "size": [24, 24], "origin": "center"}, {"name": "Wings", "crop": [0, 72], "size": [24, 24], "origin": "center"},
{"name": "Embiggener", "crop": [24, 72], "size": [24, 24], "origin": "center"}, {"name": "Feather", "crop": [24, 72], "size": [24, 24], "origin": "center"},
{"name": "Pebble Helmet", "crop": [48, 72], "size": [24, 24], "origin": "center"}, {"name": "Iron Weights", "crop": [48, 72], "size": [24, 24], "origin": "center"},
{"name": "Shock Absorber", "crop": [72, 72], "size": [24, 24], "origin": "center"} {"name": "Climb Claw", "crop": [72, 72], "size": [24, 24], "origin": "center"},
{"name": "Gloves Small", "crop": [0, 96], "size": [16, 16], "origin": "center"},
{"name": "Energy Drink Small", "crop": [16, 96], "size": [16, 16], "origin": "center"},
{"name": "Boxing Glove Small", "crop": [32, 96], "size": [16, 16], "origin": "center"},
{"name": "Accelerator Small", "crop": [48, 96], "size": [16, 16], "origin": "center"},
{"name": "Enlarger Small", "crop": [64, 96], "size": [16, 16], "origin": "center"},
{"name": "Diazepam Small", "crop": [80, 96], "size": [16, 16], "origin": "center"},
{"name": "Splitter Small", "crop": [0, 112], "size": [16, 16], "origin": "center"},
{"name": "Doubler Small", "crop": [16, 112], "size": [16, 16], "origin": "center"},
{"name": "Dual Wield Small", "crop": [32, 112], "size": [16, 16], "origin": "center"},
{"name": "Ricochet Small", "crop": [48, 112], "size": [16, 16], "origin": "center"},
{"name": "Tesla Coil Small", "crop": [64, 112], "size": [16, 16], "origin": "center"},
{"name": "Recoil Coil Small", "crop": [80, 112], "size": [16, 16], "origin": "center"},
{"name": "Shock Absorber Small", "crop": [0, 128], "size": [16, 16], "origin": "center"},
{"name": "Shrinker Small", "crop": [16, 128], "size": [16, 16], "origin": "center"},
{"name": "Embiggener Small", "crop": [32, 128], "size": [16, 16], "origin": "center"},
{"name": "Pebble Helmet Small", "crop": [48, 128], "size": [16, 16], "origin": "center"},
{"name": "Boots Small", "crop": [64, 128], "size": [16, 16], "origin": "center"},
{"name": "Speedometer Small", "crop": [80, 128], "size": [16, 16], "origin": "center"},
{"name": "Wings Small", "crop": [0, 144], "size": [16, 16], "origin": "center"},
{"name": "Feather Small", "crop": [16, 144], "size": [16, 16], "origin": "center"},
{"name": "Iron Weights Small", "crop": [32, 144], "size": [16, 16], "origin": "center"},
{"name": "Climb Claw Small", "crop": [48, 144], "size": [16, 16], "origin": "center"},
{"name": "Gloves Tiny", "crop": [0, 160], "size": [8, 8], "origin": "center"},
{"name": "Energy Drink Tiny", "crop": [8, 160], "size": [8, 8], "origin": "center"},
{"name": "Boxing Glove Tiny", "crop": [16, 160], "size": [8, 8], "origin": "center"},
{"name": "Accelerator Tiny", "crop": [24, 160], "size": [8, 8], "origin": "center"},
{"name": "Enlarger Tiny", "crop": [32, 160], "size": [8, 8], "origin": "center"},
{"name": "Diazepam Tiny", "crop": [40, 160], "size": [8, 8], "origin": "center"},
{"name": "Splitter Tiny", "crop": [0, 168], "size": [8, 8], "origin": "center"},
{"name": "Doubler Tiny", "crop": [8, 168], "size": [8, 8], "origin": "center"},
{"name": "Dual Wield Tiny", "crop": [16, 168], "size": [8, 8], "origin": "center"},
{"name": "Ricochet Tiny", "crop": [24, 168], "size": [8, 8], "origin": "center"},
{"name": "Tesla Coil Tiny", "crop": [32, 168], "size": [8, 8], "origin": "center"},
{"name": "Recoil Coil Tiny", "crop": [40, 168], "size": [8, 8], "origin": "center"},
{"name": "Shock Absorber Tiny", "crop": [0, 176], "size": [8, 8], "origin": "center"},
{"name": "Shrinker Tiny", "crop": [8, 176], "size": [8, 8], "origin": "center"},
{"name": "Embiggener Tiny", "crop": [16, 176], "size": [8, 8], "origin": "center"},
{"name": "Pebble Helmet Tiny", "crop": [24, 176], "size": [8, 8], "origin": "center"},
{"name": "Boots Tiny", "crop": [32, 176], "size": [8, 8], "origin": "center"},
{"name": "Speedometer Tiny", "crop": [40, 176], "size": [8, 8], "origin": "center"},
{"name": "Wings Tiny", "crop": [0, 184], "size": [8, 8], "origin": "center"},
{"name": "Feather Tiny", "crop": [8, 184], "size": [8, 8], "origin": "center"},
{"name": "Iron Weights Tiny", "crop": [16, 184], "size": [8, 8], "origin": "center"},
{"name": "Climb Claw Tiny", "crop": [24, 184], "size": [8, 8], "origin": "center"}
] ]
} }
], ],
@@ -35,207 +79,71 @@
], ],
"defaultAnimation": "Gloves", "defaultAnimation": "Gloves",
"animations": { "animations": {
"Gloves": { "Gloves": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]},
"length": 1, "Energy Drink": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]},
"layers": [ "Boxing Glove": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]},
{ "Accelerator": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]},
"layer": 0, "Enlarger": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]},
"frames": [ "Diazepam": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]},
{"region": 0} "Splitter": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]},
] "Doubler": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]},
} "Dual Wield": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]},
] "Ricochet": {"layers": [{"layer": 0, "frames": [{"region": 9}]}]},
}, "Tesla Coil": {"layers": [{"layer": 0, "frames": [{"region": 10}]}]},
"Energy Drink": { "Recoil Coil": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]},
"length": 1, "Shock Absorber": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]},
"layers": [ "Shrinker": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]},
{ "Embiggener": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
"layer": 0, "Pebble Helmet": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]},
"frames": [ "Boots": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]},
{"region": 1} "Speedometer": {"layers": [{"layer": 0, "frames": [{"region": 17}]}]},
] "Wings": {"layers": [{"layer": 0, "frames": [{"region": 18}]}]},
} "Feather": {"layers": [{"layer": 0, "frames": [{"region": 19}]}]},
] "Iron Weights": {"layers": [{"layer": 0, "frames": [{"region": 20}]}]},
}, "Climb Claw": {"layers": [{"layer": 0, "frames": [{"region": 21}]}]},
"Accelerator": { "Gloves Small": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]},
"length": 1, "Energy Drink Small": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]},
"layers": [ "Boxing Glove Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]},
{ "Accelerator Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]},
"layer": 0, "Enlarger Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]},
"frames": [ "Diazepam Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]},
{"region": 2} "Splitter Small": {"layers": [{"layer": 0, "frames": [{"region": 28}]}]},
] "Doubler Small": {"layers": [{"layer": 0, "frames": [{"region": 29}]}]},
} "Dual Wield Small": {"layers": [{"layer": 0, "frames": [{"region": 30}]}]},
] "Ricochet Small": {"layers": [{"layer": 0, "frames": [{"region": 31}]}]},
}, "Tesla Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 32}]}]},
"Enlarger": { "Recoil Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]},
"length": 1, "Shock Absorber Small": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]},
"layers": [ "Shrinker Small": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]},
{ "Embiggener Small": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]},
"layer": 0, "Pebble Helmet Small": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]},
"frames": [ "Boots Small": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]},
{"region": 3} "Speedometer Small": {"layers": [{"layer": 0, "frames": [{"region": 39}]}]},
] "Wings Small": {"layers": [{"layer": 0, "frames": [{"region": 40}]}]},
} "Feather Small": {"layers": [{"layer": 0, "frames": [{"region": 41}]}]},
] "Iron Weights Small": {"layers": [{"layer": 0, "frames": [{"region": 42}]}]},
}, "Climb Claw Small": {"layers": [{"layer": 0, "frames": [{"region": 43}]}]},
"Boxing Glove": { "Gloves Tiny": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]},
"length": 1, "Energy Drink Tiny": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]},
"layers": [ "Boxing Glove Tiny": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]},
{ "Accelerator Tiny": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]},
"layer": 0, "Enlarger Tiny": {"layers": [{"layer": 0, "frames": [{"region": 48}]}]},
"frames": [ "Diazepam Tiny": {"layers": [{"layer": 0, "frames": [{"region": 49}]}]},
{"region": 4} "Splitter Tiny": {"layers": [{"layer": 0, "frames": [{"region": 50}]}]},
] "Doubler Tiny": {"layers": [{"layer": 0, "frames": [{"region": 51}]}]},
} "Dual Wield Tiny": {"layers": [{"layer": 0, "frames": [{"region": 52}]}]},
] "Ricochet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 53}]}]},
}, "Tesla Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 54}]}]},
"Splitter": { "Recoil Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 55}]}]},
"length": 1, "Shock Absorber Tiny": {"layers": [{"layer": 0, "frames": [{"region": 56}]}]},
"layers": [ "Shrinker Tiny": {"layers": [{"layer": 0, "frames": [{"region": 57}]}]},
{ "Embiggener Tiny": {"layers": [{"layer": 0, "frames": [{"region": 58}]}]},
"layer": 0, "Pebble Helmet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 59}]}]},
"frames": [ "Boots Tiny": {"layers": [{"layer": 0, "frames": [{"region": 60}]}]},
{"region": 5} "Speedometer Tiny": {"layers": [{"layer": 0, "frames": [{"region": 61}]}]},
] "Wings Tiny": {"layers": [{"layer": 0, "frames": [{"region": 62}]}]},
} "Feather Tiny": {"layers": [{"layer": 0, "frames": [{"region": 63}]}]},
] "Iron Weights Tiny": {"layers": [{"layer": 0, "frames": [{"region": 64}]}]},
}, "Climb Claw Tiny": {"layers": [{"layer": 0, "frames": [{"region": 65}]}]}
"Doubler": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 6}
]
}
]
},
"Dual Wield": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 7}
]
}
]
},
"Ricochet": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 8}
]
}
]
},
"Tesla Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 9}
]
}
]
},
"Boots": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 10}
]
}
]
},
"Speedometer": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 11}
]
}
]
},
"Shrinker": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 12}
]
}
]
},
"Wings": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 13}
]
}
]
},
"Feather": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 14}
]
}
]
},
"Iron Weights": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 15}
]
}
]
},
"Climb Claw": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 16}
]
}
]
},
"Recoil Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 17}
]
}
]
},
"Diazepam": {"layers": [{"layer": 0, "frames": [{"region": 18}]}]},
"Embiggener": {"layers": [{"layer": 0, "frames": [{"region": 19}]}]},
"Pebble Helmet": {"layers": [{"layer": 0, "frames": [{"region": 20}]}]},
"Shock Absorber": {"layers": [{"layer": 0, "frames": [{"region": 21}]}]}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

-567
View File
@@ -1,567 +0,0 @@
{
"anm2ed": 1,
"info": {
"fps": 60
},
"spritesheets": [
{
"path": "trinkets_small.png",
"regions": [
{
"name": "Gloves",
"crop": [
0,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Energy Drink",
"crop": [
16,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Accelerator",
"crop": [
32,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Enlarger",
"crop": [
48,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Boxing Glove",
"crop": [
64,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Splitter",
"crop": [
80,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Doubler",
"crop": [
0,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Dual Wield",
"crop": [
16,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Ricochet",
"crop": [
32,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Tesla Coil",
"crop": [
48,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Boots",
"crop": [
64,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Speedometer",
"crop": [
80,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Shrinker",
"crop": [
0,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Wings",
"crop": [
16,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Feather",
"crop": [
32,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Iron Weights",
"crop": [
48,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Climb Claw",
"crop": [
64,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Recoil Coil",
"crop": [
80,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Diazepam",
"crop": [
0,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Embiggener",
"crop": [
16,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Pebble Helmet",
"crop": [
32,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Shock Absorber",
"crop": [
48,
48
],
"size": [
16,
16
],
"origin": "center"
}
]
}
],
"layers": [
{
"name": "sprite"
}
],
"defaultAnimation": "Gloves",
"animations": {
"Gloves": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 0
}
]
}
]
},
"Energy Drink": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 1
}
]
}
]
},
"Accelerator": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 2
}
]
}
]
},
"Enlarger": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 3
}
]
}
]
},
"Boxing Glove": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 4
}
]
}
]
},
"Splitter": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 5
}
]
}
]
},
"Doubler": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 6
}
]
}
]
},
"Dual Wield": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 7
}
]
}
]
},
"Ricochet": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 8
}
]
}
]
},
"Tesla Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 9
}
]
}
]
},
"Boots": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 10
}
]
}
]
},
"Speedometer": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 11
}
]
}
]
},
"Shrinker": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 12
}
]
}
]
},
"Wings": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 13
}
]
}
]
},
"Feather": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 14
}
]
}
]
},
"Iron Weights": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 15
}
]
}
]
},
"Climb Claw": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 16
}
]
}
]
},
"Recoil Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 17
}
]
}
]
},
"Diazepam": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 18
}
]
}
]
},
"Embiggener": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 19
}
]
}
]
},
"Pebble Helmet": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 20
}
]
}
]
},
"Shock Absorber": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 21
}
]
}
]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

-567
View File
@@ -1,567 +0,0 @@
{
"anm2ed": 1,
"info": {
"fps": 60
},
"spritesheets": [
{
"path": "trinkets_tiny.png",
"regions": [
{
"name": "Gloves",
"crop": [
0,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Energy Drink",
"crop": [
8,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Accelerator",
"crop": [
16,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Enlarger",
"crop": [
24,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Boxing Glove",
"crop": [
32,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Splitter",
"crop": [
40,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Doubler",
"crop": [
0,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Dual Wield",
"crop": [
8,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Ricochet",
"crop": [
16,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Tesla Coil",
"crop": [
24,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Boots",
"crop": [
32,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Speedometer",
"crop": [
40,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Shrinker",
"crop": [
0,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Wings",
"crop": [
8,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Feather",
"crop": [
16,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Iron Weights",
"crop": [
24,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Climb Claw",
"crop": [
32,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Recoil Coil",
"crop": [
40,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Diazepam",
"crop": [
0,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Embiggener",
"crop": [
8,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Pebble Helmet",
"crop": [
16,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Shock Absorber",
"crop": [
24,
24
],
"size": [
8,
8
],
"origin": "center"
}
]
}
],
"layers": [
{
"name": "sprite"
}
],
"defaultAnimation": "Gloves",
"animations": {
"Gloves": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 0
}
]
}
]
},
"Energy Drink": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 1
}
]
}
]
},
"Accelerator": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 2
}
]
}
]
},
"Enlarger": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 3
}
]
}
]
},
"Boxing Glove": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 4
}
]
}
]
},
"Splitter": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 5
}
]
}
]
},
"Doubler": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 6
}
]
}
]
},
"Dual Wield": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 7
}
]
}
]
},
"Ricochet": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 8
}
]
}
]
},
"Tesla Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 9
}
]
}
]
},
"Boots": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 10
}
]
}
]
},
"Speedometer": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 11
}
]
}
]
},
"Shrinker": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 12
}
]
}
]
},
"Wings": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 13
}
]
}
]
},
"Feather": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 14
}
]
}
]
},
"Iron Weights": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 15
}
]
}
]
},
"Climb Claw": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 16
}
]
}
]
},
"Recoil Coil": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 17
}
]
}
]
},
"Diazepam": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 18
}
]
}
]
},
"Embiggener": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 19
}
]
}
]
},
"Pebble Helmet": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 20
}
]
}
]
},
"Shock Absorber": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 21
}
]
}
]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

+8 -34
View File
@@ -7,7 +7,9 @@
"regions": [ "regions": [
{"name": "Chevron", "crop": [4, 5], "size": [7, 5], "pivot": [7, 5]}, {"name": "Chevron", "crop": [4, 5], "size": [7, 5], "pivot": [7, 5]},
{"name": "Arrow", "crop": [4, 5], "size": [7, 5], "origin": "center"}, {"name": "Arrow", "crop": [4, 5], "size": [7, 5], "origin": "center"},
{"name": "Crosshair", "crop": [17, 1], "size": [13, 13], "pivot": [6, 6]} {"name": "Crosshair", "crop": [17, 1], "size": [13, 13], "pivot": [6, 6]},
{"name": "Chevron Small", "crop": [1, 11], "size": [5, 4], "pivot": [5, 4]},
{"name": "Chevron Tiny", "crop": [8, 11], "size": [3, 3], "pivot": [1, 3]}
] ]
} }
], ],
@@ -16,38 +18,10 @@
], ],
"defaultAnimation": "Chevron", "defaultAnimation": "Chevron",
"animations": { "animations": {
"Chevron": { "Chevron": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]},
"length": 1, "Arrow": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]},
"layers": [ "Crosshair": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]},
{ "Chevron Small": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]},
"layer": 0, "Chevron Tiny": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]}
"frames": [
{"region": 0}
]
}
]
},
"Arrow": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 1}
]
}
]
},
"Crosshair": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 2}
]
}
]
}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

+92 -168
View File
@@ -7,20 +7,52 @@
"regions": [ "regions": [
{"name": "Sword", "crop": [0, 0], "size": [24, 24], "origin": "center"}, {"name": "Sword", "crop": [0, 0], "size": [24, 24], "origin": "center"},
{"name": "Hammer", "crop": [24, 0], "size": [24, 24], "origin": "center"}, {"name": "Hammer", "crop": [24, 0], "size": [24, 24], "origin": "center"},
{"name": "Flail", "crop": [48, 0], "size": [24, 24], "origin": "center"}, {"name": "Lance", "crop": [48, 0], "size": [24, 24], "origin": "center"},
{"name": "Lance", "crop": [72, 0], "size": [24, 24], "origin": "center"}, {"name": "Fist", "crop": [72, 0], "size": [24, 24], "origin": "center"},
{"name": "Whip", "crop": [0, 24], "size": [24, 24], "origin": "center"}, {"name": "Flail", "crop": [0, 24], "size": [24, 24], "origin": "center"},
{"name": "Grapple", "crop": [24, 24], "size": [24, 24], "origin": "center"}, {"name": "Whip", "crop": [24, 24], "size": [24, 24], "origin": "center"},
{"name": "Boomerang", "crop": [48, 24], "size": [24, 24], "origin": "center"}, {"name": "Grapple", "crop": [48, 24], "size": [24, 24], "origin": "center"},
{"name": "Bow", "crop": [72, 24], "size": [24, 24], "origin": "center"}, {"name": "Boomerang", "crop": [72, 24], "size": [24, 24], "origin": "center"},
{"name": "Blaster", "crop": [0, 48], "size": [24, 24], "origin": "center"}, {"name": "Bow", "crop": [0, 48], "size": [24, 24], "origin": "center"},
{"name": "Uzi", "crop": [24, 48], "size": [24, 24], "origin": "center"}, {"name": "Uzi", "crop": [24, 48], "size": [24, 24], "origin": "center"},
{"name": "Shotgun", "crop": [48, 48], "size": [24, 24], "origin": "center"}, {"name": "Shotgun", "crop": [48, 48], "size": [24, 24], "origin": "center"},
{"name": "Flamethrower", "crop": [72, 48], "size": [24, 24], "origin": "center"}, {"name": "Blaster", "crop": [72, 48], "size": [24, 24], "origin": "center"},
{"name": "Rocket Launcher", "crop": [0, 72], "size": [24, 24], "origin": "center"}, {"name": "Flamethrower", "crop": [0, 72], "size": [24, 24], "origin": "center"},
{"name": "Mine", "crop": [24, 72], "size": [24, 24], "origin": "center"}, {"name": "Rocket Launcher", "crop": [24, 72], "size": [24, 24], "origin": "center"},
{"name": "Fist", "crop": [48, 72], "size": [24, 24], "origin": "center"}, {"name": "Mine", "crop": [48, 72], "size": [24, 24], "origin": "center"},
{"name": "Grimoire", "crop": [72, 72], "size": [24, 24], "origin": "center"} {"name": "Grimoire", "crop": [72, 72], "size": [24, 24], "origin": "center"},
{"name": "Sword Small", "crop": [0, 96], "size": [16, 16], "origin": "center"},
{"name": "Hammer Small", "crop": [16, 96], "size": [16, 16], "origin": "center"},
{"name": "Lance Small", "crop": [32, 96], "size": [16, 16], "origin": "center"},
{"name": "Fist Small", "crop": [48, 96], "size": [16, 16], "origin": "center"},
{"name": "Flail Small", "crop": [0, 112], "size": [16, 16], "origin": "center"},
{"name": "Whip Small", "crop": [16, 112], "size": [16, 16], "origin": "center"},
{"name": "Grapple Small", "crop": [32, 112], "size": [16, 16], "origin": "center"},
{"name": "Boomerang Small", "crop": [48, 112], "size": [16, 16], "origin": "center"},
{"name": "Bow Small", "crop": [0, 128], "size": [16, 16], "origin": "center"},
{"name": "Uzi Small", "crop": [16, 128], "size": [16, 16], "origin": "center"},
{"name": "Shotgun Small", "crop": [32, 128], "size": [16, 16], "origin": "center"},
{"name": "Blaster Small", "crop": [48, 128], "size": [16, 16], "origin": "center"},
{"name": "Flamethrower Small", "crop": [0, 144], "size": [16, 16], "origin": "center"},
{"name": "Rocket Launcher Small", "crop": [16, 144], "size": [16, 16], "origin": "center"},
{"name": "Mine Small", "crop": [32, 144], "size": [16, 16], "origin": "center"},
{"name": "Grimoire Small", "crop": [48, 144], "size": [16, 16], "origin": "center"},
{"name": "Sword Tiny", "crop": [0, 160], "size": [8, 8], "origin": "center"},
{"name": "Hammer Tiny", "crop": [8, 160], "size": [8, 8], "origin": "center"},
{"name": "Lance Tiny", "crop": [16, 160], "size": [8, 8], "origin": "center"},
{"name": "Fist Tiny", "crop": [24, 160], "size": [8, 8], "origin": "center"},
{"name": "Flail Tiny", "crop": [0, 168], "size": [8, 8], "origin": "center"},
{"name": "Whip Tiny", "crop": [8, 168], "size": [8, 8], "origin": "center"},
{"name": "Grapple Tiny", "crop": [16, 168], "size": [8, 8], "origin": "center"},
{"name": "Boomerang Tiny", "crop": [24, 168], "size": [8, 8], "origin": "center"},
{"name": "Bow Tiny", "crop": [0, 176], "size": [8, 8], "origin": "center"},
{"name": "Uzi Tiny", "crop": [8, 176], "size": [8, 8], "origin": "center"},
{"name": "Shotgun Tiny", "crop": [16, 176], "size": [8, 8], "origin": "center"},
{"name": "Blaster Tiny", "crop": [24, 176], "size": [8, 8], "origin": "center"},
{"name": "Flamethrower Tiny", "crop": [0, 184], "size": [8, 8], "origin": "center"},
{"name": "Rocket Launcher Tiny", "crop": [8, 184], "size": [8, 8], "origin": "center"},
{"name": "Mine Tiny", "crop": [16, 184], "size": [8, 8], "origin": "center"},
{"name": "Grimoire Tiny", "crop": [24, 184], "size": [8, 8], "origin": "center"}
] ]
} }
], ],
@@ -29,161 +61,53 @@
], ],
"defaultAnimation": "Sword", "defaultAnimation": "Sword",
"animations": { "animations": {
"Sword": { "Sword": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]},
"length": 1, "Hammer": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]},
"layers": [ "Lance": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]},
{ "Fist": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]},
"layer": 0, "Flail": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]},
"frames": [ "Whip": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]},
{"region": 0} "Grapple": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]},
] "Boomerang": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]},
} "Bow": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]},
] "Uzi": {"layers": [{"layer": 0, "frames": [{"region": 9}]}]},
}, "Shotgun": {"layers": [{"layer": 0, "frames": [{"region": 10}]}]},
"Hammer": { "Blaster": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]},
"length": 1, "Flamethrower": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]},
"layers": [ "Rocket Launcher": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]},
{ "Mine": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
"layer": 0, "Grimoire": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]},
"frames": [ "Sword Small": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]},
{"region": 1} "Hammer Small": {"layers": [{"layer": 0, "frames": [{"region": 17}]}]},
] "Lance Small": {"layers": [{"layer": 0, "frames": [{"region": 18}]}]},
} "Fist Small": {"layers": [{"layer": 0, "frames": [{"region": 19}]}]},
] "Flail Small": {"layers": [{"layer": 0, "frames": [{"region": 20}]}]},
}, "Whip Small": {"layers": [{"layer": 0, "frames": [{"region": 21}]}]},
"Flail": { "Grapple Small": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]},
"length": 1, "Boomerang Small": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]},
"layers": [ "Bow Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]},
{ "Uzi Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]},
"layer": 0, "Shotgun Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]},
"frames": [ "Blaster Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]},
{"region": 2} "Flamethrower Small": {"layers": [{"layer": 0, "frames": [{"region": 28}]}]},
] "Rocket Launcher Small": {"layers": [{"layer": 0, "frames": [{"region": 29}]}]},
} "Mine Small": {"layers": [{"layer": 0, "frames": [{"region": 30}]}]},
] "Grimoire Small": {"layers": [{"layer": 0, "frames": [{"region": 31}]}]},
}, "Sword Tiny": {"layers": [{"layer": 0, "frames": [{"region": 32}]}]},
"Lance": { "Hammer Tiny": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]},
"length": 1, "Lance Tiny": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]},
"layers": [ "Fist Tiny": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]},
{ "Flail Tiny": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]},
"layer": 0, "Whip Tiny": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]},
"frames": [ "Grapple Tiny": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]},
{"region": 3} "Boomerang Tiny": {"layers": [{"layer": 0, "frames": [{"region": 39}]}]},
] "Bow Tiny": {"layers": [{"layer": 0, "frames": [{"region": 40}]}]},
} "Uzi Tiny": {"layers": [{"layer": 0, "frames": [{"region": 41}]}]},
] "Shotgun Tiny": {"layers": [{"layer": 0, "frames": [{"region": 42}]}]},
}, "Blaster Tiny": {"layers": [{"layer": 0, "frames": [{"region": 43}]}]},
"Whip": { "Flamethrower Tiny": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]},
"length": 1, "Rocket Launcher Tiny": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]},
"layers": [ "Mine Tiny": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]},
{ "Grimoire Tiny": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]}
"layer": 0,
"frames": [
{"region": 4}
]
}
]
},
"Grapple": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 5}
]
}
]
},
"Boomerang": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 6}
]
}
]
},
"Bow": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 7}
]
}
]
},
"Blaster": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 8}
]
}
]
},
"Uzi": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 9}
]
}
]
},
"Shotgun": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 10}
]
}
]
},
"Flamethrower": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 11}
]
}
]
},
"Rocket Launcher": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 12}
]
}
]
},
"Mine": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{"region": 13}
]
}
]
},
"Fist": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
"Grimoire": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

-419
View File
@@ -1,419 +0,0 @@
{
"anm2ed": 1,
"info": {
"fps": 60
},
"spritesheets": [
{
"path": "weapon_icons_small.png",
"regions": [
{
"name": "Sword",
"crop": [
0,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Hammer",
"crop": [
16,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Flail",
"crop": [
32,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Lance",
"crop": [
48,
0
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Whip",
"crop": [
0,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Grapple",
"crop": [
16,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Boomerang",
"crop": [
32,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Bow",
"crop": [
48,
16
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Blaster",
"crop": [
0,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Uzi",
"crop": [
16,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Shotgun",
"crop": [
32,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Flamethrower",
"crop": [
48,
32
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Rocket Launcher",
"crop": [
0,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Mine",
"crop": [
16,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Fist",
"crop": [
32,
48
],
"size": [
16,
16
],
"origin": "center"
},
{
"name": "Grimoire",
"crop": [
48,
48
],
"size": [
16,
16
],
"origin": "center"
}
]
}
],
"layers": [
{
"name": "sprite"
}
],
"defaultAnimation": "Sword",
"animations": {
"Sword": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 0
}
]
}
]
},
"Hammer": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 1
}
]
}
]
},
"Flail": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 2
}
]
}
]
},
"Lance": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 3
}
]
}
]
},
"Whip": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 4
}
]
}
]
},
"Grapple": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 5
}
]
}
]
},
"Boomerang": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 6
}
]
}
]
},
"Bow": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 7
}
]
}
]
},
"Blaster": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 8
}
]
}
]
},
"Uzi": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 9
}
]
}
]
},
"Shotgun": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 10
}
]
}
]
},
"Flamethrower": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 11
}
]
}
]
},
"Rocket Launcher": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 12
}
]
}
]
},
"Mine": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 13
}
]
}
]
},
"Fist": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 14
}
]
}
]
},
"Grimoire": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 15
}
]
}
]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

-419
View File
@@ -1,419 +0,0 @@
{
"anm2ed": 1,
"info": {
"fps": 60
},
"spritesheets": [
{
"path": "weapon_icons_tiny.png",
"regions": [
{
"name": "Sword",
"crop": [
0,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Hammer",
"crop": [
8,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Flail",
"crop": [
16,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Lance",
"crop": [
24,
0
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Whip",
"crop": [
0,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Grapple",
"crop": [
8,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Boomerang",
"crop": [
16,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Bow",
"crop": [
24,
8
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Blaster",
"crop": [
0,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Uzi",
"crop": [
8,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Shotgun",
"crop": [
16,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Flamethrower",
"crop": [
24,
16
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Rocket Launcher",
"crop": [
0,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Mine",
"crop": [
8,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Fist",
"crop": [
16,
24
],
"size": [
8,
8
],
"origin": "center"
},
{
"name": "Grimoire",
"crop": [
24,
24
],
"size": [
8,
8
],
"origin": "center"
}
]
}
],
"layers": [
{
"name": "sprite"
}
],
"defaultAnimation": "Sword",
"animations": {
"Sword": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 0
}
]
}
]
},
"Hammer": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 1
}
]
}
]
},
"Flail": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 2
}
]
}
]
},
"Lance": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 3
}
]
}
]
},
"Whip": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 4
}
]
}
]
},
"Grapple": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 5
}
]
}
]
},
"Boomerang": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 6
}
]
}
]
},
"Bow": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 7
}
]
}
]
},
"Blaster": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 8
}
]
}
]
},
"Uzi": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 9
}
]
}
]
},
"Shotgun": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 10
}
]
}
]
},
"Flamethrower": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 11
}
]
}
]
},
"Rocket Launcher": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 12
}
]
}
]
},
"Mine": {
"length": 1,
"layers": [
{
"layer": 0,
"frames": [
{
"region": 13
}
]
}
]
},
"Fist": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 14
}
]
}
]
},
"Grimoire": {
"layers": [
{
"layer": 0,
"frames": [
{
"region": 15
}
]
}
]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 B

+24 -3
View File
@@ -12,6 +12,8 @@ constexpr float FLASH_CROSS = 0.5f;
constexpr float FLASH_LENGTH = 16; constexpr float FLASH_LENGTH = 16;
constexpr float FLASH_LIFE = 0.05f; constexpr float FLASH_LIFE = 0.05f;
constexpr float FLASH_WIDTH = 6; constexpr float FLASH_WIDTH = 6;
constexpr float RING_LIFE = 0.3f;
constexpr int RING_WIDTH = 2;
constexpr float AFTERIMAGE_LIFE = 0.12f; constexpr float AFTERIMAGE_LIFE = 0.12f;
constexpr float NUMBER_GAP = 8; constexpr float NUMBER_GAP = 8;
constexpr float POPUP_FADE = 0.4f; constexpr float POPUP_FADE = 0.4f;
@@ -28,9 +30,9 @@ constexpr float POPUP_SPREAD = 40;
constexpr float PARTICLE_OUTLINE = 1; constexpr float PARTICLE_OUTLINE = 1;
constexpr float PARTICLE_RANDOM_MIN = 0.4f; constexpr float PARTICLE_RANDOM_MIN = 0.4f;
constexpr float SHAKE_DECAY = 2.5f; constexpr float SHAKE_DECAY = 2.5f;
constexpr float SHAKE_LIMIT = 12; constexpr float SHAKE_LIMIT = 6;
constexpr float SHAKE_MAX = 10; constexpr float SHAKE_MAX = 5;
constexpr float SHAKE_TRAUMA_MAX = 3; constexpr float SHAKE_TRAUMA_MAX = 1.5f;
// Adds to one of the effect lists, dropping its oldest once it holds EFFECTS_MAX, so a busy fight can't flood the // Adds to one of the effect lists, dropping its oldest once it holds EFFECTS_MAX, so a busy fight can't flood the
// screen (or the renderer) with leftovers. // screen (or the renderer) with leftovers.
@@ -120,6 +122,11 @@ void Effects::flash_spawn(Vec2 position, float angle, float size, Color color)
effect_add(flashes, Flash{color, position, angle, FLASH_LIFE, size}); effect_add(flashes, Flash{color, position, angle, FLASH_LIFE, size});
} }
void Effects::ring_spawn(Vec2 position, float radius, Color color)
{
effect_add(rings, Ring{color, position, RING_LIFE, radius});
}
void Effects::dps_record(int amount) void Effects::dps_record(int amount)
{ {
hits.push_back({ticks * TICK, amount}); hits.push_back({ticks * TICK, amount});
@@ -157,6 +164,10 @@ void Effects::update()
flash.life -= TICK; flash.life -= TICK;
std::erase_if(flashes, [](Flash& flash) { return flash.life <= 0; }); std::erase_if(flashes, [](Flash& flash) { return flash.life <= 0; });
for (Ring& ring : rings)
ring.life -= TICK;
std::erase_if(rings, [](Ring& ring) { return ring.life <= 0; });
for (Afterimage& afterimage : afterimages) for (Afterimage& afterimage : afterimages)
afterimage.life -= TICK; afterimage.life -= TICK;
std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; }); std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; });
@@ -198,6 +209,15 @@ void Effects::draw(Game& game)
renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color); renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color);
} }
// Each ring eases out to its full radius as it fades.
for (Ring& ring : rings)
{
float fade = ring.life / RING_LIFE;
float radius = ring.radius * (1 - fade * fade);
for (int i = 0; i < RING_WIDTH; i++)
renderer.ring_draw(ring.position, std::max(1.0f, radius - i), color_fade(ring.color, fade));
}
// Each particle shrinks and fades out. The outline is four strips so the fading core never shows it through. // Each particle shrinks and fades out. The outline is four strips so the fading core never shows it through.
for (Particle& particle : particles) for (Particle& particle : particles)
{ {
@@ -238,5 +258,6 @@ void Effects::clear()
hits.clear(); hits.clear();
particles.clear(); particles.clear();
popups.clear(); popups.clear();
rings.clear();
shake = 0; shake = 0;
} }
+11
View File
@@ -49,6 +49,15 @@ struct Flash
float size = 0; float size = 0;
}; };
// A ring swelling out to its radius as it fades, like an airblast.
struct Ring
{
Color color;
Vec2 position;
float life = 0;
float radius = 0;
};
// A fading copy of a sprite left behind by something moving fast. // A fading copy of a sprite left behind by something moving fast.
struct Afterimage struct Afterimage
{ {
@@ -63,6 +72,7 @@ public:
void burst(Burst burst, Vec2 position, float angle, Color color); void burst(Burst burst, Vec2 position, float angle, Color color);
void afterimage_spawn(Pose pose); void afterimage_spawn(Pose pose);
void flash_spawn(Vec2 position, float angle, float size, Color color); void flash_spawn(Vec2 position, float angle, float size, Color color);
void ring_spawn(Vec2 position, float radius, Color color);
void afterimage_draw(Game& game); void afterimage_draw(Game& game);
void number_spawn(Vec2 position, int value, int target, Color color); void number_spawn(Vec2 position, int value, int target, Color color);
void dps_record(int amount); void dps_record(int amount);
@@ -80,6 +90,7 @@ private:
std::vector<Hit> hits; std::vector<Hit> hits;
std::vector<Particle> particles; std::vector<Particle> particles;
std::vector<Popup> popups; std::vector<Popup> popups;
std::vector<Ring> rings;
float shake = 0; float shake = 0;
int ticks = 0; int ticks = 0;
}; };
+71 -55
View File
@@ -3,7 +3,7 @@
#include <algorithm> #include <algorithm>
constexpr float AIR_ACCELERATION = 400; constexpr float AIR_ACCELERATION = 400;
constexpr float BOUNCE_SHAKE = 0.0005f; constexpr float BOUNCE_SHAKE = 0.00025f;
constexpr float SLAM_DAMAGE = 0.06f; constexpr float SLAM_DAMAGE = 0.06f;
constexpr float KNOCK_BASE = 60; constexpr float KNOCK_BASE = 60;
constexpr float KNOCK_LIFT = 0.35f; constexpr float KNOCK_LIFT = 0.35f;
@@ -17,11 +17,12 @@ constexpr float CLING_SPEED = 60;
constexpr float COYOTE_TIME = 0.1f; constexpr float COYOTE_TIME = 0.1f;
constexpr float DASH_COOLDOWN = 0.8f; constexpr float DASH_COOLDOWN = 0.8f;
constexpr float DASH_TIME = 0.12f; constexpr float DASH_TIME = 0.12f;
constexpr float DEATH_SHAKE = 1.5f; constexpr float DEATH_SHAKE = 0.75f;
constexpr float FACING_DEADZONE = 4; constexpr float FACING_DEADZONE = 4;
constexpr float FAST_FALL_GRAVITY = 2; constexpr float FAST_FALL_GRAVITY = 2;
constexpr float FLASH_DECAY = 8; constexpr float FLASH_DECAY = 8;
constexpr float FAST_FALL_SPEED = 700; constexpr float FAST_FALL_SPEED = 700;
constexpr float FALL_EASE = 2000;
constexpr float GROUND_ACCELERATION = 500; constexpr float GROUND_ACCELERATION = 500;
constexpr float GROUND_FRICTION = 3000; constexpr float GROUND_FRICTION = 3000;
constexpr float JUMP_BUFFER = 0.1f; constexpr float JUMP_BUFFER = 0.1f;
@@ -34,13 +35,13 @@ constexpr float ROPE_MIN = 24;
constexpr float SLIDE_DUST_INTERVAL = 0.08f; constexpr float SLIDE_DUST_INTERVAL = 0.08f;
constexpr float SWING_ACCELERATION = 500; constexpr float SWING_ACCELERATION = 500;
constexpr float SWING_SPEED_MAX = 420; constexpr float SWING_SPEED_MAX = 420;
constexpr float POUND_AREAS[LEVEL_COUNT] = {30, 40, 50}; constexpr float POUND_AREAS[LEVEL_COUNT] = {15, 20, 25};
constexpr float POUND_DAMAGES[LEVEL_COUNT] = {20, 35, 50}; constexpr float POUND_DAMAGES[LEVEL_COUNT] = {20, 35, 50};
constexpr float POUND_SCALE_MAX = 3; constexpr float POUND_SCALE_MAX = 3;
constexpr float POUND_SCALE_MIN = 0.3f;
constexpr float POUND_SPEED = 700; constexpr float POUND_SPEED = 700;
constexpr Vec2 HAND_OFFSET = {4, 2}; constexpr Vec2 HAND_OFFSET = {4, 2};
constexpr Vec2 OFFHAND_OFFSET = {-3, 4}; constexpr Vec2 OFFHAND_OFFSET = {-3, 4};
constexpr Vec2 OFFHAND_FAN = {3, 2};
constexpr float WALL_IMPACT_SPEED = 200; constexpr float WALL_IMPACT_SPEED = 200;
constexpr float WALL_LIFT = 0.25f; constexpr float WALL_LIFT = 0.25f;
constexpr float WALL_MEMORY = 0.6f; constexpr float WALL_MEMORY = 0.6f;
@@ -118,15 +119,14 @@ void Fighter::state_reset(Sim& sim, float maxHealth)
std::fill(std::begin(cooldowns), std::end(cooldowns), 0.0f); std::fill(std::begin(cooldowns), std::end(cooldowns), 0.0f);
flash = 0; flash = 0;
health = maxHealth; health = maxHealth;
offSlot = -1; std::fill(std::begin(slotSwitches), std::end(slotSwitches), 0);
slot = 0; slot = 0;
isAlive = true; isAlive = true;
} }
// Loses health with a flash and hitstop, and gets knocked away from where the hit came from (and a little upward), // Loses health with a flash and hitstop, and gets knocked away from where the hit came from (and a little upward),
// up to a speed that grows with the damage. Knocks top up rather than stack, so rapid small hits can't juggle. A hard // up to a speed that grows with the damage (see knock_take). Shock Absorber softens it. Running out of health kills it
// enough knock launches it. Shock Absorber softens it. Running out of health kills it in a burst, and its attacks // in a burst, and its attacks vanish with it.
// vanish with it.
void Fighter::damage_take(Sim& sim, Damage damage) void Fighter::damage_take(Sim& sim, Damage damage)
{ {
// Races and the sandbox never cost health. // Races and the sandbox never cost health.
@@ -138,11 +138,8 @@ void Fighter::damage_take(Sim& sim, Damage damage)
Vec2 away = center_get() - damage.source; Vec2 away = center_get() - damage.source;
Vec2 direction = vec2_normalize(vec2_normalize(vec2_length(away) > 1 ? away : Vec2{0, -1}) + Vec2{0, -KNOCK_LIFT}); Vec2 direction = vec2_normalize(vec2_normalize(vec2_length(away) > 1 ? away : Vec2{0, -1}) + Vec2{0, -KNOCK_LIFT});
float knock = std::min(KNOCK_MAX, KNOCK_BASE + damage.amount * KNOCK_PER_DAMAGE) * damage.knock * float knock = std::min(KNOCK_MAX, KNOCK_BASE + damage.amount * KNOCK_PER_DAMAGE) * damage.knock *
(isCrouching ? CROUCH_DAMP : 1) * (isRacing ? RACE_KNOCK : 1) * knock_absorb_get(); (isRacing ? RACE_KNOCK : 1) * knock_absorb_get();
float along = vec2_dot(body.velocity, direction); knock_take(direction * knock, damage.owner);
impulse_add(direction * std::max(0.0f, knock - along), false);
launcher = damage.owner;
isLaunched |= knock >= LAUNCH_START_SPEED;
if (health > 0) if (health > 0)
return; return;
@@ -212,12 +209,12 @@ void Fighter::update(Sim& sim)
rope_update(*hook); rope_update(*hook);
bool wasGrounded = body.isGrounded; bool wasGrounded = body.isGrounded;
bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS);
float fallSpeed = body.velocity.y; float fallSpeed = body.velocity.y;
float runSpeed = body.velocity.x; float runSpeed = body.velocity.x;
sim.world.obstacles = sim.weapons.solids_get(*this); sim.weapons.solids_add(*this, sim.world);
sim.world.body_move(body); sim.world.body_move(body);
sim.world.obstacles.clear(); sim.world.obstacles.clear();
sim.world.ledges.clear();
if (isLaunched) if (isLaunched)
bounce_resolve(sim, {runSpeed, fallSpeed}); bounce_resolve(sim, {runSpeed, fallSpeed});
safe_update(sim); safe_update(sim);
@@ -240,13 +237,14 @@ void Fighter::update(Sim& sim)
sim.cues->sound_play(SOUND_LAND); sim.cues->sound_play(SOUND_LAND);
sim.cues->burst_spawn(LAND_DUST, feet, 0, index); sim.cues->burst_spawn(LAND_DUST, feet, 0, index);
} }
// Iron Weights slam: the harder the landing, the harder the blast. Damage follows the landing speed, and the // Iron Weights slam: landing faster than a plain fall can go (on anything, platforms too), and the harder the
// radius its square root. // landing, the harder the blast. Every copy adds to its damage and radius. Damage follows the landing speed, and
if (body.isGrounded && !wasGrounded && fallSpeed > LAND_SPEED && isPounding) // the radius its square root.
if (body.isGrounded && !wasGrounded && fallSpeed > MAX_FALL && is_trinket_equipped(TRINKET_WEIGHTS))
{ {
float impact = std::clamp(fallSpeed / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX); float impact = std::min(fallSpeed / POUND_SPEED, POUND_SCALE_MAX);
sim.weapons.blast_spawn(sim, {.position = feet, sim.weapons.blast_spawn(sim, {.position = feet,
.area = POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact), .area = trinket_sum_get(TRINKET_WEIGHTS, POUND_AREAS) * std::sqrt(impact),
.damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact, .damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact,
.owner = index, .owner = index,
.sound = SOUND_POUND}); .sound = SOUND_POUND});
@@ -295,9 +293,10 @@ void Fighter::update(Sim& sim)
buttonsPrevious = controls.buttons; buttonsPrevious = controls.buttons;
} }
// Horizontal control and gravity. Holding S in the air falls faster, holding W floats with the Feather, and a spinning // Horizontal control and gravity. Holding S in the air falls faster (Iron Weights speeding up both how fast it gets
// flail's pull lightens gravity. Slopes change the top speed, and crouch-slides answer only to the slope. Crouching // there and how fast it goes), and letting go eases back to a plain fall rather than stopping short. Holding W floats
// on flat ground waddles along slowly. // with the Feather, and a spinning flail's pull lightens gravity. Slopes change the top speed, and crouch-slides answer
// only to the slope. Crouching on flat ground waddles along slowly.
// Airborne on a rope, moving left and right pumps the swing instead. // Airborne on a rope, moving left and right pumps the swing instead.
// Above run speed in the air, launch momentum bleeds off slowly unless steered against. Launched by a hit, there's no // Above run speed in the air, launch momentum bleeds off slowly unless steered against. Launched by a hit, there's no
// control at all, just gravity and the knock dying down. // control at all, just gravity and the knock dying down.
@@ -320,10 +319,14 @@ void Fighter::run_update(Attack* hook)
bool isAirborne = !body.isGrounded && !hook; bool isAirborne = !body.isGrounded && !hook;
bool isFastFalling = isAirborne && is_down(CONTROL_DOWN) && !isSliding; bool isFastFalling = isAirborne && is_down(CONTROL_DOWN) && !isSliding;
bool isFloating = isAirborne && is_down(CONTROL_UP); bool isFloating = isAirborne && is_down(CONTROL_UP);
float gravity = GRAVITY * (isFastFalling ? FAST_FALL_GRAVITY : 1); float weights = isFastFalling ? trinket_get(TRINKET_WEIGHTS, 1) : 1;
float gravity = GRAVITY * (isFastFalling ? FAST_FALL_GRAVITY * weights : 1);
gravity *= isFloating ? trinket_get(TRINKET_FEATHER, 1) : 1; gravity *= isFloating ? trinket_get(TRINKET_FEATHER, 1) : 1;
gravity *= isSpinning ? SPIN_GRAVITY : 1; gravity *= isSpinning ? SPIN_GRAVITY : 1;
float maxFall = isFastFalling ? FAST_FALL_SPEED * trinket_get(TRINKET_WEIGHTS, 1) : MAX_FALL; float maxFall = isFastFalling ? FAST_FALL_SPEED * weights : MAX_FALL;
if (body.velocity.y > maxFall)
body.velocity.y = std::max(maxFall, body.velocity.y - FALL_EASE * TICK);
else
body.velocity.y = std::min(body.velocity.y + gravity * TICK, maxFall); body.velocity.y = std::min(body.velocity.y + gravity * TICK, maxFall);
// Sliding: the slope pulls it downhill and little else slows it. // Sliding: the slope pulls it downhill and little else slows it.
@@ -402,7 +405,7 @@ void Fighter::slot_update(Sim& sim)
sim.cues->sound_play(SOUND_SELECT); sim.cues->sound_play(SOUND_SELECT);
if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot)) if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot))
{ {
offSlot = previous; slotSwitches[previous] = ++switches;
sim.weapons.attacks_release(*this, ATTACK_FLAIL); sim.weapons.attacks_release(*this, ATTACK_FLAIL);
} }
@@ -654,15 +657,14 @@ float Fighter::trinket_cooldown_get(int trinket)
return 0; return 0;
} }
// The radius an Iron Weights slam would blast if the fighter, pounding, landed at its current speed, or 0 if it isn't // The radius an Iron Weights slam would blast if the fighter landed at its current speed, or 0 if it isn't falling
// pounding down fast enough to slam at all. // fast enough to slam at all.
float Fighter::pound_area_get() float Fighter::pound_area_get()
{ {
bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS); if (!is_trinket_equipped(TRINKET_WEIGHTS) || body.isGrounded || body.velocity.y <= MAX_FALL)
if (!isPounding || body.isGrounded || body.velocity.y <= LAND_SPEED)
return 0; return 0;
float impact = std::clamp(body.velocity.y / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX); float impact = std::min(body.velocity.y / POUND_SPEED, POUND_SCALE_MAX);
return POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact); return trinket_sum_get(TRINKET_WEIGHTS, POUND_AREAS) * std::sqrt(impact);
} }
// Moves the fighter's middle to centre, keeping its momentum, without drawing it sliding across (teleports). // Moves the fighter's middle to centre, keeping its momentum, without drawing it sliding across (teleports).
@@ -681,6 +683,19 @@ void Fighter::impulse_add(Vec2 impulse, bool isDamped)
isJumping = false; isJumping = false;
} }
// A knock from another fighter's hit or blast, softened while crouching. It tops up the speed already going its way
// rather than stacking, so rapid small knocks can't juggle, and a hard enough one launches it (that fighter getting
// the credit for whatever it bounces into).
void Fighter::knock_take(Vec2 knock, int owner)
{
knock = knock * (isCrouching ? CROUCH_DAMP : 1);
float strength = vec2_length(knock);
Vec2 direction = vec2_normalize(knock);
impulse_add(direction * std::max(0.0f, strength - vec2_dot(body.velocity, direction)), false);
launcher = owner;
isLaunched |= strength >= LAUNCH_START_SPEED;
}
// A push held briefly without gravity or friction, so it carries even along the ground. Used by dashes and lunges. // A push held briefly without gravity or friction, so it carries even along the ground. Used by dashes and lunges.
void Fighter::burst_add(Vec2 impulse) void Fighter::burst_add(Vec2 impulse)
{ {
@@ -827,28 +842,27 @@ bool Fighter::is_released(Control control)
return !is_down(control) && buttonsPrevious >> control & 1; return !is_down(control) && buttonsPrevious >> control & 1;
} }
// Dual Wield's off-hand weapon slot: the last one switched away from if it still holds a weapon that isn't the one in // Dual Wield's off-hand weapon slots, one per copy, written to slots: the other slots holding weapons, the ones most
// hand, otherwise any other slot that does, or -1 if there is none. When weapons stack, a copy of the weapon in hand // recently switched away from first, then any never switched to, in slot order. Returns how many.
// is another weapon; otherwise it's the same one. int Fighter::off_slots_get(int* slots)
int Fighter::off_slot_get()
{ {
int held = weapon_key(slot); int held = weapon_key(slot);
int fallback = -1; int count = 0;
for (int i = 0; i < weaponSlots; i++) for (int i = 0; i < weaponSlots; i++)
{ if (weaponLoadout[i] >= 0 && weapon_key(i) != held)
if (weaponLoadout[i] < 0 || weapon_key(i) == held) slots[count++] = i;
continue; std::stable_sort(slots, slots + count, [&](int a, int b) { return slotSwitches[a] > slotSwitches[b]; });
if (i == offSlot) return std::min(count, trinket_count(TRINKET_DUAL));
return i;
fallback = fallback < 0 ? i : fallback;
}
return fallback;
} }
int Fighter::off_weapon_get() // The weapon slot a hand holds: the selected one in the main hand (0), and Dual Wield's in the off-hands (1 up), or -1
// for an off-hand holding nothing.
int Fighter::hand_slot_get(int hand)
{ {
int off = off_slot_get(); if (hand == 0)
return off < 0 ? -1 : weaponLoadout[off]; return slot;
int slots[SLOT_MAX];
return hand <= off_slots_get(slots) ? slots[hand - 1] : -1;
} }
// Moves the box alpha of the way through the last tick. Only for drawing. // Moves the box alpha of the way through the last tick. Only for drawing.
@@ -863,15 +877,17 @@ Vec2 Fighter::center_get()
return {body.box.x + body.box.w / 2, body.box.y + body.box.h / 2}; return {body.box.x + body.box.w / 2, body.box.y + body.box.h / 2};
} }
Vec2 Fighter::hand_get() // Where a hand holds its weapon: the main hand (0) in front, and Dual Wield's off-hands (1 up) behind the body, the
// first just behind it and the rest fanned out further back, alternating up and down.
Vec2 Fighter::hand_get(int hand)
{ {
return center_get() + Vec2{is_left() ? -HAND_OFFSET.x : HAND_OFFSET.x, HAND_OFFSET.y} * scale; Vec2 offset = HAND_OFFSET;
if (hand > 0)
{
int step = hand / 2;
offset = OFFHAND_OFFSET + Vec2{-OFFHAND_FAN.x * step, (hand % 2 == 0 ? -1 : 1) * OFFHAND_FAN.y * step};
} }
return center_get() + Vec2{is_left() ? -offset.x : offset.x, offset.y} * scale;
// The far hand, just behind the body, where Dual Wield holds the off-hand weapon.
Vec2 Fighter::offhand_get()
{
return center_get() + Vec2{is_left() ? -OFFHAND_OFFSET.x : OFFHAND_OFFSET.x, OFFHAND_OFFSET.y} * scale;
} }
// Full charge pulses white on and off, shared by the charge bar and the held weapon. // Full charge pulses white on and off, shared by the charge bar and the held weapon.
+7 -5
View File
@@ -77,8 +77,10 @@ public:
float contactCooldown = 0; float contactCooldown = 0;
int index = 0; int index = 0;
int launcher = 0; int launcher = 0;
int offSlot = -1;
int slot = 0; int slot = 0;
// When each weapon slot was last switched away from, counting switches (0 for never), for Dual Wield.
int slotSwitches[SLOT_MAX] = {};
int switches = 0;
int trinketItemLevels[TRINKET_COUNT] = {}; int trinketItemLevels[TRINKET_COUNT] = {};
int trinketSlotLevels[SLOT_MAX] = {}; int trinketSlotLevels[SLOT_MAX] = {};
int trinketLoadout[SLOT_MAX]; int trinketLoadout[SLOT_MAX];
@@ -105,13 +107,14 @@ public:
void update(Sim& sim); void update(Sim& sim);
void blend(float alpha); void blend(float alpha);
void impulse_add(Vec2 impulse, bool isDamped = true); void impulse_add(Vec2 impulse, bool isDamped = true);
void knock_take(Vec2 knock, int owner);
void teleport_to(Vec2 centre); void teleport_to(Vec2 centre);
void burst_add(Vec2 impulse); void burst_add(Vec2 impulse);
void charge_cancel(Sim& sim); void charge_cancel(Sim& sim);
int weapon_get(); int weapon_get();
int weapon_key(int weaponSlot); int weapon_key(int weaponSlot);
int off_slot_get(); int off_slots_get(int* slots);
int off_weapon_get(); int hand_slot_get(int hand);
int slot_level_get(int weaponSlot); int slot_level_get(int weaponSlot);
const LevelDef& level_get(int weaponSlot); const LevelDef& level_get(int weaponSlot);
void item_set(bool isTrinket, int itemSlot, int item); void item_set(bool isTrinket, int itemSlot, int item);
@@ -125,8 +128,7 @@ public:
float knock_absorb_get(); float knock_absorb_get();
float pound_area_get(); float pound_area_get();
Vec2 center_get(); Vec2 center_get();
Vec2 hand_get(); Vec2 hand_get(int hand = 0);
Vec2 offhand_get();
bool is_any_pressed(); bool is_any_pressed();
bool is_down(Control control); bool is_down(Control control);
bool is_pressed(Control control); bool is_pressed(Control control);
+42 -10
View File
@@ -179,7 +179,7 @@ void Menu::update(Game& game)
isPinnedTrinket = isTrinket; isPinnedTrinket = isTrinket;
game.audio.play(SOUND_SELECT); game.audio.play(SOUND_SELECT);
} }
if (isRight && kind == tab && isChoosable) if (isRight && is_choosable(game, isTrinket))
slot_clear(game, isTrinket, index); slot_clear(game, isTrinket, index);
} }
} }
@@ -400,7 +400,8 @@ void Menu::header_draw(Game& game, MenuLayout& layout)
// An item tab: the item grid with its scroll bar, the selection's description (scrolling, back to its top for each // An item tab: the item grid with its scroll bar, the selection's description (scrolling, back to its top for each
// new selection), and the line between the two hotbars below. The focus moves the selection through the grid (scrolling // new selection), and the line between the two hotbars below. The focus moves the selection through the grid (scrolling
// it), and confirming equips or unequips it. // it), and confirming equips or unequips it. It moves onto the hotbars too (drawn after), where a filled slot of
// either kind shows in the description, and confirming it clears it if it can be chosen.
void Menu::items_draw(Game& game, MenuLayout& layout) void Menu::items_draw(Game& game, MenuLayout& layout)
{ {
Renderer& renderer = game.renderer; Renderer& renderer = game.renderer;
@@ -432,6 +433,23 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
.isLit = i == selection, .isLit = i == selection,
.isTrinket = tab == TAB_TRINKETS}); .isTrinket = tab == TAB_TRINKETS});
} }
Fighter& fighter = game.local_get();
for (int kind = 0; kind < ITEM_TABS; kind++)
{
bool isTrinket = kind == TAB_TRINKETS;
int count = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
for (int index = 0; index < count; index++)
{
if (!ui.focus_add(loadout, index, slot_rect(isTrinket, index, count)) || loadout[index] < 0)
continue;
pinnedSlot = index;
pinnedTab = tab;
isPinnedTrinket = isTrinket;
if (ui.isConfirmed && is_choosable(game, isTrinket))
slot_clear(game, isTrinket, index);
}
}
Rect track = layout.scroll; Rect track = layout.scroll;
int scrollMax = std::max(0, row_count() - layout.rows); int scrollMax = std::max(0, row_count() - layout.rows);
@@ -441,8 +459,8 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
renderer.rect_draw({track.x, std::round(headY), track.w, std::round(headHeight)}, ui.tint); renderer.rect_draw({track.x, std::round(headY), track.w, std::round(headHeight)}, ui.tint);
renderer.rect_draw(layout.divider, ui.tint); renderer.rect_draw(layout.divider, ui.tint);
// The description is of the selection, or of a hotbar slot clicked on this tab while it still holds something. // The description is of the selection, or of a hotbar slot clicked (or focused) on this tab while it still holds
Fighter& fighter = game.local_get(); // something.
bool isTrinket = tab == TAB_TRINKETS; bool isTrinket = tab == TAB_TRINKETS;
int item = selections[tab]; int item = selections[tab];
int level = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[item]; int level = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[item];
@@ -463,6 +481,8 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
description.scroll = 0; description.scroll = 0;
described = shown; described = shown;
Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level); Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level);
if (slotLevel)
tooltip.slot = pinnedSlot;
Rect area = layout.description; Rect area = layout.description;
bool isLevelable = game.sim.match.state == MATCH_PRACTICE; bool isLevelable = game.sim.match.state == MATCH_PRACTICE;
if (isLevelable) if (isLevelable)
@@ -1043,18 +1063,30 @@ bool Menu::is_choosable(Game& game, bool isTrinket)
return match.state == MATCH_COUNTDOWN && (isTrinket ? match.isTrinketPick : match.isWeaponPick); return match.state == MATCH_COUNTDOWN && (isTrinket ? match.isTrinketPick : match.isWeaponPick);
} }
// Where a hotbar slot sits: closed, the weapon bar is top left and the trinket bar bottom left. On an item tab, // Where a hotbar slot sits: closed, the weapon bar is top left and the trinket bar bottom left, each as large as fits
// both sit under the menu either side of its middle. // its column. On an item tab, both sit under the menu either side of its middle, full size.
Rect Menu::slot_rect(bool isTrinket, int index, int count) Rect Menu::slot_rect(bool isTrinket, int index, int count)
{ {
float step = SLOT_SIZE + SLOT_GAP; IconSize size = slot_size_get(count);
Vec2 position = {HOTBAR_MARGIN + index * step, float slot = SLOT_SIZES[size];
isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - SLOT_SIZE : HOTBAR_MARGIN}; float step = slot + SLOT_GAPS[size];
Vec2 position = {HOTBAR_MARGIN + index * step, isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - slot : HOTBAR_MARGIN};
if (is_bars_open()) if (is_bars_open())
position = {isTrinket ? CANVAS_WIDTH / 2.0f + SLOT_GAP + index * step position = {isTrinket ? CANVAS_WIDTH / 2.0f + SLOT_GAP + index * step
: CANVAS_WIDTH / 2.0f - SLOT_GAP - (count - index) * step + SLOT_GAP, : CANVAS_WIDTH / 2.0f - SLOT_GAP - (count - index) * step + SLOT_GAP,
BAR_Y}; BAR_Y};
return {std::round(position.x), std::round(position.y), SLOT_SIZE, SLOT_SIZE}; return {std::round(position.x), std::round(position.y), slot, slot};
}
// The size a hotbar of count slots takes: full in the menu, and in the HUD the largest that keeps it within its column.
IconSize Menu::slot_size_get(int count)
{
if (is_bars_open())
return ICON_FULL;
for (int size = ICON_FULL; size < ICON_TINY; size++)
if (count * (SLOT_SIZES[size] + SLOT_GAPS[size]) - SLOT_GAPS[size] <= HUD_COLUMN_WIDTH)
return (IconSize)size;
return ICON_TINY;
} }
// Lays the panel out: the header across the top, and below it, right to left, the description, divider, scroll bar // Lays the panel out: the header across the top, and below it, right to left, the description, divider, scroll bar
+1
View File
@@ -74,6 +74,7 @@ public:
void target_fill(Game& game); void target_fill(Game& game);
bool is_bars_open(); bool is_bars_open();
Rect slot_rect(bool isTrinket, int index, int count); Rect slot_rect(bool isTrinket, int index, int count);
IconSize slot_size_get(int count);
bool is_choosable(Game& game, bool isTrinket); bool is_choosable(Game& game, bool isTrinket);
int target_get(Game& game, bool isTrinket); int target_get(Game& game, bool isTrinket);
+39 -3
View File
@@ -238,6 +238,11 @@ bool Renderer::init(SDL_Window* target)
gl.GenFramebuffers(1, &canvasFramebuffer); gl.GenFramebuffers(1, &canvasFramebuffer);
gl.BindFramebuffer(GL_FRAMEBUFFER, canvasFramebuffer); gl.BindFramebuffer(GL_FRAMEBUFFER, canvasFramebuffer);
gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, canvas, 0); gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, canvas, 0);
layer = texture_create(CANVAS_WIDTH, CANVAS_HEIGHT, nullptr);
gl.GenFramebuffers(1, &layerFramebuffer);
gl.BindFramebuffer(GL_FRAMEBUFFER, layerFramebuffer);
gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, layer, 0);
framebuffer = canvasFramebuffer;
gl.Viewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); gl.Viewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
gl.GenRenderbuffers(1, &upscaled); gl.GenRenderbuffers(1, &upscaled);
gl.GenFramebuffers(1, &upscaledFramebuffer); gl.GenFramebuffers(1, &upscaledFramebuffer);
@@ -410,11 +415,12 @@ void Renderer::upscaled_resize(int factor)
upscale = factor; upscale = factor;
} }
// Draws the batch so far onto the canvas (clearing it first, the first time in a frame) and empties it. // Draws the batch so far onto the canvas or the layer (clearing the canvas first, the first time in a frame) and
// empties it.
void Renderer::flush() void Renderer::flush()
{ {
gl.BindFramebuffer(GL_FRAMEBUFFER, canvasFramebuffer); gl.BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
if (!isCanvasCleared) if (framebuffer == canvasFramebuffer && !isCanvasCleared)
{ {
gl.ClearColor(BACKGROUND.r, BACKGROUND.g, BACKGROUND.b, BACKGROUND.a); gl.ClearColor(BACKGROUND.r, BACKGROUND.g, BACKGROUND.b, BACKGROUND.a);
gl.Clear(GL_COLOR_BUFFER_BIT); gl.Clear(GL_COLOR_BUFFER_BIT);
@@ -425,6 +431,36 @@ void Renderer::flush()
vertices.clear(); vertices.clear();
} }
// Sends what's drawn from here to layer end onto a clear layer of its own instead of the canvas.
void Renderer::layer_begin()
{
flush();
framebuffer = layerFramebuffer;
gl.BindFramebuffer(GL_FRAMEBUFFER, layerFramebuffer);
gl.ClearColor(0, 0, 0, 0);
gl.Clear(GL_COLOR_BUFFER_BIT);
}
// Lays the layer over the canvas at alpha, as one piece, so nothing in it shows through anything else in it. The layer
// holds its colours already multiplied by their alpha (blending onto clear), so it's laid with its colour taken whole.
void Renderer::layer_end(float alpha)
{
flush();
framebuffer = canvasFramebuffer;
uint32_t color = color_pack({alpha, alpha, alpha, alpha});
Vertex corners[QUAD_CORNERS] = {{0, 0, 0, 1, color, 0},
{CANVAS_WIDTH, 0, 1, 1, color, 0},
{CANVAS_WIDTH, CANVAS_HEIGHT, 1, 0, color, 0},
{0, CANVAS_HEIGHT, 0, 0, color, 0}};
for (int index : QUAD_ORDER)
vertices.push_back(corners[index]);
gl.BindTexture(GL_TEXTURE_2D, layer);
gl.BlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
flush();
gl.BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
gl.BindTexture(GL_TEXTURE_2D, atlas);
}
// Draws the rest of the frame onto the canvas, blits it up to the upscaled buffer, then into the letterboxed window // Draws the rest of the frame onto the canvas, blits it up to the upscaled buffer, then into the letterboxed window
// (whose rows count from the bottom) and swaps. // (whose rows count from the bottom) and swaps.
void Renderer::present() void Renderer::present()
+5
View File
@@ -63,6 +63,8 @@ public:
void line_draw(Vec2 from, Vec2 to, float width, Color color); void line_draw(Vec2 from, Vec2 to, float width, Color color);
void circle_draw(Vec2 center, float radius, Color color); void circle_draw(Vec2 center, float radius, Color color);
void ring_draw(Vec2 center, float radius, Color color); void ring_draw(Vec2 center, float radius, Color color);
void layer_begin();
void layer_end(float alpha);
void present(); void present();
void vsync_set(bool isVsync); void vsync_set(bool isVsync);
Rect letterbox_get(float width, float height); Rect letterbox_get(float width, float height);
@@ -75,6 +77,9 @@ private:
unsigned int atlas = 0; unsigned int atlas = 0;
unsigned int canvas = 0; unsigned int canvas = 0;
unsigned int canvasFramebuffer = 0; unsigned int canvasFramebuffer = 0;
unsigned int layer = 0;
unsigned int layerFramebuffer = 0;
unsigned int framebuffer = 0;
unsigned int upscaled = 0; unsigned int upscaled = 0;
unsigned int upscaledFramebuffer = 0; unsigned int upscaledFramebuffer = 0;
int shelfHeight = 0; int shelfHeight = 0;
+54 -33
View File
@@ -26,13 +26,12 @@ constexpr const char* CRAWL_ANIMATIONS[] = {"Crawl Right", "Crawl Left"};
constexpr float POUND_RING_ALPHA = 0.25f; constexpr float POUND_RING_ALPHA = 0.25f;
constexpr const char* WALK_ANIMATIONS[] = {"Walk Right", "Walk Left"}; constexpr const char* WALK_ANIMATIONS[] = {"Walk Right", "Walk Left"};
constexpr float ARROW_FADE = 0.6f;
constexpr Burst AURA_BURST = {.life = 0.3f, .size = 2, .speed = 40, .spread = 0.3f, .count = 1}; constexpr Burst AURA_BURST = {.life = 0.3f, .size = 2, .speed = 40, .spread = 0.3f, .count = 1};
constexpr float BEAM_INNER = 4; constexpr float BEAM_INNER = 4;
constexpr float BEAM_OUTER = 10; constexpr float BEAM_OUTER = 10;
constexpr float BOLT_JITTER = 4; constexpr float BOLT_JITTER = 2;
constexpr float BOLT_SEGMENT = 8; constexpr float BOLT_SEGMENT = 8;
constexpr float BOLT_WIDTH = 2; constexpr float BOLT_WIDTH = 1;
constexpr float BOOMERANG_SPIN = 20; constexpr float BOOMERANG_SPIN = 20;
constexpr float CHAIN_SPACING = 8; constexpr float CHAIN_SPACING = 8;
// How hard a charging weapon shakes against an attack filled in as far. // How hard a charging weapon shakes against an attack filled in as far.
@@ -238,29 +237,43 @@ static float kick_get(Fighter& fighter)
return fighter.kickTo * math_pow(KICK_DECAY, (fighter.kickTime - KICK_SWEEP) / TICK); return fighter.kickTo * math_pow(KICK_DECAY, (fighter.kickTime - KICK_SWEEP) / TICK);
} }
// How much of a weapon's cooldown is left, from 1 just fired to 0 ready.
static float cooldown_left_get(Fighter& fighter, int key)
{
float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1);
return total > 0 ? std::clamp(fighter.cooldowns[key] / total, 0.0f, 1.0f) : 0;
}
// A reloading weapon's pose through its cooldown: once the kick has played out it tips up to stand vertical, dips // A reloading weapon's pose through its cooldown: once the kick has played out it tips up to stand vertical, dips
// down and back like a pump, then swings back to the aim just as it's ready. // down and back like a pump, then swings back to the aim just as it's ready.
static void reload_pose(Fighter& fighter, int key, float cooldown, Draw& draw) static void reload_pose(Fighter& fighter, int key, Draw& draw)
{ {
if (!WEAPON_DEFS[fighter.weaponLoadout[key]].isReloading || cooldown <= 0) if (!WEAPON_DEFS[fighter.weaponLoadout[key]].isReloading || fighter.cooldowns[key] <= 0)
return; return;
float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1); float t = std::clamp((1 - cooldown_left_get(fighter, key) - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f);
float t = std::clamp((1 - cooldown / total - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f);
float weight = math_sin(PI * t); float weight = math_sin(PI * t);
draw.angle = angle_lerp(draw.angle, -PI / 2, weight); draw.angle = angle_lerp(draw.angle, -PI / 2, weight);
draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP); draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP);
} }
// How far a fighter's weapons fill in with its colour: with the charge as it builds, or the speed bonus if that's more, // How far a fighter's weapons (and crosshair) fill in with its colour: with the charge as it builds, or the speed bonus
// then once the charge is ready blinking between full and the speed bonus alone. // if that's more, then once the charge is ready blinking between full and the speed bonus alone.
static float charge_fill_get(Game& game, Fighter& fighter) float Scene::charge_fill_get(Fighter& fighter)
{ {
float buff = game.sim.weapons.buff_get(fighter); float buff = game.sim.weapons.buff_get(fighter);
if (fighter.charge >= 1) if (fighter.charge >= 1)
return game.scene.is_flashing(fighter) ? 1.0f : buff; return is_flashing(fighter) ? 1.0f : buff;
return std::max(buff, fighter.charge); return std::max(buff, fighter.charge);
} }
// How far a fighter's weapon (by key, or -1 for none) and crosshair shake this frame: harder the further its charge has
// got, and from fully charged as the weapon is used, easing to still as its cooldown runs out.
Vec2 Scene::weapon_shake_get(Fighter& fighter, int key)
{
float left = key < 0 ? 0 : cooldown_left_get(fighter, key);
return jitter_get(std::max(fighter.charge, left * left) * CHARGE_SHAKE);
}
static bool is_pair(const char* const (&pair)[2], const char* animation) static bool is_pair(const char* const (&pair)[2], const char* animation)
{ {
return animation == pair[0] || animation == pair[1]; return animation == pair[0] || animation == pair[1];
@@ -313,6 +326,12 @@ void Scene::flash_spawn(Vec2 position, float angle, float size, int owner)
game.effects.flash_spawn(position, angle, size, game.color_get(owner)); game.effects.flash_spawn(position, angle, size, game.color_get(owner));
} }
void Scene::ring_spawn(Vec2 position, float radius, int owner)
{
if (!isMuted)
game.effects.ring_spawn(position, radius, game.color_get(owner));
}
void Scene::fighter_afterimage_spawn(Fighter& fighter) void Scene::fighter_afterimage_spawn(Fighter& fighter)
{ {
if (!isMuted) if (!isMuted)
@@ -674,7 +693,7 @@ void Scene::attacks_draw()
if (kind == ATTACK_HOOK) if (kind == ATTACK_HOOK)
{ {
// The rope runs from the end of the grapple's base in the hand. // The rope runs from the end of the grapple's base in the hand.
Vec2 hand = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get(); Vec2 hand = fighter.hand_get(attack.hand);
Vec2 base = hand + vec2_polar(vec2_angle(attack.position - hand), GRAPPLE_HANDLE); Vec2 base = hand + vec2_polar(vec2_angle(attack.position - hand), GRAPPLE_HANDLE);
strength_line_draw(game, attack, {{base, attack.position}, ROPE_OUTER, ROPE_INNER}); strength_line_draw(game, attack, {{base, attack.position}, ROPE_OUTER, ROPE_INNER});
} }
@@ -890,7 +909,7 @@ Pose Scene::attack_pose(Attack& attack)
// their path, each a little out of step with the rest. // their path, each a little out of step with the rest.
bool isFlying = attack.velocity.x != 0 || attack.velocity.y != 0; bool isFlying = attack.velocity.x != 0 || attack.velocity.y != 0;
draw.scale = {attack.scale, attack.scale}; draw.scale = {attack.scale, attack.scale};
draw.angle = isFlying ? attack.angle : PI / 2; draw.angle = isFlying ? attack.angle - PI / 2 : 0;
pose = {&effects, "Wisp", draw, (attack.time + attack.slot * WISP_FRAME_STAGGER) * effects.fps}; pose = {&effects, "Wisp", draw, (attack.time + attack.slot * WISP_FRAME_STAGGER) * effects.fps};
break; break;
} }
@@ -905,21 +924,21 @@ Pose Scene::attack_pose(Attack& attack)
} }
// Draws the selected weapon in the player's hand, aimed at the cursor, unless it is out attacking. It fills in as it // Draws the selected weapon in the player's hand, aimed at the cursor, unless it is out attacking. It fills in as it
// charges (see charge_fill_get) and shakes harder the further the charge has got. A charging mine weapon holds up the // charges (see charge_fill_get) and shakes with its charge and cooldown (see weapon_shake_get). A charging mine weapon
// mine it's about to place instead. // holds up the mine it's about to place instead.
void Scene::held_draw(Fighter& fighter) void Scene::held_draw(Fighter& fighter)
{ {
int held = fighter.weapon_key(fighter.slot); int held = fighter.weapon_key(fighter.slot);
if (held < 0 || (game.sim.weapons.is_busy(fighter, held) && !game.sim.weapons.tether_get(fighter, held, false))) if (held < 0 || (game.sim.weapons.is_busy(fighter, held) && !game.sim.weapons.tether_get(fighter, held, 0)))
return; return;
float side = fighter.is_left() ? -1 : 1; float side = fighter.is_left() ? -1 : 1;
Draw draw = { Draw draw = {
.color = game.color_get(fighter.index), .color = game.color_get(fighter.index),
.position = fighter.hand_get() + jitter_get(fighter.charge * CHARGE_SHAKE), .position = fighter.hand_get() + weapon_shake_get(fighter, held),
.scale = {1, side}, .scale = {1, side},
.angle = vec2_angle(fighter.aim) + kick_get(fighter) * side, .angle = vec2_angle(fighter.aim) + kick_get(fighter) * side,
.flash = charge_fill_get(game, fighter), .flash = charge_fill_get(fighter),
}; };
if (fighter.charge > 0 && WEAPON_DEFS[fighter.weaponLoadout[held]].charged.kind == ATTACK_MINE) if (fighter.charge > 0 && WEAPON_DEFS[fighter.weaponLoadout[held]].charged.kind == ATTACK_MINE)
{ {
@@ -928,36 +947,38 @@ void Scene::held_draw(Fighter& fighter)
game.sprites.effects.draw(game.renderer, "Mine", 0, draw); game.sprites.effects.draw(game.renderer, "Mine", 0, draw);
return; return;
} }
reload_pose(fighter, held, fighter.cooldowns[held], draw); reload_pose(fighter, held, draw);
hand_draw(fighter, held, false, draw); hand_draw(fighter, held, 0, draw);
} }
// Dual Wield: the off-hand weapon, a darker shade of the fighter's colour, held in the far hand behind it. // Dual Wield: the off-hand weapons, a darker shade of the fighter's colour, fanned out behind it (see
// Fighter::hand_get), the furthest back first so the nearer cover them. Each shakes and reloads like the main one.
void Scene::offhand_draw(Fighter& fighter) void Scene::offhand_draw(Fighter& fighter)
{ {
int off = fighter.off_slot_get(); int offs[SLOT_MAX];
off = off < 0 ? -1 : fighter.weapon_key(off);
if (!fighter.is_trinket_equipped(TRINKET_DUAL) || off < 0 ||
(game.sim.weapons.is_busy(fighter, off) && !game.sim.weapons.tether_get(fighter, off, true)))
return;
float side = fighter.is_left() ? -1 : 1; float side = fighter.is_left() ? -1 : 1;
for (int hand = fighter.off_slots_get(offs); hand >= 1; hand--)
{
int off = fighter.weapon_key(offs[hand - 1]);
if (game.sim.weapons.is_busy(fighter, off) && !game.sim.weapons.tether_get(fighter, off, hand))
continue;
Draw draw = { Draw draw = {
.color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE), .color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE),
.position = fighter.offhand_get() + jitter_get(fighter.charge * CHARGE_SHAKE), .position = fighter.hand_get(hand) + weapon_shake_get(fighter, off),
.scale = {1, side}, .scale = {1, side},
.angle = vec2_angle(fighter.aim), .angle = vec2_angle(fighter.aim),
.flash = charge_fill_get(game, fighter), .flash = charge_fill_get(fighter),
}; };
reload_pose(fighter, off, fighter.cooldowns[off], draw); reload_pose(fighter, off, draw);
hand_draw(fighter, off, true, draw); hand_draw(fighter, off, hand, draw);
}
} }
// Draws a weapon in a hand. While its whip or hook is out, the hand holds only its base ("<name> Base"), pointing // Draws a weapon in a hand. While its whip or hook is out, the hand holds only its base ("<name> Base"), pointing
// down the lash or rope. // down the lash or rope.
void Scene::hand_draw(Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw) void Scene::hand_draw(Fighter& fighter, int weaponSlot, int hand, Draw draw)
{ {
Attack* tether = game.sim.weapons.tether_get(fighter, weaponSlot, isOffhand); Attack* tether = game.sim.weapons.tether_get(fighter, weaponSlot, hand);
std::string animation = WEAPON_DEFS[fighter.weaponLoadout[weaponSlot]].name; std::string animation = WEAPON_DEFS[fighter.weaponLoadout[weaponSlot]].name;
if (tether) if (tether)
{ {
+4 -1
View File
@@ -17,6 +17,8 @@ public:
void update(); void update();
void draw(); void draw();
bool is_flashing(Fighter& fighter); bool is_flashing(Fighter& fighter);
float charge_fill_get(Fighter& fighter);
Vec2 weapon_shake_get(Fighter& fighter, int key);
void sound_play(SoundId sound) override; void sound_play(SoundId sound) override;
void charge_play(Fighter& fighter) override; void charge_play(Fighter& fighter) override;
@@ -25,6 +27,7 @@ public:
void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) override; void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) override;
void shake_add(float amount) override; void shake_add(float amount) override;
void flash_spawn(Vec2 position, float angle, float size, int owner) override; void flash_spawn(Vec2 position, float angle, float size, int owner) override;
void ring_spawn(Vec2 position, float radius, int owner) override;
void fighter_afterimage_spawn(Fighter& fighter) override; void fighter_afterimage_spawn(Fighter& fighter) override;
void attack_afterimage_spawn(Attack& attack) override; void attack_afterimage_spawn(Attack& attack) override;
void fighter_aura_spawn(Fighter& fighter) override; void fighter_aura_spawn(Fighter& fighter) override;
@@ -55,5 +58,5 @@ private:
Pose attack_pose(Attack& attack); Pose attack_pose(Attack& attack);
void held_draw(Fighter& fighter); void held_draw(Fighter& fighter);
void offhand_draw(Fighter& fighter); void offhand_draw(Fighter& fighter);
void hand_draw(Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw); void hand_draw(Fighter& fighter, int weaponSlot, int hand, Draw draw);
}; };
+2 -2
View File
@@ -42,11 +42,11 @@ public:
int weaponSlots = 2; int weaponSlots = 2;
bool isAimShown = true; bool isAimShown = true;
bool isCollisionShown = false; bool isCollisionShown = false;
bool isDpsShown = true; bool isDpsShown = false;
bool isFullscreen = false; bool isFullscreen = false;
bool isMuted = false; bool isMuted = false;
bool isShaking = true; bool isShaking = true;
bool isSpeedShown = true; bool isSpeedShown = false;
bool isVsync = true; bool isVsync = true;
// The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded. // The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded.
char serverUrl[SERVER_URL_LENGTH] = {}; char serverUrl[SERVER_URL_LENGTH] = {};
+2
View File
@@ -23,6 +23,8 @@ public:
{} {}
virtual void flash_spawn(Vec2 position, float angle, float size, int owner) virtual void flash_spawn(Vec2 position, float angle, float size, int owner)
{} {}
virtual void ring_spawn(Vec2 position, float radius, int owner)
{}
virtual void fighter_afterimage_spawn(Fighter& fighter) virtual void fighter_afterimage_spawn(Fighter& fighter)
{} {}
virtual void attack_afterimage_spawn(Attack& attack) virtual void attack_afterimage_spawn(Attack& attack)
-4
View File
@@ -7,13 +7,9 @@ void Sprites::init(Game& game)
dummy.load(renderer, "dummy"); dummy.load(renderer, "dummy");
effects.load(renderer, "effects"); effects.load(renderer, "effects");
icons.load(renderer, "weapon_icons"); icons.load(renderer, "weapon_icons");
iconsSmall.load(renderer, "weapon_icons_small");
iconsTiny.load(renderer, "weapon_icons_tiny");
player.load(renderer, "player"); player.load(renderer, "player");
tiles.load(renderer, "tiles"); tiles.load(renderer, "tiles");
trinkets.load(renderer, "trinkets"); trinkets.load(renderer, "trinkets");
trinketsSmall.load(renderer, "trinkets_small");
trinketsTiny.load(renderer, "trinkets_tiny");
modes.load(renderer, "modes"); modes.load(renderer, "modes");
ui.load(renderer, "ui"); ui.load(renderer, "ui");
weapons.load(renderer, "weapons"); weapons.load(renderer, "weapons");
+2 -5
View File
@@ -12,15 +12,12 @@ class Sprites
public: public:
Anm2 dummy; Anm2 dummy;
Anm2 effects; Anm2 effects;
// Weapon and trinket icons at full (24 pixel), small (16) and tiny (8) sizes. // Weapon and trinket icons at full (24 pixel), small (16) and tiny (8) sizes, the smaller ones named with Small or
// Tiny after.
Anm2 icons; Anm2 icons;
Anm2 iconsSmall;
Anm2 iconsTiny;
Anm2 player; Anm2 player;
Anm2 tiles; Anm2 tiles;
Anm2 trinkets; Anm2 trinkets;
Anm2 trinketsSmall;
Anm2 trinketsTiny;
// A game mode's icon, by its name (Deathmatch, Race). // A game mode's icon, by its name (Deathmatch, Race).
Anm2 modes; Anm2 modes;
Anm2 ui; Anm2 ui;
+1
View File
@@ -119,6 +119,7 @@ template <class Stream, class SimRef> static void state_walk(Stream& stream, Sim
stream.list(sim.weapons.attacks); stream.list(sim.weapons.attacks);
World& world = sim.world; World& world = sim.world;
stream.list(world.dummies); stream.list(world.dummies);
stream.list(world.ledges);
stream.list(world.obstacles); stream.list(world.obstacles);
stream.list(world.ramps); stream.list(world.ramps);
stream.list(world.tiles); stream.list(world.tiles);
+44 -8
View File
@@ -7,6 +7,43 @@
constexpr const char* FONT_FOLDER = "font/"; constexpr const char* FONT_FOLDER = "font/";
constexpr const char* FONT_NAMES[FONT_COUNT] = {"tiny", "small", "big", "huge"}; constexpr const char* FONT_NAMES[FONT_COUNT] = {"tiny", "small", "big", "huge"};
constexpr Vec2 OUTLINE_OFFSETS[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; constexpr Vec2 OUTLINE_OFFSETS[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
constexpr int UTF8_PAIR_MASK = 0xE0;
constexpr int UTF8_PAIR_LEAD = 0xC0;
constexpr int UTF8_PAIR_BITS = 0x1F;
constexpr int UTF8_TRIPLE_MASK = 0xF0;
constexpr int UTF8_TRIPLE_LEAD = 0xE0;
constexpr int UTF8_TRIPLE_BITS = 0x0F;
constexpr int UTF8_TAIL_BITS = 0x3F;
constexpr int UTF8_TAIL_SHIFT = 6;
// Characters past Latin-1 the fonts have (the arrows), each kept in the glyph of a C1 control code from here up, as
// those are never printed.
constexpr int GLYPH_EXTRAS[] = {0x2190, 0x2191, 0x2192, 0x2193};
constexpr int GLYPH_EXTRA_FIRST = 0x80;
constexpr int LATIN1_END = 0x100;
// A UTF-8 continuation byte's bits added under code.
static int utf8_tail_add(int code, const char*& character)
{
return code << UTF8_TAIL_SHIFT | ((unsigned char)*++character & UTF8_TAIL_BITS);
}
// The glyph a string's next character is (a byte, or two or three bytes of UTF-8), leaving the pointer on its last
// byte, or -1 for none.
static int glyph_next(const char*& character)
{
int code = (unsigned char)*character;
if ((code & UTF8_PAIR_MASK) == UTF8_PAIR_LEAD && character[1])
code = utf8_tail_add(code & UTF8_PAIR_BITS, character);
else if ((code & UTF8_TRIPLE_MASK) == UTF8_TRIPLE_LEAD && character[1] && character[2])
code = utf8_tail_add(utf8_tail_add(code & UTF8_TRIPLE_BITS, character), character);
if (code >= LATIN1_END)
{
int extra = std::find(std::begin(GLYPH_EXTRAS), std::end(GLYPH_EXTRAS), code) - std::begin(GLYPH_EXTRAS);
code = extra < (int)std::size(GLYPH_EXTRAS) ? GLYPH_EXTRA_FIRST + extra : -1;
}
int index = code - GLYPH_FIRST;
return code >= 0 && index >= 0 && index < GLYPH_COUNT ? index : -1;
}
void Text::init(Renderer& renderer) void Text::init(Renderer& renderer)
{ {
@@ -54,10 +91,9 @@ void Text::load(Renderer& renderer, Font font, const char* name)
for (auto [key, value] : glyphs) for (auto [key, value] : glyphs)
{ {
simdjson::dom::object fields; simdjson::dom::object fields;
if (key.size() != 1 || value.get(fields)) const char* character = key.data();
continue; int index = key.empty() ? -1 : glyph_next(character);
int index = key[0] - GLYPH_FIRST; if (index < 0 || character != key.data() + key.size() - 1 || value.get(fields))
if (index < 0 || index >= GLYPH_COUNT)
continue; continue;
atlas.glyphs[index] = { atlas.glyphs[index] = {
.region = {sheet.x + number_get(fields, "x", 0), sheet.y + number_get(fields, "y", 0), .region = {sheet.x + number_get(fields, "x", 0), sheet.y + number_get(fields, "y", 0),
@@ -119,8 +155,8 @@ void Text::draw(Renderer& renderer, const char* string, Draw draw, Font font)
continue; continue;
} }
int index = *character - GLYPH_FIRST; int index = glyph_next(character);
if (index < 0 || index >= GLYPH_COUNT) if (index < 0)
continue; continue;
Glyph& glyph = atlas.glyphs[index]; Glyph& glyph = atlas.glyphs[index];
@@ -142,13 +178,13 @@ Vec2 Text::size_get(const char* string, Font font, float scale)
for (const char* character = string; *character; character++) for (const char* character = string; *character; character++)
{ {
int index = *character - GLYPH_FIRST; int index = *character == '\n' ? -1 : glyph_next(character);
if (*character == '\n') if (*character == '\n')
{ {
lineWidth = 0; lineWidth = 0;
size.y += atlas.lineHeight; size.y += atlas.lineHeight;
} }
else if (index >= 0 && index < GLYPH_COUNT) else if (index >= 0)
lineWidth += atlas.glyphs[index].advance; lineWidth += atlas.glyphs[index].advance;
size.x = std::max(size.x, lineWidth); size.x = std::max(size.x, lineWidth);
} }
+4 -1
View File
@@ -2,8 +2,11 @@
#include "renderer.h" #include "renderer.h"
constexpr int GLYPH_COUNT = 95; // Glyphs cover Latin-1 from the space up (its upper half written as two-byte UTF-8, like the degree sign), plus the
// arrows (see text.cpp), though only ASCII is typed.
constexpr int GLYPH_COUNT = 224;
constexpr int GLYPH_FIRST = 32; constexpr int GLYPH_FIRST = 32;
constexpr int GLYPH_TYPED_END = 127;
// The small font's line height (small.fnt's), which the HUD is laid out by. // The small font's line height (small.fnt's), which the HUD is laid out by.
constexpr float LINE_HEIGHT = 12; constexpr float LINE_HEIGHT = 12;
+12 -12
View File
@@ -71,8 +71,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
"A noble spirit embiggens the\nsmallest projectile."}, "A noble spirit embiggens the\nsmallest projectile."},
{"Diazepam", {"Diazepam",
"Steadies your aim: every attack,\nvolley and Splitter copy spreads less.", "Steadies your aim: every attack,\nvolley and Splitter copy spreads less.",
{"33% Less Spread", "66% Less Spread", "No Spread"}, {"17% Less Spread", "33% Less Spread", "50% Less Spread"},
{0.33f, 0.66f, 1}, {1 / 6.0f, 1 / 3.0f, 0.5f},
"Diazepam?\nPsychotropic drug?\nStops hands from trembling?\nQuestions?", "Diazepam?\nPsychotropic drug?\nStops hands from trembling?\nQuestions?",
1}, 1},
{"Splitter", {"Splitter",
@@ -86,7 +86,7 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
{0.1f, 0.25f, 1 / 3.0f}, {0.1f, 0.25f, 1 / 3.0f},
"Stud Multiplier"}, "Stud Multiplier"},
{"Dual Wield", {"Dual Wield",
"Your previous weapon fires from\nyour off hand too, for a cut of\nits damage.", "Your last-held weapon fires from\nyour off hand too, for a cut of\nits damage. Each copy adds another\nweapon, the next most recent.",
{"10% Off-Hand Damage", "25% Off-Hand Damage", "33% Off-Hand Damage"}, {"10% Off-Hand Damage", "25% Off-Hand Damage", "33% Off-Hand Damage"},
{0.1f, 0.25f, 1 / 3.0f}, {0.1f, 0.25f, 1 / 3.0f},
"Guns akimbo!"}, "Guns akimbo!"},
@@ -99,20 +99,20 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
"RICOCHET 2 BABY!"}, "RICOCHET 2 BABY!"},
{"Tesla Coil", {"Tesla Coil",
"Lightning chains between your\nweapons out in the world, shocking\nanything it touches.", "Lightning chains between your\nweapons out in the world, shocking\nanything it touches.",
{"4 Damage Shocks\n120 Arc Range", "7 Damage Shocks\n170 Arc Range", "10 Damage Shocks\n220 Arc Range"}, {"2 Damage Shocks\n120 Arc Range", "4 Damage Shocks\n170 Arc Range", "6 Damage Shocks\n220 Arc Range"},
{4, 7, 10}, {2, 4, 6},
"Okay this is just Technology Zero."}, "Okay this is just Technology Zero."},
{"Recoil Coil", {"Recoil Coil",
"Attacks push you away from your\naim, harder the more damage\nthey deal. Rapid fire adds up.", "Attacks push you away from your\naim, harder the more damage\nthey deal. Rapid fire adds up.",
{"12 Recoil Power", "18 Recoil Power", "24 Recoil Power"}, {"6 Recoil Power", "9 Recoil Power", "12 Recoil Power"},
{12, 18, 24}, {6, 9, 12},
"The forgotten sibling of Gravity\nand Speed..."}, "The forgotten sibling of Gravity\nand Speed..."},
{"Shock Absorber", {"Shock Absorber",
"Your own weapons move you around\nless (recoil, kickback, lunges,\nrebounds, pulls and blasts), and\nyou take " "Your own weapons move you around\nless (recoil, kickback, lunges,\nrebounds, pulls and blasts), and\nyou take "
"less knockback.", "less knockback.",
{"25% Less Weapon Movement\n17% Less Knockback Taken", "50% Less Weapon Movement\n33% Less Knockback Taken", {"12.5% Less Weapon Movement\n8% Less Knockback Taken", "25% Less Weapon Movement\n17% Less Knockback Taken",
"75% Less Weapon Movement\n50% Less Knockback Taken"}, "37.5% Less Weapon Movement\n25% Less Knockback Taken"},
{0.75f, 0.5f, 0.25f}, {0.875f, 0.75f, 0.625f},
"The mechanical kind, not the\nelectrical kind."}, "The mechanical kind, not the\nelectrical kind."},
{"Shrinker", {"Shrinker",
"Makes you smaller.", "Makes you smaller.",
@@ -147,8 +147,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
"Probably given to you from\nEnergy Drink."}, "Probably given to you from\nEnergy Drink."},
{"Feather", {"Feather",
"Hold Up in the air to float.", "Hold Up in the air to float.",
{"0.6x Gravity Holding Up", "0.45x Gravity Holding Up", "0.3x Gravity Holding Up"}, {"0.75x Gravity Holding Up", "0.625x Gravity Holding Up", "0.5x Gravity Holding Up"},
{0.6f, 0.45f, 0.3f}, {0.75f, 0.625f, 0.5f},
"Cape not included."}, "Cape not included."},
{"Iron Weights", {"Iron Weights",
"Fall even faster holding Down.\nLanding while holding Down slams\nthe ground, harder the faster you\nhit it.", "Fall even faster holding Down.\nLanding while holding Down slams\nthe ground, harder the faster you\nhit it.",
+311 -206
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
constexpr int CHAMFER = 2;
constexpr float BANNER_SCALE = 4; constexpr float BANNER_SCALE = 4;
constexpr float BANNER_Y = 110; constexpr float BANNER_Y = 110;
constexpr float BAR_GAP = 2; constexpr float BAR_GAP = 2;
@@ -19,11 +18,17 @@ constexpr float RIVAL_OUT_ALPHA = 0.25f;
constexpr float SPECTATE_GAP = 4; constexpr float SPECTATE_GAP = 4;
constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK}; constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK};
constexpr float HUD_IDLE_ALPHA = 0.5f; constexpr float HUD_IDLE_ALPHA = 0.5f;
constexpr float HUD_HOVER_ALPHA = 0.5f;
constexpr float HUD_HOVER_FADE = 0.15f; constexpr float HUD_HOVER_FADE = 0.15f;
constexpr float RIVAL_PADDING = 2; constexpr float RIVAL_PADDING = 2;
constexpr int RIVAL_COLUMNS_MAX = 3; constexpr int RIVAL_COLUMNS_MAX = 3;
constexpr int RIVAL_FULL_SLOTS = 4; // What the other fighters' windows try, from largest to smallest, until they all fit in a couple of rows: shrinking
constexpr int RIVALS_PER_ROW = 4; // icons before the font, then leaving the icons out.
constexpr int RIVAL_ROWS_MAX = 2;
constexpr int RIVAL_STYLES = 6;
constexpr int RIVAL_ICON_STYLES = 4;
constexpr Font RIVAL_FONTS[RIVAL_STYLES] = {FONT_SMALL, FONT_SMALL, FONT_TINY, FONT_TINY, FONT_SMALL, FONT_TINY};
constexpr IconSize RIVAL_SIZES[RIVAL_ICON_STYLES] = {ICON_FULL, ICON_SMALL, ICON_SMALL, ICON_TINY};
constexpr float COOLDOWN_SLOTS = 2; constexpr float COOLDOWN_SLOTS = 2;
constexpr float MINI_ICON_SCALE = 0.5f; constexpr float MINI_ICON_SCALE = 0.5f;
constexpr float MINI_SLOT = 16; constexpr float MINI_SLOT = 16;
@@ -36,8 +41,10 @@ constexpr float READY_GAP = 2;
constexpr float TIP_GAP = 4; constexpr float TIP_GAP = 4;
constexpr float COUNTDOWN_Y = 40; constexpr float COUNTDOWN_Y = 40;
constexpr float COUNTDOWN_DIM = 0.6f; constexpr float COUNTDOWN_DIM = 0.6f;
constexpr float CHEVRON_INSET = 3; constexpr const char* ICON_SUFFIXES[ICON_SIZE_COUNT] = {"", " Small", " Tiny"};
constexpr float CHEVRON_STEP = 3; constexpr const char* CHEVRONS[ICON_SIZE_COUNT] = {"Chevron", "Chevron Small", "Chevron Tiny"};
constexpr float CHEVRON_INSETS[ICON_SIZE_COUNT] = {3, 2, 2};
constexpr float CHEVRON_STEPS[ICON_SIZE_COUNT] = {3, 2, 2};
constexpr float CHARGE_BAR_HEIGHT = 8; constexpr float CHARGE_BAR_HEIGHT = 8;
constexpr float CHARGE_BAR_WIDTH = 20; constexpr float CHARGE_BAR_WIDTH = 20;
constexpr float DPS_SCALE = 2; constexpr float DPS_SCALE = 2;
@@ -52,21 +59,20 @@ constexpr float SCORE_HOTBAR_GAP = 8;
constexpr float STATUS_BAR_HEIGHT = 12; constexpr float STATUS_BAR_HEIGHT = 12;
constexpr float STATUS_BAR_WIDTH = 80; constexpr float STATUS_BAR_WIDTH = 80;
constexpr float STATUS_GAP = 6; constexpr float STATUS_GAP = 6;
constexpr float PANEL_BORDER = 2; // How thick a slot's (and its cooldown bar's) edge is at each size.
constexpr float SLOT_STROKES[ICON_SIZE_COUNT] = {PANEL_BORDER, PANEL_BORDER, 1};
constexpr float HOVER_BORDER = 1; constexpr float HOVER_BORDER = 1;
constexpr float HOVER_GAP = 1; constexpr float HOVER_GAP = 1;
constexpr float TARGET_GAP = 1; constexpr float TARGET_GAP = 1;
constexpr float TARGET_HEIGHT = 2; constexpr float TARGET_HEIGHT = 2;
constexpr float TARGET_INSET = 4; constexpr float TARGET_INSET = 4;
constexpr float CARET_BLINK = 0.5f; constexpr float CARET_BLINK = 0.5f;
constexpr float COOLDOWN_BAR_HEIGHT = 7; constexpr float COOLDOWN_BAR_HEIGHTS[ICON_SIZE_COUNT] = {7, 6, 6};
constexpr float COOLDOWN_FADE_IN = 0.1f; constexpr float COOLDOWN_FADE_IN = 0.1f;
constexpr float COOLDOWN_FADE_OUT = 0.25f; constexpr float COOLDOWN_FADE_OUT = 0.25f;
constexpr float COOLDOWN_GAP = 4; constexpr float COOLDOWN_GAP = 4;
constexpr float COOLDOWN_SPACING = 26; constexpr float COOLDOWN_ROW_GAP = 2;
constexpr float COOLDOWN_COMPACT_SPACING = 14;
constexpr float COOLDOWN_SLIDE = 12; constexpr float COOLDOWN_SLIDE = 12;
constexpr int COOLDOWN_FULL_ROWS = 5;
constexpr float COOLDOWN_VISIBLE = 0.25f; constexpr float COOLDOWN_VISIBLE = 0.25f;
constexpr float OFFSCREEN_ARROW_SCALE = 2; constexpr float OFFSCREEN_ARROW_SCALE = 2;
constexpr float OFFSCREEN_MARGIN = 8; constexpr float OFFSCREEN_MARGIN = 8;
@@ -89,7 +95,7 @@ constexpr float PREVIEW_CAPTION_GAP = 8;
constexpr float PREVIEW_FEET = 120; constexpr float PREVIEW_FEET = 120;
constexpr float PREVIEW_NAME_Y = 16; constexpr float PREVIEW_NAME_Y = 16;
constexpr float PREVIEW_SCALE = 3; constexpr float PREVIEW_SCALE = 3;
constexpr const char PREVIEW_ANIMATION[] = "Walk Right"; constexpr const char PREVIEW_ANIMATION[] = "Preview";
constexpr float SUMMARY_ICON_GAP = 4; constexpr float SUMMARY_ICON_GAP = 4;
constexpr float NAME_PING_GAP = 3; constexpr float NAME_PING_GAP = 3;
// A queued game's area: its fill and edge, the queuer's icon over it (how high it floats and bobs, and how fast), and // A queued game's area: its fill and edge, the queuer's icon over it (how high it floats and bobs, and how fast), and
@@ -114,7 +120,12 @@ constexpr float TRACK_DOT = 6;
constexpr float TRACK_HEIGHT = 2; constexpr float TRACK_HEIGHT = 2;
constexpr float TRACK_WIDTH = 200; constexpr float TRACK_WIDTH = 200;
constexpr float SECONDS_PER_MINUTE = 60; constexpr float SECONDS_PER_MINUTE = 60;
constexpr Vec2 LABEL_OFFSET = {4, 2}; // Slot numbers: their font and corner at each size (none on tiny slots).
constexpr Font LABEL_FONTS[ICON_TINY] = {FONT_SMALL, FONT_TINY};
constexpr Vec2 LABEL_OFFSETS[ICON_TINY] = {{4, 2}, {3, 2}};
// Where a full slot's number sits from its icon's corner.
constexpr Vec2 ICON_LABEL_OFFSET = {LABEL_OFFSETS[ICON_FULL].x - (SLOT_SIZE - ICON_SIZE) / 2,
LABEL_OFFSETS[ICON_FULL].y - (SLOT_SIZE - ICON_SIZE) / 2};
constexpr const char* TIPS[] = { constexpr const char* TIPS[] = {
"Big hits launch fighters, and slamming into walls while launched hurts.", "Big hits launch fighters, and slamming into walls while launched hurts.",
@@ -155,6 +166,12 @@ constexpr const char* TIPS[] = {
static CountdownInfo countdown_info_get(Game& game, bool isPad); static CountdownInfo countdown_info_get(Game& game, bool isPad);
// How opaque a HUD element (other than a hotbar slot) is, fading as the mouse comes over it.
static float hover_alpha_get(float hover)
{
return 1 - (1 - HUD_HOVER_ALPHA) * hover;
}
void Ui::init(Game& game) void Ui::init(Game& game)
{ {
for (int i = 0; i < MAX_FIGHTERS; i++) for (int i = 0; i < MAX_FIGHTERS; i++)
@@ -194,6 +211,7 @@ void Ui::draw(Game& game)
// The menu covers the match overlays. Hotbars sit under them, except in the sandbox menu they slide into. With // The menu covers the match overlays. Hotbars sit under them, except in the sandbox menu they slide into. With
// several players on this screen in a match, each gets a compact panel along the bottom instead. // several players on this screen in a match, each gets a compact panel along the bottom instead.
statusRect = {};
queues_draw(game); queues_draw(game);
bars_draw(game); bars_draw(game);
game.chat.overhead_draw(game); game.chat.overhead_draw(game);
@@ -204,8 +222,8 @@ void Ui::draw(Game& game)
bool isShared = game.sim.match.state != MATCH_PRACTICE && game.local_count() > 1; bool isShared = game.sim.match.state != MATCH_PRACTICE && game.local_count() > 1;
if (!isBarsRaised && !isShared && !isWatching) if (!isBarsRaised && !isShared && !isWatching)
{ {
hotbar_draw(game);
cooldowns_draw(game); cooldowns_draw(game);
hotbar_draw(game);
} }
if (isShared) if (isShared)
panels_draw(game); panels_draw(game);
@@ -283,12 +301,11 @@ void Ui::bars_draw(Game& game)
} }
} }
// A bordered bar filled from the left by fill, from 0 to 1. // A bar, its edge stroke thick, filled from the left by fill, from 0 to 1.
void Ui::bar_draw(Renderer& renderer, Rect bar, float fill, Color color) void Ui::bar_draw(Renderer& renderer, Rect bar, float fill, Color color, float stroke)
{ {
panel_draw(renderer, bar, color); panel_draw(renderer, bar, color, -1, stroke);
renderer.rect_draw({bar.x + PANEL_BORDER, bar.y + PANEL_BORDER, std::round((bar.w - PANEL_BORDER * 2) * fill), renderer.rect_draw({bar.x + stroke, bar.y + stroke, std::round((bar.w - stroke * 2) * fill), bar.h - stroke * 2},
bar.h - PANEL_BORDER * 2},
color); color);
} }
@@ -329,7 +346,9 @@ void Ui::match_draw(Game& game)
length += SDL_snprintf(text + length, sizeof(text) - length, i > 0 ? " - %d" : "%d", match.wins[i]); length += SDL_snprintf(text + length, sizeof(text) - length, i > 0 ? " - %d" : "%d", match.wins[i]);
Vec2 scale = {SCORE_SCALE, SCORE_SCALE}; Vec2 scale = {SCORE_SCALE, SCORE_SCALE};
float scoreWidth = game.text.size_get(text, FONT_SMALL, SCORE_SCALE).x; float scoreWidth = game.text.size_get(text, FONT_SMALL, SCORE_SCALE).x;
float hotbarRight = HOTBAR_MARGIN + game.local_get().weaponSlots * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP; int weapons = game.local_get().weaponSlots;
Rect last = game.menu.slot_rect(false, weapons - 1, weapons);
float hotbarRight = last.x + last.w;
float x = std::round(std::max((CANVAS_WIDTH - scoreWidth) / 2, hotbarRight + SCORE_HOTBAR_GAP)); float x = std::round(std::max((CANVAS_WIDTH - scoreWidth) / 2, hotbarRight + SCORE_HOTBAR_GAP));
float center = x + scoreWidth / 2; float center = x + scoreWidth / 2;
for (int i = 0; i < game.sim.fighterCount; i++) for (int i = 0; i < game.sim.fighterCount; i++)
@@ -598,6 +617,7 @@ LoadoutLayout Ui::loadout_layout(Game& game, float top, float bottom)
continue; continue;
int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[i]; int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[i];
Tooltip& tooltip = layout.tooltips[kind][i] = tooltip_build(game, isTrinket, item, level); Tooltip& tooltip = layout.tooltips[kind][i] = tooltip_build(game, isTrinket, item, level);
tooltip.slot = i;
tooltip.flavor[0] = '\0'; tooltip.flavor[0] = '\0';
tooltip.rowCount = 0; tooltip.rowCount = 0;
if (isTrinket && !isBrief) if (isTrinket && !isBrief)
@@ -702,7 +722,7 @@ void Ui::loadout_draw(Game& game, float alpha, float top)
tooltip_text_draw(game, layout.tooltips[kind][i], tooltip_text_draw(game, layout.tooltips[kind][i],
{.color = color, .position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}}, {.color = color, .position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}},
font); font);
icon_draw(game, isTrinket, item, tooltip_icon_draw(game, layout.tooltips[kind][i],
{.color = color, {.color = color,
.position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2, .position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2,
panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}}); panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}});
@@ -797,8 +817,10 @@ void Ui::status_draw(Game& game)
{ {
char text[TOOLTIP_LENGTH]; char text[TOOLTIP_LENGTH];
float right = CANVAS_WIDTH - HOTBAR_MARGIN; float right = CANVAS_WIDTH - HOTBAR_MARGIN;
float left = right;
float y = HOTBAR_MARGIN; float y = HOTBAR_MARGIN;
float rowHeight = game.text.line_height_get(FONT_SMALL, DPS_SCALE); float rowHeight = game.text.line_height_get(FONT_SMALL, DPS_SCALE);
game.renderer.layer_begin();
if (game.sim.match.state != MATCH_PRACTICE && !game.sim.match.is_racing()) if (game.sim.match.state != MATCH_PRACTICE && !game.sim.match.is_racing())
{ {
@@ -809,17 +831,16 @@ void Ui::status_draw(Game& game)
bar_draw(game.renderer, bar, std::max(0.0f, fighter.health) / maxHealth, tint); bar_draw(game.renderer, bar, std::max(0.0f, fighter.health) / maxHealth, tint);
SDL_snprintf(text, sizeof(text), "%d", (int)std::ceil(std::max(0.0f, fighter.health))); SDL_snprintf(text, sizeof(text), "%d", (int)std::ceil(std::max(0.0f, fighter.health)));
float width = game.text.size_get(text, FONT_SMALL, DPS_SCALE).x; float width = game.text.size_get(text, FONT_SMALL, DPS_SCALE).x;
game.text.draw( left = std::round(bar.x - STATUS_GAP - width);
game.renderer, text, game.text.draw(game.renderer, text, {.color = tint, .position = {left, y}, .scale = {DPS_SCALE, DPS_SCALE}});
{.color = tint, .position = {std::round(bar.x - STATUS_GAP - width), y}, .scale = {DPS_SCALE, DPS_SCALE}});
y += rowHeight; y += rowHeight;
} }
// The DPS and speed meters each show only if they're on in the settings. // The DPS and speed meters each show only if they're on in the settings.
auto meter_draw = [&](const char* label) { auto meter_draw = [&](const char* label) {
float width = game.text.size_get(label, FONT_SMALL, DPS_SCALE).x; float x = std::round(right - game.text.size_get(label, FONT_SMALL, DPS_SCALE).x);
game.text.draw(game.renderer, label, left = std::min(left, x);
{.color = tint, .position = {std::round(right - width), y}, .scale = {DPS_SCALE, DPS_SCALE}}); game.text.draw(game.renderer, label, {.color = tint, .position = {x, y}, .scale = {DPS_SCALE, DPS_SCALE}});
y += rowHeight; y += rowHeight;
}; };
if (game.settings.isDpsShown) if (game.settings.isDpsShown)
@@ -832,10 +853,12 @@ void Ui::status_draw(Game& game)
SDL_snprintf(text, sizeof(text), "%d u/s", (int)vec2_length(game.local_get().body.velocity)); SDL_snprintf(text, sizeof(text), "%d u/s", (int)vec2_length(game.local_get().body.velocity));
meter_draw(text); meter_draw(text);
} }
statusRect = {left, HOTBAR_MARGIN, right - left, y - HOTBAR_MARGIN};
game.renderer.layer_end(hover_alpha_get(statusHover));
} }
// The weapon bar (lit on the selected slot) and the trinket bar (lit where filled). The menu decides where // The weapon bar (lit on the selected slot) and the trinket bar (lit where filled). The menu decides where
// they sit. Hovering a filled slot outside the menu shows its tooltip. // they sit, and how large. Hovering a filled slot outside the menu shows its tooltip.
void Ui::hotbar_draw(Game& game) void Ui::hotbar_draw(Game& game)
{ {
Vec2 mouse = game.input.mouse; Vec2 mouse = game.input.mouse;
@@ -848,21 +871,22 @@ void Ui::hotbar_draw(Game& game)
int index = isTrinket ? i - fighter.weaponSlots : i; int index = isTrinket ? i - fighter.weaponSlots : i;
int count = isTrinket ? fighter.trinketSlots : fighter.weaponSlots; int count = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
int item = isTrinket ? fighter.trinketLoadout[index] : fighter.weaponLoadout[index]; int item = isTrinket ? fighter.trinketLoadout[index] : fighter.weaponLoadout[index];
// In the HUD, slots are dimmed until the mouse comes over them; under the menu they're at full brightness.
float hover = slotHovers[isTrinket][index];
Slot slot = { Slot slot = {
.rect = game.menu.slot_rect(isTrinket, index, count), .rect = game.menu.slot_rect(isTrinket, index, count),
.alpha = game.menu.is_bars_open() ? 1 : HUD_IDLE_ALPHA + (1 - HUD_IDLE_ALPHA) * hover, .size = game.menu.slot_size_get(count),
.item = item, .item = item,
.level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[index], .level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[index],
.label = index + 1, .label = index + 1,
.isHoverable = true, .isHoverable = true,
.isLit = isTrinket ? item >= 0 : index == fighter.slot, .isLit = isTrinket ? item >= 0 : index == fighter.slot,
.isTrinket = isTrinket, .isTrinket = isTrinket,
.isOpaque = true,
.isTarget = game.menu.target_get(game, isTrinket) == index, .isTarget = game.menu.target_get(game, isTrinket) == index,
}; };
// In the HUD, slots are see-through until the mouse comes over them. Under the menu they're solid.
float hover = slotHovers[isTrinket][index];
game.renderer.layer_begin();
slot_draw(game, slot); slot_draw(game, slot);
game.renderer.layer_end(game.menu.is_bars_open() ? 1 : HUD_IDLE_ALPHA + (1 - HUD_IDLE_ALPHA) * hover);
if (item >= 0 && is_point_in_rect(mouse, slot.rect)) if (item >= 0 && is_point_in_rect(mouse, slot.rect))
hovered = slot; hovered = slot;
} }
@@ -870,48 +894,51 @@ void Ui::hotbar_draw(Game& game)
if (hovered.item < 0 || game.menu.isOpen) if (hovered.item < 0 || game.menu.isOpen)
return; return;
Tooltip tooltip = tooltip_build(game, hovered.isTrinket, hovered.item, hovered.level); Tooltip tooltip = tooltip_build(game, hovered.isTrinket, hovered.item, hovered.level);
tooltip.slot = hovered.label - 1;
tooltip_draw(game, tooltip); tooltip_draw(game, tooltip);
} }
// A bordered box with the item's icon, a stack of chevrons in the corner for its level, and an optional number. // A bordered box with the item's icon, a stack of chevrons in the corner for its level, and an optional number (left
// off tiny boxes). Only full-size boxes have room for the hover border.
void Ui::slot_draw(Game& game, Slot slot) void Ui::slot_draw(Game& game, Slot slot)
{ {
Renderer& renderer = game.renderer; Renderer& renderer = game.renderer;
bool isHovered = slot.isHoverable && !isFocusShown && is_point_in_rect(game.input.mouse, slot.rect); bool isHovered = slot.isHoverable && !isFocusShown && is_point_in_rect(game.input.mouse, slot.rect);
Color base = slot.isLit || isHovered || slot.isTarget ? tint : shade; Color color = color_fade(slot.isLit || isHovered || slot.isTarget ? tint : shade, slot.alpha);
Color color = slot.isOpaque ? color_shade(base, slot.alpha) : color_fade(base, slot.alpha); panel_draw(renderer, slot.rect, color, -1, SLOT_STROKES[slot.size]);
float fill = slot.isOpaque ? 1 : slot.alpha;
panel_draw(renderer, slot.rect, color, fill);
if (slot.isTarget) if (slot.isTarget)
renderer.rect_draw({slot.rect.x + TARGET_INSET, slot.rect.y + slot.rect.h + TARGET_GAP, renderer.rect_draw({slot.rect.x + TARGET_INSET, slot.rect.y + slot.rect.h + TARGET_GAP,
slot.rect.w - TARGET_INSET * 2, TARGET_HEIGHT}, slot.rect.w - TARGET_INSET * 2, TARGET_HEIGHT},
color); color);
if (isHovered) if (isHovered && slot.size == ICON_FULL)
{ {
Rect rect = slot.rect; Rect rect = slot.rect;
float inset = PANEL_BORDER + HOVER_GAP; float inset = PANEL_BORDER + HOVER_GAP;
chamfer_draw(renderer, {rect.x + inset, rect.y + inset, rect.w - inset * 2, rect.h - inset * 2}, color); chamfer_draw(renderer, {rect.x + inset, rect.y + inset, rect.w - inset * 2, rect.h - inset * 2}, color);
inset += HOVER_BORDER; inset += HOVER_BORDER;
chamfer_draw(renderer, {rect.x + inset, rect.y + inset, rect.w - inset * 2, rect.h - inset * 2}, chamfer_draw(renderer, {rect.x + inset, rect.y + inset, rect.w - inset * 2, rect.h - inset * 2},
color_fade(BLACK, fill)); color_fade(BLACK, slot.alpha));
} }
if (slot.item >= 0) if (slot.item >= 0)
{ {
icon_draw(game, slot.isTrinket, slot.item, icon_draw(game, slot.isTrinket, slot.item,
{.color = color, .position = {slot.rect.x + slot.rect.w / 2, slot.rect.y + slot.rect.h / 2}}); {.color = color, .position = {slot.rect.x + slot.rect.w / 2, slot.rect.y + slot.rect.h / 2}},
slot.size);
Vec2 corner = {slot.rect.x + slot.rect.w - CHEVRON_INSET, slot.rect.y + slot.rect.h - CHEVRON_INSET}; float inset = CHEVRON_INSETS[slot.size];
Vec2 corner = {slot.rect.x + slot.rect.w - inset, slot.rect.y + slot.rect.h - inset};
for (int i = 0; i <= slot.level; i++) for (int i = 0; i <= slot.level; i++)
game.sprites.ui.draw(renderer, "Chevron", 0, game.sprites.ui.draw(renderer, CHEVRONS[slot.size], 0,
{.color = color, .position = {corner.x, corner.y - i * CHEVRON_STEP}}); {.color = color, .position = {corner.x, corner.y - i * CHEVRON_STEPS[slot.size]}});
} }
if (slot.label <= 0) if (slot.label <= 0 || slot.size == ICON_TINY)
return; return;
char label[] = {char('0' + slot.label), 0}; char label[] = {char('0' + slot.label), 0};
game.text.draw(renderer, label, Vec2 offset = LABEL_OFFSETS[slot.size];
{.color = color, .position = {slot.rect.x + LABEL_OFFSET.x, slot.rect.y + LABEL_OFFSET.y}}); game.text.draw(renderer, label, {.color = color, .position = {slot.rect.x + offset.x, slot.rect.y + offset.y}},
LABEL_FONTS[slot.size]);
} }
// A number for a stats table: whole when it's whole, otherwise to two decimal places at most. // A number for a stats table: whole when it's whole, otherwise to two decimal places at most.
@@ -920,6 +947,9 @@ static void number_format(char* text, int size, float value)
SDL_snprintf(text, size, "%g", std::round(value * 100) / 100); SDL_snprintf(text, size, "%g", std::round(value * 100) / 100);
} }
// The stats table's last row, how far a weapon pushes its fighter.
constexpr int LUNGE_ROW = 8;
// What a summary row shows for an attack, or "" where it doesn't apply. // What a summary row shows for an attack, or "" where it doesn't apply.
static void stat_format(char* text, int size, int row, const AttackSummary& summary, const AttackDef& attack, static void stat_format(char* text, int size, int row, const AttackSummary& summary, const AttackDef& attack,
bool isCharged) bool isCharged)
@@ -976,36 +1006,42 @@ static void stat_format(char* text, int size, int row, const AttackSummary& summ
if (summary.spread > 0) if (summary.spread > 0)
{ {
number_format(number, sizeof(number), summary.spread); number_format(number, sizeof(number), summary.spread);
SDL_snprintf(text, size, "%s deg", number); SDL_snprintf(text, size, "%s°", number);
} }
break; break;
case 8: case LUNGE_ROW:
if (summary.lunge != 0) if (summary.lunge != 0)
SDL_snprintf(text, size, summary.lunge > 0 ? "%g forward" : "%g back", SDL_snprintf(text, size, "%g", std::round(std::abs(summary.lunge)));
std::round(std::abs(summary.lunge)));
break; break;
} }
} }
// Fills a weapon tooltip's stats table: a row per stat the weapon has (fired, damage, how often, range, blast, speed, // Fills a weapon tooltip's stats table: a row per stat the weapon has (fired, damage, how often, range, blast, speed,
// how many out at once, spread, lunge), always in that order, for its attack and its charged attack as this fighter // how many out at once, spread, lunge), always in that order, for its attack and its charged attack as this fighter
// would fire them. Values its trinkets change are marked; with a preview, ones the next level changes add "> next". // would fire them. A weapon that only pushes its fighter back calls its lunge Recoil. Values its trinkets change are
// marked; with a preview, ones the next level changes add "→ next".
void Ui::stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip) void Ui::stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip)
{ {
constexpr const char* LABELS[] = {"Fires", "Damage", "Every", "Range", "Blast", constexpr const char* LABELS[] = {"Fires", "Damage", "Every", "Range", "Blast",
"Speed", "Out at once", "Spread", "Lunge"}; "Speed", "Out at once", "Spread", "Lunge"};
const WeaponDef& def = WEAPON_DEFS[weapon]; const WeaponDef& def = WEAPON_DEFS[weapon];
AttackSummary summaries[2] = {attack_summary_get(fighter, weapon, level, false),
attack_summary_get(fighter, weapon, level, true)};
AttackSummary nexts[2] = {};
for (int column = 0; column < 2 && isPreview; column++)
nexts[column] = attack_summary_get(fighter, weapon, level + 1, column == 1);
bool isRecoil = summaries[0].lunge <= 0 && summaries[1].lunge <= 0;
tooltip.rowCount = 0; tooltip.rowCount = 0;
for (int row = 0; row < (int)std::size(LABELS); row++) for (int row = 0; row < (int)std::size(LABELS); row++)
{ {
StatRow& stat = tooltip.rows[tooltip.rowCount]; StatRow& stat = tooltip.rows[tooltip.rowCount];
stat = {.label = LABELS[row]}; stat = {.label = row == LUNGE_ROW && isRecoil ? "Recoil" : LABELS[row]};
bool isShown = false; bool isShown = false;
for (int column = 0; column < 2; column++) for (int column = 0; column < 2; column++)
{ {
bool isCharged = column == 1; bool isCharged = column == 1;
const AttackDef& attack = isCharged ? def.charged : def.normal; const AttackDef& attack = isCharged ? def.charged : def.normal;
AttackSummary summary = attack_summary_get(fighter, weapon, level, isCharged); AttackSummary& summary = summaries[column];
char* value = stat.values[column]; char* value = stat.values[column];
stat_format(value, STAT_LENGTH, row, summary, attack, isCharged); stat_format(value, STAT_LENGTH, row, summary, attack, isCharged);
bool changes[] = {summary.isPiecesChanged, bool changes[] = {summary.isPiecesChanged,
@@ -1021,10 +1057,9 @@ void Ui::stats_table_build(Fighter& fighter, int weapon, int level, bool isPrevi
if (isPreview && *value) if (isPreview && *value)
{ {
char next[STAT_LENGTH]; char next[STAT_LENGTH];
stat_format(next, sizeof(next), row, attack_summary_get(fighter, weapon, level + 1, isCharged), attack, stat_format(next, sizeof(next), row, nexts[column], attack, isCharged);
isCharged);
if (SDL_strcmp(next, value) != 0) if (SDL_strcmp(next, value) != 0)
SDL_snprintf(value + SDL_strlen(value), STAT_LENGTH - SDL_strlen(value), " > %s", next); SDL_snprintf(value + SDL_strlen(value), STAT_LENGTH - SDL_strlen(value), " → %s", next);
} }
isShown |= *value != '\0'; isShown |= *value != '\0';
} }
@@ -1074,7 +1109,7 @@ void Ui::table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, cons
font); font);
} }
// A trinket's effects at one level with the next level's after a ">", line by line. Where a line only changes in its // A trinket's effects at one level with the next level's after an arrow, line by line. Where a line only changes in its
// numbers, just those show both values ("1.15x > 1.3x Attack Rate"); otherwise the whole line does ("Wall Cling > Wall // numbers, just those show both values ("1.15x > 1.3x Attack Rate"); otherwise the whole line does ("Wall Cling > Wall
// Cling, Wall Kick"), split onto a line of its own after the first when that's too wide. // Cling, Wall Kick"), split onto a line of its own after the first when that's too wide.
static void effects_preview_get(Game& game, const char* now, const char* next, char* text, int size) static void effects_preview_get(Game& game, const char* now, const char* next, char* text, int size)
@@ -1126,12 +1161,12 @@ static void effects_preview_get(Game& game, const char* now, const char* next, c
else if (isNumbers) else if (isNumbers)
for (size_t w = 0; w < wordsBefore.size(); w++) for (size_t w = 0; w < wordsBefore.size(); w++)
line += (w > 0 ? " " : "") + line += (w > 0 ? " " : "") +
(wordsBefore[w] == wordsAfter[w] ? wordsBefore[w] : wordsBefore[w] + " > " + wordsAfter[w]); (wordsBefore[w] == wordsAfter[w] ? wordsBefore[w] : wordsBefore[w] + " → " + wordsAfter[w]);
else if (before.empty()) else if (before.empty())
line = "> " + after; line = "→ " + after;
else else
{ {
line = before + " > " + after; line = before + " → " + after;
if (game.text.size_get(line.c_str()).x > PREVIEW_LINE_MAX) if (game.text.size_get(line.c_str()).x > PREVIEW_LINE_MAX)
line = before + "\n> " + after; line = before + "\n> " + after;
} }
@@ -1148,6 +1183,11 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
tooltip.isTrinket = isTrinket; tooltip.isTrinket = isTrinket;
tooltip.item = item; tooltip.item = item;
Fighter& fighter = owner >= 0 ? game.sim.fighters[owner] : game.local_get(); Fighter& fighter = owner >= 0 ? game.sim.fighters[owner] : game.local_get();
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
int slots = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
tooltip.slot = std::find(loadout, loadout + slots, item) - loadout;
if (tooltip.slot >= slots)
tooltip.slot = -1;
if (isTrinket) if (isTrinket)
{ {
@@ -1159,7 +1199,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
else else
SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped)); SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped));
tooltip.name = def.name; tooltip.name = def.name;
// In the sandbox, the effects show what the next level changes after a ">", as weapons' stats do. // In the sandbox, the effects show what the next level changes after an arrow, as weapons' stats do.
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1; bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
char effects[TOOLTIP_LENGTH]; char effects[TOOLTIP_LENGTH];
if (isPreview) if (isPreview)
@@ -1167,7 +1207,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
else else
SDL_strlcpy(effects, def.effects[level], sizeof(effects)); SDL_strlcpy(effects, def.effects[level], sizeof(effects));
if (isPreview) if (isPreview)
SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), "(Level %d, > Level %d)\n%s\n%s", level + 1, level + 2, SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), "(Level %d → Level %d)\n%s\n%s", level + 1, level + 2,
effects, equipped); effects, equipped);
else else
SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), "(Level %d)\n%s\n%s", level + 1, effects, equipped); SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), "(Level %d)\n%s\n%s", level + 1, effects, equipped);
@@ -1179,9 +1219,9 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
const WeaponDef& def = WEAPON_DEFS[item]; const WeaponDef& def = WEAPON_DEFS[item];
tooltip.name = def.name; tooltip.name = def.name;
// In the sandbox, values that change at the next level show it after a ">". // In the sandbox, values that change at the next level show it after an arrow.
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1; bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), isPreview ? "(Level %d, > Level %d)" : "(Level %d)", level + 1, SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), isPreview ? "(Level %d → Level %d)" : "(Level %d)", level + 1,
level + 2); level + 2);
stats_table_build(fighter, item, level, isPreview, tooltip); stats_table_build(fighter, item, level, isPreview, tooltip);
SDL_snprintf(tooltip.description, sizeof(tooltip.description), "\n%s\n\n%s", def.blurb, def.chargeBlurb); SDL_snprintf(tooltip.description, sizeof(tooltip.description), "\n%s\n\n%s", def.blurb, def.chargeBlurb);
@@ -1237,7 +1277,8 @@ void Ui::tooltip_page_draw(Game& game, Page& page, Rect area, Tooltip& tooltip)
page_begin(game, page, area); page_begin(game, page, area);
float iconY = area.y - page.scroll; float iconY = area.y - page.scroll;
if (tooltip.item >= 0 && iconY >= area.y) if (tooltip.item >= 0 && iconY >= area.y)
icon_draw(game, tooltip.isTrinket, tooltip.item, tooltip_icon_draw(
game, tooltip,
{.color = tint, {.color = tint,
.position = {area.x + area.w - (isScrolling ? SCROLL_WIDTH + TOOLTIP_PADDING : 0) - ICON_SIZE / 2, .position = {area.x + area.w - (isScrolling ? SCROLL_WIDTH + TOOLTIP_PADDING : 0) - ICON_SIZE / 2,
iconY + ICON_SIZE / 2}}); iconY + ICON_SIZE / 2}});
@@ -1293,32 +1334,43 @@ void Ui::tooltip_draw(Game& game, Tooltip& tooltip)
panel_draw(game.renderer, panel, tint); panel_draw(game.renderer, panel, tint);
tooltip_text_draw(game, tooltip, {.position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}}); tooltip_text_draw(game, tooltip, {.position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}});
if (tooltip.item >= 0) if (tooltip.item >= 0)
icon_draw(game, tooltip.isTrinket, tooltip.item, tooltip_icon_draw(game, tooltip,
{.color = tint, {.color = tint,
.position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2, .position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2,
panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}}); panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}});
} }
// Black box with a bordered, cut-cornered edge in the given color. The fill fades with the border unless given an // A tooltip's item icon, centred on the placement, with the hotbar slot it's in numbered over its top left corner as on
// alpha of its own. // the hotbar.
void Ui::panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha) void Ui::tooltip_icon_draw(Game& game, Tooltip& tooltip, Draw placement)
{ {
chamfer_draw(renderer, rect, border); icon_draw(game, tooltip.isTrinket, tooltip.item, placement);
chamfer_draw(renderer, if (tooltip.slot < 0)
{rect.x + PANEL_BORDER, rect.y + PANEL_BORDER, rect.w - PANEL_BORDER * 2, rect.h - PANEL_BORDER * 2}, return;
color_fade(BLACK, fillAlpha >= 0 ? fillAlpha : border.a)); char label[] = {char('1' + tooltip.slot), 0};
Vec2 corner = placement.position - Vec2{ICON_SIZE / 2, ICON_SIZE / 2} + ICON_LABEL_OFFSET;
game.text.draw(game.renderer, label, {.color = placement.color, .position = corner});
} }
// Fills a rect with its corners cut diagonally, one pixel per step. // Black box with a cut-cornered edge stroke thick in the given color (its corners cut as deep). The fill fades with
void Ui::chamfer_draw(Renderer& renderer, Rect rect, Color color) // the border unless given an alpha of its own.
void Ui::panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha, float stroke)
{ {
for (int step = 0; step < CHAMFER; step++) chamfer_draw(renderer, rect, border, stroke);
chamfer_draw(renderer, {rect.x + stroke, rect.y + stroke, rect.w - stroke * 2, rect.h - stroke * 2},
color_fade(BLACK, fillAlpha >= 0 ? fillAlpha : border.a), stroke);
}
// Fills a rect with its corners cut diagonally, cut pixels deep, one pixel per step.
void Ui::chamfer_draw(Renderer& renderer, Rect rect, Color color, float cut)
{ {
float inset = CHAMFER - step; for (int step = 0; step < cut; step++)
{
float inset = cut - step;
renderer.rect_draw({rect.x + inset, rect.y + step, rect.w - inset * 2, 1}, color); renderer.rect_draw({rect.x + inset, rect.y + step, rect.w - inset * 2, 1}, color);
renderer.rect_draw({rect.x + inset, rect.y + rect.h - 1 - step, rect.w - inset * 2, 1}, color); renderer.rect_draw({rect.x + inset, rect.y + rect.h - 1 - step, rect.w - inset * 2, 1}, color);
} }
renderer.rect_draw({rect.x, rect.y + CHAMFER, rect.w, rect.h - CHAMFER * 2}, color); renderer.rect_draw({rect.x, rect.y + cut, rect.w, rect.h - cut * 2}, color);
} }
void Ui::widgets_begin(float alpha, bool isLive) void Ui::widgets_begin(float alpha, bool isLive)
@@ -1496,7 +1548,7 @@ bool Ui::field_draw(Game& game, Rect box, char* text, int size)
text[--length] = '\0'; text[--length] = '\0';
for (const char* character = input.typed; *character && length < size - 1; character++) for (const char* character = input.typed; *character && length < size - 1; character++)
{ {
if (*character < GLYPH_FIRST || *character >= GLYPH_FIRST + GLYPH_COUNT) if ((unsigned char)*character < GLYPH_FIRST || (unsigned char)*character >= GLYPH_TYPED_END)
continue; continue;
text[length++] = *character; text[length++] = *character;
text[length] = '\0'; text[length] = '\0';
@@ -1676,8 +1728,8 @@ void Ui::fighter_preview_draw(Game& game, Rect area, const char* name, Color col
game.text.draw(game.renderer, name, game.text.draw(game.renderer, name,
{.color = color, .position = {std::round(centre - nameSize.x / 2), area.y + PREVIEW_NAME_Y}}); {.color = color, .position = {std::round(centre - nameSize.x / 2), area.y + PREVIEW_NAME_Y}});
Anm2& player = game.sprites.player; Anm2& player = game.sprites.player;
Anm2Animation* walk = player.animation_get(PREVIEW_ANIMATION); Anm2Animation* preview = player.animation_get(PREVIEW_ANIMATION);
float frame = walk ? std::fmod(SDL_GetTicks() / 1000.0f * player.fps, (float)walk->length) : 0; float frame = preview ? std::fmod(SDL_GetTicks() / 1000.0f * player.fps, (float)preview->length) : 0;
player.draw(game.renderer, PREVIEW_ANIMATION, frame, player.draw(game.renderer, PREVIEW_ANIMATION, frame,
{.color = color, .position = {centre, feet}, .scale = {PREVIEW_SCALE, PREVIEW_SCALE}}); {.color = color, .position = {centre, feet}, .scale = {PREVIEW_SCALE, PREVIEW_SCALE}});
if (!caption || !*caption) if (!caption || !*caption)
@@ -1692,6 +1744,11 @@ void Ui::fighter_preview_draw(Game& game, Rect area, const char* name, Color col
constexpr float PING_BAR_WIDTH = 2; constexpr float PING_BAR_WIDTH = 2;
constexpr float PING_BAR_GAP = 1; constexpr float PING_BAR_GAP = 1;
constexpr int PING_BARS = 4; constexpr int PING_BARS = 4;
constexpr float PING_BARS_WIDTH = PING_BARS * (PING_BAR_WIDTH + PING_BAR_GAP) - PING_BAR_GAP;
constexpr float PING_BAR_STEP = 2;
constexpr float PING_BARS_HEIGHT = PING_BARS * PING_BAR_STEP;
constexpr float PING_BARS_DROP = 3;
constexpr float PING_UNLIT_SHADE = 0.5f;
constexpr float PING_LIMITS[PING_BARS - 1] = {60, 120, 200}; constexpr float PING_LIMITS[PING_BARS - 1] = {60, 120, 200};
constexpr Color PING_COLORS[PING_BARS] = { constexpr Color PING_COLORS[PING_BARS] = {
{0.9f, 0.25f, 0.2f, 1}, {0.95f, 0.6f, 0.2f, 1}, {0.9f, 0.85f, 0.25f, 1}, {0.35f, 0.85f, 0.35f, 1}}; {0.9f, 0.25f, 0.2f, 1}, {0.95f, 0.6f, 0.2f, 1}, {0.9f, 0.85f, 0.25f, 1}, {0.35f, 0.85f, 0.35f, 1}};
@@ -1704,37 +1761,48 @@ static std::string ping_text_get(float ping)
float Ui::ping_width_get(Game& game, float ping) float Ui::ping_width_get(Game& game, float ping)
{ {
std::string text = ping_text_get(ping); std::string text = ping_text_get(ping);
float bars = PING_BARS * (PING_BAR_WIDTH + PING_BAR_GAP) - PING_BAR_GAP; return PING_BARS_WIDTH + (text.empty() ? 0 : PING_BAR_GAP * 2 + game.text.size_get(text.c_str()).x);
return bars + (text.empty() ? 0 : PING_BAR_GAP * 2 + game.text.size_get(text.c_str()).x); }
// How many of a connection's bars are lit (none while it's unknown).
static int ping_lit_get(float ping)
{
if (ping < 0)
return 0;
int lit = PING_BARS;
for (float limit : PING_LIMITS)
lit -= ping >= limit;
return std::max(lit, 1);
} }
float Ui::ping_draw(Game& game, Vec2 corner, float ping, float alpha) float Ui::ping_draw(Game& game, Vec2 corner, float ping, float alpha)
{ {
int lit = 0; ping_bars_draw(game, {corner.x, corner.y + LINE_HEIGHT - PING_BARS_DROP}, ping, alpha);
if (ping >= 0) int lit = ping_lit_get(ping);
{
lit = PING_BARS;
for (float limit : PING_LIMITS)
lit -= ping >= limit;
lit = std::max(lit, 1);
}
Color color = lit > 0 ? PING_COLORS[lit - 1] : shade;
float x = std::round(corner.x);
float bottom = std::round(corner.y + LINE_HEIGHT - 3);
for (int i = 0; i < PING_BARS; i++)
{
float height = 2 + i * 2;
game.renderer.rect_draw({x, bottom - height, PING_BAR_WIDTH, height},
color_fade(i < lit ? color : color_shade(shade, 0.5f), alpha));
x += PING_BAR_WIDTH + PING_BAR_GAP;
}
std::string text = ping_text_get(ping); std::string text = ping_text_get(ping);
if (!text.empty()) if (!text.empty())
game.text.draw(game.renderer, text.c_str(), game.text.draw(game.renderer, text.c_str(),
{.color = color_fade(color, alpha), .position = {x + PING_BAR_GAP, corner.y}}); {.color = color_fade(PING_COLORS[lit - 1], alpha),
.position = {std::round(corner.x) + PING_BARS_WIDTH + PING_BAR_GAP * 2, corner.y}});
return ping_width_get(game, ping); return ping_width_get(game, ping);
} }
// A connection's signal bars alone, standing on corner (their bottom left).
void Ui::ping_bars_draw(Game& game, Vec2 corner, float ping, float alpha)
{
int lit = ping_lit_get(ping);
Color color = lit > 0 ? PING_COLORS[lit - 1] : shade;
float x = std::round(corner.x);
float bottom = std::round(corner.y);
for (int i = 0; i < PING_BARS; i++)
{
float height = PING_BAR_STEP * (i + 1);
game.renderer.rect_draw({x, bottom - height, PING_BAR_WIDTH, height},
color_fade(i < lit ? color : color_shade(shade, PING_UNLIT_SHADE), alpha));
x += PING_BAR_WIDTH + PING_BAR_GAP;
}
}
// What a game's rules make for, in a few lines under a title with its mode's icon: what it's about, how many play, the // What a game's rules make for, in a few lines under a title with its mode's icon: what it's about, how many play, the
// slots, how items are handed out, and how it's won. The "game tooltip". // slots, how items are handed out, and how it's won. The "game tooltip".
static std::vector<std::string> game_summary_lines(const Rules& rules) static std::vector<std::string> game_summary_lines(const Rules& rules)
@@ -1887,8 +1955,7 @@ void Ui::update(Game& game)
hintAlpha = std::max(0.0f, hintAlpha - TICK / HINT_FADE); hintAlpha = std::max(0.0f, hintAlpha - TICK / HINT_FADE);
spectate_update(game); spectate_update(game);
// The HUD's hotbar slots and the other fighters' windows brighten while the mouse is over them, and dim again // The HUD's elements change while the mouse is over them, and change back after.
// after.
Vec2 mouse = game.input.mouse; Vec2 mouse = game.input.mouse;
auto hover_step = [&](float& hover, bool isHovered) { auto hover_step = [&](float& hover, bool isHovered) {
hover = std::clamp(hover + (isHovered ? 1 : -1) * TICK / HUD_HOVER_FADE, 0.0f, 1.0f); hover = std::clamp(hover + (isHovered ? 1 : -1) * TICK / HUD_HOVER_FADE, 0.0f, 1.0f);
@@ -1907,6 +1974,13 @@ void Ui::update(Game& game)
int k = std::find(rivals.fighters, rivals.fighters + rivals.count, i) - rivals.fighters; int k = std::find(rivals.fighters, rivals.fighters + rivals.count, i) - rivals.fighters;
hover_step(rivalHovers[i], k < rivals.count && is_point_in_rect(mouse, rivals.cards[k])); hover_step(rivalHovers[i], k < rivals.count && is_point_in_rect(mouse, rivals.cards[k]));
} }
for (int slot = 0; slot < SLOT_MAX; slot++)
{
hover_step(weaponBars[slot].hover, weaponBars[slot].fade > 0 && is_point_in_rect(mouse, weaponBars[slot].rect));
hover_step(trinketBars[slot].hover,
trinketBars[slot].fade > 0 && is_point_in_rect(mouse, trinketBars[slot].rect));
}
hover_step(statusHover, is_point_in_rect(mouse, statusRect));
// Every slot cools on its own bar: weapons whose cooldown is long enough to notice (copies that don't stack share // Every slot cools on its own bar: weapons whose cooldown is long enough to notice (copies that don't stack share
// the first one's), and trinkets with a cooldown of their own (one bar for all copies). A bar keeps its item's // the first one's), and trinkets with a cooldown of their own (one bar for all copies). A bar keeps its item's
@@ -1938,60 +2012,50 @@ void Ui::update(Game& game)
bars_arrange(trinketBars); bars_arrange(trinketBars);
} }
// Every item cooling down shows at once, each on its own bar two slots long (whatever the hotbar's size) with its icon. // Every item cooling down shows at once, a row each: weapons downward from under the weapon hotbar, trinkets upward
// Every bar cooling down, a row each: weapons downward from under the weapon hotbar, trinkets upward from over the // from over the trinket hotbar. A kind's bars are two of its slots long, with its icon just right at its hotbar's
// trinket hotbar, each starting far enough off its hotbar that the icons (taller than the bars) clear its slots. Past a // size, and start far enough off the hotbar that the icons (taller than the bars) clear its slots. Each drains with
// handful of one kind at once, its rows tighten and their icons shrink to half size so a full loadout still fits. // its cooldown and fades with its bar.
void Ui::cooldowns_draw(Game& game) void Ui::cooldowns_draw(Game& game)
{ {
float width = COOLDOWN_SLOTS * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP; Fighter& fighter = game.local_get();
for (int kind = 0; kind < 2; kind++) for (int kind = 0; kind < 2; kind++)
{ {
bool isTrinket = kind == 1; bool isTrinket = kind == 1;
IconSize size = game.menu.slot_size_get(isTrinket ? fighter.trinketSlots : fighter.weaponSlots);
float height = COOLDOWN_BAR_HEIGHTS[size];
float icon = ICON_PIXELS[size];
float width = COOLDOWN_SLOTS * (SLOT_SIZES[size] + SLOT_GAPS[size]) - SLOT_GAPS[size];
float spacing = std::max(icon, height) + COOLDOWN_ROW_GAP;
float start = HOTBAR_MARGIN + SLOT_SIZES[size] + COOLDOWN_GAP + std::ceil(std::max(0.0f, (icon - height) / 2));
CooldownBar* bars = isTrinket ? trinketBars : weaponBars; CooldownBar* bars = isTrinket ? trinketBars : weaponBars;
int showing = 0;
for (int slot = 0; slot < SLOT_MAX; slot++)
showing += bars[slot].fade > 0;
bool isCompact = showing > COOLDOWN_FULL_ROWS;
float spacing = isCompact ? COOLDOWN_COMPACT_SPACING : COOLDOWN_SPACING;
float iconScale = isCompact ? MINI_ICON_SCALE : 1;
float clearance = std::max(0.0f, (ICON_SIZE * iconScale - COOLDOWN_BAR_HEIGHT) / 2);
for (int slot = 0; slot < SLOT_MAX; slot++) for (int slot = 0; slot < SLOT_MAX; slot++)
{ {
CooldownBar& bar = bars[slot]; CooldownBar& bar = bars[slot];
if (bar.fade <= 0 || bar.row < 0 || bar.item < 0) if (bar.fade <= 0 || bar.row < 0 || bar.item < 0)
continue; continue;
float step = std::round(bar.row * spacing); float step = std::round(bar.row * spacing);
float y = isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - SLOT_SIZE - COOLDOWN_GAP - COOLDOWN_BAR_HEIGHT - Rect rect = {HOTBAR_MARGIN, isTrinket ? CANVAS_HEIGHT - start - height - step : start + step, width,
std::ceil(clearance) - step height};
: HOTBAR_MARGIN + SLOT_SIZE + COOLDOWN_GAP + std::ceil(clearance) + step; float middle = rect.y + height / 2;
cooldown_bar_draw(game, {HOTBAR_MARGIN, y, width, COOLDOWN_BAR_HEIGHT}, bar, isTrinket, bar.item, bar.rect = {rect.x, middle - spacing / 2, width + COOLDOWN_GAP + icon, spacing};
iconScale); game.renderer.layer_begin();
bar_draw(game.renderer, rect, bar.fill, tint, SLOT_STROKES[size]);
icon_draw(game, isTrinket, bar.item,
{.color = tint, .position = {std::round(rect.x + width + COOLDOWN_GAP + icon / 2), middle}},
size);
game.renderer.layer_end(bar.fade * hover_alpha_get(bar.hover));
} }
} }
} }
// A bordered bar draining with the cooldown and its item's icon just right of it, at full size so its pixels stay
// whole. Both fade with the bar.
void Ui::cooldown_bar_draw(Game& game, Rect rect, CooldownBar& bar, bool isTrinket, int item, float iconScale)
{
Color color = color_fade(tint, bar.fade);
bar_draw(game.renderer, rect, bar.fill, color);
float icon = ICON_SIZE * iconScale;
icon_draw(game, isTrinket, item,
{.color = color,
.position = {std::round(rect.x + rect.w + COOLDOWN_GAP + icon / 2), rect.y + rect.h / 2},
.scale = {iconScale, iconScale}});
}
// A weapon's or trinket's HUD icon, centred on the placement. // A weapon's or trinket's HUD icon, centred on the placement.
void Ui::icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSize size) void Ui::icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSize size)
{ {
Sprites& sprites = game.sprites; char name[TOOLTIP_LENGTH];
Anm2* sheets[2][ICON_SIZE_COUNT] = {{&sprites.icons, &sprites.iconsSmall, &sprites.iconsTiny}, SDL_snprintf(name, sizeof(name), "%s%s", isTrinket ? TRINKET_DEFS[item].name : WEAPON_DEFS[item].name,
{&sprites.trinkets, &sprites.trinketsSmall, &sprites.trinketsTiny}}; ICON_SUFFIXES[size]);
Anm2& art = *sheets[isTrinket][size]; (isTrinket ? game.sprites.trinkets : game.sprites.icons).draw(game.renderer, name, 0, placement);
art.draw(game.renderer, isTrinket ? TRINKET_DEFS[item].name : WEAPON_DEFS[item].name, 0, placement);
} }
// An arrow at the screen's edge for every other living fighter out of view, pointing toward it, in its colour, with // An arrow at the screen's edge for every other living fighter out of view, pointing toward it, in its colour, with
@@ -2026,8 +2090,9 @@ void Ui::offscreen_draw(Game& game, Vec2 camera)
} }
} }
// Each of this screen's fighters' crosshairs in its colour: at the mouse for the keyboard's (and whenever the menu is // Each of this screen's fighters' crosshairs in its colour, filling in and shaking with its weapon's charge and
// open), otherwise where its stick aims. With none (a bot playing), the mouse's. // cooldown: at the mouse for the keyboard's (and whenever the menu is open), otherwise where its stick aims. With none
// (a bot playing), the mouse's.
void Ui::crosshairs_draw(Game& game, Vec2 camera) void Ui::crosshairs_draw(Game& game, Vec2 camera)
{ {
Input& input = game.input; Input& input = game.input;
@@ -2039,9 +2104,13 @@ void Ui::crosshairs_draw(Game& game, Vec2 camera)
isAny = true; isAny = true;
bool isMouse = (game.devices[i] == DEVICE_KEYBOARD && !input.devices[DEVICE_KEYBOARD].isAiming) || bool isMouse = (game.devices[i] == DEVICE_KEYBOARD && !input.devices[DEVICE_KEYBOARD].isAiming) ||
(i == game.local && game.menu.isOpen); (i == game.local && game.menu.isOpen);
Vec2 aim = isMouse ? input.mouse : game.sim.fighters[i].controls.cursor - camera; Fighter& fighter = game.sim.fighters[i];
Vec2 aim = (isMouse ? input.mouse : fighter.controls.cursor - camera) +
game.scene.weapon_shake_get(fighter, fighter.weapon_key(fighter.slot));
game.sprites.ui.draw(game.renderer, "Crosshair", 0, game.sprites.ui.draw(game.renderer, "Crosshair", 0,
{.color = game.color_get(i), .position = {std::floor(aim.x), std::floor(aim.y)}}); {.color = game.color_get(i),
.position = {std::floor(aim.x), std::floor(aim.y)},
.flash = game.scene.charge_fill_get(fighter)});
} }
if (!isAny) if (!isAny)
game.sprites.ui.draw(game.renderer, "Crosshair", 0, game.sprites.ui.draw(game.renderer, "Crosshair", 0,
@@ -2194,16 +2263,21 @@ void Ui::spectate_draw(Game& game)
text_center_draw(game, text, y + LINE_HEIGHT, 1, shade); text_center_draw(game, text, y + LINE_HEIGHT, 1, shade);
} }
// Lays out the other fighters' windows in rows of up to four along the bottom right (more rows stacking upwards, as // A fighter's connection for its window, online (PING_NONE otherwise).
// even as they go), never left of the screen's middle (nor over the local hotbar and cooldowns). A window is as wide as static float rival_ping_get(Game& game, int fighter)
// its icon row, but no wider than one holding two weapons and two trinkets at full size: past that, or when the row {
// doesn't fit, icons shrink to small, then tiny. Each kind wraps onto more rows past three across, and tiny icons wrap return game.session.isActive ? game.session.ping_get(game, game.sim.match.owners[fighter]) : PING_NONE;
// further if they still don't fit. Names and health use the small font, or the tiny one for every window if any doesn't }
// fit.
// Lays out the other fighters' windows in the HUD's right column, from the bottom up to the readouts above, in no more
// than two rows: as many to a row as fit across (the rows as even as they go). A window is as wide as its icons, name
// or health (with its connection's bars, online). Without icons, the health number can go to make room. When they don't
// all fit, the icons shrink to small, then the font to tiny, then the icons to tiny, then the icons are left out. Each
// kind of icon wraps onto more rows past three across, and tiny icons wrap further if they still don't fit. If even
// names and health alone don't fit, the windows split the column evenly and the names are cut short.
RivalLayout Ui::rivals_layout(Game& game) RivalLayout Ui::rivals_layout(Game& game)
{ {
RivalLayout layout; RivalLayout layout;
Fighter& local = game.local_get();
int slots[2] = {}; int slots[2] = {};
for (int i = 0; i < game.sim.fighterCount; i++) for (int i = 0; i < game.sim.fighterCount; i++)
{ {
@@ -2215,57 +2289,63 @@ RivalLayout Ui::rivals_layout(Game& game)
} }
if (layout.count == 0) if (layout.count == 0)
return layout; return layout;
int cardRows = (layout.count + RIVALS_PER_ROW - 1) / RIVALS_PER_ROW;
int perRow = (layout.count + cardRows - 1) / cardRows;
float step = SLOT_SIZE + SLOT_GAP; float top = statusRect.h > 0 ? statusRect.y + statusRect.h + RIVAL_GAP : HOTBAR_MARGIN;
float left = float room = CANVAS_HEIGHT - HOTBAR_MARGIN - top;
std::max(CANVAS_WIDTH / 2.0f,
HOTBAR_MARGIN + std::max(local.trinketSlots * step, COOLDOWN_SLOTS * step + ICON_SIZE) + RIVAL_GAP);
float room = CANVAS_WIDTH - HOTBAR_MARGIN - left;
float inset = PANEL_BORDER + RIVAL_PADDING; float inset = PANEL_BORDER + RIVAL_PADDING;
auto icons_width = [&](int size, int columns) { bool isRace = game.sim.match.is_racing();
return columns * (ICON_PIXELS[size] + RIVAL_ICON_GAP) + RIVAL_DIVIDER - RIVAL_ICON_GAP;
};
float widthMax = icons_width(ICON_FULL, RIVAL_FULL_SLOTS) + inset * 2;
char health[TOOLTIP_LENGTH]; char health[TOOLTIP_LENGTH];
SDL_snprintf(health, sizeof(health), "%d", (int)std::ceil(game.sim.match.health_get())); SDL_snprintf(health, sizeof(health), "%d", (int)std::ceil(game.sim.match.health_get()));
float minimum = game.text.size_get(health, FONT_TINY).x + RIVAL_PADDING + RIVAL_BAR_MIN + inset * 2;
float width = 0; float width = 0;
for (int size = ICON_FULL; size < ICON_SIZE_COUNT; size++) float height = 0;
int perRow = 1;
bool isFit = false;
for (int style = 0; style < RIVAL_STYLES && !isFit; style++)
{ {
// No more than a few of a kind side by side, wrapping onto more rows to keep to that; tiny icons can wrap layout.font = RIVAL_FONTS[style];
layout.isIconShown = style < RIVAL_ICON_STYLES;
layout.size = layout.isIconShown ? RIVAL_SIZES[style] : ICON_TINY;
layout.line = game.text.line_height_get(layout.font);
float text = 0;
float ping = 0;
for (int k = 0; k < layout.count; k++)
{
int fighter = layout.fighters[k];
ping = std::max(ping, rival_ping_get(game, fighter) == PING_NONE ? 0 : NAME_PING_GAP + PING_BARS_WIDTH);
text = std::max(text, game.text.size_get(name_get(game, fighter), layout.font).x);
}
float number = layout.isIconShown ? game.text.size_get(health, layout.font).x + RIVAL_PADDING : 0;
text = isRace ? text + ping : std::max(text, number + RIVAL_BAR_MIN + ping);
// No more than a few of a kind side by side, wrapping onto more rows to keep to that. Tiny icons can wrap
// further to fit. // further to fit.
int rowsMin = (std::max(slots[0], slots[1]) + RIVAL_COLUMNS_MAX - 1) / RIVAL_COLUMNS_MAX; float cell = ICON_PIXELS[layout.size] + RIVAL_ICON_GAP;
int rowsMax = size == ICON_TINY ? SLOT_MAX : rowsMin; int rowsMin = std::max(1, (std::max(slots[0], slots[1]) + RIVAL_COLUMNS_MAX - 1) / RIVAL_COLUMNS_MAX);
for (int rows = std::max(1, rowsMin); rows <= rowsMax; rows++) int rowsMax = layout.isIconShown && layout.size == ICON_TINY ? SLOT_MAX : rowsMin;
for (int rows = rowsMin; rows <= rowsMax && !isFit; rows++)
{ {
layout.rows = layout.isIconShown ? rows : 0;
for (int kind = 0; kind < 2; kind++) for (int kind = 0; kind < 2; kind++)
layout.columns[kind] = (slots[kind] + rows - 1) / rows; layout.columns[kind] = (slots[kind] + rows - 1) / rows;
float icons = icons_width(size, layout.columns[0] + layout.columns[1]) + inset * 2; float icons = layout.isIconShown
width = std::ceil(std::max(icons, minimum)); ? (layout.columns[0] + layout.columns[1]) * cell + RIVAL_DIVIDER - RIVAL_ICON_GAP
layout.size = (IconSize)size; : 0;
layout.rows = rows; width = std::ceil(std::max(icons, text) + inset * 2);
bool isFit = icons <= widthMax && perRow * width + (perRow - 1) * RIVAL_GAP <= room; height = inset * 2 + layout.line + (isRace ? 0 : RIVAL_PADDING + layout.line) +
if (isFit) (layout.isIconShown ? RIVAL_PADDING + rows * cell - RIVAL_ICON_GAP : 0);
size = ICON_SIZE_COUNT; perRow = std::clamp((int)((HUD_COLUMN_WIDTH + RIVAL_GAP) / (width + RIVAL_GAP)), 1, layout.count);
if (size == ICON_SIZE_COUNT) int cardRows = (layout.count + perRow - 1) / perRow;
break; perRow = (layout.count + cardRows - 1) / cardRows;
isFit = width <= HUD_COLUMN_WIDTH && cardRows <= RIVAL_ROWS_MAX &&
cardRows * (height + RIVAL_GAP) - RIVAL_GAP <= room;
} }
} }
if (!isFit)
{
perRow = (layout.count + RIVAL_ROWS_MAX - 1) / RIVAL_ROWS_MAX;
width = std::floor((HUD_COLUMN_WIDTH - (perRow - 1) * RIVAL_GAP) / perRow);
}
float inner = width - inset * 2;
layout.font = game.sim.match.is_racing() || game.text.size_get(health).x + RIVAL_PADDING + RIVAL_BAR_MIN <= inner
? FONT_SMALL
: FONT_TINY;
for (int k = 0; k < layout.count; k++)
if (game.text.size_get(name_get(game, layout.fighters[k])).x > inner)
layout.font = FONT_TINY;
layout.line = game.text.line_height_get(layout.font);
float healthHeight = game.sim.match.is_racing() ? 0 : layout.line + RIVAL_PADDING;
float icons = layout.rows * (ICON_PIXELS[layout.size] + RIVAL_ICON_GAP) - RIVAL_ICON_GAP;
float height = inset + layout.line + RIVAL_PADDING + healthHeight + icons + inset;
for (int k = 0; k < layout.count; k++) for (int k = 0; k < layout.count; k++)
{ {
int row = k / perRow; int row = k / perRow;
@@ -2277,10 +2357,11 @@ RivalLayout Ui::rivals_layout(Game& game)
return layout; return layout;
} }
// Every other fighter's window in a match, bordered in its colour: its name centred, its health with a bar beside it, // Every other fighter's window in a match, bordered in its colour: its name centred (cut short if it doesn't fit), its
// then its weapons, a divider, and its trinkets, the weapon in hand lit. A window is dimmed (a fighter out of the round // health (online with its connection's bars after), then (with room) its weapons, a divider, and its trinkets, the
// more so) until the mouse comes over it, when it eases to full brightness. Hovering an // weapon in hand lit. Each is laid over the game as one piece,
// icon shows the item's tooltip, as that fighter has it, in its colour. // see-through for a fighter out of the round, and fades while the mouse is over it. Hovering an icon shows the item's
// tooltip, as that fighter has it, in its colour.
void Ui::rivals_draw(Game& game) void Ui::rivals_draw(Game& game)
{ {
Match& match = game.sim.match; Match& match = game.sim.match;
@@ -2299,41 +2380,63 @@ void Ui::rivals_draw(Game& game)
int index = layout.fighters[k]; int index = layout.fighters[k];
Fighter& fighter = game.sim.fighters[index]; Fighter& fighter = game.sim.fighters[index];
Rect card = layout.cards[k]; Rect card = layout.cards[k];
float idle = fighter.isAlive || match.is_racing() ? HUD_IDLE_ALPHA : RIVAL_OUT_ALPHA; float base = fighter.isAlive || match.is_racing() || index == spectated ? 1 : RIVAL_OUT_ALPHA;
float alpha = index == spectated ? 1 : idle + (1 - idle) * rivalHovers[index]; Color color = game.color_get(index);
// Faded by darkening toward the solid black fill, so the window stays opaque and nothing in it shows through
// anything else.
Color color = color_shade(game.color_get(index), alpha);
Color dim = color_shade(color, SECONDARY_SHADE); Color dim = color_shade(color, SECONDARY_SHADE);
panel_draw(renderer, card, color, 1); renderer.layer_begin();
panel_draw(renderer, card, color);
float y = card.y + inset; float y = card.y + inset;
Font font = layout.font; Font font = layout.font;
const char* name = name_get(game, index); // The connection's bars go at the end of the health row, or beside the name in a race.
Vec2 size = game.text.size_get(name, font); bool isRace = match.is_racing();
game.text.draw( float ping = rival_ping_get(game, index);
renderer, name, float pingWidth = ping == PING_NONE ? 0 : NAME_PING_GAP + PING_BARS_WIDTH;
{.color = color, float nameRoom = card.w - inset * 2 - (isRace ? pingWidth : 0);
.position = {std::round(card.x + (card.w - size.x) / 2), std::round(y + (layout.line - size.y) / 2)}}, std::string name = name_get(game, index);
font); std::string shown = name;
while (game.text.size_get(shown.c_str(), font).x > nameRoom && !name.empty())
{
name.pop_back();
shown = name + "...";
}
Vec2 size = game.text.size_get(shown.c_str(), font);
float left = std::round(card.x + (card.w - size.x - (isRace ? pingWidth : 0)) / 2);
game.text.draw(renderer, shown.c_str(),
{.color = color, .position = {left, std::round(y + (layout.line - size.y) / 2)}}, font);
float pingBottom = std::round(y + (layout.line + PING_BARS_HEIGHT) / 2);
if (isRace && ping != PING_NONE)
ping_bars_draw(game, {left + size.x + NAME_PING_GAP, pingBottom}, ping, 1);
y += layout.line + RIVAL_PADDING; y += layout.line + RIVAL_PADDING;
if (!match.is_racing()) // Its health as a bar, with the number before it when there's room.
if (!isRace)
{ {
char text[TOOLTIP_LENGTH]; char text[TOOLTIP_LENGTH];
SDL_snprintf(text, sizeof(text), "%d", (int)std::ceil(std::max(0.0f, fighter.health))); SDL_snprintf(text, sizeof(text), "%d", (int)std::ceil(std::max(0.0f, fighter.health)));
size = game.text.size_get(text, font); size = game.text.size_get(text, font);
float x = card.x + inset; float x = card.x + inset;
float right = card.x + card.w - inset - pingWidth;
if (size.x + RIVAL_PADDING + RIVAL_BAR_MIN <= right - x)
{
game.text.draw(renderer, text, game.text.draw(renderer, text,
{.color = color, .position = {x, std::round(y + (layout.line - size.y) / 2)}}, font); {.color = color, .position = {x, std::round(y + (layout.line - size.y) / 2)}}, font);
x += size.x + RIVAL_PADDING; x += size.x + RIVAL_PADDING;
}
float barHeight = std::min(HEALTH_BAR_HEIGHT, layout.line - 1); float barHeight = std::min(HEALTH_BAR_HEIGHT, layout.line - 1);
Rect bar = {x, std::round(y + (layout.line - barHeight) / 2), card.x + card.w - inset - x, barHeight}; Rect bar = {x, std::round(y + (layout.line - barHeight) / 2), right - x, barHeight};
bar_draw(renderer, bar, std::clamp(fighter.health / match.health_get(), 0.0f, 1.0f), color); bar_draw(renderer, bar, std::clamp(fighter.health / match.health_get(), 0.0f, 1.0f), color);
if (ping != PING_NONE)
ping_bars_draw(game, {right + NAME_PING_GAP, pingBottom + layout.line + RIVAL_PADDING}, ping, 1);
y += layout.line + RIVAL_PADDING; y += layout.line + RIVAL_PADDING;
} }
// Weapons then trinkets, each in its columns, centred with the divider between. // Weapons then trinkets, each in its columns, centred with the divider between.
if (!layout.isIconShown)
{
renderer.layer_end(base * hover_alpha_get(rivalHovers[index]));
continue;
}
float iconsWidth = (layout.columns[0] + layout.columns[1]) * cell + RIVAL_DIVIDER - RIVAL_ICON_GAP; float iconsWidth = (layout.columns[0] + layout.columns[1]) * cell + RIVAL_DIVIDER - RIVAL_ICON_GAP;
float x = std::round(card.x + (card.w - iconsWidth) / 2); float x = std::round(card.x + (card.w - iconsWidth) / 2);
for (int kind = 0; kind < 2; kind++) for (int kind = 0; kind < 2; kind++)
@@ -2366,6 +2469,7 @@ void Ui::rivals_draw(Game& game)
x += RIVAL_DIVIDER + RIVAL_ICON_GAP; x += RIVAL_DIVIDER + RIVAL_ICON_GAP;
} }
} }
renderer.layer_end(base * hover_alpha_get(rivalHovers[index]));
} }
if (hoveredFighter < 0 || game.menu.isOpen) if (hoveredFighter < 0 || game.menu.isOpen)
@@ -2375,6 +2479,7 @@ void Ui::rivals_draw(Game& game)
int item = (isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout)[hoveredSlot]; int item = (isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout)[hoveredSlot];
int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[hoveredSlot]; int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[hoveredSlot];
Tooltip tooltip = tooltip_build(game, isTrinket, item, level, hoveredFighter); Tooltip tooltip = tooltip_build(game, isTrinket, item, level, hoveredFighter);
tooltip.slot = hoveredSlot;
// The tooltip takes that fighter's colour for the moment, so it's clear whose item it is. // The tooltip takes that fighter's colour for the moment, so it's clear whose item it is.
Color colors[2] = {tint, shade}; Color colors[2] = {tint, shade};
tint = game.color_get(hoveredFighter); tint = game.color_get(hoveredFighter);
+29 -14
View File
@@ -7,20 +7,25 @@
struct Game; struct Game;
constexpr int CHAMFER = 2;
constexpr int TOOLTIP_LENGTH = 512; constexpr int TOOLTIP_LENGTH = 512;
constexpr int WEAPON_HANDS = 2; constexpr int WEAPON_HANDS = 2;
constexpr float HOTBAR_MARGIN = 8; constexpr float HOTBAR_MARGIN = 8;
constexpr float PANEL_BORDER = 2;
constexpr float SLOT_GAP = 4; constexpr float SLOT_GAP = 4;
constexpr float SLOT_SIZE = 28; constexpr float SLOT_SIZE = 28;
constexpr float WIDGET_GAP = 6; constexpr float WIDGET_GAP = 6;
constexpr float WIDGET_HEIGHT = 14; constexpr float WIDGET_HEIGHT = 14;
// One item's cooldown bar: how far it has faded in, how much cooldown is left (0 to 1), and the row it sits in among // One item's cooldown bar: how far it has faded in, how much cooldown is left (0 to 1), and the row it sits in among
// the bars showing (eased toward its place as others come and go, -1 while hidden). // the bars showing (eased toward its place as others come and go, -1 while hidden). Where it was last drawn, and how
// far the mouse being over it has faded it.
struct CooldownBar struct CooldownBar
{ {
Rect rect;
float fade = 0; float fade = 0;
float fill = 0; float fill = 0;
float hover = 0;
float row = -1; float row = -1;
int item = -1; int item = -1;
}; };
@@ -48,9 +53,15 @@ enum IconSize
ICON_SIZE_COUNT ICON_SIZE_COUNT
}; };
constexpr float ICON_PIXELS[ICON_SIZE_COUNT] = {24, 16, 8}; constexpr float ICON_PIXELS[ICON_SIZE_COUNT] = {24, 16, 8};
// A hotbar slot at each icon size, and the gap between slots.
constexpr float SLOT_SIZES[ICON_SIZE_COUNT] = {SLOT_SIZE, 20, 12};
constexpr float SLOT_GAPS[ICON_SIZE_COUNT] = {SLOT_GAP, 3, 2};
// How wide the HUD's columns down the left (hotbars and cooldowns) and right (readouts and other fighters) can be.
constexpr float HUD_COLUMN_WIDTH = CANVAS_WIDTH / 4.0f;
// The windows for the other fighters along the bottom right in a match: which fighters, where each window sits, the // The windows for the other fighters along the bottom right in a match: which fighters, where each window sits, the
// icon size that fits them all, and how many icon columns and rows each kind takes (the same on every window). // icon size that fits them all (if they show at all), and how many icon columns and rows each kind takes (the same on
// every window).
struct RivalLayout struct RivalLayout
{ {
int count = 0; int count = 0;
@@ -61,6 +72,7 @@ struct RivalLayout
int rows = 1; int rows = 1;
Font font = FONT_SMALL; Font font = FONT_SMALL;
float line = 0; float line = 0;
bool isIconShown = true;
}; };
// What a match's countdown shows, gathered once for wherever it's drawn. // What a match's countdown shows, gathered once for wherever it's drawn.
@@ -80,9 +92,10 @@ struct Tooltip
int rowCount = 0; int rowCount = 0;
char description[TOOLTIP_LENGTH] = {}; char description[TOOLTIP_LENGTH] = {};
char flavor[TOOLTIP_LENGTH] = {}; char flavor[TOOLTIP_LENGTH] = {};
// The item it's about, whose icon it shows top right. // The item it's about, whose icon it shows top right, numbered with the hotbar slot it's in (-1 for none).
bool isTrinket = false; bool isTrinket = false;
int item = -1; int item = -1;
int slot = -1;
}; };
// How much of each item the round's loadout overview shows, from most to least: everything, everything in the small // How much of each item the round's loadout overview shows, from most to least: everything, everything in the small
@@ -141,21 +154,20 @@ struct FocusTarget
bool isPreferred = false; bool isPreferred = false;
}; };
// One hotbar or menu box. Item is a weapon or trinket number, or -1 for empty, and label 0 means none. // One hotbar or menu box, its icon (and the marks on it) at size. Item is a weapon or trinket number, or -1 for empty,
// Lit boxes draw in the tint, the rest gray. A hoverable box under the mouse lights up with an inner border too. // and label 0 means none. Lit boxes draw in the tint, the rest gray. A hoverable box under the mouse lights up with an
// inner border too.
struct Slot struct Slot
{ {
Rect rect; Rect rect;
float alpha = 1; float alpha = 1;
IconSize size = ICON_FULL;
int item = -1; int item = -1;
int level = 0; int level = 0;
int label = 0; int label = 0;
bool isHoverable = false; bool isHoverable = false;
bool isLit = false; bool isLit = false;
bool isTrinket = false; bool isTrinket = false;
// Whether it fades by darkening toward its solid black fill rather than turning see-through, so every pixel stays
// opaque and overlapping parts (icon, chevrons, number) don't show through each other.
bool isOpaque = false;
// Whether it's the menu's target slot, marked by a bar under it. // Whether it's the menu's target slot, marked by a bar under it.
bool isTarget = false; bool isTarget = false;
}; };
@@ -185,13 +197,14 @@ public:
void update(Game& game); void update(Game& game);
void draw(Game& game); void draw(Game& game);
void slot_draw(Game& game, Slot slot); void slot_draw(Game& game, Slot slot);
void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1); void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1, float stroke = PANEL_BORDER);
Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level, int owner = -1); Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level, int owner = -1);
void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip); void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip);
void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font, void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font,
float alpha); float alpha);
Vec2 tooltip_size(Game& game, Tooltip& tooltip, Font font = FONT_SMALL); Vec2 tooltip_size(Game& game, Tooltip& tooltip, Font font = FONT_SMALL);
void tooltip_text_draw(Game& game, Tooltip& tooltip, Draw draw, Font font = FONT_SMALL); void tooltip_text_draw(Game& game, Tooltip& tooltip, Draw draw, Font font = FONT_SMALL);
void tooltip_icon_draw(Game& game, Tooltip& tooltip, Draw placement);
LoadoutLayout loadout_layout(Game& game, float top, float bottom); LoadoutLayout loadout_layout(Game& game, float top, float bottom);
void tooltip_page_draw(Game& game, Page& page, Rect area, Tooltip& tooltip); void tooltip_page_draw(Game& game, Page& page, Rect area, Tooltip& tooltip);
const char* name_get(Game& game, int fighter); const char* name_get(Game& game, int fighter);
@@ -209,6 +222,7 @@ public:
bool fold_draw(Game& game, Rect row, const char* label, bool isOpen); bool fold_draw(Game& game, Rect row, const char* label, bool isOpen);
void fighter_preview_draw(Game& game, Rect area, const char* name, Color tint, const char* caption); void fighter_preview_draw(Game& game, Rect area, const char* name, Color tint, const char* caption);
float ping_draw(Game& game, Vec2 corner, float ping, float alpha); float ping_draw(Game& game, Vec2 corner, float ping, float alpha);
void ping_bars_draw(Game& game, Vec2 corner, float ping, float alpha);
void queues_draw(Game& game); void queues_draw(Game& game);
void queue_tooltip_draw(Game& game); void queue_tooltip_draw(Game& game);
float ping_width_get(Game& game, float ping); float ping_width_get(Game& game, float ping);
@@ -257,10 +271,12 @@ private:
float hintAlpha = 1; float hintAlpha = 1;
// The switching controls as they were last tick, to catch presses while spectating. // The switching controls as they were last tick, to catch presses while spectating.
bool spectateHeld[3] = {}; bool spectateHeld[3] = {};
// How far each HUD hotbar slot (weapons, then trinkets) and each other fighter's window has come up from dim to // How far the mouse being over each HUD element has changed it, from 0 to 1: hotbar slots (weapons, then trinkets)
// full brightness, from 0 to 1, rising while the mouse is over it. // come up from see-through to solid, and the rest (other fighters' windows, the readouts) fade.
Rect statusRect;
float slotHovers[2][SLOT_MAX] = {}; float slotHovers[2][SLOT_MAX] = {};
float rivalHovers[MAX_FIGHTERS] = {}; float rivalHovers[MAX_FIGHTERS] = {};
float statusHover = 0;
float loadoutTop = 0; float loadoutTop = 0;
float widgetAlpha = 1; float widgetAlpha = 1;
int tip = 0; int tip = 0;
@@ -271,7 +287,7 @@ private:
bool isWidgetLive = false; bool isWidgetLive = false;
void bars_draw(Game& game); void bars_draw(Game& game);
void bar_draw(Renderer& renderer, Rect bar, float fill, Color color); void bar_draw(Renderer& renderer, Rect bar, float fill, Color color, float stroke = PANEL_BORDER);
void match_draw(Game& game); void match_draw(Game& game);
void loadout_draw(Game& game, float alpha, float top); void loadout_draw(Game& game, float alpha, float top);
void race_draw(Game& game); void race_draw(Game& game);
@@ -281,7 +297,6 @@ private:
void text_center_draw(Game& game, const char* text, float y, float scale, Color color); void text_center_draw(Game& game, const char* text, float y, float scale, Color color);
void hotbar_draw(Game& game); void hotbar_draw(Game& game);
void cooldowns_draw(Game& game); void cooldowns_draw(Game& game);
void cooldown_bar_draw(Game& game, Rect rect, CooldownBar& bar, bool isTrinket, int item, float iconScale);
void icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSize size = ICON_FULL); void icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSize size = ICON_FULL);
void offscreen_draw(Game& game, Vec2 camera); void offscreen_draw(Game& game, Vec2 camera);
void status_draw(Game& game); void status_draw(Game& game);
@@ -294,7 +309,7 @@ private:
void pick_send(Game& game, int fighter, bool isTrinket, int slot, int item); void pick_send(Game& game, int fighter, bool isTrinket, int slot, int item);
void crosshairs_draw(Game& game, Vec2 camera); void crosshairs_draw(Game& game, Vec2 camera);
void tooltip_draw(Game& game, Tooltip& tooltip); void tooltip_draw(Game& game, Tooltip& tooltip);
void chamfer_draw(Renderer& renderer, Rect rect, Color color); void chamfer_draw(Renderer& renderer, Rect rect, Color color, float cut = CHAMFER);
Color widget_color(bool isLit); Color widget_color(bool isLit);
bool is_hovered(Game& game, Rect box); bool is_hovered(Game& game, Rect box);
bool number_widget(Game& game, Rect box, float& value, NumberRange range, void* id); bool number_widget(Game& game, Rect box, float& value, NumberRange range, void* id);
+78 -63
View File
@@ -1,6 +1,7 @@
#include "weapon.h" #include "weapon.h"
#include "sim.h" #include "sim.h"
#include <algorithm> #include <algorithm>
#include <functional>
constexpr int ATTACKS_MAX = 255; constexpr int ATTACKS_MAX = 255;
constexpr int DAMAGE_STEP = 5; constexpr int DAMAGE_STEP = 5;
@@ -8,12 +9,11 @@ constexpr int HEAVY_DAMAGE = 40;
constexpr int HOOK_SUBSTEPS = 4; constexpr int HOOK_SUBSTEPS = 4;
constexpr int TESLA_LINKS = 5; constexpr int TESLA_LINKS = 5;
constexpr float TESLA_INTERVAL = 0.2f; constexpr float TESLA_INTERVAL = 0.2f;
constexpr float TESLA_RADIUS = 4; constexpr float TESLA_RADIUS = 2;
constexpr float TESLA_RANGES[LEVEL_COUNT] = {120, 170, 220}; constexpr float TESLA_RANGES[LEVEL_COUNT] = {120, 170, 220};
constexpr float DEGREE = PI / 180; constexpr float DEGREE = PI / 180;
constexpr float ARROW_LINGER = 2.5f; constexpr float ARROW_LINGER = 2.5f;
constexpr float ARROW_SPAN = 18; constexpr float ARROW_RADIUS = 6;
constexpr float ARROW_THICKNESS = 2;
constexpr float BEAM_RADIUS = 4; constexpr float BEAM_RADIUS = 4;
constexpr float BOOMERANG_LIFETIME = 6; constexpr float BOOMERANG_LIFETIME = 6;
constexpr float BOOMERANG_RADIUS = 9; constexpr float BOOMERANG_RADIUS = 9;
@@ -30,8 +30,6 @@ constexpr float TRAJECTORY_REACH = 400;
constexpr float TRAJECTORY_STEP = 4; constexpr float TRAJECTORY_STEP = 4;
constexpr float FIRE_SHAKE = 0.5f; constexpr float FIRE_SHAKE = 0.5f;
constexpr float FIREBALL_RADIUS = 5; constexpr float FIREBALL_RADIUS = 5;
constexpr float FLAME_BRAKE = 0.9f;
constexpr float FLAME_BRAKE_REACH = 16;
constexpr float FLAIL_BOUNCE = 0.3f; constexpr float FLAIL_BOUNCE = 0.3f;
constexpr float FLAIL_BOUNCE_MAX = 0.9f; constexpr float FLAIL_BOUNCE_MAX = 0.9f;
constexpr float FLAIL_BOUNCE_PER_RICOCHET = 0.2f; constexpr float FLAIL_BOUNCE_PER_RICOCHET = 0.2f;
@@ -72,7 +70,7 @@ constexpr float PUNCH_RADIUS = 6;
constexpr float PUNCH_THRUST = 0.4f; constexpr float PUNCH_THRUST = 0.4f;
constexpr float SLASH_DISTANCE = 12; constexpr float SLASH_DISTANCE = 12;
constexpr float SLASH_GAP = 14; constexpr float SLASH_GAP = 14;
constexpr float SHAKE_PER_DAMAGE = 0.006f; constexpr float SHAKE_PER_DAMAGE = 0.003f;
constexpr float SLAM_RATIO = 0.75f; constexpr float SLAM_RATIO = 0.75f;
constexpr float SPEED_DAMAGE = 0.05f; constexpr float SPEED_DAMAGE = 0.05f;
// A normal attack let go partway through a charge hits harder, up to this much more just short of full. // A normal attack let go partway through a charge hits harder, up to this much more just short of full.
@@ -418,9 +416,9 @@ int shapes_get(Sim& sim, Attack& attack, Capsule* shapes)
// Fires the weapon in hand with the given charge if it's ready. Full charge uses the charged attack. // Fires the weapon in hand with the given charge if it's ready. Full charge uses the charged attack.
// With the Doubler on, the same attack repeats a moment later for a fraction of the damage (stacked Doublers each // With the Doubler on, the same attack repeats a moment later for a fraction of the damage (stacked Doublers each
// add a repeat of their own, one after another). With Dual Wield on, the off-hand weapon fires too, for a fraction of // add a repeat of their own, one after another). With Dual Wield on, an off-hand weapon per copy fires too (see
// its damage. Every weapon (see Fighter::weapon_key) keeps its own cooldown, in either hand, so swapping to another // Fighter::off_slots_get), each for a fraction of its damage. Every weapon (see Fighter::weapon_key) keeps its own
// weapon never inherits the last one's wait. // cooldown, in any hand, so swapping to another weapon never inherits the last one's wait.
void Weapons::fire(Sim& sim, Fighter& fighter, float charge) void Weapons::fire(Sim& sim, Fighter& fighter, float charge)
{ {
int held = fighter.weapon_key(fighter.slot); int held = fighter.weapon_key(fighter.slot);
@@ -439,13 +437,22 @@ void Weapons::fire(Sim& sim, Fighter& fighter, float charge)
} }
} }
int off = fighter.off_slot_get(); // Each Dual Wield copy fires one off-hand weapon at its own level's cut, the strongest cut going to the first.
off = off < 0 ? -1 : fighter.weapon_key(off); float powers[SLOT_MAX];
float dual = fighter.trinket_get(TRINKET_DUAL, 0); int copies = 0;
if (dual > 0 && off >= 0 && off != held && fighter.cooldowns[off] <= 0 && !is_busy(fighter, off)) for (int i = 0; i < fighter.trinketSlots; i++)
if (fighter.trinketLoadout[i] == TRINKET_DUAL)
powers[copies++] = TRINKET_DEFS[TRINKET_DUAL].values[fighter.trinketSlotLevels[i]];
std::sort(powers, powers + copies, std::greater<float>());
int offs[SLOT_MAX];
int count = fighter.off_slots_get(offs);
for (int hand = 1; hand <= count; hand++)
{ {
int off = fighter.weapon_key(offs[hand - 1]);
if (fighter.cooldowns[off] > 0 || is_busy(fighter, off))
continue;
is_spawned(sim, fighter, is_spawned(sim, fighter,
{.charge = charge, .power = dual, .weaponSlot = off, .isOffhand = true, .isSecondary = true}); {.charge = charge, .power = powers[hand - 1], .weaponSlot = off, .hand = hand, .isSecondary = true});
fighter.cooldowns[off] = fighter.level_get(off).cooldown / gloves; fighter.cooldowns[off] = fighter.level_get(off).cooldown / gloves;
} }
} }
@@ -659,7 +666,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
Attack attack = { Attack attack = {
.stats = stats, .stats = stats,
.origin = fighter.hand_get(), .origin = fighter.hand_get(shot.hand),
.velocity = aim * stats.speed, .velocity = aim * stats.speed,
.aimAngle = vec2_angle(aim), .aimAngle = vec2_angle(aim),
.angle = vec2_angle(aim), .angle = vec2_angle(aim),
@@ -671,9 +678,9 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
.owner = fighter.index, .owner = fighter.index,
.weapon = held, .weapon = held,
.key = key, .key = key,
.hand = shot.hand,
.isCharged = isCharged, .isCharged = isCharged,
.isLeft = fighter.is_left(), .isLeft = fighter.is_left(),
.isOffhand = shot.isOffhand,
.isSecondary = shot.isSecondary, .isSecondary = shot.isSecondary,
}; };
attack.position = attack.origin + aim * MUZZLE_DISTANCE; attack.position = attack.origin + aim * MUZZLE_DISTANCE;
@@ -763,6 +770,12 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
fighter.index); fighter.index);
if (isCharged) if (isCharged)
sim.cues->shake_add(def.shake * FIRE_SHAKE); sim.cues->shake_add(def.shake * FIRE_SHAKE);
// A fireball goes off with an airblast at the muzzle, as wide as the fireball's own blast.
if (kind == ATTACK_FIREBALL)
blast_spawn(sim, {.position = muzzle,
.area = stats.area * fighter.trinket_get(TRINKET_ENLARGER, 1),
.owner = fighter.index,
.isHarmless = true});
// An uppercut launches its fighter the way it rises, cancelling any speed against that first. Shock Absorber // An uppercut launches its fighter the way it rises, cancelling any speed against that first. Shock Absorber
// softens every push the shot gives its own fighter: lunges, kickback and Recoil Coil. // softens every push the shot gives its own fighter: lunges, kickback and Recoil Coil.
@@ -855,20 +868,6 @@ void Weapons::update(Sim& sim)
attack.time += TICK; attack.time += TICK;
} }
tesla_update(sim); tesla_update(sim);
// Standing in your own lingering flames brakes you, launches included.
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = sim.fighters[i];
for (Attack& attack : attacks)
{
bool isNear = vec2_length(attack.position - fighter.center_get()) < FLAME_BRAKE_REACH;
if (attack.owner != i || attack.def()->kind != ATTACK_FLAME || attack.isDead || !isNear)
continue;
fighter.body.velocity = fighter.body.velocity * FLAME_BRAKE;
break;
}
}
attacks_cap(sim); attacks_cap(sim);
std::erase_if(attacks, [](Attack& attack) { return attack.isDead; }); std::erase_if(attacks, [](Attack& attack) { return attack.isDead; });
attacks.insert(attacks.end(), pending.begin(), pending.end()); attacks.insert(attacks.end(), pending.begin(), pending.end());
@@ -1002,7 +1001,10 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
// path so they can't skip past targets either. // path so they can't skip past targets either.
Vec2 start = attack.position; Vec2 start = attack.position;
int steps = vec2_length(attack.velocity) * TICK / PROJECTILE_STEP + 1; int steps = vec2_length(attack.velocity) * TICK / PROJECTILE_STEP + 1;
attack.radius = (def.kind == ATTACK_BULLET ? SLUG_RADIUS : BULLET_RADIUS) * attack.scale; attack.radius = (def.kind == ATTACK_BULLET ? SLUG_RADIUS
: def.kind == ATTACK_ARROW ? ARROW_RADIUS
: BULLET_RADIUS) *
attack.scale;
for (int step = 0; step < steps && !attack.isDead; step++) for (int step = 0; step < steps && !attack.isDead; step++)
{ {
Vec2 next = attack.position + attack.velocity * (TICK / steps); Vec2 next = attack.position + attack.velocity * (TICK / steps);
@@ -1080,7 +1082,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
attack.hitsLeft = attack.time > 0 ? 0 : attack.hitsLeft; attack.hitsLeft = attack.time > 0 ? 0 : attack.hitsLeft;
if (attack.ricochets == 0) if (attack.ricochets == 0)
{ {
attack.origin = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get(); attack.origin = fighter.hand_get(attack.hand);
attack.angle = vec2_angle(attack.tip - attack.origin); attack.angle = vec2_angle(attack.tip - attack.origin);
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE); attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
} }
@@ -1091,7 +1093,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
// once. A plain lash rolls out from the handle, going with the hand, sweeping down across its cone from // once. A plain lash rolls out from the handle, going with the hand, sweeping down across its cone from
// the upper edge, and hits anywhere in the cone out to its front until it cracks at full reach (short of // the upper edge, and hits anywhere in the cone out to its front until it cracks at full reach (short of
// any wall). // any wall).
attack.origin = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get(); attack.origin = fighter.hand_get(attack.hand);
attack.radius = BEAM_RADIUS * attack.scale; attack.radius = BEAM_RADIUS * attack.scale;
attack.isDead = progress >= 1; attack.isDead = progress >= 1;
if (attack.isCharged) if (attack.isCharged)
@@ -1151,7 +1153,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
case ATTACK_SPIN: case ATTACK_SPIN:
{ {
// Keeps circling while left is held, finishing the current lap once let go. // Keeps circling while left is held, finishing the current lap once let go.
int hand = attack.isOffhand ? fighter.off_slot_get() : fighter.slot; int hand = fighter.hand_slot_get(attack.hand);
bool isHeld = fighter.is_down(CONTROL_ATTACK) && hand >= 0 && fighter.weapon_key(hand) == attack.key; bool isHeld = fighter.is_down(CONTROL_ATTACK) && hand >= 0 && fighter.weapon_key(hand) == attack.key;
if (isHeld && attack.time >= attack.duration) if (isHeld && attack.time >= attack.duration)
{ {
@@ -1500,7 +1502,7 @@ void Weapons::teleport_move(Sim& sim, Attack& attack)
attack.isDead = true; attack.isDead = true;
Vec2 spot = attack.position; Vec2 spot = attack.position;
if (!attack.isCopy && !attack.isOffhand && fighter.isAlive) if (!attack.isCopy && attack.hand == 0 && fighter.isAlive)
{ {
sim.cues->burst_spawn(PORTAL_BURST, fighter.center_get(), 0, attack.owner); sim.cues->burst_spawn(PORTAL_BURST, fighter.center_get(), 0, attack.owner);
spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get())); spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get()));
@@ -1600,7 +1602,7 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
{ {
std::vector<Path> paths; std::vector<Path> paths;
int key = fighter.weapon_key(fighter.slot); int key = fighter.weapon_key(fighter.slot);
if (key < 0 || (is_busy(fighter, key) && !tether_get(fighter, key, false))) if (key < 0 || (is_busy(fighter, key) && !tether_get(fighter, key, 0)))
return paths; return paths;
const WeaponDef& weapon = WEAPON_DEFS[fighter.weaponLoadout[key]]; const WeaponDef& weapon = WEAPON_DEFS[fighter.weaponLoadout[key]];
const LevelDef& level = fighter.level_get(key); const LevelDef& level = fighter.level_get(key);
@@ -1766,9 +1768,34 @@ void Weapons::attack_explode(Sim& sim, Attack& attack)
} }
// Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every // Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every
// fighter away from the centre, harder the closer they are. // fighter away from the centre, harder the closer they are. A harmless blast (an airblast) only knocks everyone but its
// owner within its area, launching them if close enough, shown as a ring.
void Weapons::blast_spawn(Sim& sim, Blast blast) void Weapons::blast_spawn(Sim& sim, Blast blast)
{ {
float reach = blast.area * (blast.isHarmless ? 1 : KNOCKBACK_REACH);
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = sim.fighters[i];
Vec2 away = fighter.center_get() - blast.position;
float distance = vec2_length(away);
// Shock Absorber softens its wearer's own blasts as weapon movement, and anyone else's as knockback.
float absorb = i == blast.owner ? fighter.trinket_get(TRINKET_ABSORBER, 1) : fighter.knock_absorb_get();
bool isPushed = blast.isHarmless ? i != blast.owner : blast.isPushing;
if (!isPushed || !fighter.isAlive || distance >= reach)
continue;
Vec2 push =
vec2_normalize(distance > 0 ? away : Vec2{0, -1}) * KNOCKBACK_SPEED * (1 - distance / reach) * absorb;
if (blast.isHarmless)
fighter.knock_take(push, blast.owner);
else
fighter.impulse_add(push);
}
if (blast.isHarmless)
{
sim.cues->ring_spawn(blast.position, reach, blast.owner);
return;
}
Attack attack = { Attack attack = {
.stats = {.damage = blast.damage, .range = blast.area}, .stats = {.damage = blast.damage, .range = blast.area},
.origin = blast.position, .origin = blast.position,
@@ -1785,42 +1812,29 @@ void Weapons::blast_spawn(Sim& sim, Blast blast)
}; };
attack_remember(attack); attack_remember(attack);
pending.push_back(attack); pending.push_back(attack);
float reach = blast.area * KNOCKBACK_REACH;
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = sim.fighters[i];
Vec2 away = fighter.center_get() - blast.position;
float distance = vec2_length(away);
// Shock Absorber softens its wearer's own blasts as weapon movement, and anyone else's as knockback.
float absorb = i == blast.owner ? fighter.trinket_get(TRINKET_ABSORBER, 1) : fighter.knock_absorb_get();
if (blast.isPushing && fighter.isAlive && distance < reach)
fighter.impulse_add(vec2_normalize(distance > 0 ? away : Vec2{0, -1}) * KNOCKBACK_SPEED *
(1 - distance / reach) * absorb);
}
sim.cues->sound_play(blast.sound); sim.cues->sound_play(blast.sound);
sim.cues->shake_add(EXPLOSION_DEF.shake); sim.cues->shake_add(EXPLOSION_DEF.shake);
sim.cues->burst_spawn(BLAST_BURST, blast.position, 0, blast.owner); sim.cues->burst_spawn(BLAST_BURST, blast.position, 0, blast.owner);
} }
// The whip lashing or hook flying out from this hand, which leaves only the weapon's base in it. // The whip lashing or hook flying out from this hand, which leaves only the weapon's base in it.
Attack* Weapons::tether_get(Fighter& fighter, int key, bool isOffhand) Attack* Weapons::tether_get(Fighter& fighter, int key, int hand)
{ {
for (Attack& attack : attacks) for (Attack& attack : attacks)
{ {
AttackKind kind = attack.def()->kind; AttackKind kind = attack.def()->kind;
if ((kind == ATTACK_WHIP || kind == ATTACK_HOOK) && attack.owner == fighter.index && attack.key == key && if ((kind == ATTACK_WHIP || kind == ATTACK_HOOK) && attack.owner == fighter.index && attack.key == key &&
attack.isOffhand == isOffhand && !attack.isCopy && !attack.isDead) attack.hand == hand && !attack.isCopy && !attack.isDead)
return &attack; return &attack;
} }
return nullptr; return nullptr;
} }
// The fighter's own attacks it can stand on or bump into: stuck arrows and charged boomerangs in flight. // Adds the fighter's own attacks it can stand on to the world for its next move: charged boomerangs in flight as
std::vector<Rect> Weapons::solids_get(Fighter& fighter) // obstacles it bumps into, and stuck arrows (until they start fading) as ledges it can only land on from above (and
// drop through).
void Weapons::solids_add(Fighter& fighter, World& world)
{ {
std::vector<Rect> solids;
for (Attack& attack : attacks) for (Attack& attack : attacks)
{ {
if (attack.owner != fighter.index || attack.isDead) if (attack.owner != fighter.index || attack.isDead)
@@ -1829,17 +1843,18 @@ std::vector<Rect> Weapons::solids_get(Fighter& fighter)
if (kind == ATTACK_BOOMERANG && attack.isCharged) if (kind == ATTACK_BOOMERANG && attack.isCharged)
{ {
float size = BOOMERANG_RADIUS * attack.scale; float size = BOOMERANG_RADIUS * attack.scale;
solids.push_back({attack.position.x - size, attack.position.y - size, size * 2, size * 2}); world.obstacles.push_back({attack.position.x - size, attack.position.y - size, size * 2, size * 2});
} }
if (kind == ATTACK_ARROW && attack.isAttached) // A stuck arrow's ledge is the box around its hit shape.
if (kind == ATTACK_ARROW && attack.isAttached && attack.time < attack.duration - ARROW_FADE)
{ {
Vec2 middle = attack.position - vec2_polar(attack.angle, ARROW_SPAN / 2); Vec2 low = {std::min(attack.position.x, attack.tip.x), std::min(attack.position.y, attack.tip.y)};
Vec2 half = {std::abs(math_cos(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS, Vec2 high = {std::max(attack.position.x, attack.tip.x), std::max(attack.position.y, attack.tip.y)};
std::abs(math_sin(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS}; float radius = attack.radius;
solids.push_back({middle.x - half.x, middle.y - half.y, half.x * 2, half.y * 2}); world.ledges.push_back(
{low.x - radius, low.y - radius, high.x - low.x + radius * 2, high.y - low.y + radius * 2});
} }
} }
return solids;
} }
// Whether the fighter has a flail spinning around it. // Whether the fighter has a flail spinning around it.
+51 -41
View File
@@ -13,6 +13,7 @@ constexpr float WHIP_UNFURL = 0.6f;
class Fighter; class Fighter;
struct Dummy; struct Dummy;
struct Sim; struct Sim;
class World;
constexpr int LEVEL_COUNT = 3; constexpr int LEVEL_COUNT = 3;
constexpr int WEAPON_COUNT = 16; constexpr int WEAPON_COUNT = 16;
@@ -96,6 +97,8 @@ struct LevelDef
int limit = 0; int limit = 0;
}; };
// How long a stuck arrow fades out at the end of its life, no longer a ledge to stand on.
constexpr float ARROW_FADE = 0.6f;
constexpr float ARROW_GRAVITY = 500; constexpr float ARROW_GRAVITY = 500;
constexpr float MINE_ARM_TIME = 0.3f; constexpr float MINE_ARM_TIME = 0.3f;
constexpr float ROCKET_ACCELERATION = 600; constexpr float ROCKET_ACCELERATION = 600;
@@ -199,13 +202,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_SLASH, .kind = ATTACK_SLASH,
.sound = SOUND_SLASH, .sound = SOUND_SLASH,
.duration = 0.3f, .duration = 0.3f,
.shake = 0.15f, .shake = 0.075f,
.spread = 7}, .spread = 7},
.charged = {.label = "flying slash", .charged = {.label = "flying slash",
.kind = ATTACK_SLASH, .kind = ATTACK_SLASH,
.sound = SOUND_SLASH_HEAVY, .sound = SOUND_SLASH_HEAVY,
.duration = 0.6f, .duration = 0.6f,
.shake = 0.45f, .shake = 0.225f,
.spread = 5}, .spread = 5},
.levels = .levels =
{ {
@@ -231,13 +234,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_SWING, .kind = ATTACK_SWING,
.sound = SOUND_SWING, .sound = SOUND_SWING,
.duration = 0.3f, .duration = 0.3f,
.shake = 0.3f, .shake = 0.15f,
.spread = 6}, .spread = 6},
.charged = {.label = "slam", .charged = {.label = "slam",
.kind = ATTACK_SWING, .kind = ATTACK_SWING,
.sound = SOUND_SWING_HEAVY, .sound = SOUND_SWING_HEAVY,
.duration = 0.45f, .duration = 0.45f,
.shake = 0.7f, .shake = 0.35f,
.spread = 6}, .spread = 6},
.levels = .levels =
{ {
@@ -253,9 +256,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.chargeBlurb = "Charge to lunge toward the cursor\nwith a harder poke.", .chargeBlurb = "Charge to lunge toward the cursor\nwith a harder poke.",
.flavor = "Works best when riding an ostrich.", .flavor = "Works best when riding an ostrich.",
.hit = HIT_PIERCE, .hit = HIT_PIERCE,
.normal = {.label = "poke", .kind = ATTACK_POKE, .sound = SOUND_POKE, .shake = 0.15f, .spread = 4}, .normal = {.label = "poke", .kind = ATTACK_POKE, .sound = SOUND_POKE, .shake = 0.075f, .spread = 4},
.charged = .charged =
{.label = "lunging poke", .kind = ATTACK_POKE, .sound = SOUND_POKE_HEAVY, .shake = 0.4f, .spread = 3}, {.label = "lunging poke", .kind = ATTACK_POKE, .sound = SOUND_POKE_HEAVY, .shake = 0.2f, .spread = 3},
.levels = .levels =
{ {
{.normal = {.damage = 12, .range = 90, .speed = 500}, {.normal = {.damage = 12, .range = 90, .speed = 500},
@@ -280,13 +283,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_PUNCH, .kind = ATTACK_PUNCH,
.sound = SOUND_PUNCH, .sound = SOUND_PUNCH,
.duration = 0.12f, .duration = 0.12f,
.shake = 0.05f, .shake = 0.025f,
.spread = 18}, .spread = 18},
.charged = {.label = "uppercut", .charged = {.label = "uppercut",
.kind = ATTACK_UPPERCUT, .kind = ATTACK_UPPERCUT,
.sound = SOUND_UPPERCUT, .sound = SOUND_UPPERCUT,
.duration = 0.35f, .duration = 0.35f,
.shake = 0.5f, .shake = 0.25f,
.spread = 5}, .spread = 5},
.levels = .levels =
{ {
@@ -314,13 +317,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_SPIN, .kind = ATTACK_SPIN,
.sound = SOUND_FLAIL, .sound = SOUND_FLAIL,
.duration = 0.4f, .duration = 0.4f,
.shake = 0.15f, .shake = 0.075f,
.spread = 8, .spread = 8,
.hits = 99}, .hits = 99},
.charged = {.label = "ball throw", .charged = {.label = "ball throw",
.kind = ATTACK_FLAIL, .kind = ATTACK_FLAIL,
.sound = SOUND_FLAIL_HEAVY, .sound = SOUND_FLAIL_HEAVY,
.shake = 0.3f, .shake = 0.15f,
.spread = 6, .spread = 6,
.hits = 99}, .hits = 99},
.levels = .levels =
@@ -349,13 +352,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_WHIP, .kind = ATTACK_WHIP,
.sound = SOUND_WHIP, .sound = SOUND_WHIP,
.duration = 0.22f, .duration = 0.22f,
.shake = 0.1f, .shake = 0.05f,
.spread = 7.5f}, .spread = 7.5f},
.charged = {.label = "lash + blast", .charged = {.label = "lash + blast",
.kind = ATTACK_WHIP, .kind = ATTACK_WHIP,
.sound = SOUND_WHIP_HEAVY, .sound = SOUND_WHIP_HEAVY,
.duration = 0.12f, .duration = 0.12f,
.shake = 0.3f, .shake = 0.15f,
.spread = 0.5f, .spread = 0.5f,
.hits = 0}, .hits = 0},
.levels = .levels =
@@ -378,11 +381,11 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.chargeBlurb = "Charge to plunge the hook in\ninstantly and get yanked to it.", .chargeBlurb = "Charge to plunge the hook in\ninstantly and get yanked to it.",
.flavor = "Get over here!", .flavor = "Get over here!",
.hit = HIT_PIERCE, .hit = HIT_PIERCE,
.normal = {.label = "hook", .kind = ATTACK_HOOK, .sound = SOUND_GRAPPLE, .shake = 0.2f, .spread = 2}, .normal = {.label = "hook", .kind = ATTACK_HOOK, .sound = SOUND_GRAPPLE, .shake = 0.1f, .spread = 2},
.charged = {.label = "yanking hook", .charged = {.label = "yanking hook",
.kind = ATTACK_HOOK, .kind = ATTACK_HOOK,
.sound = SOUND_GRAPPLE_HEAVY, .sound = SOUND_GRAPPLE_HEAVY,
.shake = 0.4f, .shake = 0.2f,
.spread = 1}, .spread = 1},
.levels = .levels =
{ {
@@ -413,7 +416,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_BOOMERANG, .kind = ATTACK_BOOMERANG,
.sound = SOUND_BOOMERANG, .sound = SOUND_BOOMERANG,
.duration = 0.35f, .duration = 0.35f,
.shake = 0.15f, .shake = 0.075f,
.spread = 8, .spread = 8,
.hits = 2}, .hits = 2},
.charged = {.label = "big boomerang", .charged = {.label = "big boomerang",
@@ -422,7 +425,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.duration = 0.4f, .duration = 0.4f,
.hold = 1.2f, .hold = 1.2f,
.scale = 2, .scale = 2,
.shake = 0.3f, .shake = 0.15f,
.spread = 6, .spread = 6,
.hits = 4}, .hits = 4},
.levels = .levels =
@@ -448,9 +451,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.chargeBlurb = "The longer the draw, the faster\nand harder they fly.", .chargeBlurb = "The longer the draw, the faster\nand harder they fly.",
.flavor = "Erm, actually, bows require\nimmense upper body strengt-", .flavor = "Erm, actually, bows require\nimmense upper body strengt-",
.hit = HIT_PIERCE, .hit = HIT_PIERCE,
.normal = {.label = "arrow", .kind = ATTACK_ARROW, .sound = SOUND_BOW, .shake = 0.05f, .spread = 6}, .normal = {.label = "arrow", .kind = ATTACK_ARROW, .sound = SOUND_BOW, .shake = 0.025f, .spread = 6},
.charged = .charged =
{.label = "drawn arrow", .kind = ATTACK_ARROW, .sound = SOUND_BOW_HEAVY, .shake = 0.3f, .spread = 3}, {.label = "drawn arrow", .kind = ATTACK_ARROW, .sound = SOUND_BOW_HEAVY, .shake = 0.15f, .spread = 3},
.levels = .levels =
{ {
{.normal = {.damage = 22, .range = 360, .speed = 320, .volley = 1}, {.normal = {.damage = 22, .range = 360, .speed = 320, .volley = 1},
@@ -474,11 +477,11 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.hit = HIT_BULLET, .hit = HIT_BULLET,
.flash = 0.7f, .flash = 0.7f,
.kick = -0.15f, .kick = -0.15f,
.normal = {.label = "bullet", .kind = ATTACK_BULLET, .sound = SOUND_UZI, .shake = 0.02f, .spread = 7}, .normal = {.label = "bullet", .kind = ATTACK_BULLET, .sound = SOUND_UZI, .shake = 0.01f, .spread = 7},
.charged = {.label = "bullet", .charged = {.label = "bullet",
.kind = ATTACK_BULLET, .kind = ATTACK_BULLET,
.sound = SOUND_UZI_HEAVY, .sound = SOUND_UZI_HEAVY,
.shake = 0.25f, .shake = 0.125f,
.spread = 6, .spread = 6,
.scatter = 1.1f}, .scatter = 1.1f},
.levels = .levels =
@@ -510,14 +513,14 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.normal = {.label = "pellet", .normal = {.label = "pellet",
.kind = ATTACK_BULLET, .kind = ATTACK_BULLET,
.sound = SOUND_SHOTGUN, .sound = SOUND_SHOTGUN,
.shake = 0.3f, .shake = 0.15f,
.spread = 6, .spread = 6,
.scatter = 2.3f}, .scatter = 2.3f},
.charged = {.label = "slug", .charged = {.label = "slug",
.kind = ATTACK_BULLET, .kind = ATTACK_BULLET,
.sound = SOUND_SHOTGUN_HEAVY, .sound = SOUND_SHOTGUN_HEAVY,
.scale = 2.5f, .scale = 2.5f,
.shake = 0.5f, .shake = 0.25f,
.spread = 4, .spread = 4,
.hits = 3}, .hits = 3},
.levels = .levels =
@@ -544,12 +547,12 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.hit = HIT_ENERGY, .hit = HIT_ENERGY,
.flash = 1, .flash = 1,
.kick = -0.5f, .kick = -0.5f,
.normal = {.label = "bolt", .kind = ATTACK_PROJECTILE, .sound = SOUND_BLAST, .shake = 0.08f, .spread = 2}, .normal = {.label = "bolt", .kind = ATTACK_PROJECTILE, .sound = SOUND_BLAST, .shake = 0.04f, .spread = 2},
.charged = {.label = "beam", .charged = {.label = "beam",
.kind = ATTACK_BEAM, .kind = ATTACK_BEAM,
.sound = SOUND_BEAM, .sound = SOUND_BEAM,
.duration = 0.3f, .duration = 0.3f,
.shake = 0.35f, .shake = 0.175f,
.spread = 0.5f}, .spread = 0.5f},
.levels = .levels =
{ {
@@ -568,24 +571,29 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
{ {
.name = "Flamethrower", .name = "Flamethrower",
.blurb = "Sprays flames that drift up and\nlinger, burning whatever they touch.", .blurb = "Sprays flames that drift up and\nlinger, burning whatever they touch.",
.chargeBlurb = "Charge to hurl a giant fireball\nthat explodes on impact or after\na short distance.", .chargeBlurb = "Charge to hurl a giant fireball\nthat explodes on impact or after\na short distance, with "
"an airblast\nthat shoves fighters near you away.",
.flavor = "Mmmph mmmph mmph!!", .flavor = "Mmmph mmmph mmph!!",
.hit = HIT_FIRE, .hit = HIT_FIRE,
.kick = -0.1f, .kick = -0.1f,
.normal = .normal = {.label = "flame",
{.label = "flame", .kind = ATTACK_FLAME, .sound = SOUND_FLAME, .shake = 0.01f, .spread = 16, .hits = 2}, .kind = ATTACK_FLAME,
.sound = SOUND_FLAME,
.shake = 0.005f,
.spread = 16,
.hits = 2},
.charged = .charged =
{.label = "fireball", .kind = ATTACK_FIREBALL, .sound = SOUND_FIREBALL, .shake = 0.4f, .spread = 6}, {.label = "fireball", .kind = ATTACK_FIREBALL, .sound = SOUND_FIREBALL, .shake = 0.2f, .spread = 6},
.levels = .levels =
{ {
{.normal = {.damage = 2, .duration = 1.5f, .range = 115, .speed = 500}, {.normal = {.damage = 2, .duration = 1.5f, .range = 115, .speed = 500},
.charged = {.area = 40, .damage = 30, .range = 160, .scale = 2, .speed = 300}, .charged = {.area = 40, .damage = 30, .lunge = -110, .range = 160, .scale = 1, .speed = 300},
.cooldown = 0.08f}, .cooldown = 0.08f},
{.normal = {.damage = 3, .duration = 2.25f, .range = 140, .speed = 650}, {.normal = {.damage = 3, .duration = 2.25f, .range = 140, .speed = 650},
.charged = {.area = 55, .damage = 42, .range = 180, .scale = 2.5f, .speed = 300}, .charged = {.area = 55, .damage = 42, .lunge = -140, .range = 180, .scale = 1.25f, .speed = 300},
.cooldown = 0.08f}, .cooldown = 0.08f},
{.normal = {.damage = 4, .duration = 3, .range = 165, .speed = 800}, {.normal = {.damage = 4, .duration = 3, .range = 165, .speed = 800},
.charged = {.area = 70, .damage = 54, .range = 200, .scale = 3, .speed = 300}, .charged = {.area = 70, .damage = 54, .lunge = -165, .range = 200, .scale = 1.5f, .speed = 300},
.cooldown = 0.08f}, .cooldown = 0.08f},
}, },
.bot = {.rangeMax = 110}, .bot = {.rangeMax = 110},
@@ -597,9 +605,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.flavor = "RAWKET LAWNCHAIR", .flavor = "RAWKET LAWNCHAIR",
.hit = HIT_EXPLOSIVE, .hit = HIT_EXPLOSIVE,
.kick = -0.6f, .kick = -0.6f,
.normal = {.label = "rocket", .kind = ATTACK_ROCKET, .sound = SOUND_ROCKET, .shake = 0.15f, .spread = 5}, .normal = {.label = "rocket", .kind = ATTACK_ROCKET, .sound = SOUND_ROCKET, .shake = 0.075f, .spread = 5},
.charged = .charged =
{.label = "missile", .kind = ATTACK_ROCKET, .sound = SOUND_ROCKET_HEAVY, .shake = 0.3f, .spread = 4}, {.label = "missile", .kind = ATTACK_ROCKET, .sound = SOUND_ROCKET_HEAVY, .shake = 0.15f, .spread = 4},
.levels = .levels =
{ {
{.normal = {.area = 28, .damage = 14, .lunge = -120, .range = 500, .speed = 120}, {.normal = {.area = 28, .damage = 14, .lunge = -120, .range = 500, .speed = 120},
@@ -627,7 +635,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_MINE, .kind = ATTACK_MINE,
.sound = SOUND_MINE, .sound = SOUND_MINE,
.duration = 0.25f, .duration = 0.25f,
.shake = 0.1f, .shake = 0.05f,
.spread = 6, .spread = 6,
.hits = 0}, .hits = 0},
.levels = .levels =
@@ -663,13 +671,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] =
.kind = ATTACK_WISP, .kind = ATTACK_WISP,
.sound = SOUND_CAST, .sound = SOUND_CAST,
.duration = 0.35f, .duration = 0.35f,
.shake = 0.05f, .shake = 0.025f,
.spread = 4}, .spread = 4},
.charged = {.label = "teleport", .charged = {.label = "teleport",
.kind = ATTACK_TELEPORT, .kind = ATTACK_TELEPORT,
.sound = SOUND_TELEPORT_WARP, .sound = SOUND_TELEPORT_WARP,
.duration = 0.175f, .duration = 0.175f,
.shake = 0.3f}, .shake = 0.15f},
.levels = .levels =
{ {
{.normal = {.damage = 24, .range = 320, .speed = 330, .volley = 1}, {.normal = {.damage = 24, .range = 320, .speed = 330, .volley = 1},
@@ -717,7 +725,7 @@ struct AttackSummary
AttackSummary attack_summary_get(Fighter& fighter, int weapon, int level, bool isCharged); AttackSummary attack_summary_get(Fighter& fighter, int weapon, int level, bool isCharged);
// A blast's attack: an explosion with no weapon of its own to describe it. // A blast's attack: an explosion with no weapon of its own to describe it.
inline constexpr AttackDef EXPLOSION_DEF = {.kind = ATTACK_EXPLOSION, .duration = 0.35f, .shake = 0.5f}; inline constexpr AttackDef EXPLOSION_DEF = {.kind = ATTACK_EXPLOSION, .duration = 0.35f, .shake = 0.25f};
// A live attack, carrying its level's numbers from when it was fired. Secondary attacks (off-hand and Doubler) // A live attack, carrying its level's numbers from when it was fired. Secondary attacks (off-hand and Doubler)
// draw gray. Bounces is how many ricochets it has // draw gray. Bounces is how many ricochets it has
@@ -762,6 +770,8 @@ struct Attack
int ricochets = 0; int ricochets = 0;
int weapon = 0; int weapon = 0;
int key = -1; int key = -1;
// The hand it came from: the main hand (0) or a Dual Wield off-hand (1 up).
int hand = 0;
bool isAttached = false; bool isAttached = false;
bool isCharged = false; bool isCharged = false;
bool isCopy = false; bool isCopy = false;
@@ -770,7 +780,6 @@ struct Attack
bool isGathering = false; bool isGathering = false;
bool isGentle = false; bool isGentle = false;
bool isLeft = false; bool isLeft = false;
bool isOffhand = false;
bool isPulling = false; bool isPulling = false;
bool isRebounded = false; bool isRebounded = false;
bool isReturning = false; bool isReturning = false;
@@ -825,6 +834,7 @@ struct Blast
SoundId sound; SoundId sound;
int weapon = -1; int weapon = -1;
bool isGentle = false; bool isGentle = false;
bool isHarmless = false;
bool isPushing = false; bool isPushing = false;
bool isSecondary = false; bool isSecondary = false;
}; };
@@ -842,8 +852,8 @@ struct Shot
float charge = 0; float charge = 0;
float power = 1; float power = 1;
int weaponSlot = 0; int weaponSlot = 0;
int hand = 0;
bool isEcho = false; bool isEcho = false;
bool isOffhand = false;
bool isSecondary = false; bool isSecondary = false;
}; };
@@ -891,7 +901,7 @@ public:
void wisp_move(Sim& sim, Attack& attack); void wisp_move(Sim& sim, Attack& attack);
void wisps_release(Fighter& fighter); void wisps_release(Fighter& fighter);
void teleport_move(Sim& sim, Attack& attack); void teleport_move(Sim& sim, Attack& attack);
std::vector<Rect> solids_get(Fighter& fighter); void solids_add(Fighter& fighter, World& world);
private: private:
std::vector<Attack> pending; std::vector<Attack> pending;
@@ -902,7 +912,7 @@ private:
void attack_hit(Sim& sim, Attack& attack, int target, Vec2 point); void attack_hit(Sim& sim, Attack& attack, int target, Vec2 point);
std::vector<Capsule> chain_get(Sim& sim, Fighter& fighter); std::vector<Capsule> chain_get(Sim& sim, Fighter& fighter);
void tesla_update(Sim& sim); void tesla_update(Sim& sim);
Attack* tether_get(Fighter& fighter, int key, bool isOffhand); Attack* tether_get(Fighter& fighter, int key, int hand);
void hook_attach(Sim& sim, Attack& attack); void hook_attach(Sim& sim, Attack& attack);
void piece_launch(Sim& sim, Attack& piece); void piece_launch(Sim& sim, Attack& piece);
void attack_explode(Sim& sim, Attack& attack); void attack_explode(Sim& sim, Attack& attack);
+18 -9
View File
@@ -426,10 +426,10 @@ void World::body_move(Body& body)
} }
// One step of movement, resolving x then y against the grid and any obstacles (which only stop bodies moving into // One step of movement, resolving x then y against the grid and any obstacles (which only stop bodies moving into
// them, never ones already inside). Platforms only catch bodies falling onto them. Then ramps: steep ones push bodies // them, never ones already inside). Platforms, and ledges like them, only catch bodies falling onto them. Then ramps:
// out sideways, walkable ones carry the feet, and grounded bodies stay stuck to the ground going over crests and down // steep ones push bodies out sideways, walkable ones carry the feet, and grounded bodies stay stuck to the ground going
// slopes unless they're jumping. A grounded body steps up a ledge as low as a walkable slope leaves under its edge, // over crests and down slopes unless they're jumping. A grounded body steps up a ledge as low as a walkable slope
// which carries it from a ramp's crest onto the ground beside it. // leaves under its edge, which carries it from a ramp's crest onto the ground beside it.
void World::body_step(Body& body, float time) void World::body_step(Body& body, float time)
{ {
Rect& box = body.box; Rect& box = body.box;
@@ -484,6 +484,16 @@ void World::body_step(Body& body, float time)
body.isGrounded |= isDown; body.isGrounded |= isDown;
body.velocity.y = 0; body.velocity.y = 0;
} }
for (Rect& ledge : ledges)
{
bool isAcross = box.x < ledge.x + ledge.w && box.x + box.w > ledge.x;
bool isLanding = isDown && !body.isDropping && previousBottom <= ledge.y + BODY_EPSILON;
if (!isAcross || !isLanding || box.y + box.h <= ledge.y)
continue;
box.y = ledge.y - box.h;
body.isGrounded = true;
body.velocity.y = 0;
}
body.slope = 0; body.slope = 0;
ramp_push(body); ramp_push(body);
@@ -624,13 +634,12 @@ bool World::is_solid_at(Vec2 point)
return is_solid(tile); return is_solid(tile);
} }
// What attacks collide with and hooks grab: solid tiles anywhere, and platforms within their drawn strip. Bodies // What attacks collide with and hooks grab: whatever's solid (slopes below their ramp's line included), and platforms
// still pass up through platforms and can drop through them. // within their drawn strip. Bodies still pass up through platforms and can drop through them.
bool World::is_blocking_at(Vec2 point) bool World::is_blocking_at(Vec2 point)
{ {
Tile tile = tile_at(point); return is_solid_at(point) ||
return is_solid(tile) || (tile_at(point) == TILE_PLATFORM && point.y - std::floor(point.y / TILE_SIZE) * TILE_SIZE < PLATFORM_HEIGHT);
(tile == TILE_PLATFORM && point.y - std::floor(point.y / TILE_SIZE) * TILE_SIZE < PLATFORM_HEIGHT);
} }
// Whether any part of the box overlaps a tile of the given kind. // Whether any part of the box overlaps a tile of the given kind.
+4 -2
View File
@@ -84,12 +84,14 @@ constexpr int SPAWN_MAX = 16;
// Tile grid of any size, its collision, and the things standing in it. Spawns are where fighters stand at the start // Tile grid of any size, its collision, and the things standing in it. Spawns are where fighters stand at the start
// of a round, marked 1 to 8 in the map. Obstacles are extra solid boxes for the body moving right now, like a // of a round, marked 1 to 8 in the map. Obstacles are extra solid boxes for the body moving right now, like a
// fighter's own solid attacks. The map is the deathmatch arena or a race course stitched from chunks. Ramp tiles are // fighter's own solid attacks, and ledges extra boxes it can only land on from above, like platforms. The map is the
// solid below their ramp's line, and bodies meet ramps by their own rules rather than the grid's. // deathmatch arena or a race course stitched from chunks. Ramp tiles are solid below their ramp's line, and bodies meet
// ramps by their own rules rather than the grid's.
class World class World
{ {
public: public:
std::vector<Dummy> dummies; std::vector<Dummy> dummies;
std::vector<Rect> ledges;
std::vector<Rect> obstacles; std::vector<Rect> obstacles;
std::vector<Ramp> ramps; std::vector<Ramp> ramps;
std::vector<Tile> tiles; std::vector<Tile> tiles;
+23 -6
View File
@@ -26,6 +26,19 @@ FONTS = ROOT / "resources" / "font"
TTF = FONTS / "pixel_purl.ttf" TTF = FONTS / "pixel_purl.ttf"
FIRST = 32 FIRST = 32
COUNT = 95 COUNT = 95
# Characters past ASCII, each after the last in its own cell (written into the .fnt as UTF-8).
EXTRA = "°←↑→↓"
# Small glyphs drawn here instead of taken from the TTF: the degree sign a pixel wider than its own, and arrows (the
# TTF has none), the up one turned for the rest.
ARROW_UP = ["..##..", ".####.", "######", "..##..", "..##..", "..##.."]
ARROW_RIGHT = ["".join(ARROW_UP[len(ARROW_UP) - 1 - y][x] for y in range(len(ARROW_UP))) for x in range(len(ARROW_UP[0]))]
SMALL_DRAWN = {
"°": {"rows": [".##.", "#..#", "#..#", "#..#", ".##."], "left": 0, "top": 2, "advance": 5},
"←": {"rows": [row[::-1] for row in ARROW_RIGHT], "left": 0, "top": 5, "advance": 7},
"↑": {"rows": ARROW_UP, "left": 0, "top": 5, "advance": 7},
"→": {"rows": ARROW_RIGHT, "left": 0, "top": 5, "advance": 7},
"↓": {"rows": ARROW_UP[::-1], "left": 0, "top": 5, "advance": 7},
}
COLUMNS = 16 COLUMNS = 16
EM_PIXELS = 16 EM_PIXELS = 16
LINE_HEIGHT = 12 LINE_HEIGHT = 12
@@ -77,12 +90,14 @@ TINY = {
'v': ["...", "#.#", "#.#", "#.#", ".#."], 'w': [".....", "#...#", "#.#.#", "#.#.#", ".#.#."], 'v': ["...", "#.#", "#.#", "#.#", ".#."], 'w': [".....", "#...#", "#.#.#", "#.#.#", ".#.#."],
'x': ["...", "#.#", ".#.", ".#.", "#.#"], 'y': ["...", "#.#", "#.#", ".##", "..#", "##."], 'x': ["...", "#.#", ".#.", ".#.", "#.#"], 'y': ["...", "#.#", "#.#", ".##", "..#", "##."],
'z': ["...", "###", ".#.", "#..", "###"], '{': [".##", ".#.", "##.", ".#.", ".##"], '|': ["#", "#", "#", "#", "#"], 'z': ["...", "###", ".#.", "#..", "###"], '{': [".##", ".#.", "##.", ".#.", ".##"], '|': ["#", "#", "#", "#", "#"],
'}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."], '}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."], '°': [".##.", "#..#", ".##."],
'←': ["....", ".#..", "####", ".#.."], '↑': [".#.", "###", ".#.", ".#."], '→': ["....", "..#.", "####", "..#."],
'↓': ["...", ".#.", ".#.", "###", ".#."],
} }
def characters(): def characters():
return [chr(FIRST + i) for i in range(COUNT)] return [chr(FIRST + i) for i in range(COUNT)] + list(EXTRA)
# Each glyph as rows of '#'/'.' with where its top-left sits relative to the pen at the line's top, and its advance. # Each glyph as rows of '#'/'.' with where its top-left sits relative to the pen at the line's top, and its advance.
@@ -97,8 +112,10 @@ def tiny_glyphs():
def small_glyphs(): def small_glyphs():
font = ImageFont.truetype(str(TTF), EM_PIXELS) font = ImageFont.truetype(str(TTF), EM_PIXELS)
glyphs = {} glyphs = dict(SMALL_DRAWN)
for character in characters(): for character in characters():
if character in glyphs:
continue
# Drawn with the line's top at the origin's y, as the game always has; the bounding box gives the pixels. # Drawn with the line's top at the origin's y, as the game always has; the bounding box gives the pixels.
image = Image.new("L", (64, 64), 0) image = Image.new("L", (64, 64), 0)
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
@@ -127,7 +144,7 @@ def font_write(name, glyphs, scale, line_height, outline):
right = max(glyph["left"] + len(glyph["rows"][0]) for glyph in glyphs.values() if glyph["rows"]) right = max(glyph["left"] + len(glyph["rows"][0]) for glyph in glyphs.values() if glyph["rows"])
bottom = max(glyph["top"] + len(glyph["rows"]) for glyph in glyphs.values() if glyph["rows"]) bottom = max(glyph["top"] + len(glyph["rows"]) for glyph in glyphs.values() if glyph["rows"])
cell_w, cell_h = right - left, max(bottom, line_height) cell_w, cell_h = right - left, max(bottom, line_height)
rows = (COUNT + COLUMNS - 1) // COLUMNS rows = (len(glyphs) + COLUMNS - 1) // COLUMNS
image = Image.new("RGBA", (COLUMNS * (cell_w + 1), rows * (cell_h + 1)), (0, 0, 0, 0)) image = Image.new("RGBA", (COLUMNS * (cell_w + 1), rows * (cell_h + 1)), (0, 0, 0, 0))
entries = {} entries = {}
for i, character in enumerate(characters()): for i, character in enumerate(characters()):
@@ -145,10 +162,10 @@ def font_write(name, glyphs, scale, line_height, outline):
if scale is None: if scale is None:
del header["scale"] del header["scale"]
lines = ["{"] + [f' {json.dumps(key)}: {json.dumps(value)},' for key, value in header.items()] + [' "glyphs": {'] lines = ["{"] + [f' {json.dumps(key)}: {json.dumps(value)},' for key, value in header.items()] + [' "glyphs": {']
lines += [f' {json.dumps(character)}: {json.dumps(entry)}{"," if i < COUNT - 1 else ""}' lines += [f' {json.dumps(character, ensure_ascii=False)}: {json.dumps(entry)}{"," if i < len(entries) - 1 else ""}'
for i, (character, entry) in enumerate(entries.items())] for i, (character, entry) in enumerate(entries.items())]
lines += [" }", "}"] lines += [" }", "}"]
(FONTS / f"{name}.fnt").write_text("\n".join(lines) + "\n") (FONTS / f"{name}.fnt").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"{name}: {image.width}x{image.height}, cells {cell_w}x{cell_h}") print(f"{name}: {image.width}x{image.height}, cells {cell_w}x{cell_h}")
-133
View File
@@ -1,133 +0,0 @@
# Makes the small (16x16) and tiny (8x8) icon sheets from the full 24x24 weapon and trinket icon sheets, with an anm2
# for each listing the same regions and animations at the new size.
#
# The icons are a black body with grey detail inside a one pixel white outline, all tinted when drawn. Shrinking that
# outline along with the rest would smear it, so only the body is shrunk (a cell is body where enough of what it covers
# was, grey where most of that was grey) and a fresh one pixel outline is drawn around it at the new size. Tiny icons
# have no room for that, so they're the shrunk silhouette in white, keeping black only where it's thick enough.
#
# python tools/icons_resize.py
import json
import pathlib
from PIL import Image
ROOT = pathlib.Path(__file__).resolve().parent.parent
GFX = ROOT / "resources" / "gfx"
SHEETS = ["weapon_icons", "trinkets"]
SIZES = {"small": 16, "tiny": 8}
CELL = 24
BODY_COVERAGE = 0.4
SHAPE_COVERAGE = 0.5
INNER_COVERAGE = 0.6
WHITE = (255, 255, 255, 255)
GREY = (128, 128, 128, 255)
BLACK = (0, 0, 0, 255)
def is_body(pixel):
return pixel[3] > 0 and pixel[:3] != (255, 255, 255)
def coverage_get(source, x, y, scale):
"""How much of a cell at the new size was opaque, body, and grey body, as shares of the cell."""
opaque = covered = grey = total = 0.0
for sy in range(int(y * scale), int(-(-(y + 1) * scale // 1))):
for sx in range(int(x * scale), int(-(-(x + 1) * scale // 1))):
# How much of the source pixel falls inside this cell.
weight = (min(sx + 1, (x + 1) * scale) - max(sx, x * scale)) * \
(min(sy + 1, (y + 1) * scale) - max(sy, y * scale))
total += weight
pixel = source[sx, sy]
opaque += weight * (pixel[3] > 0)
if is_body(pixel):
covered += weight
grey += weight * (pixel[:3] == (128, 128, 128))
return opaque / total, covered / total, grey / total
def icon_tiny(icon, size):
"""Too small for an outline around a body: the whole silhouette in white, with black (or grey) left only where it's
thick enough to keep a white rim all round."""
source = icon.load()
scale = CELL / size
cells = [[coverage_get(source, x, y, scale) for x in range(size)] for y in range(size)]
shape = [[cells[y][x][0] >= SHAPE_COVERAGE for x in range(size)] for y in range(size)]
result = Image.new("RGBA", (size, size), (0, 0, 0, 0))
pixels = result.load()
for y in range(size):
for x in range(size):
if not shape[y][x]:
continue
opaque, covered, grey = cells[y][x]
inner = all(0 <= x + dx < size and 0 <= y + dy < size and shape[y + dy][x + dx]
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))
if inner and covered >= INNER_COVERAGE:
pixels[x, y] = GREY if grey > covered / 2 else BLACK
else:
pixels[x, y] = WHITE
return result
def icon_shrink(icon, size):
source = icon.load()
scale = CELL / size
body = [[None] * size for _ in range(size)]
for y in range(size):
for x in range(size):
covered = grey = total = 0.0
for sy in range(int(y * scale), int(-(-(y + 1) * scale // 1))):
for sx in range(int(x * scale), int(-(-(x + 1) * scale // 1))):
# How much of the source pixel falls inside this cell.
weight = (min(sx + 1, (x + 1) * scale) - max(sx, x * scale)) * \
(min(sy + 1, (y + 1) * scale) - max(sy, y * scale))
total += weight
pixel = source[sx, sy]
if is_body(pixel):
covered += weight
grey += weight * (pixel[:3] == (128, 128, 128))
if covered / total >= BODY_COVERAGE:
body[y][x] = GREY if grey > covered / 2 else BLACK
result = Image.new("RGBA", (size, size), (0, 0, 0, 0))
pixels = result.load()
for y in range(size):
for x in range(size):
if body[y][x]:
pixels[x, y] = body[y][x]
continue
touching = any(0 <= x + dx < size and 0 <= y + dy < size and body[y + dy][x + dx]
for dx in (-1, 0, 1) for dy in (-1, 0, 1))
if touching:
pixels[x, y] = WHITE
return result
def main():
for sheet in SHEETS:
art = json.loads((GFX / f"{sheet}.anm2").read_text())
image = Image.open(GFX / f"{sheet}.png").convert("RGBA")
for suffix, size in SIZES.items():
columns, rows = image.width // CELL, image.height // CELL
result = Image.new("RGBA", (columns * size, rows * size), (0, 0, 0, 0))
for row in range(rows):
for column in range(columns):
icon = image.crop((column * CELL, row * CELL, (column + 1) * CELL, (row + 1) * CELL))
shrink = icon_tiny if suffix == "tiny" else icon_shrink
result.paste(shrink(icon, size), (column * size, row * size))
name = f"{sheet}_{suffix}"
result.save(GFX / f"{name}.png")
shrunk = json.loads(json.dumps(art))
spritesheet = shrunk["spritesheets"][0]
spritesheet["path"] = f"{name}.png"
for region in spritesheet["regions"]:
region["crop"] = [region["crop"][0] // CELL * size, region["crop"][1] // CELL * size]
region["size"] = [size, size]
(GFX / f"{name}.anm2").write_text(json.dumps(shrunk, indent=2) + "\n")
print(f"{name}: {result.width}x{result.height}")
if __name__ == "__main__":
main()