diff --git a/resources/font/big.fnt b/resources/font/big.fnt index 0d7b6a8..b6218c1 100644 --- a/resources/font/big.fnt +++ b/resources/font/big.fnt @@ -98,6 +98,11 @@ "{": {"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": 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} } } diff --git a/resources/font/big.png b/resources/font/big.png index 4296bbb..a64738c 100644 Binary files a/resources/font/big.png and b/resources/font/big.png differ diff --git a/resources/font/huge.fnt b/resources/font/huge.fnt index d7c1488..f8add34 100644 --- a/resources/font/huge.fnt +++ b/resources/font/huge.fnt @@ -98,6 +98,11 @@ "{": {"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": 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} } } diff --git a/resources/font/huge.png b/resources/font/huge.png index 00e2c10..c8a79f4 100644 Binary files a/resources/font/huge.png and b/resources/font/huge.png differ diff --git a/resources/font/small.fnt b/resources/font/small.fnt index f9d6022..3b2b6e2 100644 --- a/resources/font/small.fnt +++ b/resources/font/small.fnt @@ -98,6 +98,11 @@ "{": {"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": 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} } } diff --git a/resources/font/small.png b/resources/font/small.png index 305401f..4889468 100644 Binary files a/resources/font/small.png and b/resources/font/small.png differ diff --git a/resources/font/tiny.fnt b/resources/font/tiny.fnt index 1ddad17..afa4f2f 100644 --- a/resources/font/tiny.fnt +++ b/resources/font/tiny.fnt @@ -97,6 +97,11 @@ "{": {"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": 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} } } diff --git a/resources/font/tiny.png b/resources/font/tiny.png index b84e6ef..2e3817f 100644 Binary files a/resources/font/tiny.png and b/resources/font/tiny.png differ diff --git a/resources/gfx/dummy.png b/resources/gfx/dummy.png index d3bd1ad..cb14e76 100644 Binary files a/resources/gfx/dummy.png and b/resources/gfx/dummy.png differ diff --git a/resources/gfx/effects.anm2 b/resources/gfx/effects.anm2 index 9470054..bbf9d82 100644 --- a/resources/gfx/effects.anm2 +++ b/resources/gfx/effects.anm2 @@ -6,13 +6,13 @@ "path": "effects.png", "regions": [ {"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": "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 2", "crop": [41, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, - {"name": "Flame 3", "crop": [73, 64], "size": [13, 15], "pivot": [6.5, 9.5]}, - {"name": "Flame 4", "crop": [105, 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": [42, 58], "size": [11, 21], "pivot": [5, 16]}, + {"name": "Flame 3", "crop": [74, 58], "size": [11, 21], "pivot": [5, 16]}, + {"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": "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"}, @@ -20,10 +20,10 @@ {"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": "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 2", "crop": [36, 193], "size": [24, 15], "pivot": [17, 7.5]}, - {"name": "Wisp 3", "crop": [68, 193], "size": [24, 15], "pivot": [17, 7.5]}, - {"name": "Wisp 4", "crop": [100, 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": [40, 196], "size": [15, 24], "pivot": [7.5, 17]}, + {"name": "Wisp 3", "crop": [72, 196], "size": [15, 24], "pivot": [7.5, 17]}, + {"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}]}]}, "Punch": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]}, "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} + ] + } + ] + } } } diff --git a/resources/gfx/effects.png b/resources/gfx/effects.png index b6935eb..22d87ba 100644 Binary files a/resources/gfx/effects.png and b/resources/gfx/effects.png differ diff --git a/resources/gfx/modes.png b/resources/gfx/modes.png index 29557a4..4790f88 100644 Binary files a/resources/gfx/modes.png and b/resources/gfx/modes.png differ diff --git a/resources/gfx/player.anm2 b/resources/gfx/player.anm2 index 3f092ce..8fc0187 100644 --- a/resources/gfx/player.anm2 +++ b/resources/gfx/player.anm2 @@ -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}]}]}, "Fall": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]}, "Slide": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]}, diff --git a/resources/gfx/tiles.png b/resources/gfx/tiles.png index ff7e06f..9d74884 100644 Binary files a/resources/gfx/tiles.png and b/resources/gfx/tiles.png differ diff --git a/resources/gfx/trinkets.anm2 b/resources/gfx/trinkets.anm2 index 3c3c291..ad2d548 100644 --- a/resources/gfx/trinkets.anm2 +++ b/resources/gfx/trinkets.anm2 @@ -7,26 +7,70 @@ "regions": [ {"name": "Gloves", "crop": [0, 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": "Enlarger", "crop": [72, 0], "size": [24, 24], "origin": "center"}, - {"name": "Boxing Glove", "crop": [96, 0], "size": [24, 24], "origin": "center"}, - {"name": "Splitter", "crop": [120, 0], "size": [24, 24], "origin": "center"}, - {"name": "Doubler", "crop": [0, 24], "size": [24, 24], "origin": "center"}, - {"name": "Dual Wield", "crop": [24, 24], "size": [24, 24], "origin": "center"}, - {"name": "Ricochet", "crop": [48, 24], "size": [24, 24], "origin": "center"}, - {"name": "Tesla Coil", "crop": [72, 24], "size": [24, 24], "origin": "center"}, - {"name": "Boots", "crop": [96, 24], "size": [24, 24], "origin": "center"}, - {"name": "Speedometer", "crop": [120, 24], "size": [24, 24], "origin": "center"}, - {"name": "Shrinker", "crop": [0, 48], "size": [24, 24], "origin": "center"}, - {"name": "Wings", "crop": [24, 48], "size": [24, 24], "origin": "center"}, - {"name": "Feather", "crop": [48, 48], "size": [24, 24], "origin": "center"}, - {"name": "Iron Weights", "crop": [72, 48], "size": [24, 24], "origin": "center"}, - {"name": "Climb Claw", "crop": [96, 48], "size": [24, 24], "origin": "center"}, - {"name": "Recoil Coil", "crop": [120, 48], "size": [24, 24], "origin": "center"}, - {"name": "Diazepam", "crop": [0, 72], "size": [24, 24], "origin": "center"}, - {"name": "Embiggener", "crop": [24, 72], "size": [24, 24], "origin": "center"}, - {"name": "Pebble Helmet", "crop": [48, 72], "size": [24, 24], "origin": "center"}, - {"name": "Shock Absorber", "crop": [72, 72], "size": [24, 24], "origin": "center"} + {"name": "Boxing Glove", "crop": [48, 0], "size": [24, 24], "origin": "center"}, + {"name": "Accelerator", "crop": [72, 0], "size": [24, 24], "origin": "center"}, + {"name": "Enlarger", "crop": [96, 0], "size": [24, 24], "origin": "center"}, + {"name": "Diazepam", "crop": [120, 0], "size": [24, 24], "origin": "center"}, + {"name": "Splitter", "crop": [0, 24], "size": [24, 24], "origin": "center"}, + {"name": "Doubler", "crop": [24, 24], "size": [24, 24], "origin": "center"}, + {"name": "Dual Wield", "crop": [48, 24], "size": [24, 24], "origin": "center"}, + {"name": "Ricochet", "crop": [72, 24], "size": [24, 24], "origin": "center"}, + {"name": "Tesla Coil", "crop": [96, 24], "size": [24, 24], "origin": "center"}, + {"name": "Recoil Coil", "crop": [120, 24], "size": [24, 24], "origin": "center"}, + {"name": "Shock Absorber", "crop": [0, 48], "size": [24, 24], "origin": "center"}, + {"name": "Shrinker", "crop": [24, 48], "size": [24, 24], "origin": "center"}, + {"name": "Embiggener", "crop": [48, 48], "size": [24, 24], "origin": "center"}, + {"name": "Pebble Helmet", "crop": [72, 48], "size": [24, 24], "origin": "center"}, + {"name": "Boots", "crop": [96, 48], "size": [24, 24], "origin": "center"}, + {"name": "Speedometer", "crop": [120, 48], "size": [24, 24], "origin": "center"}, + {"name": "Wings", "crop": [0, 72], "size": [24, 24], "origin": "center"}, + {"name": "Feather", "crop": [24, 72], "size": [24, 24], "origin": "center"}, + {"name": "Iron Weights", "crop": [48, 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", "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}]}]} + "Gloves": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]}, + "Energy Drink": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]}, + "Boxing Glove": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]}, + "Accelerator": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]}, + "Enlarger": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]}, + "Diazepam": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]}, + "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}]}]}, + "Recoil Coil": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]}, + "Shock Absorber": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]}, + "Shrinker": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]}, + "Embiggener": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]}, + "Pebble Helmet": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]}, + "Boots": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]}, + "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}]}]}, + "Gloves Small": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]}, + "Energy Drink Small": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]}, + "Boxing Glove Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]}, + "Accelerator Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]}, + "Enlarger Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]}, + "Diazepam Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]}, + "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}]}]}, + "Recoil Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]}, + "Shock Absorber Small": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]}, + "Shrinker Small": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]}, + "Embiggener Small": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]}, + "Pebble Helmet Small": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]}, + "Boots Small": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]}, + "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}]}]}, + "Gloves Tiny": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]}, + "Energy Drink Tiny": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]}, + "Boxing Glove Tiny": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]}, + "Accelerator Tiny": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]}, + "Enlarger Tiny": {"layers": [{"layer": 0, "frames": [{"region": 48}]}]}, + "Diazepam Tiny": {"layers": [{"layer": 0, "frames": [{"region": 49}]}]}, + "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}]}]}, + "Recoil Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 55}]}]}, + "Shock Absorber Tiny": {"layers": [{"layer": 0, "frames": [{"region": 56}]}]}, + "Shrinker Tiny": {"layers": [{"layer": 0, "frames": [{"region": 57}]}]}, + "Embiggener Tiny": {"layers": [{"layer": 0, "frames": [{"region": 58}]}]}, + "Pebble Helmet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 59}]}]}, + "Boots Tiny": {"layers": [{"layer": 0, "frames": [{"region": 60}]}]}, + "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}]}]} } } diff --git a/resources/gfx/trinkets.png b/resources/gfx/trinkets.png index 8984312..fcb8099 100644 Binary files a/resources/gfx/trinkets.png and b/resources/gfx/trinkets.png differ diff --git a/resources/gfx/trinkets_small.anm2 b/resources/gfx/trinkets_small.anm2 deleted file mode 100644 index c1b4278..0000000 --- a/resources/gfx/trinkets_small.anm2 +++ /dev/null @@ -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 - } - ] - } - ] - } - } -} diff --git a/resources/gfx/trinkets_small.png b/resources/gfx/trinkets_small.png deleted file mode 100644 index 3fd15ab..0000000 Binary files a/resources/gfx/trinkets_small.png and /dev/null differ diff --git a/resources/gfx/trinkets_tiny.anm2 b/resources/gfx/trinkets_tiny.anm2 deleted file mode 100644 index fc49b79..0000000 --- a/resources/gfx/trinkets_tiny.anm2 +++ /dev/null @@ -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 - } - ] - } - ] - } - } -} diff --git a/resources/gfx/trinkets_tiny.png b/resources/gfx/trinkets_tiny.png deleted file mode 100644 index e12c39c..0000000 Binary files a/resources/gfx/trinkets_tiny.png and /dev/null differ diff --git a/resources/gfx/ui.anm2 b/resources/gfx/ui.anm2 index 29e3c64..b3c25f1 100644 --- a/resources/gfx/ui.anm2 +++ b/resources/gfx/ui.anm2 @@ -7,7 +7,9 @@ "regions": [ {"name": "Chevron", "crop": [4, 5], "size": [7, 5], "pivot": [7, 5]}, {"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", "animations": { - "Chevron": { - "length": 1, - "layers": [ - { - "layer": 0, - "frames": [ - {"region": 0} - ] - } - ] - }, - "Arrow": { - "length": 1, - "layers": [ - { - "layer": 0, - "frames": [ - {"region": 1} - ] - } - ] - }, - "Crosshair": { - "length": 1, - "layers": [ - { - "layer": 0, - "frames": [ - {"region": 2} - ] - } - ] - } + "Chevron": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]}, + "Arrow": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]}, + "Crosshair": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]}, + "Chevron Small": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]}, + "Chevron Tiny": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]} } } diff --git a/resources/gfx/ui.png b/resources/gfx/ui.png index 419faa2..57230d4 100644 Binary files a/resources/gfx/ui.png and b/resources/gfx/ui.png differ diff --git a/resources/gfx/weapon_icons.anm2 b/resources/gfx/weapon_icons.anm2 index 9152806..ccb0f7a 100644 --- a/resources/gfx/weapon_icons.anm2 +++ b/resources/gfx/weapon_icons.anm2 @@ -7,20 +7,52 @@ "regions": [ {"name": "Sword", "crop": [0, 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": [72, 0], "size": [24, 24], "origin": "center"}, - {"name": "Whip", "crop": [0, 24], "size": [24, 24], "origin": "center"}, - {"name": "Grapple", "crop": [24, 24], "size": [24, 24], "origin": "center"}, - {"name": "Boomerang", "crop": [48, 24], "size": [24, 24], "origin": "center"}, - {"name": "Bow", "crop": [72, 24], "size": [24, 24], "origin": "center"}, - {"name": "Blaster", "crop": [0, 48], "size": [24, 24], "origin": "center"}, + {"name": "Lance", "crop": [48, 0], "size": [24, 24], "origin": "center"}, + {"name": "Fist", "crop": [72, 0], "size": [24, 24], "origin": "center"}, + {"name": "Flail", "crop": [0, 24], "size": [24, 24], "origin": "center"}, + {"name": "Whip", "crop": [24, 24], "size": [24, 24], "origin": "center"}, + {"name": "Grapple", "crop": [48, 24], "size": [24, 24], "origin": "center"}, + {"name": "Boomerang", "crop": [72, 24], "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": "Shotgun", "crop": [48, 48], "size": [24, 24], "origin": "center"}, - {"name": "Flamethrower", "crop": [72, 48], "size": [24, 24], "origin": "center"}, - {"name": "Rocket Launcher", "crop": [0, 72], "size": [24, 24], "origin": "center"}, - {"name": "Mine", "crop": [24, 72], "size": [24, 24], "origin": "center"}, - {"name": "Fist", "crop": [48, 72], "size": [24, 24], "origin": "center"}, - {"name": "Grimoire", "crop": [72, 72], "size": [24, 24], "origin": "center"} + {"name": "Blaster", "crop": [72, 48], "size": [24, 24], "origin": "center"}, + {"name": "Flamethrower", "crop": [0, 72], "size": [24, 24], "origin": "center"}, + {"name": "Rocket Launcher", "crop": [24, 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": "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", "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}]}]} + "Sword": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]}, + "Hammer": {"layers": [{"layer": 0, "frames": [{"region": 1}]}]}, + "Lance": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]}, + "Fist": {"layers": [{"layer": 0, "frames": [{"region": 3}]}]}, + "Flail": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]}, + "Whip": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]}, + "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}]}]}, + "Blaster": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]}, + "Flamethrower": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]}, + "Rocket Launcher": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]}, + "Mine": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]}, + "Grimoire": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]}, + "Sword Small": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]}, + "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}]}]}, + "Grapple Small": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]}, + "Boomerang Small": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]}, + "Bow Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]}, + "Uzi Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]}, + "Shotgun Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]}, + "Blaster Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]}, + "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}]}]}, + "Hammer Tiny": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]}, + "Lance Tiny": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]}, + "Fist Tiny": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]}, + "Flail Tiny": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]}, + "Whip Tiny": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]}, + "Grapple Tiny": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]}, + "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}]}]}, + "Flamethrower Tiny": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]}, + "Rocket Launcher Tiny": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]}, + "Mine Tiny": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]}, + "Grimoire Tiny": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]} } } diff --git a/resources/gfx/weapon_icons.png b/resources/gfx/weapon_icons.png index 0951bdd..d377804 100644 Binary files a/resources/gfx/weapon_icons.png and b/resources/gfx/weapon_icons.png differ diff --git a/resources/gfx/weapon_icons_small.anm2 b/resources/gfx/weapon_icons_small.anm2 deleted file mode 100644 index 6f2b36c..0000000 --- a/resources/gfx/weapon_icons_small.anm2 +++ /dev/null @@ -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 - } - ] - } - ] - } - } -} diff --git a/resources/gfx/weapon_icons_small.png b/resources/gfx/weapon_icons_small.png deleted file mode 100644 index ad86b56..0000000 Binary files a/resources/gfx/weapon_icons_small.png and /dev/null differ diff --git a/resources/gfx/weapon_icons_tiny.anm2 b/resources/gfx/weapon_icons_tiny.anm2 deleted file mode 100644 index e986a8b..0000000 --- a/resources/gfx/weapon_icons_tiny.anm2 +++ /dev/null @@ -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 - } - ] - } - ] - } - } -} diff --git a/resources/gfx/weapon_icons_tiny.png b/resources/gfx/weapon_icons_tiny.png deleted file mode 100644 index 34fdc04..0000000 Binary files a/resources/gfx/weapon_icons_tiny.png and /dev/null differ diff --git a/src/effects.cpp b/src/effects.cpp index 7186b91..17c5721 100644 --- a/src/effects.cpp +++ b/src/effects.cpp @@ -12,6 +12,8 @@ constexpr float FLASH_CROSS = 0.5f; constexpr float FLASH_LENGTH = 16; constexpr float FLASH_LIFE = 0.05f; constexpr float FLASH_WIDTH = 6; +constexpr float RING_LIFE = 0.3f; +constexpr int RING_WIDTH = 2; constexpr float AFTERIMAGE_LIFE = 0.12f; constexpr float NUMBER_GAP = 8; constexpr float POPUP_FADE = 0.4f; @@ -28,9 +30,9 @@ constexpr float POPUP_SPREAD = 40; constexpr float PARTICLE_OUTLINE = 1; constexpr float PARTICLE_RANDOM_MIN = 0.4f; constexpr float SHAKE_DECAY = 2.5f; -constexpr float SHAKE_LIMIT = 12; -constexpr float SHAKE_MAX = 10; -constexpr float SHAKE_TRAUMA_MAX = 3; +constexpr float SHAKE_LIMIT = 6; +constexpr float SHAKE_MAX = 5; +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 // 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}); } +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) { hits.push_back({ticks * TICK, amount}); @@ -157,6 +164,10 @@ void Effects::update() flash.life -= TICK; 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) afterimage.life -= TICK; 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); } + // 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. for (Particle& particle : particles) { @@ -238,5 +258,6 @@ void Effects::clear() hits.clear(); particles.clear(); popups.clear(); + rings.clear(); shake = 0; } diff --git a/src/effects.h b/src/effects.h index 9e26239..0ddcc56 100644 --- a/src/effects.h +++ b/src/effects.h @@ -49,6 +49,15 @@ struct Flash 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. struct Afterimage { @@ -63,6 +72,7 @@ public: void burst(Burst burst, Vec2 position, float angle, Color color); void afterimage_spawn(Pose pose); 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 number_spawn(Vec2 position, int value, int target, Color color); void dps_record(int amount); @@ -80,6 +90,7 @@ private: std::vector hits; std::vector particles; std::vector popups; + std::vector rings; float shake = 0; int ticks = 0; }; diff --git a/src/fighter.cpp b/src/fighter.cpp index fae4c07..4fe8101 100644 --- a/src/fighter.cpp +++ b/src/fighter.cpp @@ -3,7 +3,7 @@ #include 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 KNOCK_BASE = 60; constexpr float KNOCK_LIFT = 0.35f; @@ -17,11 +17,12 @@ constexpr float CLING_SPEED = 60; constexpr float COYOTE_TIME = 0.1f; constexpr float DASH_COOLDOWN = 0.8f; 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 FAST_FALL_GRAVITY = 2; constexpr float FLASH_DECAY = 8; constexpr float FAST_FALL_SPEED = 700; +constexpr float FALL_EASE = 2000; constexpr float GROUND_ACCELERATION = 500; constexpr float GROUND_FRICTION = 3000; constexpr float JUMP_BUFFER = 0.1f; @@ -34,13 +35,13 @@ constexpr float ROPE_MIN = 24; constexpr float SLIDE_DUST_INTERVAL = 0.08f; constexpr float SWING_ACCELERATION = 500; 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_SCALE_MAX = 3; -constexpr float POUND_SCALE_MIN = 0.3f; constexpr float POUND_SPEED = 700; constexpr Vec2 HAND_OFFSET = {4, 2}; constexpr Vec2 OFFHAND_OFFSET = {-3, 4}; +constexpr Vec2 OFFHAND_FAN = {3, 2}; constexpr float WALL_IMPACT_SPEED = 200; constexpr float WALL_LIFT = 0.25f; 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); flash = 0; health = maxHealth; - offSlot = -1; + std::fill(std::begin(slotSwitches), std::end(slotSwitches), 0); slot = 0; isAlive = true; } // 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 -// enough knock launches it. Shock Absorber softens it. Running out of health kills it in a burst, and its attacks -// vanish with it. +// up to a speed that grows with the damage (see knock_take). Shock Absorber softens it. Running out of health kills it +// in a burst, and its attacks vanish with it. void Fighter::damage_take(Sim& sim, Damage damage) { // 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 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 * - (isCrouching ? CROUCH_DAMP : 1) * (isRacing ? RACE_KNOCK : 1) * knock_absorb_get(); - float along = vec2_dot(body.velocity, direction); - impulse_add(direction * std::max(0.0f, knock - along), false); - launcher = damage.owner; - isLaunched |= knock >= LAUNCH_START_SPEED; + (isRacing ? RACE_KNOCK : 1) * knock_absorb_get(); + knock_take(direction * knock, damage.owner); if (health > 0) return; @@ -212,12 +209,12 @@ void Fighter::update(Sim& sim) rope_update(*hook); bool wasGrounded = body.isGrounded; - bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS); float fallSpeed = body.velocity.y; 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.obstacles.clear(); + sim.world.ledges.clear(); if (isLaunched) bounce_resolve(sim, {runSpeed, fallSpeed}); safe_update(sim); @@ -240,13 +237,14 @@ void Fighter::update(Sim& sim) sim.cues->sound_play(SOUND_LAND); 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 - // radius its square root. - if (body.isGrounded && !wasGrounded && fallSpeed > LAND_SPEED && isPounding) + // Iron Weights slam: landing faster than a plain fall can go (on anything, platforms too), and the harder the + // landing, the harder the blast. Every copy adds to its damage and radius. Damage follows the landing speed, and + // 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, - .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, .owner = index, .sound = SOUND_POUND}); @@ -295,9 +293,10 @@ void Fighter::update(Sim& sim) buttonsPrevious = controls.buttons; } -// Horizontal control and gravity. Holding S in the air falls faster, holding W floats 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. +// Horizontal control and gravity. Holding S in the air falls faster (Iron Weights speeding up both how fast it gets +// there and how fast it goes), and letting go eases back to a plain fall rather than stopping short. Holding W floats +// 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. // 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. @@ -320,11 +319,15 @@ void Fighter::run_update(Attack* hook) bool isAirborne = !body.isGrounded && !hook; bool isFastFalling = isAirborne && is_down(CONTROL_DOWN) && !isSliding; 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 *= isSpinning ? SPIN_GRAVITY : 1; - float maxFall = isFastFalling ? FAST_FALL_SPEED * trinket_get(TRINKET_WEIGHTS, 1) : MAX_FALL; - body.velocity.y = std::min(body.velocity.y + gravity * TICK, maxFall); + 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); // Sliding: the slope pulls it downhill and little else slows it. if (isSliding) @@ -402,7 +405,7 @@ void Fighter::slot_update(Sim& sim) sim.cues->sound_play(SOUND_SELECT); if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot)) { - offSlot = previous; + slotSwitches[previous] = ++switches; sim.weapons.attacks_release(*this, ATTACK_FLAIL); } @@ -654,15 +657,14 @@ float Fighter::trinket_cooldown_get(int trinket) 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 -// pounding down fast enough to slam at all. +// The radius an Iron Weights slam would blast if the fighter landed at its current speed, or 0 if it isn't falling +// fast enough to slam at all. float Fighter::pound_area_get() { - bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS); - if (!isPounding || body.isGrounded || body.velocity.y <= LAND_SPEED) + if (!is_trinket_equipped(TRINKET_WEIGHTS) || body.isGrounded || body.velocity.y <= MAX_FALL) return 0; - float impact = std::clamp(body.velocity.y / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX); - return POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact); + float impact = std::min(body.velocity.y / POUND_SPEED, POUND_SCALE_MAX); + 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). @@ -681,6 +683,19 @@ void Fighter::impulse_add(Vec2 impulse, bool isDamped) 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. void Fighter::burst_add(Vec2 impulse) { @@ -827,28 +842,27 @@ bool Fighter::is_released(Control control) 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 -// hand, otherwise any other slot that does, or -1 if there is none. When weapons stack, a copy of the weapon in hand -// is another weapon; otherwise it's the same one. -int Fighter::off_slot_get() +// Dual Wield's off-hand weapon slots, one per copy, written to slots: the other slots holding weapons, the ones most +// recently switched away from first, then any never switched to, in slot order. Returns how many. +int Fighter::off_slots_get(int* slots) { int held = weapon_key(slot); - int fallback = -1; + int count = 0; for (int i = 0; i < weaponSlots; i++) - { - if (weaponLoadout[i] < 0 || weapon_key(i) == held) - continue; - if (i == offSlot) - return i; - fallback = fallback < 0 ? i : fallback; - } - return fallback; + if (weaponLoadout[i] >= 0 && weapon_key(i) != held) + slots[count++] = i; + std::stable_sort(slots, slots + count, [&](int a, int b) { return slotSwitches[a] > slotSwitches[b]; }); + return std::min(count, trinket_count(TRINKET_DUAL)); } -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(); - return off < 0 ? -1 : weaponLoadout[off]; + if (hand == 0) + 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. @@ -863,15 +877,17 @@ Vec2 Fighter::center_get() 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; -} - -// 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; + 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; } // Full charge pulses white on and off, shared by the charge bar and the held weapon. diff --git a/src/fighter.h b/src/fighter.h index cd20783..ff8fe14 100644 --- a/src/fighter.h +++ b/src/fighter.h @@ -77,8 +77,10 @@ public: float contactCooldown = 0; int index = 0; int launcher = 0; - int offSlot = -1; 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 trinketSlotLevels[SLOT_MAX] = {}; int trinketLoadout[SLOT_MAX]; @@ -105,13 +107,14 @@ public: void update(Sim& sim); void blend(float alpha); void impulse_add(Vec2 impulse, bool isDamped = true); + void knock_take(Vec2 knock, int owner); void teleport_to(Vec2 centre); void burst_add(Vec2 impulse); void charge_cancel(Sim& sim); int weapon_get(); int weapon_key(int weaponSlot); - int off_slot_get(); - int off_weapon_get(); + int off_slots_get(int* slots); + int hand_slot_get(int hand); int slot_level_get(int weaponSlot); const LevelDef& level_get(int weaponSlot); void item_set(bool isTrinket, int itemSlot, int item); @@ -125,8 +128,7 @@ public: float knock_absorb_get(); float pound_area_get(); Vec2 center_get(); - Vec2 hand_get(); - Vec2 offhand_get(); + Vec2 hand_get(int hand = 0); bool is_any_pressed(); bool is_down(Control control); bool is_pressed(Control control); diff --git a/src/menu.cpp b/src/menu.cpp index 00af937..6b6add7 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -179,7 +179,7 @@ void Menu::update(Game& game) isPinnedTrinket = isTrinket; game.audio.play(SOUND_SELECT); } - if (isRight && kind == tab && isChoosable) + if (isRight && is_choosable(game, isTrinket)) 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 // 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) { Renderer& renderer = game.renderer; @@ -432,6 +433,23 @@ void Menu::items_draw(Game& game, MenuLayout& layout) .isLit = i == selection, .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; 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(layout.divider, ui.tint); - // The description is of the selection, or of a hotbar slot clicked on this tab while it still holds something. - Fighter& fighter = game.local_get(); + // The description is of the selection, or of a hotbar slot clicked (or focused) on this tab while it still holds + // something. bool isTrinket = tab == TAB_TRINKETS; int item = selections[tab]; int level = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[item]; @@ -463,6 +481,8 @@ void Menu::items_draw(Game& game, MenuLayout& layout) description.scroll = 0; described = shown; Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level); + if (slotLevel) + tooltip.slot = pinnedSlot; Rect area = layout.description; bool isLevelable = game.sim.match.state == MATCH_PRACTICE; if (isLevelable) @@ -1043,18 +1063,30 @@ bool Menu::is_choosable(Game& game, bool isTrinket) 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, -// both sit under the menu either side of its middle. +// Where a hotbar slot sits: closed, the weapon bar is top left and the trinket bar bottom left, each as large as fits +// 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) { - float step = SLOT_SIZE + SLOT_GAP; - Vec2 position = {HOTBAR_MARGIN + index * step, - isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - SLOT_SIZE : HOTBAR_MARGIN}; + IconSize size = slot_size_get(count); + float slot = SLOT_SIZES[size]; + float step = slot + SLOT_GAPS[size]; + Vec2 position = {HOTBAR_MARGIN + index * step, isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - slot : HOTBAR_MARGIN}; if (is_bars_open()) position = {isTrinket ? CANVAS_WIDTH / 2.0f + SLOT_GAP + index * step : CANVAS_WIDTH / 2.0f - SLOT_GAP - (count - index) * step + SLOT_GAP, 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 diff --git a/src/menu.h b/src/menu.h index ad87f02..05ced7d 100644 --- a/src/menu.h +++ b/src/menu.h @@ -74,6 +74,7 @@ public: void target_fill(Game& game); bool is_bars_open(); Rect slot_rect(bool isTrinket, int index, int count); + IconSize slot_size_get(int count); bool is_choosable(Game& game, bool isTrinket); int target_get(Game& game, bool isTrinket); diff --git a/src/renderer.cpp b/src/renderer.cpp index e5931a9..4fe7505 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -238,6 +238,11 @@ bool Renderer::init(SDL_Window* target) gl.GenFramebuffers(1, &canvasFramebuffer); gl.BindFramebuffer(GL_FRAMEBUFFER, canvasFramebuffer); 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.GenRenderbuffers(1, &upscaled); gl.GenFramebuffers(1, &upscaledFramebuffer); @@ -410,11 +415,12 @@ void Renderer::upscaled_resize(int 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() { - gl.BindFramebuffer(GL_FRAMEBUFFER, canvasFramebuffer); - if (!isCanvasCleared) + gl.BindFramebuffer(GL_FRAMEBUFFER, framebuffer); + if (framebuffer == canvasFramebuffer && !isCanvasCleared) { gl.ClearColor(BACKGROUND.r, BACKGROUND.g, BACKGROUND.b, BACKGROUND.a); gl.Clear(GL_COLOR_BUFFER_BIT); @@ -425,6 +431,36 @@ void Renderer::flush() 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 // (whose rows count from the bottom) and swaps. void Renderer::present() diff --git a/src/renderer.h b/src/renderer.h index f6ceb7d..65872d9 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -63,6 +63,8 @@ public: void line_draw(Vec2 from, Vec2 to, float width, Color color); void circle_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 vsync_set(bool isVsync); Rect letterbox_get(float width, float height); @@ -75,6 +77,9 @@ private: unsigned int atlas = 0; unsigned int canvas = 0; unsigned int canvasFramebuffer = 0; + unsigned int layer = 0; + unsigned int layerFramebuffer = 0; + unsigned int framebuffer = 0; unsigned int upscaled = 0; unsigned int upscaledFramebuffer = 0; int shelfHeight = 0; diff --git a/src/scene.cpp b/src/scene.cpp index e91bbe1..5eb73dd 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -26,13 +26,12 @@ constexpr const char* CRAWL_ANIMATIONS[] = {"Crawl Right", "Crawl Left"}; constexpr float POUND_RING_ALPHA = 0.25f; 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 float BEAM_INNER = 4; constexpr float BEAM_OUTER = 10; -constexpr float BOLT_JITTER = 4; +constexpr float BOLT_JITTER = 2; constexpr float BOLT_SEGMENT = 8; -constexpr float BOLT_WIDTH = 2; +constexpr float BOLT_WIDTH = 1; constexpr float BOOMERANG_SPIN = 20; constexpr float CHAIN_SPACING = 8; // 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); } +// 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 // 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; - float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1); - float t = std::clamp((1 - cooldown / total - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f); + float t = std::clamp((1 - cooldown_left_get(fighter, key) - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f); float weight = math_sin(PI * t); draw.angle = angle_lerp(draw.angle, -PI / 2, weight); 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, -// then once the charge is ready blinking between full and the speed bonus alone. -static float charge_fill_get(Game& game, Fighter& fighter) +// How far a fighter's weapons (and crosshair) fill in with its colour: with the charge as it builds, or the speed bonus +// if that's more, then once the charge is ready blinking between full and the speed bonus alone. +float Scene::charge_fill_get(Fighter& fighter) { float buff = game.sim.weapons.buff_get(fighter); 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); } +// 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) { 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)); } +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) { if (!isMuted) @@ -674,7 +693,7 @@ void Scene::attacks_draw() if (kind == ATTACK_HOOK) { // 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); 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. bool isFlying = attack.velocity.x != 0 || attack.velocity.y != 0; 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}; 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 -// charges (see charge_fill_get) and shakes harder the further the charge has got. A charging mine weapon holds up the -// mine it's about to place instead. +// charges (see charge_fill_get) and shakes with its charge and cooldown (see weapon_shake_get). A charging mine weapon +// holds up the mine it's about to place instead. void Scene::held_draw(Fighter& fighter) { 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; float side = fighter.is_left() ? -1 : 1; Draw draw = { .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}, .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) { @@ -928,36 +947,38 @@ void Scene::held_draw(Fighter& fighter) game.sprites.effects.draw(game.renderer, "Mine", 0, draw); return; } - reload_pose(fighter, held, fighter.cooldowns[held], draw); - hand_draw(fighter, held, false, draw); + reload_pose(fighter, held, 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) { - int off = fighter.off_slot_get(); - 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; - + int offs[SLOT_MAX]; float side = fighter.is_left() ? -1 : 1; - Draw draw = { - .color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE), - .position = fighter.offhand_get() + jitter_get(fighter.charge * CHARGE_SHAKE), - .scale = {1, side}, - .angle = vec2_angle(fighter.aim), - .flash = charge_fill_get(game, fighter), - }; - reload_pose(fighter, off, fighter.cooldowns[off], draw); - hand_draw(fighter, off, true, draw); + 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 = { + .color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE), + .position = fighter.hand_get(hand) + weapon_shake_get(fighter, off), + .scale = {1, side}, + .angle = vec2_angle(fighter.aim), + .flash = charge_fill_get(fighter), + }; + reload_pose(fighter, off, 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 (" Base"), pointing // 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; if (tether) { diff --git a/src/scene.h b/src/scene.h index 9d5af2b..10ed19c 100644 --- a/src/scene.h +++ b/src/scene.h @@ -17,6 +17,8 @@ public: void update(); void draw(); 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 charge_play(Fighter& fighter) override; @@ -25,6 +27,7 @@ public: void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) override; void shake_add(float amount) 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 attack_afterimage_spawn(Attack& attack) override; void fighter_aura_spawn(Fighter& fighter) override; @@ -55,5 +58,5 @@ private: Pose attack_pose(Attack& attack); void held_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); }; diff --git a/src/settings.h b/src/settings.h index 8b0276a..76bbba7 100644 --- a/src/settings.h +++ b/src/settings.h @@ -42,11 +42,11 @@ public: int weaponSlots = 2; bool isAimShown = true; bool isCollisionShown = false; - bool isDpsShown = true; + bool isDpsShown = false; bool isFullscreen = false; bool isMuted = false; bool isShaking = true; - bool isSpeedShown = true; + bool isSpeedShown = false; bool isVsync = true; // The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded. char serverUrl[SERVER_URL_LENGTH] = {}; diff --git a/src/sim.h b/src/sim.h index a8c2ff2..37abb29 100644 --- a/src/sim.h +++ b/src/sim.h @@ -23,6 +23,8 @@ public: {} 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 attack_afterimage_spawn(Attack& attack) diff --git a/src/sprites.cpp b/src/sprites.cpp index 8935188..f59b1bc 100644 --- a/src/sprites.cpp +++ b/src/sprites.cpp @@ -7,13 +7,9 @@ void Sprites::init(Game& game) dummy.load(renderer, "dummy"); effects.load(renderer, "effects"); icons.load(renderer, "weapon_icons"); - iconsSmall.load(renderer, "weapon_icons_small"); - iconsTiny.load(renderer, "weapon_icons_tiny"); player.load(renderer, "player"); tiles.load(renderer, "tiles"); trinkets.load(renderer, "trinkets"); - trinketsSmall.load(renderer, "trinkets_small"); - trinketsTiny.load(renderer, "trinkets_tiny"); modes.load(renderer, "modes"); ui.load(renderer, "ui"); weapons.load(renderer, "weapons"); diff --git a/src/sprites.h b/src/sprites.h index d02027c..a46b55d 100644 --- a/src/sprites.h +++ b/src/sprites.h @@ -12,15 +12,12 @@ class Sprites public: Anm2 dummy; 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 iconsSmall; - Anm2 iconsTiny; Anm2 player; Anm2 tiles; Anm2 trinkets; - Anm2 trinketsSmall; - Anm2 trinketsTiny; // A game mode's icon, by its name (Deathmatch, Race). Anm2 modes; Anm2 ui; diff --git a/src/state.cpp b/src/state.cpp index d656122..26df114 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -119,6 +119,7 @@ template static void state_walk(Stream& stream, Sim stream.list(sim.weapons.attacks); World& world = sim.world; stream.list(world.dummies); + stream.list(world.ledges); stream.list(world.obstacles); stream.list(world.ramps); stream.list(world.tiles); diff --git a/src/text.cpp b/src/text.cpp index df1bee9..1c79bfa 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -7,6 +7,43 @@ constexpr const char* FONT_FOLDER = "font/"; constexpr const char* FONT_NAMES[FONT_COUNT] = {"tiny", "small", "big", "huge"}; 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) { @@ -54,10 +91,9 @@ void Text::load(Renderer& renderer, Font font, const char* name) for (auto [key, value] : glyphs) { simdjson::dom::object fields; - if (key.size() != 1 || value.get(fields)) - continue; - int index = key[0] - GLYPH_FIRST; - if (index < 0 || index >= GLYPH_COUNT) + const char* character = key.data(); + int index = key.empty() ? -1 : glyph_next(character); + if (index < 0 || character != key.data() + key.size() - 1 || value.get(fields)) continue; atlas.glyphs[index] = { .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; } - int index = *character - GLYPH_FIRST; - if (index < 0 || index >= GLYPH_COUNT) + int index = glyph_next(character); + if (index < 0) continue; 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++) { - int index = *character - GLYPH_FIRST; + int index = *character == '\n' ? -1 : glyph_next(character); if (*character == '\n') { lineWidth = 0; size.y += atlas.lineHeight; } - else if (index >= 0 && index < GLYPH_COUNT) + else if (index >= 0) lineWidth += atlas.glyphs[index].advance; size.x = std::max(size.x, lineWidth); } diff --git a/src/text.h b/src/text.h index 88bcc23..9a79c54 100644 --- a/src/text.h +++ b/src/text.h @@ -2,8 +2,11 @@ #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_TYPED_END = 127; // The small font's line height (small.fnt's), which the HUD is laid out by. constexpr float LINE_HEIGHT = 12; diff --git a/src/trinket.h b/src/trinket.h index 7f597e6..24bde3f 100644 --- a/src/trinket.h +++ b/src/trinket.h @@ -71,8 +71,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = { "A noble spirit embiggens the\nsmallest projectile."}, {"Diazepam", "Steadies your aim: every attack,\nvolley and Splitter copy spreads less.", - {"33% Less Spread", "66% Less Spread", "No Spread"}, - {0.33f, 0.66f, 1}, + {"17% Less Spread", "33% Less Spread", "50% Less Spread"}, + {1 / 6.0f, 1 / 3.0f, 0.5f}, "Diazepam?\nPsychotropic drug?\nStops hands from trembling?\nQuestions?", 1}, {"Splitter", @@ -86,7 +86,7 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = { {0.1f, 0.25f, 1 / 3.0f}, "Stud Multiplier"}, {"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"}, {0.1f, 0.25f, 1 / 3.0f}, "Guns akimbo!"}, @@ -99,20 +99,20 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = { "RICOCHET 2 BABY!"}, {"Tesla Coil", "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"}, - {4, 7, 10}, + {"2 Damage Shocks\n120 Arc Range", "4 Damage Shocks\n170 Arc Range", "6 Damage Shocks\n220 Arc Range"}, + {2, 4, 6}, "Okay this is just Technology Zero."}, {"Recoil Coil", "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"}, - {12, 18, 24}, + {"6 Recoil Power", "9 Recoil Power", "12 Recoil Power"}, + {6, 9, 12}, "The forgotten sibling of Gravity\nand Speed..."}, {"Shock Absorber", "Your own weapons move you around\nless (recoil, kickback, lunges,\nrebounds, pulls and blasts), and\nyou take " "less knockback.", - {"25% Less Weapon Movement\n17% Less Knockback Taken", "50% Less Weapon Movement\n33% Less Knockback Taken", - "75% Less Weapon Movement\n50% Less Knockback Taken"}, - {0.75f, 0.5f, 0.25f}, + {"12.5% Less Weapon Movement\n8% Less Knockback Taken", "25% Less Weapon Movement\n17% Less Knockback Taken", + "37.5% Less Weapon Movement\n25% Less Knockback Taken"}, + {0.875f, 0.75f, 0.625f}, "The mechanical kind, not the\nelectrical kind."}, {"Shrinker", "Makes you smaller.", @@ -147,8 +147,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = { "Probably given to you from\nEnergy Drink."}, {"Feather", "Hold Up in the air to float.", - {"0.6x Gravity Holding Up", "0.45x Gravity Holding Up", "0.3x Gravity Holding Up"}, - {0.6f, 0.45f, 0.3f}, + {"0.75x Gravity Holding Up", "0.625x Gravity Holding Up", "0.5x Gravity Holding Up"}, + {0.75f, 0.625f, 0.5f}, "Cape not included."}, {"Iron Weights", "Fall even faster holding Down.\nLanding while holding Down slams\nthe ground, harder the faster you\nhit it.", diff --git a/src/ui.cpp b/src/ui.cpp index 240fd07..475e1ec 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3,7 +3,6 @@ #include #include -constexpr int CHAMFER = 2; constexpr float BANNER_SCALE = 4; constexpr float BANNER_Y = 110; constexpr float BAR_GAP = 2; @@ -19,11 +18,17 @@ constexpr float RIVAL_OUT_ALPHA = 0.25f; constexpr float SPECTATE_GAP = 4; constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK}; constexpr float HUD_IDLE_ALPHA = 0.5f; +constexpr float HUD_HOVER_ALPHA = 0.5f; constexpr float HUD_HOVER_FADE = 0.15f; constexpr float RIVAL_PADDING = 2; constexpr int RIVAL_COLUMNS_MAX = 3; -constexpr int RIVAL_FULL_SLOTS = 4; -constexpr int RIVALS_PER_ROW = 4; +// What the other fighters' windows try, from largest to smallest, until they all fit in a couple of rows: shrinking +// 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 MINI_ICON_SCALE = 0.5f; constexpr float MINI_SLOT = 16; @@ -36,8 +41,10 @@ constexpr float READY_GAP = 2; constexpr float TIP_GAP = 4; constexpr float COUNTDOWN_Y = 40; constexpr float COUNTDOWN_DIM = 0.6f; -constexpr float CHEVRON_INSET = 3; -constexpr float CHEVRON_STEP = 3; +constexpr const char* ICON_SUFFIXES[ICON_SIZE_COUNT] = {"", " Small", " Tiny"}; +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_WIDTH = 20; 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_WIDTH = 80; 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_GAP = 1; constexpr float TARGET_GAP = 1; constexpr float TARGET_HEIGHT = 2; constexpr float TARGET_INSET = 4; 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_OUT = 0.25f; constexpr float COOLDOWN_GAP = 4; -constexpr float COOLDOWN_SPACING = 26; -constexpr float COOLDOWN_COMPACT_SPACING = 14; +constexpr float COOLDOWN_ROW_GAP = 2; constexpr float COOLDOWN_SLIDE = 12; -constexpr int COOLDOWN_FULL_ROWS = 5; constexpr float COOLDOWN_VISIBLE = 0.25f; constexpr float OFFSCREEN_ARROW_SCALE = 2; constexpr float OFFSCREEN_MARGIN = 8; @@ -89,7 +95,7 @@ constexpr float PREVIEW_CAPTION_GAP = 8; constexpr float PREVIEW_FEET = 120; constexpr float PREVIEW_NAME_Y = 16; 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 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 @@ -114,7 +120,12 @@ constexpr float TRACK_DOT = 6; constexpr float TRACK_HEIGHT = 2; constexpr float TRACK_WIDTH = 200; 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[] = { "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); +// 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) { 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 // several players on this screen in a match, each gets a compact panel along the bottom instead. + statusRect = {}; queues_draw(game); bars_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; if (!isBarsRaised && !isShared && !isWatching) { - hotbar_draw(game); cooldowns_draw(game); + hotbar_draw(game); } if (isShared) 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. -void Ui::bar_draw(Renderer& renderer, Rect bar, float fill, Color color) +// 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, float stroke) { - panel_draw(renderer, bar, color); - renderer.rect_draw({bar.x + PANEL_BORDER, bar.y + PANEL_BORDER, std::round((bar.w - PANEL_BORDER * 2) * fill), - bar.h - PANEL_BORDER * 2}, + panel_draw(renderer, bar, color, -1, stroke); + renderer.rect_draw({bar.x + stroke, bar.y + stroke, std::round((bar.w - stroke * 2) * fill), bar.h - stroke * 2}, 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]); Vec2 scale = {SCORE_SCALE, SCORE_SCALE}; 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 center = x + scoreWidth / 2; for (int i = 0; i < game.sim.fighterCount; i++) @@ -598,6 +617,7 @@ LoadoutLayout Ui::loadout_layout(Game& game, float top, float bottom) continue; int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[i]; Tooltip& tooltip = layout.tooltips[kind][i] = tooltip_build(game, isTrinket, item, level); + tooltip.slot = i; tooltip.flavor[0] = '\0'; tooltip.rowCount = 0; if (isTrinket && !isBrief) @@ -702,10 +722,10 @@ void Ui::loadout_draw(Game& game, float alpha, float top) tooltip_text_draw(game, layout.tooltips[kind][i], {.color = color, .position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}}, font); - icon_draw(game, isTrinket, item, - {.color = color, - .position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2, - panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}}); + tooltip_icon_draw(game, layout.tooltips[kind][i], + {.color = color, + .position = {panel.x + panel.w - 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]; float right = CANVAS_WIDTH - HOTBAR_MARGIN; + float left = right; float y = HOTBAR_MARGIN; 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()) { @@ -809,17 +831,16 @@ void Ui::status_draw(Game& game) 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))); float width = game.text.size_get(text, FONT_SMALL, DPS_SCALE).x; - game.text.draw( - game.renderer, text, - {.color = tint, .position = {std::round(bar.x - STATUS_GAP - width), y}, .scale = {DPS_SCALE, DPS_SCALE}}); + left = std::round(bar.x - STATUS_GAP - width); + game.text.draw(game.renderer, text, {.color = tint, .position = {left, y}, .scale = {DPS_SCALE, DPS_SCALE}}); y += rowHeight; } // The DPS and speed meters each show only if they're on in the settings. auto meter_draw = [&](const char* label) { - float width = game.text.size_get(label, FONT_SMALL, DPS_SCALE).x; - game.text.draw(game.renderer, label, - {.color = tint, .position = {std::round(right - width), y}, .scale = {DPS_SCALE, DPS_SCALE}}); + float x = std::round(right - game.text.size_get(label, FONT_SMALL, DPS_SCALE).x); + left = std::min(left, x); + game.text.draw(game.renderer, label, {.color = tint, .position = {x, y}, .scale = {DPS_SCALE, DPS_SCALE}}); y += rowHeight; }; 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)); 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 -// 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) { Vec2 mouse = game.input.mouse; @@ -848,21 +871,22 @@ void Ui::hotbar_draw(Game& game) int index = isTrinket ? i - fighter.weaponSlots : i; int count = isTrinket ? fighter.trinketSlots : fighter.weaponSlots; 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 = { .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, .level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[index], .label = index + 1, .isHoverable = true, .isLit = isTrinket ? item >= 0 : index == fighter.slot, .isTrinket = isTrinket, - .isOpaque = true, .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); + 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)) hovered = slot; } @@ -870,48 +894,51 @@ void Ui::hotbar_draw(Game& game) if (hovered.item < 0 || game.menu.isOpen) return; Tooltip tooltip = tooltip_build(game, hovered.isTrinket, hovered.item, hovered.level); + tooltip.slot = hovered.label - 1; 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) { Renderer& renderer = game.renderer; 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 = slot.isOpaque ? color_shade(base, slot.alpha) : color_fade(base, slot.alpha); - float fill = slot.isOpaque ? 1 : slot.alpha; - panel_draw(renderer, slot.rect, color, fill); + Color color = color_fade(slot.isLit || isHovered || slot.isTarget ? tint : shade, slot.alpha); + panel_draw(renderer, slot.rect, color, -1, SLOT_STROKES[slot.size]); if (slot.isTarget) renderer.rect_draw({slot.rect.x + TARGET_INSET, slot.rect.y + slot.rect.h + TARGET_GAP, slot.rect.w - TARGET_INSET * 2, TARGET_HEIGHT}, color); - if (isHovered) + if (isHovered && slot.size == ICON_FULL) { Rect rect = slot.rect; float inset = PANEL_BORDER + HOVER_GAP; chamfer_draw(renderer, {rect.x + inset, rect.y + inset, rect.w - inset * 2, rect.h - inset * 2}, color); inset += HOVER_BORDER; 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) { 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++) - game.sprites.ui.draw(renderer, "Chevron", 0, - {.color = color, .position = {corner.x, corner.y - i * CHEVRON_STEP}}); + game.sprites.ui.draw(renderer, CHEVRONS[slot.size], 0, + {.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; char label[] = {char('0' + slot.label), 0}; - game.text.draw(renderer, label, - {.color = color, .position = {slot.rect.x + LABEL_OFFSET.x, slot.rect.y + LABEL_OFFSET.y}}); + Vec2 offset = LABEL_OFFSETS[slot.size]; + 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. @@ -920,6 +947,9 @@ static void number_format(char* text, int size, float value) 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. static void stat_format(char* text, int size, int row, const AttackSummary& summary, const AttackDef& attack, bool isCharged) @@ -976,36 +1006,42 @@ static void stat_format(char* text, int size, int row, const AttackSummary& summ if (summary.spread > 0) { number_format(number, sizeof(number), summary.spread); - SDL_snprintf(text, size, "%s deg", number); + SDL_snprintf(text, size, "%s°", number); } break; - case 8: + case LUNGE_ROW: if (summary.lunge != 0) - SDL_snprintf(text, size, summary.lunge > 0 ? "%g forward" : "%g back", - std::round(std::abs(summary.lunge))); + SDL_snprintf(text, size, "%g", std::round(std::abs(summary.lunge))); break; } } // 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 -// 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) { constexpr const char* LABELS[] = {"Fires", "Damage", "Every", "Range", "Blast", "Speed", "Out at once", "Spread", "Lunge"}; 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; for (int row = 0; row < (int)std::size(LABELS); row++) { StatRow& stat = tooltip.rows[tooltip.rowCount]; - stat = {.label = LABELS[row]}; + stat = {.label = row == LUNGE_ROW && isRecoil ? "Recoil" : LABELS[row]}; bool isShown = false; for (int column = 0; column < 2; column++) { bool isCharged = column == 1; 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]; stat_format(value, STAT_LENGTH, row, summary, attack, isCharged); bool changes[] = {summary.isPiecesChanged, @@ -1021,10 +1057,9 @@ void Ui::stats_table_build(Fighter& fighter, int weapon, int level, bool isPrevi if (isPreview && *value) { char next[STAT_LENGTH]; - stat_format(next, sizeof(next), row, attack_summary_get(fighter, weapon, level + 1, isCharged), attack, - isCharged); + stat_format(next, sizeof(next), row, nexts[column], attack, isCharged); 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'; } @@ -1074,7 +1109,7 @@ void Ui::table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, cons 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 // 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) @@ -1126,12 +1161,12 @@ static void effects_preview_get(Game& game, const char* now, const char* next, c else if (isNumbers) for (size_t w = 0; w < wordsBefore.size(); w++) 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()) - line = "> " + after; + line = "→ " + after; else { - line = before + " > " + after; + line = before + " → " + after; if (game.text.size_get(line.c_str()).x > PREVIEW_LINE_MAX) 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.item = item; 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) { @@ -1159,7 +1199,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o else SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped)); 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; char effects[TOOLTIP_LENGTH]; if (isPreview) @@ -1167,7 +1207,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o else SDL_strlcpy(effects, def.effects[level], sizeof(effects)); 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); else 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]; 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; - 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); stats_table_build(fighter, item, level, isPreview, tooltip); SDL_snprintf(tooltip.description, sizeof(tooltip.description), "\n%s\n\n%s", def.blurb, def.chargeBlurb); @@ -1237,10 +1277,11 @@ void Ui::tooltip_page_draw(Game& game, Page& page, Rect area, Tooltip& tooltip) page_begin(game, page, area); float iconY = area.y - page.scroll; if (tooltip.item >= 0 && iconY >= area.y) - icon_draw(game, tooltip.isTrinket, tooltip.item, - {.color = tint, - .position = {area.x + area.w - (isScrolling ? SCROLL_WIDTH + TOOLTIP_PADDING : 0) - ICON_SIZE / 2, - iconY + ICON_SIZE / 2}}); + tooltip_icon_draw( + game, tooltip, + {.color = tint, + .position = {area.x + area.w - (isScrolling ? SCROLL_WIDTH + TOOLTIP_PADDING : 0) - ICON_SIZE / 2, + iconY + ICON_SIZE / 2}}); auto lines = [&](const char* text, Color color) { for (const char* start = text;;) { @@ -1293,32 +1334,43 @@ void Ui::tooltip_draw(Game& game, Tooltip& tooltip) panel_draw(game.renderer, panel, tint); tooltip_text_draw(game, tooltip, {.position = {panel.x + TOOLTIP_PADDING, panel.y + TOOLTIP_PADDING}}); if (tooltip.item >= 0) - icon_draw(game, tooltip.isTrinket, tooltip.item, - {.color = tint, - .position = {panel.x + panel.w - TOOLTIP_PADDING - ICON_SIZE / 2, - panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}}); + tooltip_icon_draw(game, tooltip, + {.color = tint, + .position = {panel.x + panel.w - 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 -// alpha of its own. -void Ui::panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha) +// A tooltip's item icon, centred on the placement, with the hotbar slot it's in numbered over its top left corner as on +// the hotbar. +void Ui::tooltip_icon_draw(Game& game, Tooltip& tooltip, Draw placement) { - chamfer_draw(renderer, rect, border); - chamfer_draw(renderer, - {rect.x + PANEL_BORDER, rect.y + PANEL_BORDER, rect.w - PANEL_BORDER * 2, rect.h - PANEL_BORDER * 2}, - color_fade(BLACK, fillAlpha >= 0 ? fillAlpha : border.a)); + icon_draw(game, tooltip.isTrinket, tooltip.item, placement); + if (tooltip.slot < 0) + return; + 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. -void Ui::chamfer_draw(Renderer& renderer, Rect rect, Color color) +// Black box with a cut-cornered edge stroke thick in the given color (its corners cut as deep). The fill fades with +// 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) +{ + for (int step = 0; step < cut; step++) { - float inset = CHAMFER - 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 + 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) @@ -1496,7 +1548,7 @@ bool Ui::field_draw(Game& game, Rect box, char* text, int size) text[--length] = '\0'; 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; text[length++] = *character; 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, {.color = color, .position = {std::round(centre - nameSize.x / 2), area.y + PREVIEW_NAME_Y}}); Anm2& player = game.sprites.player; - Anm2Animation* walk = player.animation_get(PREVIEW_ANIMATION); - float frame = walk ? std::fmod(SDL_GetTicks() / 1000.0f * player.fps, (float)walk->length) : 0; + Anm2Animation* preview = player.animation_get(PREVIEW_ANIMATION); + float frame = preview ? std::fmod(SDL_GetTicks() / 1000.0f * player.fps, (float)preview->length) : 0; player.draw(game.renderer, PREVIEW_ANIMATION, frame, {.color = color, .position = {centre, feet}, .scale = {PREVIEW_SCALE, PREVIEW_SCALE}}); 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_GAP = 1; 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 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}}; @@ -1704,37 +1761,48 @@ static std::string ping_text_get(float ping) float Ui::ping_width_get(Game& game, float ping) { std::string text = ping_text_get(ping); - float bars = PING_BARS * (PING_BAR_WIDTH + PING_BAR_GAP) - PING_BAR_GAP; - return bars + (text.empty() ? 0 : PING_BAR_GAP * 2 + game.text.size_get(text.c_str()).x); + return PING_BARS_WIDTH + (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) { - int lit = 0; - if (ping >= 0) - { - 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; - } + ping_bars_draw(game, {corner.x, corner.y + LINE_HEIGHT - PING_BARS_DROP}, ping, alpha); + int lit = ping_lit_get(ping); std::string text = ping_text_get(ping); if (!text.empty()) 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); } +// 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 // slots, how items are handed out, and how it's won. The "game tooltip". static std::vector game_summary_lines(const Rules& rules) @@ -1887,8 +1955,7 @@ void Ui::update(Game& game) hintAlpha = std::max(0.0f, hintAlpha - TICK / HINT_FADE); spectate_update(game); - // The HUD's hotbar slots and the other fighters' windows brighten while the mouse is over them, and dim again - // after. + // The HUD's elements change while the mouse is over them, and change back after. Vec2 mouse = game.input.mouse; auto hover_step = [&](float& hover, bool isHovered) { 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; 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 // 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); } -// Every item cooling down shows at once, each on its own bar two slots long (whatever the hotbar's size) with its icon. -// Every bar cooling down, a row each: weapons downward from under the weapon hotbar, trinkets upward from over the -// trinket hotbar, each starting far enough off its hotbar that the icons (taller than the bars) clear its slots. Past a -// handful of one kind at once, its rows tighten and their icons shrink to half size so a full loadout still fits. +// Every item cooling down shows at once, a row each: weapons downward from under the weapon hotbar, trinkets upward +// from over the trinket hotbar. A kind's bars are two of its slots long, with its icon just right at its hotbar's +// size, and start far enough off the hotbar that the icons (taller than the bars) clear its slots. Each drains with +// its cooldown and fades with its bar. 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++) { 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; - 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++) { CooldownBar& bar = bars[slot]; if (bar.fade <= 0 || bar.row < 0 || bar.item < 0) continue; float step = std::round(bar.row * spacing); - float y = isTrinket ? CANVAS_HEIGHT - HOTBAR_MARGIN - SLOT_SIZE - COOLDOWN_GAP - COOLDOWN_BAR_HEIGHT - - std::ceil(clearance) - step - : HOTBAR_MARGIN + SLOT_SIZE + COOLDOWN_GAP + std::ceil(clearance) + step; - cooldown_bar_draw(game, {HOTBAR_MARGIN, y, width, COOLDOWN_BAR_HEIGHT}, bar, isTrinket, bar.item, - iconScale); + Rect rect = {HOTBAR_MARGIN, isTrinket ? CANVAS_HEIGHT - start - height - step : start + step, width, + height}; + float middle = rect.y + height / 2; + bar.rect = {rect.x, middle - spacing / 2, width + COOLDOWN_GAP + icon, spacing}; + 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. void Ui::icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSize size) { - Sprites& sprites = game.sprites; - Anm2* sheets[2][ICON_SIZE_COUNT] = {{&sprites.icons, &sprites.iconsSmall, &sprites.iconsTiny}, - {&sprites.trinkets, &sprites.trinketsSmall, &sprites.trinketsTiny}}; - Anm2& art = *sheets[isTrinket][size]; - art.draw(game.renderer, isTrinket ? TRINKET_DEFS[item].name : WEAPON_DEFS[item].name, 0, placement); + char name[TOOLTIP_LENGTH]; + SDL_snprintf(name, sizeof(name), "%s%s", isTrinket ? TRINKET_DEFS[item].name : WEAPON_DEFS[item].name, + ICON_SUFFIXES[size]); + (isTrinket ? game.sprites.trinkets : game.sprites.icons).draw(game.renderer, 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 @@ -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 -// open), otherwise where its stick aims. With none (a bot playing), the mouse's. +// Each of this screen's fighters' crosshairs in its colour, filling in and shaking with its weapon's charge and +// 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) { Input& input = game.input; @@ -2039,9 +2104,13 @@ void Ui::crosshairs_draw(Game& game, Vec2 camera) isAny = true; bool isMouse = (game.devices[i] == DEVICE_KEYBOARD && !input.devices[DEVICE_KEYBOARD].isAiming) || (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, - {.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) 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); } -// Lays out the other fighters' windows in rows of up to four along the bottom right (more rows stacking upwards, as -// even as they go), never left of the screen's middle (nor over the local hotbar and cooldowns). A window is as wide as -// 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 -// 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. +// A fighter's connection for its window, online (PING_NONE otherwise). +static float rival_ping_get(Game& game, int fighter) +{ + return game.session.isActive ? game.session.ping_get(game, game.sim.match.owners[fighter]) : PING_NONE; +} + +// 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 layout; - Fighter& local = game.local_get(); int slots[2] = {}; for (int i = 0; i < game.sim.fighterCount; i++) { @@ -2215,57 +2289,63 @@ RivalLayout Ui::rivals_layout(Game& game) } if (layout.count == 0) 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 left = - 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 top = statusRect.h > 0 ? statusRect.y + statusRect.h + RIVAL_GAP : HOTBAR_MARGIN; + float room = CANVAS_HEIGHT - HOTBAR_MARGIN - top; float inset = PANEL_BORDER + RIVAL_PADDING; - auto icons_width = [&](int size, int columns) { - return columns * (ICON_PIXELS[size] + RIVAL_ICON_GAP) + RIVAL_DIVIDER - RIVAL_ICON_GAP; - }; - float widthMax = icons_width(ICON_FULL, RIVAL_FULL_SLOTS) + inset * 2; + bool isRace = game.sim.match.is_racing(); char health[TOOLTIP_LENGTH]; 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; - 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 - // further to fit. - int rowsMin = (std::max(slots[0], slots[1]) + RIVAL_COLUMNS_MAX - 1) / RIVAL_COLUMNS_MAX; - int rowsMax = size == ICON_TINY ? SLOT_MAX : rowsMin; - for (int rows = std::max(1, rowsMin); rows <= rowsMax; rows++) + 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. + float cell = ICON_PIXELS[layout.size] + RIVAL_ICON_GAP; + int rowsMin = std::max(1, (std::max(slots[0], slots[1]) + RIVAL_COLUMNS_MAX - 1) / RIVAL_COLUMNS_MAX); + 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++) layout.columns[kind] = (slots[kind] + rows - 1) / rows; - float icons = icons_width(size, layout.columns[0] + layout.columns[1]) + inset * 2; - width = std::ceil(std::max(icons, minimum)); - layout.size = (IconSize)size; - layout.rows = rows; - bool isFit = icons <= widthMax && perRow * width + (perRow - 1) * RIVAL_GAP <= room; - if (isFit) - size = ICON_SIZE_COUNT; - if (size == ICON_SIZE_COUNT) - break; + float icons = layout.isIconShown + ? (layout.columns[0] + layout.columns[1]) * cell + RIVAL_DIVIDER - RIVAL_ICON_GAP + : 0; + width = std::ceil(std::max(icons, text) + inset * 2); + height = inset * 2 + layout.line + (isRace ? 0 : RIVAL_PADDING + layout.line) + + (layout.isIconShown ? RIVAL_PADDING + rows * cell - RIVAL_ICON_GAP : 0); + perRow = std::clamp((int)((HUD_COLUMN_WIDTH + RIVAL_GAP) / (width + RIVAL_GAP)), 1, layout.count); + int cardRows = (layout.count + perRow - 1) / perRow; + 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++) { int row = k / perRow; @@ -2277,10 +2357,11 @@ RivalLayout Ui::rivals_layout(Game& game) return layout; } -// Every other fighter's window in a match, bordered in its colour: its name centred, its health with a bar beside it, -// then its weapons, a divider, and its trinkets, the weapon in hand lit. A window is dimmed (a fighter out of the round -// more so) until the mouse comes over it, when it eases to full brightness. Hovering an -// icon shows the item's tooltip, as that fighter has it, in its colour. +// Every other fighter's window in a match, bordered in its colour: its name centred (cut short if it doesn't fit), its +// health (online with its connection's bars after), then (with room) its weapons, a divider, and its trinkets, the +// weapon in hand lit. Each is laid over the game as one piece, +// 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) { Match& match = game.sim.match; @@ -2299,41 +2380,63 @@ void Ui::rivals_draw(Game& game) int index = layout.fighters[k]; Fighter& fighter = game.sim.fighters[index]; Rect card = layout.cards[k]; - float idle = fighter.isAlive || match.is_racing() ? HUD_IDLE_ALPHA : RIVAL_OUT_ALPHA; - float alpha = index == spectated ? 1 : idle + (1 - idle) * rivalHovers[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); + float base = fighter.isAlive || match.is_racing() || index == spectated ? 1 : RIVAL_OUT_ALPHA; + Color color = game.color_get(index); 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; Font font = layout.font; - const char* name = name_get(game, index); - Vec2 size = game.text.size_get(name, font); - game.text.draw( - renderer, name, - {.color = color, - .position = {std::round(card.x + (card.w - size.x) / 2), std::round(y + (layout.line - size.y) / 2)}}, - font); + // The connection's bars go at the end of the health row, or beside the name in a race. + bool isRace = match.is_racing(); + float ping = rival_ping_get(game, index); + float pingWidth = ping == PING_NONE ? 0 : NAME_PING_GAP + PING_BARS_WIDTH; + float nameRoom = card.w - inset * 2 - (isRace ? pingWidth : 0); + std::string name = name_get(game, index); + 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; - if (!match.is_racing()) + // Its health as a bar, with the number before it when there's room. + if (!isRace) { char text[TOOLTIP_LENGTH]; SDL_snprintf(text, sizeof(text), "%d", (int)std::ceil(std::max(0.0f, fighter.health))); size = game.text.size_get(text, font); float x = card.x + inset; - game.text.draw(renderer, text, - {.color = color, .position = {x, std::round(y + (layout.line - size.y) / 2)}}, font); - x += size.x + RIVAL_PADDING; + float right = card.x + card.w - inset - pingWidth; + if (size.x + RIVAL_PADDING + RIVAL_BAR_MIN <= right - x) + { + game.text.draw(renderer, text, + {.color = color, .position = {x, std::round(y + (layout.line - size.y) / 2)}}, font); + x += size.x + RIVAL_PADDING; + } 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); + if (ping != PING_NONE) + ping_bars_draw(game, {right + NAME_PING_GAP, pingBottom + layout.line + RIVAL_PADDING}, ping, 1); y += layout.line + RIVAL_PADDING; } // 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 x = std::round(card.x + (card.w - iconsWidth) / 2); for (int kind = 0; kind < 2; kind++) @@ -2366,6 +2469,7 @@ void Ui::rivals_draw(Game& game) x += RIVAL_DIVIDER + RIVAL_ICON_GAP; } } + renderer.layer_end(base * hover_alpha_get(rivalHovers[index])); } 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 level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[hoveredSlot]; 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. Color colors[2] = {tint, shade}; tint = game.color_get(hoveredFighter); diff --git a/src/ui.h b/src/ui.h index cd421d3..02d7934 100644 --- a/src/ui.h +++ b/src/ui.h @@ -7,20 +7,25 @@ struct Game; +constexpr int CHAMFER = 2; constexpr int TOOLTIP_LENGTH = 512; constexpr int WEAPON_HANDS = 2; constexpr float HOTBAR_MARGIN = 8; +constexpr float PANEL_BORDER = 2; constexpr float SLOT_GAP = 4; constexpr float SLOT_SIZE = 28; constexpr float WIDGET_GAP = 6; 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 -// 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 { + Rect rect; float fade = 0; float fill = 0; + float hover = 0; float row = -1; int item = -1; }; @@ -48,9 +53,15 @@ enum IconSize ICON_SIZE_COUNT }; 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 -// 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 { int count = 0; @@ -61,6 +72,7 @@ struct RivalLayout int rows = 1; Font font = FONT_SMALL; float line = 0; + bool isIconShown = true; }; // What a match's countdown shows, gathered once for wherever it's drawn. @@ -80,9 +92,10 @@ struct Tooltip int rowCount = 0; char description[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; 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 @@ -141,21 +154,20 @@ struct FocusTarget bool isPreferred = false; }; -// One hotbar or menu box. Item is a weapon or trinket number, or -1 for empty, 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. +// 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, +// 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 { Rect rect; float alpha = 1; + IconSize size = ICON_FULL; int item = -1; int level = 0; int label = 0; bool isHoverable = false; bool isLit = 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. bool isTarget = false; }; @@ -185,13 +197,14 @@ public: void update(Game& game); void draw(Game& game); 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); 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, float alpha); 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_icon_draw(Game& game, Tooltip& tooltip, Draw placement); LoadoutLayout loadout_layout(Game& game, float top, float bottom); void tooltip_page_draw(Game& game, Page& page, Rect area, Tooltip& tooltip); 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); 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); + void ping_bars_draw(Game& game, Vec2 corner, float ping, float alpha); void queues_draw(Game& game); void queue_tooltip_draw(Game& game); float ping_width_get(Game& game, float ping); @@ -257,10 +271,12 @@ private: float hintAlpha = 1; // The switching controls as they were last tick, to catch presses while spectating. bool spectateHeld[3] = {}; - // How far each HUD hotbar slot (weapons, then trinkets) and each other fighter's window has come up from dim to - // full brightness, from 0 to 1, rising while the mouse is over it. + // How far the mouse being over each HUD element has changed it, from 0 to 1: hotbar slots (weapons, then trinkets) + // come up from see-through to solid, and the rest (other fighters' windows, the readouts) fade. + Rect statusRect; float slotHovers[2][SLOT_MAX] = {}; float rivalHovers[MAX_FIGHTERS] = {}; + float statusHover = 0; float loadoutTop = 0; float widgetAlpha = 1; int tip = 0; @@ -271,7 +287,7 @@ private: bool isWidgetLive = false; 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 loadout_draw(Game& game, float alpha, float top); 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 hotbar_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 offscreen_draw(Game& game, Vec2 camera); void status_draw(Game& game); @@ -294,7 +309,7 @@ private: void pick_send(Game& game, int fighter, bool isTrinket, int slot, int item); void crosshairs_draw(Game& game, Vec2 camera); 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); bool is_hovered(Game& game, Rect box); bool number_widget(Game& game, Rect box, float& value, NumberRange range, void* id); diff --git a/src/weapon.cpp b/src/weapon.cpp index 3cf3953..7ee2f58 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -1,6 +1,7 @@ #include "weapon.h" #include "sim.h" #include +#include constexpr int ATTACKS_MAX = 255; constexpr int DAMAGE_STEP = 5; @@ -8,12 +9,11 @@ constexpr int HEAVY_DAMAGE = 40; constexpr int HOOK_SUBSTEPS = 4; constexpr int TESLA_LINKS = 5; 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 DEGREE = PI / 180; constexpr float ARROW_LINGER = 2.5f; -constexpr float ARROW_SPAN = 18; -constexpr float ARROW_THICKNESS = 2; +constexpr float ARROW_RADIUS = 6; constexpr float BEAM_RADIUS = 4; constexpr float BOOMERANG_LIFETIME = 6; constexpr float BOOMERANG_RADIUS = 9; @@ -30,8 +30,6 @@ constexpr float TRAJECTORY_REACH = 400; constexpr float TRAJECTORY_STEP = 4; constexpr float FIRE_SHAKE = 0.5f; 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_MAX = 0.9f; 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 SLASH_DISTANCE = 12; 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 SPEED_DAMAGE = 0.05f; // 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. // 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 -// its damage. Every weapon (see Fighter::weapon_key) keeps its own cooldown, in either hand, so swapping to another -// weapon never inherits the last one's wait. +// add a repeat of their own, one after another). With Dual Wield on, an off-hand weapon per copy fires too (see +// Fighter::off_slots_get), each for a fraction of its damage. Every weapon (see Fighter::weapon_key) keeps its own +// 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) { 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(); - off = off < 0 ? -1 : fighter.weapon_key(off); - float dual = fighter.trinket_get(TRINKET_DUAL, 0); - if (dual > 0 && off >= 0 && off != held && fighter.cooldowns[off] <= 0 && !is_busy(fighter, off)) + // Each Dual Wield copy fires one off-hand weapon at its own level's cut, the strongest cut going to the first. + float powers[SLOT_MAX]; + int copies = 0; + 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()); + 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, - {.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; } } @@ -659,7 +666,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot) Attack attack = { .stats = stats, - .origin = fighter.hand_get(), + .origin = fighter.hand_get(shot.hand), .velocity = aim * stats.speed, .aimAngle = vec2_angle(aim), .angle = vec2_angle(aim), @@ -671,9 +678,9 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot) .owner = fighter.index, .weapon = held, .key = key, + .hand = shot.hand, .isCharged = isCharged, .isLeft = fighter.is_left(), - .isOffhand = shot.isOffhand, .isSecondary = shot.isSecondary, }; attack.position = attack.origin + aim * MUZZLE_DISTANCE; @@ -763,6 +770,12 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot) fighter.index); if (isCharged) 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 // 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; } 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); std::erase_if(attacks, [](Attack& attack) { return attack.isDead; }); 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. Vec2 start = attack.position; 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++) { 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; 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.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 // the upper edge, and hits anywhere in the cone out to its front until it cracks at full reach (short of // 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.isDead = progress >= 1; if (attack.isCharged) @@ -1151,7 +1153,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack) case ATTACK_SPIN: { // 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; if (isHeld && attack.time >= attack.duration) { @@ -1500,7 +1502,7 @@ void Weapons::teleport_move(Sim& sim, Attack& attack) attack.isDead = true; 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); spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get())); @@ -1600,7 +1602,7 @@ std::vector Weapons::paths_get(Sim& sim, Fighter& fighter) { std::vector paths; 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; const WeaponDef& weapon = WEAPON_DEFS[fighter.weaponLoadout[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 -// 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) { + 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 = { .stats = {.damage = blast.damage, .range = blast.area}, .origin = blast.position, @@ -1785,42 +1812,29 @@ void Weapons::blast_spawn(Sim& sim, Blast blast) }; attack_remember(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->shake_add(EXPLOSION_DEF.shake); 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. -Attack* Weapons::tether_get(Fighter& fighter, int key, bool isOffhand) +Attack* Weapons::tether_get(Fighter& fighter, int key, int hand) { for (Attack& attack : attacks) { AttackKind kind = attack.def()->kind; 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 nullptr; } -// The fighter's own attacks it can stand on or bump into: stuck arrows and charged boomerangs in flight. -std::vector Weapons::solids_get(Fighter& fighter) +// Adds the fighter's own attacks it can stand on to the world for its next move: charged boomerangs in flight as +// 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 solids; for (Attack& attack : attacks) { if (attack.owner != fighter.index || attack.isDead) @@ -1829,17 +1843,18 @@ std::vector Weapons::solids_get(Fighter& fighter) if (kind == ATTACK_BOOMERANG && attack.isCharged) { 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 half = {std::abs(math_cos(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS, - std::abs(math_sin(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS}; - solids.push_back({middle.x - half.x, middle.y - half.y, half.x * 2, half.y * 2}); + Vec2 low = {std::min(attack.position.x, attack.tip.x), std::min(attack.position.y, attack.tip.y)}; + Vec2 high = {std::max(attack.position.x, attack.tip.x), std::max(attack.position.y, attack.tip.y)}; + float radius = attack.radius; + 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. diff --git a/src/weapon.h b/src/weapon.h index 2879a7d..b93ba0d 100644 --- a/src/weapon.h +++ b/src/weapon.h @@ -13,6 +13,7 @@ constexpr float WHIP_UNFURL = 0.6f; class Fighter; struct Dummy; struct Sim; +class World; constexpr int LEVEL_COUNT = 3; constexpr int WEAPON_COUNT = 16; @@ -96,6 +97,8 @@ struct LevelDef 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 MINE_ARM_TIME = 0.3f; constexpr float ROCKET_ACCELERATION = 600; @@ -199,13 +202,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_SLASH, .sound = SOUND_SLASH, .duration = 0.3f, - .shake = 0.15f, + .shake = 0.075f, .spread = 7}, .charged = {.label = "flying slash", .kind = ATTACK_SLASH, .sound = SOUND_SLASH_HEAVY, .duration = 0.6f, - .shake = 0.45f, + .shake = 0.225f, .spread = 5}, .levels = { @@ -231,13 +234,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_SWING, .sound = SOUND_SWING, .duration = 0.3f, - .shake = 0.3f, + .shake = 0.15f, .spread = 6}, .charged = {.label = "slam", .kind = ATTACK_SWING, .sound = SOUND_SWING_HEAVY, .duration = 0.45f, - .shake = 0.7f, + .shake = 0.35f, .spread = 6}, .levels = { @@ -253,9 +256,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .chargeBlurb = "Charge to lunge toward the cursor\nwith a harder poke.", .flavor = "Works best when riding an ostrich.", .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 = - {.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 = { {.normal = {.damage = 12, .range = 90, .speed = 500}, @@ -280,13 +283,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_PUNCH, .sound = SOUND_PUNCH, .duration = 0.12f, - .shake = 0.05f, + .shake = 0.025f, .spread = 18}, .charged = {.label = "uppercut", .kind = ATTACK_UPPERCUT, .sound = SOUND_UPPERCUT, .duration = 0.35f, - .shake = 0.5f, + .shake = 0.25f, .spread = 5}, .levels = { @@ -314,13 +317,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_SPIN, .sound = SOUND_FLAIL, .duration = 0.4f, - .shake = 0.15f, + .shake = 0.075f, .spread = 8, .hits = 99}, .charged = {.label = "ball throw", .kind = ATTACK_FLAIL, .sound = SOUND_FLAIL_HEAVY, - .shake = 0.3f, + .shake = 0.15f, .spread = 6, .hits = 99}, .levels = @@ -349,13 +352,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_WHIP, .sound = SOUND_WHIP, .duration = 0.22f, - .shake = 0.1f, + .shake = 0.05f, .spread = 7.5f}, .charged = {.label = "lash + blast", .kind = ATTACK_WHIP, .sound = SOUND_WHIP_HEAVY, .duration = 0.12f, - .shake = 0.3f, + .shake = 0.15f, .spread = 0.5f, .hits = 0}, .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.", .flavor = "Get over here!", .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", .kind = ATTACK_HOOK, .sound = SOUND_GRAPPLE_HEAVY, - .shake = 0.4f, + .shake = 0.2f, .spread = 1}, .levels = { @@ -413,7 +416,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_BOOMERANG, .sound = SOUND_BOOMERANG, .duration = 0.35f, - .shake = 0.15f, + .shake = 0.075f, .spread = 8, .hits = 2}, .charged = {.label = "big boomerang", @@ -422,7 +425,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .duration = 0.4f, .hold = 1.2f, .scale = 2, - .shake = 0.3f, + .shake = 0.15f, .spread = 6, .hits = 4}, .levels = @@ -448,9 +451,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .chargeBlurb = "The longer the draw, the faster\nand harder they fly.", .flavor = "Erm, actually, bows require\nimmense upper body strengt-", .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 = - {.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 = { {.normal = {.damage = 22, .range = 360, .speed = 320, .volley = 1}, @@ -474,11 +477,11 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .hit = HIT_BULLET, .flash = 0.7f, .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", .kind = ATTACK_BULLET, .sound = SOUND_UZI_HEAVY, - .shake = 0.25f, + .shake = 0.125f, .spread = 6, .scatter = 1.1f}, .levels = @@ -510,14 +513,14 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .normal = {.label = "pellet", .kind = ATTACK_BULLET, .sound = SOUND_SHOTGUN, - .shake = 0.3f, + .shake = 0.15f, .spread = 6, .scatter = 2.3f}, .charged = {.label = "slug", .kind = ATTACK_BULLET, .sound = SOUND_SHOTGUN_HEAVY, .scale = 2.5f, - .shake = 0.5f, + .shake = 0.25f, .spread = 4, .hits = 3}, .levels = @@ -544,12 +547,12 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .hit = HIT_ENERGY, .flash = 1, .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", .kind = ATTACK_BEAM, .sound = SOUND_BEAM, .duration = 0.3f, - .shake = 0.35f, + .shake = 0.175f, .spread = 0.5f}, .levels = { @@ -568,24 +571,29 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = { .name = "Flamethrower", .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!!", .hit = HIT_FIRE, .kick = -0.1f, - .normal = - {.label = "flame", .kind = ATTACK_FLAME, .sound = SOUND_FLAME, .shake = 0.01f, .spread = 16, .hits = 2}, + .normal = {.label = "flame", + .kind = ATTACK_FLAME, + .sound = SOUND_FLAME, + .shake = 0.005f, + .spread = 16, + .hits = 2}, .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 = { {.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}, {.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}, {.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}, }, .bot = {.rangeMax = 110}, @@ -597,9 +605,9 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .flavor = "RAWKET LAWNCHAIR", .hit = HIT_EXPLOSIVE, .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 = - {.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 = { {.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, .sound = SOUND_MINE, .duration = 0.25f, - .shake = 0.1f, + .shake = 0.05f, .spread = 6, .hits = 0}, .levels = @@ -663,13 +671,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = .kind = ATTACK_WISP, .sound = SOUND_CAST, .duration = 0.35f, - .shake = 0.05f, + .shake = 0.025f, .spread = 4}, .charged = {.label = "teleport", .kind = ATTACK_TELEPORT, .sound = SOUND_TELEPORT_WARP, .duration = 0.175f, - .shake = 0.3f}, + .shake = 0.15f}, .levels = { {.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); // 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) // draw gray. Bounces is how many ricochets it has @@ -762,6 +770,8 @@ struct Attack int ricochets = 0; int weapon = 0; 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 isCharged = false; bool isCopy = false; @@ -770,7 +780,6 @@ struct Attack bool isGathering = false; bool isGentle = false; bool isLeft = false; - bool isOffhand = false; bool isPulling = false; bool isRebounded = false; bool isReturning = false; @@ -825,6 +834,7 @@ struct Blast SoundId sound; int weapon = -1; bool isGentle = false; + bool isHarmless = false; bool isPushing = false; bool isSecondary = false; }; @@ -842,8 +852,8 @@ struct Shot float charge = 0; float power = 1; int weaponSlot = 0; + int hand = 0; bool isEcho = false; - bool isOffhand = false; bool isSecondary = false; }; @@ -891,7 +901,7 @@ public: void wisp_move(Sim& sim, Attack& attack); void wisps_release(Fighter& fighter); void teleport_move(Sim& sim, Attack& attack); - std::vector solids_get(Fighter& fighter); + void solids_add(Fighter& fighter, World& world); private: std::vector pending; @@ -902,7 +912,7 @@ private: void attack_hit(Sim& sim, Attack& attack, int target, Vec2 point); std::vector chain_get(Sim& sim, Fighter& fighter); 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 piece_launch(Sim& sim, Attack& piece); void attack_explode(Sim& sim, Attack& attack); diff --git a/src/world.cpp b/src/world.cpp index 8ee0823..d06af49 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -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 -// them, never ones already inside). Platforms only catch bodies falling onto them. Then ramps: steep ones push bodies -// out sideways, walkable ones carry the feet, and grounded bodies stay stuck to the ground going over crests and down -// slopes unless they're jumping. A grounded body steps up a ledge as low as a walkable slope leaves under its edge, -// which carries it from a ramp's crest onto the ground beside it. +// them, never ones already inside). Platforms, and ledges like them, only catch bodies falling onto them. Then ramps: +// steep ones push bodies out sideways, walkable ones carry the feet, and grounded bodies stay stuck to the ground going +// over crests and down slopes unless they're jumping. A grounded body steps up a ledge as low as a walkable slope +// 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) { Rect& box = body.box; @@ -484,6 +484,16 @@ void World::body_step(Body& body, float time) body.isGrounded |= isDown; 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; ramp_push(body); @@ -624,13 +634,12 @@ bool World::is_solid_at(Vec2 point) return is_solid(tile); } -// What attacks collide with and hooks grab: solid tiles anywhere, and platforms within their drawn strip. Bodies -// still pass up through platforms and can drop through them. +// What attacks collide with and hooks grab: whatever's solid (slopes below their ramp's line included), and platforms +// within their drawn strip. Bodies still pass up through platforms and can drop through them. bool World::is_blocking_at(Vec2 point) { - Tile tile = tile_at(point); - return is_solid(tile) || - (tile == TILE_PLATFORM && point.y - std::floor(point.y / TILE_SIZE) * TILE_SIZE < PLATFORM_HEIGHT); + return is_solid_at(point) || + (tile_at(point) == 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. diff --git a/src/world.h b/src/world.h index 359c769..03a53af 100644 --- a/src/world.h +++ b/src/world.h @@ -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 // 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 -// solid below their ramp's line, and bodies meet ramps by their own rules rather than the grid's. +// fighter's own solid attacks, and ledges extra boxes it can only land on from above, like platforms. The map is the +// 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 { public: std::vector dummies; + std::vector ledges; std::vector obstacles; std::vector ramps; std::vector tiles; diff --git a/tools/fonts_export.py b/tools/fonts_export.py index fcf5a3f..3b7a9d5 100644 --- a/tools/fonts_export.py +++ b/tools/fonts_export.py @@ -26,6 +26,19 @@ FONTS = ROOT / "resources" / "font" TTF = FONTS / "pixel_purl.ttf" FIRST = 32 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 EM_PIXELS = 16 LINE_HEIGHT = 12 @@ -77,12 +90,14 @@ TINY = { 'v': ["...", "#.#", "#.#", "#.#", ".#."], 'w': [".....", "#...#", "#.#.#", "#.#.#", ".#.#."], 'x': ["...", "#.#", ".#.", ".#.", "#.#"], 'y': ["...", "#.#", "#.#", ".##", "..#", "##."], 'z': ["...", "###", ".#.", "#..", "###"], '{': [".##", ".#.", "##.", ".#.", ".##"], '|': ["#", "#", "#", "#", "#"], - '}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."], + '}': ["##.", ".#.", ".##", ".#.", "##."], '~': ["....", ".#.#", "#.#."], '°': [".##.", "#..#", ".##."], + '←': ["....", ".#..", "####", ".#.."], '↑': [".#.", "###", ".#.", ".#."], '→': ["....", "..#.", "####", "..#."], + '↓': ["...", ".#.", ".#.", "###", ".#."], } 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. @@ -97,8 +112,10 @@ def tiny_glyphs(): def small_glyphs(): font = ImageFont.truetype(str(TTF), EM_PIXELS) - glyphs = {} + glyphs = dict(SMALL_DRAWN) 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. image = Image.new("L", (64, 64), 0) 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"]) 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) - 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)) entries = {} for i, character in enumerate(characters()): @@ -145,10 +162,10 @@ def font_write(name, glyphs, scale, line_height, outline): if scale is None: del header["scale"] 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())] 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}") diff --git a/tools/icons_resize.py b/tools/icons_resize.py deleted file mode 100644 index 2d32379..0000000 --- a/tools/icons_resize.py +++ /dev/null @@ -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()