HUD pass, weapon and trinket rework, and font arrows
HUD: - Hotbars fit a quarter of the screen, dropping to small then tiny slots (thinner outlines when tiny), with cooldown bars matching their size. - HUD elements are drawn on their own layer and composited at one opacity: hotbar slots are see-through until hovered, the rest fade while hovered. - Rival windows fit the right column in at most two rows, shrinking icons then font, then dropping icons for name, health and ping bars. - Tooltips number equipped items' slots and say Recoil for weapons that only push back. Previews use arrows, spread uses degrees. - Crosshair fills in and shakes with charge, and weapons shake after firing until their cooldown ends. DPS and speed meters off by default. Gameplay: - Dual Wield stacks: each copy fires another recently held weapon from a fanned off-hand, at its own level's cut. - Flamethrower: no slowing, smaller fireball with some recoil, and an airblast that knocks others away. - Iron Weights slam on any landing faster than a plain fall (platforms too), stacking radius and damage, and speeding fast falls. - Stuck arrows are one-way ledges until they fade, matching their (slightly larger) hit size. Attacks now collide with slopes. - Halved Tesla Coil, Recoil Coil, Diazepam, Shock Absorber, Feather and screenshake values. Art and tools: - Weapon and trinket icons merged into one sheet each (normal, small, tiny), cells in catalog order. Grimoire wisps stand upright. - Fonts gain the degree sign and four arrows, decoded from UTF-8. - Menu fighter preview walks at quarter speed. - Unequip from either catalog tab by right click or gamepad. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -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}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -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}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -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}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -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}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 8.9 KiB |
@@ -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}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 8.9 KiB |
@@ -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}]}]},
|
||||
|
||||
|
Before Width: | Height: | Size: 613 B After Width: | Height: | Size: 9.0 KiB |
@@ -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}]}]}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 697 B |
@@ -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}]}]}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -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}]}]}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.3 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 504 B |
@@ -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;
|
||||
}
|
||||
@@ -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<Hit> hits;
|
||||
std::vector<Particle> particles;
|
||||
std::vector<Popup> popups;
|
||||
std::vector<Ring> rings;
|
||||
float shake = 0;
|
||||
int ticks = 0;
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ("<name> 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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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] = {};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -119,6 +119,7 @@ template <class Stream, class SimRef> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
|
||||
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<std::string> game_summary_lines(const Rules& rules)
|
||||
@@ -1887,8 +1955,7 @@ void Ui::update(Game& game)
|
||||
hintAlpha = std::max(0.0f, hintAlpha - TICK / HINT_FADE);
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "weapon.h"
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
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<float>());
|
||||
int offs[SLOT_MAX];
|
||||
int count = fighter.off_slots_get(offs);
|
||||
for (int hand = 1; hand <= count; hand++)
|
||||
{
|
||||
int off = fighter.weapon_key(offs[hand - 1]);
|
||||
if (fighter.cooldowns[off] > 0 || is_busy(fighter, off))
|
||||
continue;
|
||||
is_spawned(sim, fighter,
|
||||
{.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<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
{
|
||||
std::vector<Path> 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<Rect> 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<Rect> solids;
|
||||
for (Attack& attack : attacks)
|
||||
{
|
||||
if (attack.owner != fighter.index || attack.isDead)
|
||||
@@ -1829,17 +1843,18 @@ std::vector<Rect> Weapons::solids_get(Fighter& fighter)
|
||||
if (kind == ATTACK_BOOMERANG && attack.isCharged)
|
||||
{
|
||||
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.
|
||||
|
||||
@@ -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<Rect> solids_get(Fighter& fighter);
|
||||
void solids_add(Fighter& fighter, World& world);
|
||||
|
||||
private:
|
||||
std::vector<Attack> pending;
|
||||
@@ -902,7 +912,7 @@ private:
|
||||
void attack_hit(Sim& sim, Attack& attack, int target, Vec2 point);
|
||||
std::vector<Capsule> 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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Dummy> dummies;
|
||||
std::vector<Rect> ledges;
|
||||
std::vector<Rect> obstacles;
|
||||
std::vector<Ramp> ramps;
|
||||
std::vector<Tile> tiles;
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||