Lobby browser, names and passwords, The Good Stuff, smarter queues, and a balance pass
- Lobbies: a name (default "Lobby <code>"), Unlisted or Public, and an optional password. The Multiplayer page scrolls: a Browser of public lobbies (name, count, padlock, code, little player previews, Join with a password field when locked), Join by code, Create, then Settings. Errors show under whatever caused them. Links take ?pass= (and --pass), and Copy Link includes it. Protocol 11. - Queue areas center on their host along the floor's flat stretch, squeezing their slots to fit, and move to a nearby surface when the host's is too short. - An airblast steals an enemy's flail ball onto its own chain for a while. Flail balls reel themselves back after 15 seconds. Doubler echoes of the Grimoire teleport move you too. - The Good Stuff: a new trinket, the inverse of Diazepam (more spread). Spread is now a multiplier, and it widens or narrows the Vacuum's cone. - Balance: Sword basic slash 25% smaller and charged slash half the size, Whip basic lash half the damage and reach, Recoil Coil recoil halved, Speedometer no longer speeds you up. - Fixes: fashions survive coming back from a game, Quit shows only in the sandbox menu, and the chat log keeps above the menu's tip.
This commit is contained in:
1 parent
af41b53717
commit
37560547d4
17 files changed
+666
-277
No files matched your search
+117
-113
@@ -12,22 +12,23 @@
|
|||||||
{"name": "Hourglass", "crop": [96, 0], "size": [24, 24], "origin": "center"},
|
{"name": "Hourglass", "crop": [96, 0], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Enlarger", "crop": [120, 0], "size": [24, 24], "origin": "center"},
|
{"name": "Enlarger", "crop": [120, 0], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Diazepam", "crop": [0, 24], "size": [24, 24], "origin": "center"},
|
{"name": "Diazepam", "crop": [0, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Splitter", "crop": [24, 24], "size": [24, 24], "origin": "center"},
|
{"name": "The Good Stuff", "crop": [24, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Doubler", "crop": [48, 24], "size": [24, 24], "origin": "center"},
|
{"name": "Splitter", "crop": [48, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Dual Wield", "crop": [72, 24], "size": [24, 24], "origin": "center"},
|
{"name": "Doubler", "crop": [72, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Ricochet", "crop": [96, 24], "size": [24, 24], "origin": "center"},
|
{"name": "Dual Wield", "crop": [96, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Tesla Coil", "crop": [120, 24], "size": [24, 24], "origin": "center"},
|
{"name": "Ricochet", "crop": [120, 24], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Recoil Coil", "crop": [0, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Tesla Coil", "crop": [0, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Shock Absorber", "crop": [24, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Recoil Coil", "crop": [24, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Shrinker", "crop": [48, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Shock Absorber", "crop": [48, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Embiggener", "crop": [72, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Shrinker", "crop": [72, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Pebble Helmet", "crop": [96, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Embiggener", "crop": [96, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Boots", "crop": [120, 48], "size": [24, 24], "origin": "center"},
|
{"name": "Pebble Helmet", "crop": [120, 48], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Speedometer", "crop": [0, 72], "size": [24, 24], "origin": "center"},
|
{"name": "Boots", "crop": [0, 72], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Wings", "crop": [24, 72], "size": [24, 24], "origin": "center"},
|
{"name": "Speedometer", "crop": [24, 72], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Feather", "crop": [48, 72], "size": [24, 24], "origin": "center"},
|
{"name": "Wings", "crop": [48, 72], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Iron Weights", "crop": [72, 72], "size": [24, 24], "origin": "center"},
|
{"name": "Feather", "crop": [72, 72], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Climb Claw", "crop": [96, 72], "size": [24, 24], "origin": "center"},
|
{"name": "Iron Weights", "crop": [96, 72], "size": [24, 24], "origin": "center"},
|
||||||
|
{"name": "Climb Claw", "crop": [120, 72], "size": [24, 24], "origin": "center"},
|
||||||
{"name": "Gloves Small", "crop": [0, 96], "size": [16, 16], "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": "Energy Drink Small", "crop": [16, 96], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Boxing Glove Small", "crop": [32, 96], "size": [16, 16], "origin": "center"},
|
{"name": "Boxing Glove Small", "crop": [32, 96], "size": [16, 16], "origin": "center"},
|
||||||
@@ -35,22 +36,23 @@
|
|||||||
{"name": "Hourglass Small", "crop": [64, 96], "size": [16, 16], "origin": "center"},
|
{"name": "Hourglass Small", "crop": [64, 96], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Enlarger Small", "crop": [80, 96], "size": [16, 16], "origin": "center"},
|
{"name": "Enlarger Small", "crop": [80, 96], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Diazepam Small", "crop": [0, 112], "size": [16, 16], "origin": "center"},
|
{"name": "Diazepam Small", "crop": [0, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Splitter Small", "crop": [16, 112], "size": [16, 16], "origin": "center"},
|
{"name": "The Good Stuff Small", "crop": [16, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Doubler Small", "crop": [32, 112], "size": [16, 16], "origin": "center"},
|
{"name": "Splitter Small", "crop": [32, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Dual Wield Small", "crop": [48, 112], "size": [16, 16], "origin": "center"},
|
{"name": "Doubler Small", "crop": [48, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Ricochet Small", "crop": [64, 112], "size": [16, 16], "origin": "center"},
|
{"name": "Dual Wield Small", "crop": [64, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Tesla Coil Small", "crop": [80, 112], "size": [16, 16], "origin": "center"},
|
{"name": "Ricochet Small", "crop": [80, 112], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Recoil Coil Small", "crop": [0, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Tesla Coil Small", "crop": [0, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Shock Absorber Small", "crop": [16, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Recoil Coil Small", "crop": [16, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Shrinker Small", "crop": [32, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Shock Absorber Small", "crop": [32, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Embiggener Small", "crop": [48, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Shrinker Small", "crop": [48, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Pebble Helmet Small", "crop": [64, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Embiggener Small", "crop": [64, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Boots Small", "crop": [80, 128], "size": [16, 16], "origin": "center"},
|
{"name": "Pebble Helmet Small", "crop": [80, 128], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Speedometer Small", "crop": [0, 144], "size": [16, 16], "origin": "center"},
|
{"name": "Boots Small", "crop": [0, 144], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Wings Small", "crop": [16, 144], "size": [16, 16], "origin": "center"},
|
{"name": "Speedometer Small", "crop": [16, 144], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Feather Small", "crop": [32, 144], "size": [16, 16], "origin": "center"},
|
{"name": "Wings Small", "crop": [32, 144], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Iron Weights Small", "crop": [48, 144], "size": [16, 16], "origin": "center"},
|
{"name": "Feather Small", "crop": [48, 144], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Climb Claw Small", "crop": [64, 144], "size": [16, 16], "origin": "center"},
|
{"name": "Iron Weights Small", "crop": [64, 144], "size": [16, 16], "origin": "center"},
|
||||||
|
{"name": "Climb Claw Small", "crop": [80, 144], "size": [16, 16], "origin": "center"},
|
||||||
{"name": "Gloves Tiny", "crop": [0, 160], "size": [8, 8], "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": "Energy Drink Tiny", "crop": [8, 160], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Boxing Glove Tiny", "crop": [16, 160], "size": [8, 8], "origin": "center"},
|
{"name": "Boxing Glove Tiny", "crop": [16, 160], "size": [8, 8], "origin": "center"},
|
||||||
@@ -58,28 +60,27 @@
|
|||||||
{"name": "Hourglass Tiny", "crop": [32, 160], "size": [8, 8], "origin": "center"},
|
{"name": "Hourglass Tiny", "crop": [32, 160], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Enlarger Tiny", "crop": [40, 160], "size": [8, 8], "origin": "center"},
|
{"name": "Enlarger Tiny", "crop": [40, 160], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Diazepam Tiny", "crop": [0, 168], "size": [8, 8], "origin": "center"},
|
{"name": "Diazepam Tiny", "crop": [0, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Splitter Tiny", "crop": [8, 168], "size": [8, 8], "origin": "center"},
|
{"name": "The Good Stuff Tiny", "crop": [8, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Doubler Tiny", "crop": [16, 168], "size": [8, 8], "origin": "center"},
|
{"name": "Splitter Tiny", "crop": [16, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Dual Wield Tiny", "crop": [24, 168], "size": [8, 8], "origin": "center"},
|
{"name": "Doubler Tiny", "crop": [24, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Ricochet Tiny", "crop": [32, 168], "size": [8, 8], "origin": "center"},
|
{"name": "Dual Wield Tiny", "crop": [32, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Tesla Coil Tiny", "crop": [40, 168], "size": [8, 8], "origin": "center"},
|
{"name": "Ricochet Tiny", "crop": [40, 168], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Recoil Coil Tiny", "crop": [0, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Tesla Coil Tiny", "crop": [0, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Shock Absorber Tiny", "crop": [8, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Recoil Coil Tiny", "crop": [8, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Shrinker Tiny", "crop": [16, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Shock Absorber Tiny", "crop": [16, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Embiggener Tiny", "crop": [24, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Shrinker Tiny", "crop": [24, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Pebble Helmet Tiny", "crop": [32, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Embiggener Tiny", "crop": [32, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Boots Tiny", "crop": [40, 176], "size": [8, 8], "origin": "center"},
|
{"name": "Pebble Helmet Tiny", "crop": [40, 176], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Speedometer Tiny", "crop": [0, 184], "size": [8, 8], "origin": "center"},
|
{"name": "Boots Tiny", "crop": [0, 184], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Wings Tiny", "crop": [8, 184], "size": [8, 8], "origin": "center"},
|
{"name": "Speedometer Tiny", "crop": [8, 184], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Feather Tiny", "crop": [16, 184], "size": [8, 8], "origin": "center"},
|
{"name": "Wings Tiny", "crop": [16, 184], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Iron Weights Tiny", "crop": [24, 184], "size": [8, 8], "origin": "center"},
|
{"name": "Feather Tiny", "crop": [24, 184], "size": [8, 8], "origin": "center"},
|
||||||
{"name": "Climb Claw Tiny", "crop": [32, 184], "size": [8, 8], "origin": "center"}
|
{"name": "Iron Weights Tiny", "crop": [32, 184], "size": [8, 8], "origin": "center"},
|
||||||
|
{"name": "Climb Claw Tiny", "crop": [40, 184], "size": [8, 8], "origin": "center"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layers": [
|
"layers": [{"name": "sprite"}],
|
||||||
{"name": "sprite"}
|
|
||||||
],
|
|
||||||
"defaultAnimation": "Gloves",
|
"defaultAnimation": "Gloves",
|
||||||
"animations": {
|
"animations": {
|
||||||
"Gloves": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]},
|
"Gloves": {"layers": [{"layer": 0, "frames": [{"region": 0}]}]},
|
||||||
@@ -89,67 +90,70 @@
|
|||||||
"Hourglass": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]},
|
"Hourglass": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]},
|
||||||
"Enlarger": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]},
|
"Enlarger": {"layers": [{"layer": 0, "frames": [{"region": 5}]}]},
|
||||||
"Diazepam": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]},
|
"Diazepam": {"layers": [{"layer": 0, "frames": [{"region": 6}]}]},
|
||||||
"Splitter": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]},
|
"The Good Stuff": {"layers": [{"layer": 0, "frames": [{"region": 7}]}]},
|
||||||
"Doubler": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]},
|
"Splitter": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]},
|
||||||
"Dual Wield": {"layers": [{"layer": 0, "frames": [{"region": 9}]}]},
|
"Doubler": {"layers": [{"layer": 0, "frames": [{"region": 9}]}]},
|
||||||
"Ricochet": {"layers": [{"layer": 0, "frames": [{"region": 10}]}]},
|
"Dual Wield": {"layers": [{"layer": 0, "frames": [{"region": 10}]}]},
|
||||||
"Tesla Coil": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]},
|
"Ricochet": {"layers": [{"layer": 0, "frames": [{"region": 11}]}]},
|
||||||
"Recoil Coil": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]},
|
"Tesla Coil": {"layers": [{"layer": 0, "frames": [{"region": 12}]}]},
|
||||||
"Shock Absorber": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]},
|
"Recoil Coil": {"layers": [{"layer": 0, "frames": [{"region": 13}]}]},
|
||||||
"Shrinker": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
|
"Shock Absorber": {"layers": [{"layer": 0, "frames": [{"region": 14}]}]},
|
||||||
"Embiggener": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]},
|
"Shrinker": {"layers": [{"layer": 0, "frames": [{"region": 15}]}]},
|
||||||
"Pebble Helmet": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]},
|
"Embiggener": {"layers": [{"layer": 0, "frames": [{"region": 16}]}]},
|
||||||
"Boots": {"layers": [{"layer": 0, "frames": [{"region": 17}]}]},
|
"Pebble Helmet": {"layers": [{"layer": 0, "frames": [{"region": 17}]}]},
|
||||||
"Speedometer": {"layers": [{"layer": 0, "frames": [{"region": 18}]}]},
|
"Boots": {"layers": [{"layer": 0, "frames": [{"region": 18}]}]},
|
||||||
"Wings": {"layers": [{"layer": 0, "frames": [{"region": 19}]}]},
|
"Speedometer": {"layers": [{"layer": 0, "frames": [{"region": 19}]}]},
|
||||||
"Feather": {"layers": [{"layer": 0, "frames": [{"region": 20}]}]},
|
"Wings": {"layers": [{"layer": 0, "frames": [{"region": 20}]}]},
|
||||||
"Iron Weights": {"layers": [{"layer": 0, "frames": [{"region": 21}]}]},
|
"Feather": {"layers": [{"layer": 0, "frames": [{"region": 21}]}]},
|
||||||
"Climb Claw": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]},
|
"Iron Weights": {"layers": [{"layer": 0, "frames": [{"region": 22}]}]},
|
||||||
"Gloves Small": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]},
|
"Climb Claw": {"layers": [{"layer": 0, "frames": [{"region": 23}]}]},
|
||||||
"Energy Drink Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]},
|
"Gloves Small": {"layers": [{"layer": 0, "frames": [{"region": 24}]}]},
|
||||||
"Boxing Glove Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]},
|
"Energy Drink Small": {"layers": [{"layer": 0, "frames": [{"region": 25}]}]},
|
||||||
"Accelerator Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]},
|
"Boxing Glove Small": {"layers": [{"layer": 0, "frames": [{"region": 26}]}]},
|
||||||
"Hourglass Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]},
|
"Accelerator Small": {"layers": [{"layer": 0, "frames": [{"region": 27}]}]},
|
||||||
"Enlarger Small": {"layers": [{"layer": 0, "frames": [{"region": 28}]}]},
|
"Hourglass Small": {"layers": [{"layer": 0, "frames": [{"region": 28}]}]},
|
||||||
"Diazepam Small": {"layers": [{"layer": 0, "frames": [{"region": 29}]}]},
|
"Enlarger Small": {"layers": [{"layer": 0, "frames": [{"region": 29}]}]},
|
||||||
"Splitter Small": {"layers": [{"layer": 0, "frames": [{"region": 30}]}]},
|
"Diazepam Small": {"layers": [{"layer": 0, "frames": [{"region": 30}]}]},
|
||||||
"Doubler Small": {"layers": [{"layer": 0, "frames": [{"region": 31}]}]},
|
"The Good Stuff Small": {"layers": [{"layer": 0, "frames": [{"region": 31}]}]},
|
||||||
"Dual Wield Small": {"layers": [{"layer": 0, "frames": [{"region": 32}]}]},
|
"Splitter Small": {"layers": [{"layer": 0, "frames": [{"region": 32}]}]},
|
||||||
"Ricochet Small": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]},
|
"Doubler Small": {"layers": [{"layer": 0, "frames": [{"region": 33}]}]},
|
||||||
"Tesla Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]},
|
"Dual Wield Small": {"layers": [{"layer": 0, "frames": [{"region": 34}]}]},
|
||||||
"Recoil Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]},
|
"Ricochet Small": {"layers": [{"layer": 0, "frames": [{"region": 35}]}]},
|
||||||
"Shock Absorber Small": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]},
|
"Tesla Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 36}]}]},
|
||||||
"Shrinker Small": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]},
|
"Recoil Coil Small": {"layers": [{"layer": 0, "frames": [{"region": 37}]}]},
|
||||||
"Embiggener Small": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]},
|
"Shock Absorber Small": {"layers": [{"layer": 0, "frames": [{"region": 38}]}]},
|
||||||
"Pebble Helmet Small": {"layers": [{"layer": 0, "frames": [{"region": 39}]}]},
|
"Shrinker Small": {"layers": [{"layer": 0, "frames": [{"region": 39}]}]},
|
||||||
"Boots Small": {"layers": [{"layer": 0, "frames": [{"region": 40}]}]},
|
"Embiggener Small": {"layers": [{"layer": 0, "frames": [{"region": 40}]}]},
|
||||||
"Speedometer Small": {"layers": [{"layer": 0, "frames": [{"region": 41}]}]},
|
"Pebble Helmet Small": {"layers": [{"layer": 0, "frames": [{"region": 41}]}]},
|
||||||
"Wings Small": {"layers": [{"layer": 0, "frames": [{"region": 42}]}]},
|
"Boots Small": {"layers": [{"layer": 0, "frames": [{"region": 42}]}]},
|
||||||
"Feather Small": {"layers": [{"layer": 0, "frames": [{"region": 43}]}]},
|
"Speedometer Small": {"layers": [{"layer": 0, "frames": [{"region": 43}]}]},
|
||||||
"Iron Weights Small": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]},
|
"Wings Small": {"layers": [{"layer": 0, "frames": [{"region": 44}]}]},
|
||||||
"Climb Claw Small": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]},
|
"Feather Small": {"layers": [{"layer": 0, "frames": [{"region": 45}]}]},
|
||||||
"Gloves Tiny": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]},
|
"Iron Weights Small": {"layers": [{"layer": 0, "frames": [{"region": 46}]}]},
|
||||||
"Energy Drink Tiny": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]},
|
"Climb Claw Small": {"layers": [{"layer": 0, "frames": [{"region": 47}]}]},
|
||||||
"Boxing Glove Tiny": {"layers": [{"layer": 0, "frames": [{"region": 48}]}]},
|
"Gloves Tiny": {"layers": [{"layer": 0, "frames": [{"region": 48}]}]},
|
||||||
"Accelerator Tiny": {"layers": [{"layer": 0, "frames": [{"region": 49}]}]},
|
"Energy Drink Tiny": {"layers": [{"layer": 0, "frames": [{"region": 49}]}]},
|
||||||
"Hourglass Tiny": {"layers": [{"layer": 0, "frames": [{"region": 50}]}]},
|
"Boxing Glove Tiny": {"layers": [{"layer": 0, "frames": [{"region": 50}]}]},
|
||||||
"Enlarger Tiny": {"layers": [{"layer": 0, "frames": [{"region": 51}]}]},
|
"Accelerator Tiny": {"layers": [{"layer": 0, "frames": [{"region": 51}]}]},
|
||||||
"Diazepam Tiny": {"layers": [{"layer": 0, "frames": [{"region": 52}]}]},
|
"Hourglass Tiny": {"layers": [{"layer": 0, "frames": [{"region": 52}]}]},
|
||||||
"Splitter Tiny": {"layers": [{"layer": 0, "frames": [{"region": 53}]}]},
|
"Enlarger Tiny": {"layers": [{"layer": 0, "frames": [{"region": 53}]}]},
|
||||||
"Doubler Tiny": {"layers": [{"layer": 0, "frames": [{"region": 54}]}]},
|
"Diazepam Tiny": {"layers": [{"layer": 0, "frames": [{"region": 54}]}]},
|
||||||
"Dual Wield Tiny": {"layers": [{"layer": 0, "frames": [{"region": 55}]}]},
|
"The Good Stuff Tiny": {"layers": [{"layer": 0, "frames": [{"region": 55}]}]},
|
||||||
"Ricochet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 56}]}]},
|
"Splitter Tiny": {"layers": [{"layer": 0, "frames": [{"region": 56}]}]},
|
||||||
"Tesla Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 57}]}]},
|
"Doubler Tiny": {"layers": [{"layer": 0, "frames": [{"region": 57}]}]},
|
||||||
"Recoil Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 58}]}]},
|
"Dual Wield Tiny": {"layers": [{"layer": 0, "frames": [{"region": 58}]}]},
|
||||||
"Shock Absorber Tiny": {"layers": [{"layer": 0, "frames": [{"region": 59}]}]},
|
"Ricochet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 59}]}]},
|
||||||
"Shrinker Tiny": {"layers": [{"layer": 0, "frames": [{"region": 60}]}]},
|
"Tesla Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 60}]}]},
|
||||||
"Embiggener Tiny": {"layers": [{"layer": 0, "frames": [{"region": 61}]}]},
|
"Recoil Coil Tiny": {"layers": [{"layer": 0, "frames": [{"region": 61}]}]},
|
||||||
"Pebble Helmet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 62}]}]},
|
"Shock Absorber Tiny": {"layers": [{"layer": 0, "frames": [{"region": 62}]}]},
|
||||||
"Boots Tiny": {"layers": [{"layer": 0, "frames": [{"region": 63}]}]},
|
"Shrinker Tiny": {"layers": [{"layer": 0, "frames": [{"region": 63}]}]},
|
||||||
"Speedometer Tiny": {"layers": [{"layer": 0, "frames": [{"region": 64}]}]},
|
"Embiggener Tiny": {"layers": [{"layer": 0, "frames": [{"region": 64}]}]},
|
||||||
"Wings Tiny": {"layers": [{"layer": 0, "frames": [{"region": 65}]}]},
|
"Pebble Helmet Tiny": {"layers": [{"layer": 0, "frames": [{"region": 65}]}]},
|
||||||
"Feather Tiny": {"layers": [{"layer": 0, "frames": [{"region": 66}]}]},
|
"Boots Tiny": {"layers": [{"layer": 0, "frames": [{"region": 66}]}]},
|
||||||
"Iron Weights Tiny": {"layers": [{"layer": 0, "frames": [{"region": 67}]}]},
|
"Speedometer Tiny": {"layers": [{"layer": 0, "frames": [{"region": 67}]}]},
|
||||||
"Climb Claw Tiny": {"layers": [{"layer": 0, "frames": [{"region": 68}]}]}
|
"Wings Tiny": {"layers": [{"layer": 0, "frames": [{"region": 68}]}]},
|
||||||
|
"Feather Tiny": {"layers": [{"layer": 0, "frames": [{"region": 69}]}]},
|
||||||
|
"Iron Weights Tiny": {"layers": [{"layer": 0, "frames": [{"region": 70}]}]},
|
||||||
|
"Climb Claw Tiny": {"layers": [{"layer": 0, "frames": [{"region": 71}]}]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.0 KiB |
+54
-5
@@ -100,11 +100,15 @@ enum LeaveReason
|
|||||||
LEAVE_KICKED
|
LEAVE_KICKED
|
||||||
};
|
};
|
||||||
|
|
||||||
// A lobby: its members in the order they joined (the first hosting), and its rooms, the sandbox first.
|
// A lobby: its name, the password to join it (empty for none), whether the lobby list shows it, its members in the
|
||||||
|
// order they joined (the first hosting), and its rooms, the sandbox first.
|
||||||
struct Lobby
|
struct Lobby
|
||||||
{
|
{
|
||||||
|
std::string name;
|
||||||
|
std::string password;
|
||||||
std::vector<int> members;
|
std::vector<int> members;
|
||||||
std::vector<int> rooms;
|
std::vector<int> rooms;
|
||||||
|
bool isPublic = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// What the network's threads hand the game loop: a member's connection opening or closing, one of their data channels
|
// What the network's threads hand the game loop: a member's connection opening or closing, one of their data channels
|
||||||
@@ -329,7 +333,8 @@ static void lobby_enter(int id, const std::string& code)
|
|||||||
list += (list.empty() ? "" : ",") + std::string("{\"id\":") + std::to_string(other) +
|
list += (list.empty() ? "" : ",") + std::string("{\"id\":") + std::to_string(other) +
|
||||||
",\"name\":" + json_quote(members[other].name) + "}";
|
",\"name\":" + json_quote(members[other].name) + "}";
|
||||||
send(id, "{\"type\":\"welcome\",\"id\":" + std::to_string(id) + ",\"code\":" + json_quote(code) +
|
send(id, "{\"type\":\"welcome\",\"id\":" + std::to_string(id) + ",\"code\":" + json_quote(code) +
|
||||||
",\"host\":" + std::to_string(lobby.members.front()) + ",\"members\":[" + list + "]}");
|
",\"name\":" + json_quote(lobby.name) + ",\"host\":" + std::to_string(lobby.members.front()) +
|
||||||
|
",\"members\":[" + list + "]}");
|
||||||
for (int other : lobby.members)
|
for (int other : lobby.members)
|
||||||
if (other != id)
|
if (other != id)
|
||||||
send(other, "{\"type\":\"member_join\",\"id\":" + std::to_string(id) +
|
send(other, "{\"type\":\"member_join\",\"id\":" + std::to_string(id) +
|
||||||
@@ -360,7 +365,35 @@ static void lobby_exit(int id, LeaveReason leave = LEAVE_QUIT, const std::string
|
|||||||
",\"host\":" + std::to_string(lobby.members.front()) + reason + "}");
|
",\"host\":" + std::to_string(lobby.members.front()) + reason + "}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The lobby messages, in JSON: making, joining and leaving lobbies, kicking, and keeping the connection busy.
|
// Tells a member about every public lobby: its code and name, how many are in it, whether it needs a password, and each
|
||||||
|
// player's color and fashion (as their fighter in its sandbox has them, or as they brought it while in a game), for
|
||||||
|
// little previews of them.
|
||||||
|
static void lobbies_send(int id)
|
||||||
|
{
|
||||||
|
std::string list;
|
||||||
|
for (auto& [code, lobby] : lobbies)
|
||||||
|
{
|
||||||
|
if (!lobby.isPublic || lobby.rooms.empty())
|
||||||
|
continue;
|
||||||
|
Sim& sandbox = rooms[lobby.rooms.front()].sim;
|
||||||
|
std::string players;
|
||||||
|
for (int member : lobby.members)
|
||||||
|
{
|
||||||
|
int fighter = sandbox.match.fighter_find(sandbox, member);
|
||||||
|
Profile& profile = members[member].profile;
|
||||||
|
int color = fighter >= 0 ? sandbox.match.colors[fighter] : profile.color;
|
||||||
|
int fashion = fighter >= 0 ? sandbox.match.fashions[fighter] : profile.fashion;
|
||||||
|
players += (players.empty() ? "" : ",") + std::string("{\"color\":") + std::to_string(color) +
|
||||||
|
",\"fashion\":" + std::to_string(fashion) + "}";
|
||||||
|
}
|
||||||
|
list += (list.empty() ? "" : ",") + std::string("{\"code\":") + json_quote(code) +
|
||||||
|
",\"name\":" + json_quote(lobby.name) + ",\"locked\":" + (lobby.password.empty() ? "false" : "true") +
|
||||||
|
",\"players\":[" + players + "]}";
|
||||||
|
}
|
||||||
|
send(id, "{\"type\":\"lobbies\",\"lobbies\":[" + list + "]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lobby messages, in JSON: listing, making, joining and leaving lobbies, kicking, and keeping the connection busy.
|
||||||
static void text_handle(int id, const std::string& text)
|
static void text_handle(int id, const std::string& text)
|
||||||
{
|
{
|
||||||
simdjson::dom::element message;
|
simdjson::dom::element message;
|
||||||
@@ -371,12 +404,14 @@ static void text_handle(int id, const std::string& text)
|
|||||||
// Answered, so proxies see the connection busy both ways.
|
// Answered, so proxies see the connection busy both ways.
|
||||||
if (type == "ping")
|
if (type == "ping")
|
||||||
return send(id, "{\"type\":\"pong\"}");
|
return send(id, "{\"type\":\"pong\"}");
|
||||||
if (type == "create" || type == "join")
|
if (type == "list" || type == "create" || type == "join")
|
||||||
{
|
{
|
||||||
int64_t version = 0;
|
int64_t version = 0;
|
||||||
std::string_view name;
|
std::string_view name;
|
||||||
if (message["version"].get(version) || version != PROTOCOL_VERSION)
|
if (message["version"].get(version) || version != PROTOCOL_VERSION)
|
||||||
return error_send(id, "Different game version");
|
return error_send(id, "Different game version");
|
||||||
|
if (type == "list")
|
||||||
|
return lobbies_send(id);
|
||||||
if (!members[id].code.empty())
|
if (!members[id].code.empty())
|
||||||
return error_send(id, "Already in a lobby");
|
return error_send(id, "Already in a lobby");
|
||||||
members[id].name = name_clean(message["name"].get(name) ? "" : name);
|
members[id].name = name_clean(message["name"].get(name) ? "" : name);
|
||||||
@@ -385,7 +420,15 @@ static void text_handle(int id, const std::string& text)
|
|||||||
std::string code = code_make();
|
std::string code = code_make();
|
||||||
if (code.empty())
|
if (code.empty())
|
||||||
return error_send(id, "No free lobby codes");
|
return error_send(id, "No free lobby codes");
|
||||||
std::printf("Lobby %s opened (%zu open)\n", code.c_str(), lobbies.size() + 1);
|
// A lobby's name defaults to its code, and it starts unlisted unless asked otherwise.
|
||||||
|
std::string_view field;
|
||||||
|
bool isPublic = false;
|
||||||
|
Lobby& lobby = lobbies[code];
|
||||||
|
lobby.name = text_clean(message["lobby"].get(field) ? "" : field, LOBBY_NAME_MAX);
|
||||||
|
lobby.name = lobby.name.empty() ? "Lobby " + code : lobby.name;
|
||||||
|
lobby.password = text_clean(message["password"].get(field) ? "" : field, LOBBY_PASSWORD_MAX);
|
||||||
|
lobby.isPublic = !message["public"].get(isPublic) && isPublic;
|
||||||
|
std::printf("Lobby %s opened (%zu open)\n", code.c_str(), lobbies.size());
|
||||||
return lobby_enter(id, code);
|
return lobby_enter(id, code);
|
||||||
}
|
}
|
||||||
std::string_view raw;
|
std::string_view raw;
|
||||||
@@ -399,6 +442,11 @@ static void text_handle(int id, const std::string& text)
|
|||||||
return error_send(id, "No lobby with that code");
|
return error_send(id, "No lobby with that code");
|
||||||
if ((int)lobby->second.members.size() >= LOBBY_MEMBERS_MAX)
|
if ((int)lobby->second.members.size() >= LOBBY_MEMBERS_MAX)
|
||||||
return error_send(id, "That lobby is full");
|
return error_send(id, "That lobby is full");
|
||||||
|
std::string_view password;
|
||||||
|
if (message["password"].get(password))
|
||||||
|
password = "";
|
||||||
|
if (!lobby->second.password.empty() && password != lobby->second.password)
|
||||||
|
return error_send(id, password.empty() ? LOBBY_PASSWORD_NEEDED : "Wrong password");
|
||||||
return lobby_enter(id, code);
|
return lobby_enter(id, code);
|
||||||
}
|
}
|
||||||
if (type == "leave")
|
if (type == "leave")
|
||||||
@@ -634,6 +682,7 @@ static void game_open(int sandboxSerial, int queuer)
|
|||||||
members[players[k]].profile = profile_get(sandbox.sim.fighters[seats[k]]);
|
members[players[k]].profile = profile_get(sandbox.sim.fighters[seats[k]]);
|
||||||
std::memcpy(members[players[k]].profile.name, lobby.names[seats[k]], sizeof(lobby.names[seats[k]]));
|
std::memcpy(members[players[k]].profile.name, lobby.names[seats[k]], sizeof(lobby.names[seats[k]]));
|
||||||
members[players[k]].profile.color = lobby.colors[seats[k]];
|
members[players[k]].profile.color = lobby.colors[seats[k]];
|
||||||
|
members[players[k]].profile.fashion = lobby.fashions[seats[k]];
|
||||||
}
|
}
|
||||||
int serial = room_open(sandbox.code);
|
int serial = room_open(sandbox.code);
|
||||||
Room& game = rooms[serial];
|
Room& game = rooms[serial];
|
||||||
|
|||||||
+4
-1
@@ -364,7 +364,7 @@ void Chat::command(Game& game, const std::string& text)
|
|||||||
system("Unknown command. Type /help for a list of commands.");
|
system("Unknown command. Type /help for a list of commands.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the log goes: the bottom right, just above the other players' windows when they're showing.
|
// Where the log goes: the bottom right, just above the other players' windows (or a tip) when they're showing.
|
||||||
Rect Chat::log_area_get(Game& game)
|
Rect Chat::log_area_get(Game& game)
|
||||||
{
|
{
|
||||||
float bottom = CANVAS_HEIGHT - HOTBAR_MARGIN;
|
float bottom = CANVAS_HEIGHT - HOTBAR_MARGIN;
|
||||||
@@ -375,6 +375,9 @@ Rect Chat::log_area_get(Game& game)
|
|||||||
for (int k = 0; k < rivals.count; k++)
|
for (int k = 0; k < rivals.count; k++)
|
||||||
bottom = std::min(bottom, rivals.cards[k].y - CHAT_GAP);
|
bottom = std::min(bottom, rivals.cards[k].y - CHAT_GAP);
|
||||||
}
|
}
|
||||||
|
// A tip along the bottom (under the menu) keeps the log above it.
|
||||||
|
if (game.menu.isOpen && game.settings.isTipsShown)
|
||||||
|
bottom = std::min(bottom, game.ui.tip_low_get() - CHAT_GAP);
|
||||||
return {CANVAS_WIDTH - HOTBAR_MARGIN - CHAT_WIDTH, 0, CHAT_WIDTH, bottom};
|
return {CANVAS_WIDTH - HOTBAR_MARGIN - CHAT_WIDTH, 0, CHAT_WIDTH, bottom};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-13
@@ -227,20 +227,23 @@ extern "C" EMSCRIPTEN_KEEPALIVE void frame_hidden()
|
|||||||
|
|
||||||
// Runs the game in a window (on the web, filling the page and driven by the browser's frames), or with "--balance N"
|
// Runs the game in a window (on the web, filling the page and driven by the browser's frames), or with "--balance N"
|
||||||
// plays N bot matches headless and prints the results ("--balance N race" plays races).
|
// plays N bot matches headless and prints the results ("--balance N race" plays races).
|
||||||
// The lobby to join on starting, if any: from the page's ?lobby= in the browser, or --join on the command line.
|
// A launch option, if given: from the page's ?<query>= in the browser, or <flag> <value> on the command line. A lobby
|
||||||
static std::string lobby_requested(int argc, char** argv)
|
// to join on starting is ?lobby= or --join, with its password (if it needs one) ?pass= or --pass.
|
||||||
|
static std::string launch_option_get(int argc, char** argv, const char* query, const char* flag)
|
||||||
{
|
{
|
||||||
#ifdef SDL_PLATFORM_EMSCRIPTEN
|
#ifdef SDL_PLATFORM_EMSCRIPTEN
|
||||||
char* code = (char*)EM_ASM_PTR({
|
char* found = (char*)EM_ASM_PTR(
|
||||||
var code = new URLSearchParams(location.search).get('lobby');
|
{
|
||||||
return code ? stringToNewUTF8(code) : 0;
|
var value = new URLSearchParams(location.search).get(UTF8ToString($0));
|
||||||
});
|
return value ? stringToNewUTF8(value) : 0;
|
||||||
std::string lobby = code ? code : "";
|
},
|
||||||
free(code);
|
query);
|
||||||
return lobby;
|
std::string option = found ? found : "";
|
||||||
|
free(found);
|
||||||
|
return option;
|
||||||
#else
|
#else
|
||||||
for (int i = 1; i + 1 < argc; i++)
|
for (int i = 1; i + 1 < argc; i++)
|
||||||
if (SDL_strcmp(argv[i], "--join") == 0)
|
if (SDL_strcmp(argv[i], flag) == 0)
|
||||||
return argv[i + 1];
|
return argv[i + 1];
|
||||||
return "";
|
return "";
|
||||||
#endif
|
#endif
|
||||||
@@ -283,11 +286,11 @@ int main(int argc, char** argv)
|
|||||||
game.renderer.atlas_upload();
|
game.renderer.atlas_upload();
|
||||||
game.settings.apply(game);
|
game.settings.apply(game);
|
||||||
game.settings.restore(game);
|
game.settings.restore(game);
|
||||||
std::string lobby = lobby_requested(argc, argv);
|
std::string lobby = launch_option_get(argc, argv, "lobby", "--join");
|
||||||
if (!lobby.empty())
|
if (!lobby.empty())
|
||||||
{
|
{
|
||||||
game.net.lobby_join(game, lobby.c_str());
|
game.net.lobby_join(game, lobby, launch_option_get(argc, argv, "pass", "--pass"));
|
||||||
game.menu.online_open();
|
game.menu.online_open(lobby);
|
||||||
}
|
}
|
||||||
game.camera = camera_target_get(game);
|
game.camera = camera_target_get(game);
|
||||||
game.cameraPrevious = game.camera;
|
game.cameraPrevious = game.camera;
|
||||||
|
|||||||
+42
-32
@@ -620,15 +620,17 @@ void Match::sandbox_return(Sim& sim)
|
|||||||
fighter.state_reset(sim, 1);
|
fighter.state_reset(sim, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Online, opens a fighter's queued game: an area on the ground big enough for everyone it's for, as near the fighter
|
// Online, opens a fighter's queued game: an area on flat ground with a slot for everyone it's for, centered on the
|
||||||
// as there's room (on the floor below it, or the nearest stretch of floor to either side that's flat, clear overhead
|
// fighter where there's room, with the fighter standing still in its middle, readied up. It goes on the floor at or
|
||||||
// and not another queue's), with the fighter standing still at its left end, readied up.
|
// below the fighter, or the nearest one to either side with room, along a stretch that's flat, clear overhead and not
|
||||||
|
// another queue's, squeezing its slots (down to QUEUE_SLOT_MIN) to fit a short one.
|
||||||
void Match::queue_open(Sim& sim, int fighter, const Rules& wanted)
|
void Match::queue_open(Sim& sim, int fighter, const Rules& wanted)
|
||||||
{
|
{
|
||||||
World& world = sim.world;
|
World& world = sim.world;
|
||||||
Queue& queue = queues[fighter];
|
Queue& queue = queues[fighter];
|
||||||
queue = {.rules = rules_clean(wanted), .isActive = true};
|
queue = {.rules = rules_clean(wanted), .isActive = true};
|
||||||
float width = queue.rules.players * QUEUE_SLOT_WIDTH + QUEUE_MARGIN * 2;
|
float width = queue.rules.players * QUEUE_SLOT_WIDTH + QUEUE_MARGIN * 2;
|
||||||
|
float narrowest = queue.rules.players * QUEUE_SLOT_MIN + QUEUE_MARGIN * 2;
|
||||||
Vec2 size = world.size_get();
|
Vec2 size = world.size_get();
|
||||||
Fighter& queuer = sim.fighters[fighter];
|
Fighter& queuer = sim.fighters[fighter];
|
||||||
Vec2 feet = {queuer.body.box.x + queuer.body.box.w / 2, queuer.body.box.y + queuer.body.box.h};
|
Vec2 feet = {queuer.body.box.x + queuer.body.box.w / 2, queuer.body.box.y + queuer.body.box.h};
|
||||||
@@ -640,41 +642,49 @@ void Match::queue_open(Sim& sim, int fighter, const Rules& wanted)
|
|||||||
return y;
|
return y;
|
||||||
return -1.0f;
|
return -1.0f;
|
||||||
};
|
};
|
||||||
auto is_fitting = [&](float left, float floor) {
|
// Whether a queue's area could stand over a point on a floor: flat ground, clear overhead, nobody else's queue.
|
||||||
if (left < 0 || left + width > size.x || floor < QUEUE_HEIGHT)
|
auto is_standable = [&](float x, float floor) {
|
||||||
|
if (x < 0 || x > size.x || !world.is_blocking_at({x, floor + 1}) || world.is_blocking_at({x, floor - 1}))
|
||||||
return false;
|
return false;
|
||||||
for (float x = left; x <= left + width; x += QUEUE_PROBE_STEP)
|
for (float up = QUEUE_PROBE_STEP; up <= QUEUE_HEIGHT; up += QUEUE_PROBE_STEP)
|
||||||
{
|
if (world.is_solid_at({x, floor - up}))
|
||||||
if (!world.is_blocking_at({x, floor + 1}) || world.is_blocking_at({x, floor - 1}))
|
|
||||||
return false;
|
return false;
|
||||||
for (float up = QUEUE_PROBE_STEP; up <= QUEUE_HEIGHT; up += QUEUE_PROBE_STEP)
|
|
||||||
if (world.is_solid_at({x, floor - up}))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Rect area = {left, floor - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
|
|
||||||
for (int i = 0; i < sim.fighterCount; i++)
|
for (int i = 0; i < sim.fighterCount; i++)
|
||||||
if (i != fighter && queues[i].isActive && is_rect_overlapping(area, queues[i].area))
|
{
|
||||||
|
Rect other = queues[i].area;
|
||||||
|
if (i != fighter && queues[i].isActive && x >= other.x && x <= other.x + other.w && floor > other.y &&
|
||||||
|
floor - QUEUE_HEIGHT < other.y + other.h)
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
float start = std::round(feet.x - QUEUE_MARGIN - QUEUE_SLOT_WIDTH / 2);
|
// Places the area on the stretch of a floor around x, as wide as it can be up to its full width and centered on
|
||||||
queue.area = {start, feet.y - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
|
// middle where the stretch allows, if the stretch is long enough for it squeezed.
|
||||||
for (float offset = 0; offset < size.x; offset += QUEUE_PROBE_STEP)
|
auto place = [&](float x, float floor, float middle) {
|
||||||
{
|
if (floor < QUEUE_HEIGHT || !is_standable(x, floor))
|
||||||
bool isFound = false;
|
return false;
|
||||||
for (float left : {start + offset, start - offset})
|
float left = x;
|
||||||
{
|
float right = x;
|
||||||
float floor = floor_find(left + width / 2, std::round(feet.y - TILE_SIZE));
|
while (left - x > -width && is_standable(left - QUEUE_PROBE_STEP, floor))
|
||||||
if (floor < 0 || !is_fitting(left, floor))
|
left -= QUEUE_PROBE_STEP;
|
||||||
continue;
|
while (right - x < width && is_standable(right + QUEUE_PROBE_STEP, floor))
|
||||||
queue.area = {left, floor - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
|
right += QUEUE_PROBE_STEP;
|
||||||
isFound = true;
|
float span = std::min(width, right - left);
|
||||||
break;
|
if (span < narrowest)
|
||||||
}
|
return false;
|
||||||
if (isFound)
|
float center = std::clamp(middle, left + span / 2, right - span / 2);
|
||||||
break;
|
queue.area = {std::round(center - span / 2), floor - QUEUE_HEIGHT, std::round(span), QUEUE_HEIGHT};
|
||||||
}
|
return true;
|
||||||
queuer.position_set(sim, {queue.area.x + QUEUE_MARGIN + QUEUE_SLOT_WIDTH / 2, queue.area.y + queue.area.h});
|
};
|
||||||
|
|
||||||
|
queue.area = {std::round(feet.x - width / 2), feet.y - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
|
||||||
|
float below = std::round(feet.y - TILE_SIZE);
|
||||||
|
bool isPlaced = place(feet.x, floor_find(feet.x, below), feet.x);
|
||||||
|
for (float offset = QUEUE_PROBE_STEP; offset < size.x && !isPlaced; offset += QUEUE_PROBE_STEP)
|
||||||
|
for (float x : {feet.x + offset, feet.x - offset})
|
||||||
|
if (!isPlaced)
|
||||||
|
isPlaced = place(x, floor_find(x, below), x);
|
||||||
|
queuer.position_set(sim, {queue.area.x + queue.area.w / 2, queue.area.y + queue.area.h});
|
||||||
std::erase_if(sim.weapons.attacks, [&](Attack& attack) { return attack.owner == fighter; });
|
std::erase_if(sim.weapons.attacks, [&](Attack& attack) { return attack.owner == fighter; });
|
||||||
queuedIn[fighter] = fighter;
|
queuedIn[fighter] = fighter;
|
||||||
isQueueReady[fighter] = false;
|
isQueueReady[fighter] = false;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ constexpr int TIMER_MAX = 10;
|
|||||||
// A queued game's area: room per player, a margin each end, its height, how finely the ground's checked for one, and
|
// A queued game's area: room per player, a margin each end, its height, how finely the ground's checked for one, and
|
||||||
// how far below its floor feet still count as in it.
|
// how far below its floor feet still count as in it.
|
||||||
constexpr float QUEUE_SLOT_WIDTH = 24;
|
constexpr float QUEUE_SLOT_WIDTH = 24;
|
||||||
|
// The narrowest a queue's slots squeeze to, to fit a short stretch of floor (a fighter is BOX_WIDTH wide).
|
||||||
|
constexpr float QUEUE_SLOT_MIN = 14;
|
||||||
constexpr float QUEUE_MARGIN = 6;
|
constexpr float QUEUE_MARGIN = 6;
|
||||||
constexpr float QUEUE_HEIGHT = 40;
|
constexpr float QUEUE_HEIGHT = 40;
|
||||||
constexpr float QUEUE_PROBE_STEP = 4;
|
constexpr float QUEUE_PROBE_STEP = 4;
|
||||||
|
|||||||
+220
-46
@@ -13,6 +13,15 @@ constexpr float BUTTON_WIDTH = 120;
|
|||||||
constexpr float CODE_FIELD_WIDTH = 60;
|
constexpr float CODE_FIELD_WIDTH = 60;
|
||||||
constexpr float CODE_SCALE = 2;
|
constexpr float CODE_SCALE = 2;
|
||||||
constexpr float JOIN_BUTTON_WIDTH = 40;
|
constexpr float JOIN_BUTTON_WIDTH = 40;
|
||||||
|
constexpr float PASSWORD_FIELD_WIDTH = 100;
|
||||||
|
// The Unlisted and Public choices' spacing, and how often the lobby list is asked for while it shows.
|
||||||
|
constexpr float RADIO_SPACING = 96;
|
||||||
|
constexpr Uint64 LISTING_INTERVAL = 3000;
|
||||||
|
// A listed lobby's padlock (its shackle the top part), and how faint the rule under each lobby is.
|
||||||
|
constexpr float LOCK_WIDTH = 7;
|
||||||
|
constexpr float LOCK_HEIGHT = 8;
|
||||||
|
constexpr float LOCK_SHACKLE = 3;
|
||||||
|
constexpr float LISTING_RULE = 0.4f;
|
||||||
constexpr float COPY_BUTTON_WIDTH = 80;
|
constexpr float COPY_BUTTON_WIDTH = 80;
|
||||||
constexpr Uint64 COPIED_SHOWN = 1500;
|
constexpr Uint64 COPIED_SHOWN = 1500;
|
||||||
constexpr const char LOBBY_PAGE_DEFAULT[] = "https://shweetz.net/smack-a-brother/";
|
constexpr const char LOBBY_PAGE_DEFAULT[] = "https://shweetz.net/smack-a-brother/";
|
||||||
@@ -446,9 +455,9 @@ void Menu::about_draw(Game& game, Rect area)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lays the header out: the tabs shown, then in a match its buttons (Ready while a round counts down, and Forfeit, or
|
// Lays the header out: the tabs shown, then in a match its buttons (Ready while a round counts down, and Forfeit, or
|
||||||
// one to stop spectating), and on desktop Quit last. Each cell is as wide as its name (a button's longest label) and
|
// one to stop spectating), and in the sandbox on desktop, Quit. Each cell is as wide as its name (a button's longest
|
||||||
// padding, with the room left shared out evenly, so the names sit evenly spaced whatever is showing. Tab names are big
|
// label) and padding, with the room left shared out evenly, so the names sit evenly spaced whatever is showing. Tab
|
||||||
// when they all fit.
|
// names are big when they all fit.
|
||||||
HeaderLayout Menu::header_layout_get(Game& game, Rect area)
|
HeaderLayout Menu::header_layout_get(Game& game, Rect area)
|
||||||
{
|
{
|
||||||
Text& text = game.text;
|
Text& text = game.text;
|
||||||
@@ -464,7 +473,8 @@ HeaderLayout Menu::header_layout_get(Game& game, Rect area)
|
|||||||
header.buttons[header.buttonCount++] = BUTTON_FORFEIT;
|
header.buttons[header.buttonCount++] = BUTTON_FORFEIT;
|
||||||
}
|
}
|
||||||
#ifndef __EMSCRIPTEN__
|
#ifndef __EMSCRIPTEN__
|
||||||
header.buttons[header.buttonCount++] = BUTTON_QUIT;
|
if (match.state == MATCH_PRACTICE)
|
||||||
|
header.buttons[header.buttonCount++] = BUTTON_QUIT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
float buttons = 0;
|
float buttons = 0;
|
||||||
@@ -790,10 +800,188 @@ static Color member_color_get(Game& game, int member)
|
|||||||
return color >= 0 ? PALETTE[color] : game.ui.tint;
|
return color >= 0 ? PALETTE[color] : game.ui.tint;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Online play. Outside a lobby: make one, or type a lobby's code to join it, with how the last attempt is going (or
|
// Text made safe for a link's query (letters and digits as they are, everything else as %XX).
|
||||||
// why it failed), and under it where lobbies are hosted. In one: its code (to read out or copy to friends) and leaving,
|
static std::string url_encode(const std::string& text)
|
||||||
// queueing a game in its sandbox, and everyone in it with how their connection is doing, then the games being played,
|
{
|
||||||
// to spectate.
|
std::string encoded;
|
||||||
|
for (unsigned char c : text)
|
||||||
|
{
|
||||||
|
char hex[4];
|
||||||
|
SDL_snprintf(hex, sizeof(hex), "%%%02X", c);
|
||||||
|
encoded += SDL_isalnum(c) ? std::string(1, (char)c) : std::string(hex);
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A padlock with its top-left corner at a point: a shackle over a body.
|
||||||
|
static void lock_draw(Renderer& renderer, Vec2 corner, Color color)
|
||||||
|
{
|
||||||
|
renderer.rect_draw({corner.x + 1, corner.y, LOCK_WIDTH - 2, 1}, color);
|
||||||
|
renderer.rect_draw({corner.x + 1, corner.y, 1, LOCK_SHACKLE}, color);
|
||||||
|
renderer.rect_draw({corner.x + LOCK_WIDTH - 2, corner.y, 1, LOCK_SHACKLE}, color);
|
||||||
|
renderer.rect_draw({corner.x, corner.y + LOCK_SHACKLE, LOCK_WIDTH, LOCK_HEIGHT - LOCK_SHACKLE}, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online play outside a lobby, a scrolling page: the public lobbies (asked for every LISTING_INTERVAL while this
|
||||||
|
// shows), each with its name, how full it is, a padlock when it needs a password, its code, a little of each player and
|
||||||
|
// a Join button (asking for the password first when there is one), then joining one by its code, making one (its name,
|
||||||
|
// its password and who can find it), and where lobbies are hosted. How the last attempt is going (or why it failed)
|
||||||
|
// shows under whichever made it.
|
||||||
|
void Menu::lobbies_draw(Game& game, Rect area)
|
||||||
|
{
|
||||||
|
Ui& ui = game.ui;
|
||||||
|
Net& net = game.net;
|
||||||
|
Text& text = game.text;
|
||||||
|
Renderer& renderer = game.renderer;
|
||||||
|
Uint64 now = SDL_GetTicks();
|
||||||
|
if (now >= nextListing)
|
||||||
|
{
|
||||||
|
net.lobbies_request(game);
|
||||||
|
nextListing = now + LISTING_INTERVAL;
|
||||||
|
}
|
||||||
|
if (net.error == LOBBY_PASSWORD_NEEDED)
|
||||||
|
passwordCode = joinTarget;
|
||||||
|
bool isBusy = net.state == NET_CONNECTING || net.state == NET_JOINING;
|
||||||
|
float rowWidth = area.w - SCROLL_WIDTH * 2;
|
||||||
|
float x = area.x + LABEL_WIDTH;
|
||||||
|
float y = 0;
|
||||||
|
auto status_draw = [&](float top) {
|
||||||
|
const char* line = net.state == NET_CONNECTING ? "Connecting to the server..."
|
||||||
|
: net.state == NET_JOINING ? "Joining..."
|
||||||
|
: net.error.c_str();
|
||||||
|
text.draw(renderer, line, {.color = net.error.empty() ? ui.shade : ui.tint, .position = {area.x, top}});
|
||||||
|
};
|
||||||
|
auto is_status_shown = [&](const std::string& target) {
|
||||||
|
return joinTarget == target && (isBusy || !net.error.empty());
|
||||||
|
};
|
||||||
|
// A Join button, with the password field before it once a locked lobby asks for one.
|
||||||
|
auto join_draw = [&](float left, float top, const std::string& code, bool isLocked) {
|
||||||
|
bool isAsking = !code.empty() && passwordCode == code;
|
||||||
|
if (isAsking)
|
||||||
|
{
|
||||||
|
ui.field_draw(game, {left, top, PASSWORD_FIELD_WIDTH, WIDGET_HEIGHT}, joinPassword, sizeof(joinPassword));
|
||||||
|
left += PASSWORD_FIELD_WIDTH + PADDING;
|
||||||
|
}
|
||||||
|
ui.widgets_enable(!isBusy && code.size() == LOBBY_CODE_LENGTH);
|
||||||
|
if (ui.button_draw(game, {left, top, JOIN_BUTTON_WIDTH, WIDGET_HEIGHT}, "Join", code.c_str()))
|
||||||
|
{
|
||||||
|
if (isLocked && !isAsking)
|
||||||
|
{
|
||||||
|
passwordCode = code;
|
||||||
|
joinPassword[0] = '\0';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
joinTarget = code;
|
||||||
|
net.lobby_join(game, code, isAsking ? joinPassword : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.widgets_enable(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.page_begin(game, onlinePage, area);
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
ui.separator_draw(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "Browser");
|
||||||
|
bool isListFailed = !net.listError.empty();
|
||||||
|
if ((isListFailed || net.listings.empty()) && ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
text.draw(renderer, isListFailed ? net.listError.c_str() : "No public lobbies right now",
|
||||||
|
{.color = isListFailed ? ui.tint : ui.shade, .position = {area.x, y + TEXT_ROW_INSET}});
|
||||||
|
for (LobbyListing& listing : net.listings)
|
||||||
|
{
|
||||||
|
char count[TOOLTIP_LENGTH];
|
||||||
|
SDL_snprintf(count, sizeof(count), "(%d/%d)", (int)listing.players.size(), LOBBY_MEMBERS_MAX);
|
||||||
|
float right = area.x + rowWidth - (listing.isLocked ? LOCK_WIDTH + PADDING : 0);
|
||||||
|
float countX = right - text.size_get(count).x;
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
{
|
||||||
|
std::string name = listing.name;
|
||||||
|
while (!name.empty() && text.size_get(name.c_str()).x > countX - area.x - PADDING)
|
||||||
|
name.pop_back();
|
||||||
|
text.draw(renderer, name.c_str(), {.color = ui.tint, .position = {area.x, y + TEXT_ROW_INSET}});
|
||||||
|
text.draw(renderer, count, {.color = ui.tint, .position = {countX, y + TEXT_ROW_INSET}});
|
||||||
|
if (listing.isLocked)
|
||||||
|
lock_draw(renderer, {right + PADDING, y + TEXT_ROW_INSET}, ui.tint);
|
||||||
|
}
|
||||||
|
if (ui.row_get(onlinePage, LINE_HEIGHT, y))
|
||||||
|
text.draw(renderer, listing.code.c_str(), {.color = ui.shade, .position = {area.x, y}});
|
||||||
|
if (ui.row_get(onlinePage, MINI_PLAYER_ROW, y))
|
||||||
|
for (size_t k = 0; k < listing.players.size(); k++)
|
||||||
|
{
|
||||||
|
auto [color, worn] = listing.players[k];
|
||||||
|
Vec2 feet = {area.x + k * (MINI_PLAYER_WIDTH + PADDING / 2) + MINI_PLAYER_WIDTH / 2,
|
||||||
|
y + MINI_PLAYER_ROW - 1};
|
||||||
|
ui.player_draw(game, feet, color >= 0 ? PALETTE[color % COLOR_COUNT] : ui.shade, 1,
|
||||||
|
fashion_clean(worn));
|
||||||
|
}
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
join_draw(area.x, y, listing.code, listing.isLocked);
|
||||||
|
if (is_status_shown(listing.code) && ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
status_draw(y + TEXT_ROW_INSET);
|
||||||
|
if (ui.row_get(onlinePage, PADDING, y))
|
||||||
|
renderer.rect_draw({area.x, std::round(y + PADDING / 2), rowWidth, 1}, color_fade(ui.shade, LISTING_RULE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
ui.separator_draw(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "Join");
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
{
|
||||||
|
ui.label_draw(game, "Code", {area.x, y});
|
||||||
|
ui.field_draw(game, {x, y, CODE_FIELD_WIDTH, WIDGET_HEIGHT}, joinCode, sizeof(joinCode));
|
||||||
|
for (char& c : joinCode)
|
||||||
|
c = (char)SDL_toupper(c);
|
||||||
|
join_draw(x + CODE_FIELD_WIDTH + PADDING, y, joinCode, false);
|
||||||
|
}
|
||||||
|
bool isListed = std::any_of(net.listings.begin(), net.listings.end(),
|
||||||
|
[&](LobbyListing& listing) { return listing.code == joinTarget; });
|
||||||
|
if (!joinTarget.empty() && !isListed && is_status_shown(joinTarget) && ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
status_draw(y + TEXT_ROW_INSET);
|
||||||
|
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
ui.separator_draw(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "Create");
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
{
|
||||||
|
ui.hint_set(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "The lobby's name. Blank\nnames it after its code.");
|
||||||
|
ui.label_draw(game, "Name", {area.x, y});
|
||||||
|
ui.field_draw(game, {x, y, rowWidth - LABEL_WIDTH, WIDGET_HEIGHT}, lobbyOptions.name,
|
||||||
|
sizeof(lobbyOptions.name));
|
||||||
|
}
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
{
|
||||||
|
ui.hint_set(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "What joining it takes.\nBlank for none.");
|
||||||
|
ui.label_draw(game, "Password", {area.x, y});
|
||||||
|
ui.field_draw(game, {x, y, rowWidth - LABEL_WIDTH, WIDGET_HEIGHT}, lobbyOptions.password,
|
||||||
|
sizeof(lobbyOptions.password));
|
||||||
|
}
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
{
|
||||||
|
ui.hint_set(game, {x, y, RADIO_SPACING, WIDGET_HEIGHT}, "Only its code or link\nfinds it.");
|
||||||
|
ui.hint_set(game, {x + RADIO_SPACING, y, RADIO_SPACING, WIDGET_HEIGHT}, "Anyone can find it in\nthe Browser.");
|
||||||
|
ui.radio_draw(game, {x, y}, "Unlisted", lobbyOptions.visibility, LOBBY_UNLISTED);
|
||||||
|
ui.radio_draw(game, {x + RADIO_SPACING, y}, "Public", lobbyOptions.visibility, LOBBY_PUBLIC);
|
||||||
|
}
|
||||||
|
if (ui.row_get(onlinePage, PLAY_HEIGHT + PADDING, y))
|
||||||
|
{
|
||||||
|
ui.widgets_enable(!isBusy);
|
||||||
|
if (ui.button_draw(game, {area.x, y, rowWidth, PLAY_HEIGHT}, "Create"))
|
||||||
|
{
|
||||||
|
joinTarget.clear();
|
||||||
|
net.lobby_create(game, lobbyOptions);
|
||||||
|
}
|
||||||
|
ui.widgets_enable(true);
|
||||||
|
}
|
||||||
|
if (is_status_shown("") && ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
status_draw(y + TEXT_ROW_INSET);
|
||||||
|
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT, y))
|
||||||
|
ui.separator_draw(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, "Settings");
|
||||||
|
if (ui.row_get(onlinePage, ROW_HEIGHT * 2, y))
|
||||||
|
game.settings.online_draw(game, {area.x, y, rowWidth, WIDGET_HEIGHT}, LABEL_WIDTH);
|
||||||
|
ui.page_end(game, onlinePage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online play. Outside a lobby, see lobbies_draw. In one: its name and code (to read out or copy to friends) and
|
||||||
|
// leaving, queueing a game in its sandbox, and everyone in it with how their connection is doing, then the games being
|
||||||
|
// played, to spectate.
|
||||||
void Menu::online_draw(Game& game, Rect area)
|
void Menu::online_draw(Game& game, Rect area)
|
||||||
{
|
{
|
||||||
Ui& ui = game.ui;
|
Ui& ui = game.ui;
|
||||||
@@ -806,43 +994,22 @@ void Menu::online_draw(Game& game, Rect area)
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (net.state != NET_LOBBY)
|
if (net.state != NET_LOBBY)
|
||||||
{
|
return lobbies_draw(game, area);
|
||||||
bool isBusy = net.state == NET_CONNECTING || net.state == NET_JOINING;
|
|
||||||
ui.separator_draw(game, {area.x, y, area.w, WIDGET_HEIGHT}, "Lobby");
|
|
||||||
y += ROW_HEIGHT;
|
|
||||||
ui.widgets_enable(!isBusy);
|
|
||||||
if (ui.button_draw(game, {area.x, y, area.w, PLAY_HEIGHT}, "Create"))
|
|
||||||
net.lobby_create(game);
|
|
||||||
y += PLAY_HEIGHT + PADDING;
|
|
||||||
ui.label_draw(game, "Code", {area.x, y});
|
|
||||||
Rect field = {area.x + LABEL_WIDTH, y, CODE_FIELD_WIDTH, WIDGET_HEIGHT};
|
|
||||||
ui.field_draw(game, field, joinCode, sizeof(joinCode));
|
|
||||||
for (char& c : joinCode)
|
|
||||||
c = (char)SDL_toupper(c);
|
|
||||||
ui.widgets_enable(!isBusy && SDL_strlen(joinCode) == LOBBY_CODE_LENGTH);
|
|
||||||
if (ui.button_draw(game, {field.x + field.w + PADDING, y, JOIN_BUTTON_WIDTH, WIDGET_HEIGHT}, "Join"))
|
|
||||||
net.lobby_join(game, joinCode);
|
|
||||||
ui.widgets_enable(true);
|
|
||||||
y += ROW_HEIGHT + PADDING;
|
|
||||||
if (net.state == NET_CONNECTING)
|
|
||||||
status_draw("Connecting to the server...", false);
|
|
||||||
else if (net.state == NET_JOINING)
|
|
||||||
status_draw("Joining...", false);
|
|
||||||
else if (!net.error.empty())
|
|
||||||
status_draw(net.error.c_str(), true);
|
|
||||||
|
|
||||||
// The online settings, as in Settings.
|
// The lobby's name, big (cut short to fit), its code faded under it, then buttons to copy the code (or a link to
|
||||||
ui.separator_draw(game, {area.x, y, area.w, WIDGET_HEIGHT}, "Online");
|
// it, with its password when it has one) and to leave.
|
||||||
y += ROW_HEIGHT;
|
|
||||||
game.settings.online_draw(game, {area.x, y, area.w, WIDGET_HEIGHT}, LABEL_WIDTH);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The lobby's code, big, with buttons to copy it (or a link to it) and to leave under it.
|
|
||||||
char line[TOOLTIP_LENGTH];
|
char line[TOOLTIP_LENGTH];
|
||||||
SDL_snprintf(line, sizeof(line), "Lobby %s", net.code.c_str());
|
std::string name = net.lobbyName;
|
||||||
text.draw(game.renderer, line, {.color = ui.tint, .position = {area.x, y}, .scale = {CODE_SCALE, CODE_SCALE}});
|
while (!name.empty() && text.size_get((name + "...").c_str(), FONT_SMALL, CODE_SCALE).x > area.w &&
|
||||||
y += text.line_height_get(FONT_SMALL, CODE_SCALE) + PADDING;
|
text.size_get(name.c_str(), FONT_SMALL, CODE_SCALE).x > area.w)
|
||||||
|
name.pop_back();
|
||||||
|
bool isCut = name != net.lobbyName;
|
||||||
|
text.draw(game.renderer, (isCut ? name + "..." : name).c_str(),
|
||||||
|
{.color = ui.tint, .position = {area.x, y}, .scale = {CODE_SCALE, CODE_SCALE}});
|
||||||
|
y += text.line_height_get(FONT_SMALL, CODE_SCALE);
|
||||||
|
SDL_snprintf(line, sizeof(line), "Code %s", net.code.c_str());
|
||||||
|
text.draw(game.renderer, line, {.color = ui.shade, .position = {area.x, y}});
|
||||||
|
y += LINE_HEIGHT + PADDING;
|
||||||
Uint64 now = SDL_GetTicks();
|
Uint64 now = SDL_GetTicks();
|
||||||
float third = std::floor((area.w - PADDING * 2) / 3);
|
float third = std::floor((area.w - PADDING * 2) / 3);
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
@@ -856,7 +1023,9 @@ void Menu::online_draw(Game& game, Rect area)
|
|||||||
: "Copy Link",
|
: "Copy Link",
|
||||||
i == 0 ? "copy code" : "copy link"))
|
i == 0 ? "copy code" : "copy link"))
|
||||||
{
|
{
|
||||||
clipboard_set(i == 0 ? net.code : lobby_page_get() + "?lobby=" + net.code);
|
std::string link = lobby_page_get() + "?lobby=" + net.code;
|
||||||
|
link += net.password.empty() ? "" : "&pass=" + url_encode(net.password);
|
||||||
|
clipboard_set(i == 0 ? net.code : link);
|
||||||
copiedButton = i;
|
copiedButton = i;
|
||||||
copiedUntil = now + COPIED_SHOWN;
|
copiedUntil = now + COPIED_SHOWN;
|
||||||
}
|
}
|
||||||
@@ -1205,13 +1374,18 @@ void Menu::players_join(Game& game)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens the menu on the Multiplayer page, as joining a lobby from a link does.
|
// Opens the menu on the Multiplayer page, as joining a lobby from a link does, with its code ready to join again (and
|
||||||
void Menu::online_open()
|
// its password asked for there, should it need one).
|
||||||
|
void Menu::online_open(const std::string& code)
|
||||||
{
|
{
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
tab = TAB_PLAY;
|
tab = TAB_PLAY;
|
||||||
page = PAGE_ONLINE;
|
page = PAGE_ONLINE;
|
||||||
isMultiplayer = true;
|
isMultiplayer = true;
|
||||||
|
SDL_strlcpy(joinCode, code.c_str(), sizeof(joinCode));
|
||||||
|
for (char& c : joinCode)
|
||||||
|
c = (char)SDL_toupper(c);
|
||||||
|
joinTarget = joinCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens the menu on the item tab a round's countdown lets fighters choose from.
|
// Opens the menu on the item tab a round's countdown lets fighters choose from.
|
||||||
|
|||||||
+11
-1
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "net.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "ui.h"
|
#include "ui.h"
|
||||||
@@ -93,7 +94,7 @@ public:
|
|||||||
void update(Game& game);
|
void update(Game& game);
|
||||||
void draw(Game& game);
|
void draw(Game& game);
|
||||||
void choice_open(Game& game);
|
void choice_open(Game& game);
|
||||||
void online_open();
|
void online_open(const std::string& code);
|
||||||
void targets_reset(Game& game);
|
void targets_reset(Game& game);
|
||||||
void target_fill(Game& game);
|
void target_fill(Game& game);
|
||||||
bool is_bars_open();
|
bool is_bars_open();
|
||||||
@@ -106,6 +107,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
Page description;
|
Page description;
|
||||||
Page lobbyPage;
|
Page lobbyPage;
|
||||||
|
Page onlinePage;
|
||||||
int described = -1;
|
int described = -1;
|
||||||
// A hotbar slot clicked to read about (its item shown instead of the selection) while still on the tab it was
|
// A hotbar slot clicked to read about (its item shown instead of the selection) while still on the tab it was
|
||||||
// clicked on, or -1.
|
// clicked on, or -1.
|
||||||
@@ -130,6 +132,13 @@ private:
|
|||||||
Rules queueRules = rules_get(MODE_DEATHMATCH);
|
Rules queueRules = rules_get(MODE_DEATHMATCH);
|
||||||
// The lobby code typed on the Multiplayer page.
|
// The lobby code typed on the Multiplayer page.
|
||||||
char joinCode[LOBBY_CODE_LENGTH + 1] = {};
|
char joinCode[LOBBY_CODE_LENGTH + 1] = {};
|
||||||
|
// A new lobby's options, the password typed to join one (shown for passwordCode's lobby), the lobby the last join
|
||||||
|
// went to (whose row shows how it's going), and when to ask for the lobby list again.
|
||||||
|
LobbyOptions lobbyOptions;
|
||||||
|
char joinPassword[LOBBY_PASSWORD_MAX + 1] = {};
|
||||||
|
std::string passwordCode;
|
||||||
|
std::string joinTarget;
|
||||||
|
Uint64 nextListing = 0;
|
||||||
// Which lobby copy button (code or link) was last used, and until when it says so.
|
// Which lobby copy button (code or link) was last used, and until when it says so.
|
||||||
int copiedButton = -1;
|
int copiedButton = -1;
|
||||||
Uint64 copiedUntil = 0;
|
Uint64 copiedUntil = 0;
|
||||||
@@ -153,6 +162,7 @@ private:
|
|||||||
void header_draw(Game& game, MenuLayout& layout);
|
void header_draw(Game& game, MenuLayout& layout);
|
||||||
void items_draw(Game& game, MenuLayout& layout);
|
void items_draw(Game& game, MenuLayout& layout);
|
||||||
void play_draw(Game& game, MenuLayout& layout);
|
void play_draw(Game& game, MenuLayout& layout);
|
||||||
|
void lobbies_draw(Game& game, Rect area);
|
||||||
void about_draw(Game& game, Rect area);
|
void about_draw(Game& game, Rect area);
|
||||||
void rules_draw(Game& game, Rect area);
|
void rules_draw(Game& game, Rect area);
|
||||||
void online_draw(Game& game, Rect area);
|
void online_draw(Game& game, Rect area);
|
||||||
|
|||||||
+80
-16
@@ -35,6 +35,8 @@ struct Net::Impl
|
|||||||
std::shared_ptr<rtc::WebSocket> socket;
|
std::shared_ptr<rtc::WebSocket> socket;
|
||||||
PeerLink link;
|
PeerLink link;
|
||||||
std::string pending;
|
std::string pending;
|
||||||
|
// The state to wait in once the pending request goes (see request_send).
|
||||||
|
NetState waiting = NET_OFFLINE;
|
||||||
simdjson::dom::parser parser;
|
simdjson::dom::parser parser;
|
||||||
Uint64 nextPing = 0;
|
Uint64 nextPing = 0;
|
||||||
// The network simulator (SAB_NET_LAG milliseconds on everything sent, SAB_NET_LOSS percent of input packets
|
// The network simulator (SAB_NET_LAG milliseconds on everything sent, SAB_NET_LOSS percent of input packets
|
||||||
@@ -107,16 +109,32 @@ void Net::shutdown()
|
|||||||
impl->posted.clear();
|
impl->posted.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Net::lobby_create(Game& game)
|
void Net::lobby_create(Game& game, LobbyOptions& options)
|
||||||
{
|
{
|
||||||
request_send(game, "{\"type\":\"create\",\"name\":" + json_quote(game.settings.name) +
|
password = options.password;
|
||||||
",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}");
|
request_send(game,
|
||||||
|
"{\"type\":\"create\",\"name\":" + json_quote(game.settings.name) +
|
||||||
|
",\"lobby\":" + json_quote(options.name) + ",\"password\":" + json_quote(options.password) +
|
||||||
|
",\"public\":" + (options.visibility == LOBBY_PUBLIC ? "true" : "false") +
|
||||||
|
",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}",
|
||||||
|
NET_JOINING);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Net::lobby_join(Game& game, const char* lobby)
|
void Net::lobby_join(Game& game, const std::string& lobby, const std::string& lobbyPassword)
|
||||||
{
|
{
|
||||||
request_send(game, "{\"type\":\"join\",\"code\":" + json_quote(lobby) + ",\"name\":" +
|
password = lobbyPassword;
|
||||||
json_quote(game.settings.name) + ",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}");
|
request_send(game,
|
||||||
|
"{\"type\":\"join\",\"code\":" + json_quote(lobby) + ",\"password\":" + json_quote(lobbyPassword) +
|
||||||
|
",\"name\":" + json_quote(game.settings.name) +
|
||||||
|
",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}",
|
||||||
|
NET_JOINING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asks the server for its public lobbies (see listings), quietly: it doesn't count as joining anything.
|
||||||
|
void Net::lobbies_request(Game& game)
|
||||||
|
{
|
||||||
|
if (state == NET_OFFLINE)
|
||||||
|
request_send(game, "{\"type\":\"list\",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}", NET_OFFLINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaves the lobby, closing the WebRTC connection but keeping the socket to the server.
|
// Leaves the lobby, closing the WebRTC connection but keeping the socket to the server.
|
||||||
@@ -131,23 +149,32 @@ void Net::lobby_leave()
|
|||||||
packets.clear();
|
packets.clear();
|
||||||
kicks.clear();
|
kicks.clear();
|
||||||
code.clear();
|
code.clear();
|
||||||
|
lobbyName.clear();
|
||||||
|
password.clear();
|
||||||
selfId = hostId = -1;
|
selfId = hostId = -1;
|
||||||
state = NET_OFFLINE;
|
state = NET_OFFLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends a create or join, connecting to the server first if need be (it goes out once the socket opens). The
|
// Sends a create or join, connecting to the server first if need be (it goes out once the socket opens). The
|
||||||
// server is the one in Settings (Host), or SAB_SERVER_URL when set (for testing against a local one).
|
// server is the one in Settings (Host), or SAB_SERVER_URL when set (for testing against a local one).
|
||||||
void Net::request_send(Game& game, const std::string& request)
|
void Net::request_send(Game& game, const std::string& request, NetState waiting)
|
||||||
{
|
{
|
||||||
error.clear();
|
bool isQuiet = waiting == NET_OFFLINE;
|
||||||
|
if (!isQuiet)
|
||||||
|
error.clear();
|
||||||
|
isListing = isQuiet;
|
||||||
if (impl->socket && impl->socket->isOpen())
|
if (impl->socket && impl->socket->isOpen())
|
||||||
{
|
{
|
||||||
impl->socket->send(request);
|
impl->socket->send(request);
|
||||||
state = NET_JOINING;
|
state = waiting;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A quiet request never takes the place of one waiting to join.
|
||||||
|
if (isQuiet && !impl->pending.empty())
|
||||||
|
return;
|
||||||
impl->pending = request;
|
impl->pending = request;
|
||||||
state = NET_CONNECTING;
|
impl->waiting = waiting;
|
||||||
|
state = isQuiet ? state : NET_CONNECTING;
|
||||||
if (impl->socket)
|
if (impl->socket)
|
||||||
return;
|
return;
|
||||||
impl->socket = std::make_shared<rtc::WebSocket>();
|
impl->socket = std::make_shared<rtc::WebSocket>();
|
||||||
@@ -164,7 +191,7 @@ void Net::request_send(Game& game, const std::string& request)
|
|||||||
return;
|
return;
|
||||||
impl->socket->send(impl->pending);
|
impl->socket->send(impl->pending);
|
||||||
impl->pending.clear();
|
impl->pending.clear();
|
||||||
state = NET_JOINING;
|
state = impl->waiting;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
impl->socket->onMessage([this, shared](rtc::message_variant data) {
|
impl->socket->onMessage([this, shared](rtc::message_variant data) {
|
||||||
@@ -182,17 +209,24 @@ void Net::request_send(Game& game, const std::string& request)
|
|||||||
lobby_leave();
|
lobby_leave();
|
||||||
impl->socket.reset();
|
impl->socket.reset();
|
||||||
impl->pending.clear();
|
impl->pending.clear();
|
||||||
if (error.empty())
|
std::string& failure = failure_get();
|
||||||
error = "Lost the connection to the server";
|
if (failure.empty())
|
||||||
|
failure = "Lost the connection to the server";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
impl->socket->onError([this, shared](std::string message) {
|
impl->socket->onError([this, shared](std::string message) {
|
||||||
shared->post([this, message]() { error = "Couldn't reach the server (" + message + ")"; });
|
shared->post([this, message]() { failure_get() = "Couldn't reach the server (" + message + ")"; });
|
||||||
});
|
});
|
||||||
const char* url = SDL_getenv("SAB_SERVER_URL");
|
const char* url = SDL_getenv("SAB_SERVER_URL");
|
||||||
impl->socket->open(url ? url : game.settings.serverUrl);
|
impl->socket->open(url ? url : game.settings.serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Where a failure goes: listError for a request only asking for the lobby list, error for everything else.
|
||||||
|
std::string& Net::failure_get()
|
||||||
|
{
|
||||||
|
return isListing ? listError : error;
|
||||||
|
}
|
||||||
|
|
||||||
NetMember* Net::member_get(int id)
|
NetMember* Net::member_get(int id)
|
||||||
{
|
{
|
||||||
for (NetMember& member : members)
|
for (NetMember& member : members)
|
||||||
@@ -213,8 +247,8 @@ void Net::message_handle(const std::string& text)
|
|||||||
|
|
||||||
if (type == "error")
|
if (type == "error")
|
||||||
{
|
{
|
||||||
error = message["message"].get(field) ? "Server error" : std::string(field);
|
failure_get() = message["message"].get(field) ? "Server error" : std::string(field);
|
||||||
if (state != NET_LOBBY)
|
if (state != NET_LOBBY && !isListing)
|
||||||
state = NET_OFFLINE;
|
state = NET_OFFLINE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -224,6 +258,7 @@ void Net::message_handle(const std::string& text)
|
|||||||
selfId = message["id"].get(number) ? -1 : (int)number;
|
selfId = message["id"].get(number) ? -1 : (int)number;
|
||||||
hostId = message["host"].get(number) ? -1 : (int)number;
|
hostId = message["host"].get(number) ? -1 : (int)number;
|
||||||
code = message["code"].get(field) ? "" : std::string(field);
|
code = message["code"].get(field) ? "" : std::string(field);
|
||||||
|
lobbyName = message["name"].get(field) ? "Lobby " + code : std::string(field);
|
||||||
simdjson::dom::array list;
|
simdjson::dom::array list;
|
||||||
if (!message["members"].get(list))
|
if (!message["members"].get(list))
|
||||||
for (simdjson::dom::element entry : list)
|
for (simdjson::dom::element entry : list)
|
||||||
@@ -237,6 +272,35 @@ void Net::message_handle(const std::string& text)
|
|||||||
link_open();
|
link_open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type == "lobbies")
|
||||||
|
{
|
||||||
|
isListing = false;
|
||||||
|
listError.clear();
|
||||||
|
listings.clear();
|
||||||
|
simdjson::dom::array list;
|
||||||
|
if (message["lobbies"].get(list))
|
||||||
|
return;
|
||||||
|
for (simdjson::dom::element entry : list)
|
||||||
|
{
|
||||||
|
LobbyListing listing;
|
||||||
|
listing.code = entry["code"].get(field) ? "" : std::string(field);
|
||||||
|
listing.name = text_clean(entry["name"].get(field) ? "" : field, LOBBY_NAME_MAX);
|
||||||
|
bool isLocked = false;
|
||||||
|
listing.isLocked = !entry["locked"].get(isLocked) && isLocked;
|
||||||
|
simdjson::dom::array players;
|
||||||
|
if (!entry["players"].get(players))
|
||||||
|
for (simdjson::dom::element player : players)
|
||||||
|
{
|
||||||
|
int64_t color = -1;
|
||||||
|
int64_t fashion = 0;
|
||||||
|
std::ignore = player["color"].get(color);
|
||||||
|
std::ignore = player["fashion"].get(fashion);
|
||||||
|
listing.players.push_back({(int)color, (int)fashion});
|
||||||
|
}
|
||||||
|
listings.push_back(listing);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type == "member_join")
|
if (type == "member_join")
|
||||||
{
|
{
|
||||||
NetMember member;
|
NetMember member;
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ enum NetState
|
|||||||
NET_LOBBY
|
NET_LOBBY
|
||||||
};
|
};
|
||||||
|
|
||||||
// Someone in the lobby (this screen's player too): their server id and name, and their connection's round trip to the
|
|
||||||
// server in milliseconds (-1 while it isn't known: this screen only measures its own).
|
|
||||||
// A line for the chat log from the server, and the member it's about (-1 for none), in whose color it shows.
|
// A line for the chat log from the server, and the member it's about (-1 for none), in whose color it shows.
|
||||||
struct NetNotice
|
struct NetNotice
|
||||||
{
|
{
|
||||||
@@ -24,6 +22,34 @@ struct NetNotice
|
|||||||
int member = -1;
|
int member = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A public lobby as the server lists it: its code and name, whether it needs a password, and each player's color
|
||||||
|
// (-1 while unassigned) and fashion, for little previews.
|
||||||
|
struct LobbyListing
|
||||||
|
{
|
||||||
|
std::string code;
|
||||||
|
std::string name;
|
||||||
|
std::vector<std::pair<int, int>> players;
|
||||||
|
bool isLocked = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Whether the lobby list shows a lobby, or only its code or link finds it.
|
||||||
|
enum LobbyVisibility
|
||||||
|
{
|
||||||
|
LOBBY_UNLISTED,
|
||||||
|
LOBBY_PUBLIC
|
||||||
|
};
|
||||||
|
|
||||||
|
// What a new lobby is made with: its name (empty for "Lobby <code>"), its password (empty for none), and who can find
|
||||||
|
// it (see LobbyVisibility).
|
||||||
|
struct LobbyOptions
|
||||||
|
{
|
||||||
|
char name[LOBBY_NAME_MAX + 1] = {};
|
||||||
|
char password[LOBBY_PASSWORD_MAX + 1] = {};
|
||||||
|
int visibility = LOBBY_UNLISTED;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Someone in the lobby (this screen's player too): their server id and name, and their connection's round trip to the
|
||||||
|
// server in milliseconds (-1 while it isn't known: this screen only measures its own).
|
||||||
struct NetMember
|
struct NetMember
|
||||||
{
|
{
|
||||||
int id = -1;
|
int id = -1;
|
||||||
@@ -44,8 +70,9 @@ public:
|
|||||||
Net();
|
Net();
|
||||||
~Net();
|
~Net();
|
||||||
|
|
||||||
void lobby_create(Game& game);
|
void lobby_create(Game& game, LobbyOptions& options);
|
||||||
void lobby_join(Game& game, const char* code);
|
void lobbies_request(Game& game);
|
||||||
|
void lobby_join(Game& game, const std::string& code, const std::string& password);
|
||||||
void lobby_leave();
|
void lobby_leave();
|
||||||
void update();
|
void update();
|
||||||
void shutdown();
|
void shutdown();
|
||||||
@@ -58,6 +85,12 @@ public:
|
|||||||
NetState state = NET_OFFLINE;
|
NetState state = NET_OFFLINE;
|
||||||
std::string code;
|
std::string code;
|
||||||
std::string error;
|
std::string error;
|
||||||
|
// The lobby's name, and the password it was made or joined with (for its link).
|
||||||
|
std::string lobbyName;
|
||||||
|
std::string password;
|
||||||
|
// The public lobbies, as the server last listed them, and why the last listing failed (empty once one comes).
|
||||||
|
std::vector<LobbyListing> listings;
|
||||||
|
std::string listError;
|
||||||
int selfId = -1;
|
int selfId = -1;
|
||||||
int hostId = -1;
|
int hostId = -1;
|
||||||
std::vector<NetMember> members;
|
std::vector<NetMember> members;
|
||||||
@@ -72,7 +105,7 @@ private:
|
|||||||
struct Impl;
|
struct Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|
||||||
void request_send(Game& game, const std::string& request);
|
void request_send(Game& game, const std::string& request, NetState waiting);
|
||||||
void send_now(const std::string& bytes, Delivery delivery);
|
void send_now(const std::string& bytes, Delivery delivery);
|
||||||
void ping_send();
|
void ping_send();
|
||||||
void message_handle(const std::string& text);
|
void message_handle(const std::string& text);
|
||||||
@@ -80,4 +113,8 @@ private:
|
|||||||
void signal_handle(const std::string& payload);
|
void signal_handle(const std::string& payload);
|
||||||
NetMember* member_get(int id);
|
NetMember* member_get(int id);
|
||||||
void host_announce(int previous);
|
void host_announce(int previous);
|
||||||
|
std::string& failure_get();
|
||||||
|
|
||||||
|
// Whether the request in flight is only asking for the lobby list, so its failures go to listError.
|
||||||
|
bool isListing = false;
|
||||||
};
|
};
|
||||||
+6
-1
@@ -27,9 +27,14 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
constexpr int PROTOCOL_VERSION = 9;
|
constexpr int PROTOCOL_VERSION = 11;
|
||||||
constexpr int LOBBY_MEMBERS_MAX = 16;
|
constexpr int LOBBY_MEMBERS_MAX = 16;
|
||||||
constexpr int LOBBY_CODE_LENGTH = 4;
|
constexpr int LOBBY_CODE_LENGTH = 4;
|
||||||
|
// The longest a lobby's name and password can be.
|
||||||
|
constexpr int LOBBY_NAME_MAX = 32;
|
||||||
|
constexpr int LOBBY_PASSWORD_MAX = 32;
|
||||||
|
// What the server says when a lobby's password is missing, which the game answers by asking for one.
|
||||||
|
constexpr const char* LOBBY_PASSWORD_NEEDED = "That lobby needs a password";
|
||||||
// Letters only, without I and O, so codes are easy to read out and type.
|
// Letters only, without I and O, so codes are easy to read out and type.
|
||||||
constexpr const char LOBBY_CODE_LETTERS[] = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
constexpr const char LOBBY_CODE_LETTERS[] = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||||
constexpr int PLAYER_NAME_MAX = 24;
|
constexpr int PLAYER_NAME_MAX = 24;
|
||||||
|
|||||||
+12
-9
@@ -14,6 +14,7 @@ enum TrinketId
|
|||||||
TRINKET_HOURGLASS,
|
TRINKET_HOURGLASS,
|
||||||
TRINKET_ENLARGER,
|
TRINKET_ENLARGER,
|
||||||
TRINKET_DIAZEPAM,
|
TRINKET_DIAZEPAM,
|
||||||
|
TRINKET_GOOD,
|
||||||
TRINKET_SPLITTER,
|
TRINKET_SPLITTER,
|
||||||
TRINKET_DOUBLER,
|
TRINKET_DOUBLER,
|
||||||
TRINKET_DUAL,
|
TRINKET_DUAL,
|
||||||
@@ -65,7 +66,7 @@ struct TrinketStats
|
|||||||
float slamArea = 0;
|
float slamArea = 0;
|
||||||
float slamDamage = 0;
|
float slamDamage = 0;
|
||||||
float speedDamage = 0;
|
float speedDamage = 0;
|
||||||
float spreadCut = 0;
|
float spread = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The stats, in the order tooltips list them.
|
// The stats, in the order tooltips list them.
|
||||||
@@ -76,7 +77,7 @@ enum TrinketStat
|
|||||||
STAT_PROJECTILE_SPEED,
|
STAT_PROJECTILE_SPEED,
|
||||||
STAT_LIFETIME,
|
STAT_LIFETIME,
|
||||||
STAT_ATTACK_SIZE,
|
STAT_ATTACK_SIZE,
|
||||||
STAT_SPREAD_CUT,
|
STAT_SPREAD,
|
||||||
STAT_COPIES,
|
STAT_COPIES,
|
||||||
STAT_COPY_DAMAGE,
|
STAT_COPY_DAMAGE,
|
||||||
STAT_REPEAT_DAMAGE,
|
STAT_REPEAT_DAMAGE,
|
||||||
@@ -141,7 +142,7 @@ inline constexpr TrinketStatDef TRINKET_STATS[STAT_COUNT] = {
|
|||||||
{&TrinketStats::projectileSpeed, "Projectile Speed", FORMAT_TIMES, STACK_SCALE, 1},
|
{&TrinketStats::projectileSpeed, "Projectile Speed", FORMAT_TIMES, STACK_SCALE, 1},
|
||||||
{&TrinketStats::lifetime, "Projectile Lifetime", FORMAT_TIMES, STACK_SCALE, 1},
|
{&TrinketStats::lifetime, "Projectile Lifetime", FORMAT_TIMES, STACK_SCALE, 1},
|
||||||
{&TrinketStats::attackSize, "Attack Size", FORMAT_TIMES, STACK_SCALE, 1},
|
{&TrinketStats::attackSize, "Attack Size", FORMAT_TIMES, STACK_SCALE, 1},
|
||||||
{&TrinketStats::spreadCut, "Spread", FORMAT_LEFT, STACK_SCALE, 0, 1},
|
{&TrinketStats::spread, "Spread", FORMAT_TIMES, STACK_SCALE, 1},
|
||||||
{&TrinketStats::copies, "Copies", FORMAT_NUMBER, STACK_SCALE, 1},
|
{&TrinketStats::copies, "Copies", FORMAT_NUMBER, STACK_SCALE, 1},
|
||||||
{&TrinketStats::copyDamage, "Damage Each", FORMAT_TIMES, STACK_BEST},
|
{&TrinketStats::copyDamage, "Damage Each", FORMAT_TIMES, STACK_BEST},
|
||||||
{&TrinketStats::repeatDamage, "Repeat Damage", FORMAT_TIMES, STACK_BEST},
|
{&TrinketStats::repeatDamage, "Repeat Damage", FORMAT_TIMES, STACK_BEST},
|
||||||
@@ -203,8 +204,12 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
|
|||||||
"A noble spirit embiggens the\nsmallest projectile."},
|
"A noble spirit embiggens the\nsmallest projectile."},
|
||||||
{"Diazepam",
|
{"Diazepam",
|
||||||
"Decreases spread on attacks.",
|
"Decreases spread on attacks.",
|
||||||
{{.spreadCut = 1 / 6.0f}, {.spreadCut = 1 / 3.0f}, {.spreadCut = 0.5f}},
|
{{.spread = 5 / 6.0f}, {.spread = 2 / 3.0f}, {.spread = 0.5f}},
|
||||||
"Diazepam?\nPsychotropic drug?\nStops hands from trembling?\nQuestions?"},
|
"Diazepam?\nPsychotropic drug?\nStops hands from trembling?\nQuestions?"},
|
||||||
|
{"The Good Stuff",
|
||||||
|
"Increases spread on attacks.",
|
||||||
|
{{.spread = 1.2f}, {.spread = 1.5f}, {.spread = 2}},
|
||||||
|
"Steady hands are for\nsurgeons."},
|
||||||
{"Splitter",
|
{"Splitter",
|
||||||
"Attacks split into smaller,\nless-damaging copies.",
|
"Attacks split into smaller,\nless-damaging copies.",
|
||||||
{{.copies = 2, .copyDamage = 1 / 3.0f},
|
{{.copies = 2, .copyDamage = 1 / 3.0f},
|
||||||
@@ -229,7 +234,7 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
|
|||||||
"Okay this is just Technology Zero."},
|
"Okay this is just Technology Zero."},
|
||||||
{"Recoil Coil",
|
{"Recoil Coil",
|
||||||
"All attacks have additional\nrecoil.",
|
"All attacks have additional\nrecoil.",
|
||||||
{{.recoil = 6}, {.recoil = 9}, {.recoil = 12}},
|
{{.recoil = 3}, {.recoil = 4.5f}, {.recoil = 6}},
|
||||||
"The forgotten sibling of Gravity\nand Speed..."},
|
"The forgotten sibling of Gravity\nand Speed..."},
|
||||||
{"Shock Absorber",
|
{"Shock Absorber",
|
||||||
"Receive less knockback, and\nattacks incur less movement.",
|
"Receive less knockback, and\nattacks incur less movement.",
|
||||||
@@ -256,10 +261,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
|
|||||||
{{.runSpeed = 1.3f}, {.runSpeed = 1.65f}, {.runSpeed = 2}},
|
{{.runSpeed = 1.3f}, {.runSpeed = 1.65f}, {.runSpeed = 2}},
|
||||||
"THE BOOT!"},
|
"THE BOOT!"},
|
||||||
{"Speedometer",
|
{"Speedometer",
|
||||||
"Moving faster deals more damage.\nSpeed and acceleration\nincreased slightly.",
|
"Moving faster deals more damage.",
|
||||||
{{.runSpeed = 1.05f, .speedDamage = 0.05f},
|
{{.speedDamage = 0.05f}, {.speedDamage = 0.1f}, {.speedDamage = 0.15f}},
|
||||||
{.runSpeed = 1.1f, .speedDamage = 0.1f},
|
|
||||||
{.runSpeed = 1.15f, .speedDamage = 0.15f}},
|
|
||||||
"When this baby hits 88 miles\nper hour..."},
|
"When this baby hits 88 miles\nper hour..."},
|
||||||
{"Wings",
|
{"Wings",
|
||||||
"Dash in the air, following\nyour aim.",
|
"Dash in the air, following\nyour aim.",
|
||||||
|
|||||||
+5
-2
@@ -223,6 +223,7 @@ constexpr Tip TIPS[] = {
|
|||||||
{"Use the Grimoire's teleporting Charged Attack to escape danger!", "Grimoire"},
|
{"Use the Grimoire's teleporting Charged Attack to escape danger!", "Grimoire"},
|
||||||
|
|
||||||
{"Diazepam reduces Weapon spread, perfect for concentrated attacks!", "Diazepam"},
|
{"Diazepam reduces Weapon spread, perfect for concentrated attacks!", "Diazepam"},
|
||||||
|
{"The Good Stuff scatters your shots wider, and widens the Vacuum's reach to match!", "The Good Stuff"},
|
||||||
{"Splitter copies fan wider with each level!", "Splitter"},
|
{"Splitter copies fan wider with each level!", "Splitter"},
|
||||||
{"The off-hand Weapon from Dual Wield is the one you last switched from!", "Dual Wield"},
|
{"The off-hand Weapon from Dual Wield is the one you last switched from!", "Dual Wield"},
|
||||||
{"Ricochet makes projectiles deal more damage the more they've bounced!", "Ricochet"},
|
{"Ricochet makes projectiles deal more damage the more they've bounced!", "Ricochet"},
|
||||||
@@ -3157,10 +3158,12 @@ void Ui::queues_draw(Game& game)
|
|||||||
renderer.rect_draw({area.x, area.y, 1, area.h}, edge);
|
renderer.rect_draw({area.x, area.y, 1, area.h}, edge);
|
||||||
renderer.rect_draw({area.x + area.w - 1, area.y, 1, area.h}, edge);
|
renderer.rect_draw({area.x + area.w - 1, area.y, 1, area.h}, edge);
|
||||||
renderer.rect_draw({area.x, area.y, area.w, 1}, edge);
|
renderer.rect_draw({area.x, area.y, area.w, 1}, edge);
|
||||||
|
// A mark along the floor for each place, lit once filled, as wide as the area leaves them.
|
||||||
|
float slot = (area.w - QUEUE_MARGIN * 2) / queue.rules.players;
|
||||||
for (int k = 0; k < queue.rules.players; k++)
|
for (int k = 0; k < queue.rules.players; k++)
|
||||||
{
|
{
|
||||||
float x = std::round(area.x + QUEUE_MARGIN + k * QUEUE_SLOT_WIDTH + 2);
|
float x = std::round(area.x + QUEUE_MARGIN + k * slot + 2);
|
||||||
renderer.rect_draw({x, area.y + area.h - 2, QUEUE_SLOT_WIDTH - 4, 2},
|
renderer.rect_draw({x, area.y + area.h - 2, std::round(slot) - 4, 2},
|
||||||
k < count ? color : color_shade(color, SECONDARY_SHADE));
|
k < count ? color : color_shade(color, SECONDARY_SHADE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ constexpr const char* HINT_KEYS[HINT_COUNT] = {"move", "jump", "down", "attack",
|
|||||||
class Ui
|
class Ui
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
void tip_draw(Game& game, float y);
|
||||||
|
float tip_low_get();
|
||||||
Color tint = WHITE;
|
Color tint = WHITE;
|
||||||
Color shade = GRAY;
|
Color shade = GRAY;
|
||||||
int adjust = 0;
|
int adjust = 0;
|
||||||
@@ -347,8 +349,6 @@ private:
|
|||||||
void hints_update(Game& game);
|
void hints_update(Game& game);
|
||||||
int tip_pick(Game& game);
|
int tip_pick(Game& game);
|
||||||
void tips_update(Game& game);
|
void tips_update(Game& game);
|
||||||
void tip_draw(Game& game, float y);
|
|
||||||
float tip_low_get();
|
|
||||||
void hints_draw(Game& game);
|
void hints_draw(Game& game);
|
||||||
void text_center_draw(Game& game, const char* text, float y, float scale, float alpha = 1);
|
void text_center_draw(Game& game, const char* text, float y, float scale, float alpha = 1);
|
||||||
void text_center_draw(Game& game, const char* text, float y, float scale, Color color);
|
void text_center_draw(Game& game, const char* text, float y, float scale, Color color);
|
||||||
|
|||||||
+32
-17
@@ -49,6 +49,8 @@ constexpr float PARTIAL_CHARGE_BONUS = 0.5f;
|
|||||||
// How many times as hard a shot hits once an airblast sends it back, anyone's, stacking with each one: twice what a
|
// How many times as hard a shot hits once an airblast sends it back, anyone's, stacking with each one: twice what a
|
||||||
// Ricochet bounce adds, as it takes aim and timing.
|
// Ricochet bounce adds, as it takes aim and timing.
|
||||||
constexpr float REFLECT_BONUS = 1.5f;
|
constexpr float REFLECT_BONUS = 1.5f;
|
||||||
|
// How long a flail ball an airblast steals stays on its new owner's chain.
|
||||||
|
constexpr float STOLEN_CHAIN_TIME = 5;
|
||||||
constexpr float SPEED_UNIT = 100;
|
constexpr float SPEED_UNIT = 100;
|
||||||
constexpr float CONTACT_INTERVAL = 0.4f;
|
constexpr float CONTACT_INTERVAL = 0.4f;
|
||||||
constexpr float WISP_GLIDE = 14;
|
constexpr float WISP_GLIDE = 14;
|
||||||
@@ -695,7 +697,7 @@ AttackSummary attack_summary_get(Fighter& fighter, int weapon, int level, Attack
|
|||||||
float gloves = fighter.stat_get(STAT_ATTACK_RATE);
|
float gloves = fighter.stat_get(STAT_ATTACK_RATE);
|
||||||
float drink = fighter.stat_get(STAT_CHARGE_RATE);
|
float drink = fighter.stat_get(STAT_CHARGE_RATE);
|
||||||
float accelerator = attack.isProjectile ? fighter.stat_get(STAT_PROJECTILE_SPEED) : 1;
|
float accelerator = attack.isProjectile ? fighter.stat_get(STAT_PROJECTILE_SPEED) : 1;
|
||||||
float steadiness = 1 - fighter.stat_get(STAT_SPREAD_CUT);
|
float spreading = fighter.stat_get(STAT_SPREAD);
|
||||||
float absorb = fighter.stat_get(STAT_MOVEMENT);
|
float absorb = fighter.stat_get(STAT_MOVEMENT);
|
||||||
|
|
||||||
AttackSummary summary;
|
AttackSummary summary;
|
||||||
@@ -714,8 +716,8 @@ AttackSummary attack_summary_get(Fighter& fighter, int weapon, int level, Attack
|
|||||||
summary.isAreaChanged = enlarger != 1 || splits > 1;
|
summary.isAreaChanged = enlarger != 1 || splits > 1;
|
||||||
summary.speed = attack.isProjectile ? stats.speed * accelerator : 0;
|
summary.speed = attack.isProjectile ? stats.speed * accelerator : 0;
|
||||||
summary.isSpeedChanged = accelerator != 1;
|
summary.isSpeedChanged = accelerator != 1;
|
||||||
summary.spread = attack.spread * steadiness;
|
summary.spread = attack.spread * spreading;
|
||||||
summary.isSpreadChanged = steadiness != 1;
|
summary.isSpreadChanged = spreading != 1;
|
||||||
summary.lunge = stats.lunge * absorb;
|
summary.lunge = stats.lunge * absorb;
|
||||||
summary.isLungeChanged = absorb != 1;
|
summary.isLungeChanged = absorb != 1;
|
||||||
summary.pieces = stats.volley * splits;
|
summary.pieces = stats.volley * splits;
|
||||||
@@ -804,7 +806,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
|||||||
// A rope or chain is as long as its shot's flight carries it, so it lengthens as the flight does.
|
// A rope or chain is as long as its shot's flight carries it, so it lengthens as the flight does.
|
||||||
if (def.isTethered)
|
if (def.isTethered)
|
||||||
stats.range *= hourglass * accelerator;
|
stats.range *= hourglass * accelerator;
|
||||||
float spread = 1 - fighter.stat_get(STAT_SPREAD_CUT);
|
float spread = fighter.stat_get(STAT_SPREAD);
|
||||||
float jitter = def.spread * DEGREE * spread;
|
float jitter = def.spread * DEGREE * spread;
|
||||||
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + sim.rng.random_get(-jitter, jitter), 1);
|
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + sim.rng.random_get(-jitter, jitter), 1);
|
||||||
float push = def.isInheriting ? push_get(fighter, aim) : 0;
|
float push = def.isInheriting ? push_get(fighter, aim) : 0;
|
||||||
@@ -826,11 +828,13 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
|||||||
.fade = stats.fade > 0 ? stats.fade * hourglass : lifetime,
|
.fade = stats.fade > 0 ? stats.fade * hourglass : lifetime,
|
||||||
.lifetime = lifetime,
|
.lifetime = lifetime,
|
||||||
.scale = def.scale * stats.scale,
|
.scale = def.scale * stats.scale,
|
||||||
|
.spreading = spread,
|
||||||
.bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0,
|
.bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0,
|
||||||
.hitsLeft = def.hits,
|
.hitsLeft = def.hits,
|
||||||
.owner = fighter.index,
|
.owner = fighter.index,
|
||||||
.weapon = held,
|
.weapon = held,
|
||||||
.key = key,
|
.key = key,
|
||||||
|
.keyWeapon = held,
|
||||||
.hand = shot.hand,
|
.hand = shot.hand,
|
||||||
.variant = variant,
|
.variant = variant,
|
||||||
.isLeft = fighter.is_left(),
|
.isLeft = fighter.is_left(),
|
||||||
@@ -1076,10 +1080,10 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
|||||||
else if (def.path != PATH_OWN)
|
else if (def.path != PATH_OWN)
|
||||||
flight_move(sim, attack);
|
flight_move(sim, attack);
|
||||||
|
|
||||||
// A mine, flail ball or grapple left out goes once its slot no longer holds the weapon that put it there (which
|
// A mine, flail ball or grapple left out goes once its slot no longer holds the weapon that keeps it (which could
|
||||||
// could no longer set it off, reel it in or let it go), a mine fizzling with a puff.
|
// no longer set it off, reel it in or let it go), a mine fizzling with a puff.
|
||||||
bool isLeftOut = def.isArming || def.isTethered;
|
bool isLeftOut = def.isArming || def.isTethered;
|
||||||
if (isLeftOut && !attack.isExplosion && fighter.weaponLoadout[attack.key] != attack.weapon)
|
if (isLeftOut && !attack.isExplosion && fighter.weaponLoadout[attack.key] != attack.keyWeapon)
|
||||||
{
|
{
|
||||||
attack.isDead = true;
|
attack.isDead = true;
|
||||||
if (def.isArming)
|
if (def.isArming)
|
||||||
@@ -1349,8 +1353,11 @@ void Weapons::flight_move(Sim& sim, Attack& attack)
|
|||||||
}
|
}
|
||||||
attack.tip = start;
|
attack.tip = start;
|
||||||
|
|
||||||
bool isOver = !attack.isDead && attack.life_get() >= attack.lifetime;
|
// A tethered shot out of time reels itself back in (a flail ball, to whoever holds its chain now).
|
||||||
if (isOver && def.isBursting)
|
bool isOver = !attack.isDead && !attack.isReturning && attack.life_get() >= attack.lifetime;
|
||||||
|
if (isOver && def.isTethered)
|
||||||
|
attack.isReturning = true;
|
||||||
|
else if (isOver && def.isBursting)
|
||||||
attack_explode(sim, attack);
|
attack_explode(sim, attack);
|
||||||
else if (isOver)
|
else if (isOver)
|
||||||
{
|
{
|
||||||
@@ -1751,7 +1758,8 @@ void Weapons::orbits_release(Fighter& fighter)
|
|||||||
|
|
||||||
// Grimoire teleport: sparkles round the fighter while it gathers, then moves them to the spot marked when it was cast
|
// Grimoire teleport: sparkles round the fighter while it gathers, then moves them to the spot marked when it was cast
|
||||||
// (backed off again if their box no longer fits), keeping all their momentum, with a blast there that hurts without
|
// (backed off again if their box no longer fits), keeping all their momentum, with a blast there that hurts without
|
||||||
// knocking anyone about. Echoes only make the blast.
|
// knocking anyone about. A Doubler's echoes teleport again, each to where the cursor pointed as it was cast. An
|
||||||
|
// off-hand's (Dual Wield) only makes the blast.
|
||||||
void Weapons::teleport_move(Sim& sim, Attack& attack)
|
void Weapons::teleport_move(Sim& sim, Attack& attack)
|
||||||
{
|
{
|
||||||
Fighter& fighter = sim.fighters[attack.owner];
|
Fighter& fighter = sim.fighters[attack.owner];
|
||||||
@@ -1759,14 +1767,14 @@ void Weapons::teleport_move(Sim& sim, Attack& attack)
|
|||||||
attack.hitsLeft = 0;
|
attack.hitsLeft = 0;
|
||||||
if (attack.time < attack.duration)
|
if (attack.time < attack.duration)
|
||||||
{
|
{
|
||||||
if (!attack.isCopy)
|
if (attack.hand == 0)
|
||||||
sim.cues->burst_spawn(WARP_BURST, fighter.center_get(), 0, attack.owner);
|
sim.cues->burst_spawn(WARP_BURST, fighter.center_get(), 0, attack.owner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
attack.isDead = true;
|
attack.isDead = true;
|
||||||
Vec2 spot = attack.position;
|
Vec2 spot = attack.position;
|
||||||
if (!attack.isCopy && attack.hand == 0 && fighter.isAlive)
|
if (attack.hand == 0 && fighter.isAlive)
|
||||||
{
|
{
|
||||||
sim.cues->burst_spawn(PORTAL_BURST, fighter.center_get(), 0, attack.owner);
|
sim.cues->burst_spawn(PORTAL_BURST, fighter.center_get(), 0, attack.owner);
|
||||||
spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get()));
|
spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get()));
|
||||||
@@ -1898,9 +1906,9 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
|||||||
float reach = stats.range * fighter.stat_get(STAT_ATTACK_SIZE) * (splits > 1 ? SPLIT_SCALE : 1);
|
float reach = stats.range * fighter.stat_get(STAT_ATTACK_SIZE) * (splits > 1 ? SPLIT_SCALE : 1);
|
||||||
for (int split = 0; split < splits; split++)
|
for (int split = 0; split < splits; split++)
|
||||||
{
|
{
|
||||||
float offset = splits > 1 ? split_offset(kind, split, splits, 1 - fighter.stat_get(STAT_SPREAD_CUT)) : 0;
|
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
||||||
Vec2 tip = muzzle + vec2_polar(vec2_angle(fighter.aim) + offset, reach);
|
Vec2 tip = muzzle + vec2_polar(vec2_angle(fighter.aim) + offset, reach);
|
||||||
paths.push_back({.steps = {muzzle, tip}, .cone = VACUUM_CONE});
|
paths.push_back({.steps = {muzzle, tip}, .cone = VACUUM_CONE * fighter.stat_get(STAT_SPREAD)});
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
@@ -1917,7 +1925,7 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
|||||||
float reach = std::min(stats.range, TRAJECTORY_REACH);
|
float reach = std::min(stats.range, TRAJECTORY_REACH);
|
||||||
for (int split = 0; split < splits; split++)
|
for (int split = 0; split < splits; split++)
|
||||||
{
|
{
|
||||||
float offset = splits > 1 ? split_offset(kind, split, splits, 1 - fighter.stat_get(STAT_SPREAD_CUT)) : 0;
|
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
||||||
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + offset, 1);
|
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + offset, 1);
|
||||||
float speed = stats.speed * fighter.stat_get(STAT_PROJECTILE_SPEED) + push_get(fighter, aim);
|
float speed = stats.speed * fighter.stat_get(STAT_PROJECTILE_SPEED) + push_get(fighter, aim);
|
||||||
Vec2 velocity = isArrow ? aim * speed : aim;
|
Vec2 velocity = isArrow ? aim * speed : aim;
|
||||||
@@ -2241,13 +2249,20 @@ void Weapons::shots_drift(Sim& sim, Attack& source, float blow, float pull, floa
|
|||||||
if (&shot == &source || shot.isExplosion || !is_drifting(shot) || !is_in_cone(source, shot.position))
|
if (&shot == &source || shot.isExplosion || !is_drifting(shot) || !is_in_cone(source, shot.position))
|
||||||
continue;
|
continue;
|
||||||
const AttackDef& def = *shot.def();
|
const AttackDef& def = *shot.def();
|
||||||
|
// A flail ball's chain comes with it, to the hand and weapon that blew it, for a while (or until that weapon's
|
||||||
|
// put away, or reels it in).
|
||||||
|
bool isChain = kind == ATTACK_FLAIL;
|
||||||
bool isTied = def.isTethered || def.isArming || def.isWielded;
|
bool isTied = def.isTethered || def.isArming || def.isWielded;
|
||||||
if (source.def()->isClaiming && shot.owner != source.owner && !isTied)
|
if (source.def()->isClaiming && shot.owner != source.owner && (!isTied || isChain))
|
||||||
{
|
{
|
||||||
shot.ownerPrevious = shot.owner;
|
shot.ownerPrevious = shot.owner;
|
||||||
shot.owner = source.owner;
|
shot.owner = source.owner;
|
||||||
shot.claimTime = shot.time;
|
shot.claimTime = shot.time;
|
||||||
shot.key = -1;
|
shot.key = isChain ? source.key : -1;
|
||||||
|
shot.keyWeapon = source.weapon;
|
||||||
|
shot.hand = source.hand;
|
||||||
|
shot.isReturning = false;
|
||||||
|
shot.lifetime = isChain ? shot.life_get() + STOLEN_CHAIN_TIME : shot.lifetime;
|
||||||
shot.isSecondary = false;
|
shot.isSecondary = false;
|
||||||
shot.struck = 0;
|
shot.struck = 0;
|
||||||
shot.hitCooldown = 0;
|
shot.hitCooldown = 0;
|
||||||
|
|||||||
+21
-14
@@ -10,6 +10,8 @@ constexpr float RICOCHET_BONUS = 0.25f;
|
|||||||
// Hits for an attack that keeps hitting (a hit interval apart) as long as it's out.
|
// Hits for an attack that keeps hitting (a hit interval apart) as long as it's out.
|
||||||
constexpr int HITS_ENDLESS = 1 << 30;
|
constexpr int HITS_ENDLESS = 1 << 30;
|
||||||
constexpr int WHIP_RAYS = 9;
|
constexpr int WHIP_RAYS = 9;
|
||||||
|
// How long a thrown flail ball stays out before it reels itself back in.
|
||||||
|
constexpr float FLAIL_LIFETIME = 15;
|
||||||
|
|
||||||
class Fighter;
|
class Fighter;
|
||||||
struct Dummy;
|
struct Dummy;
|
||||||
@@ -431,14 +433,14 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.isProjectile = true},
|
.isProjectile = true},
|
||||||
.levels =
|
.levels =
|
||||||
{
|
{
|
||||||
{.normal = {.damage = 16, .lifetime = 0.3f, .lunge = 70, .scale = 0.88f, .speed = 60},
|
{.normal = {.damage = 16, .lifetime = 0.3f, .lunge = 70, .scale = 0.66f, .speed = 60},
|
||||||
.charged = {.damage = 34, .lifetime = 0.6f, .lunge = 140, .scale = 2.12f, .speed = 150},
|
.charged = {.damage = 34, .lifetime = 0.6f, .lunge = 140, .scale = 1.06f, .speed = 150},
|
||||||
.cooldown = 0.3f},
|
.cooldown = 0.3f},
|
||||||
{.normal = {.damage = 23, .lifetime = 0.3f, .lunge = 70, .scale = 1.08f, .speed = 60},
|
{.normal = {.damage = 23, .lifetime = 0.3f, .lunge = 70, .scale = 0.81f, .speed = 60},
|
||||||
.charged = {.damage = 45, .lifetime = 0.6f, .lunge = 140, .scale = 2.5f, .speed = 150},
|
.charged = {.damage = 45, .lifetime = 0.6f, .lunge = 140, .scale = 1.25f, .speed = 150},
|
||||||
.cooldown = 0.3f},
|
.cooldown = 0.3f},
|
||||||
{.normal = {.damage = 28, .lifetime = 0.3f, .lunge = 70, .scale = 1.27f, .speed = 60},
|
{.normal = {.damage = 28, .lifetime = 0.3f, .lunge = 70, .scale = 0.9525f, .speed = 60},
|
||||||
.charged = {.damage = 55, .lifetime = 0.6f, .lunge = 140, .scale = 2.88f, .speed = 150},
|
.charged = {.damage = 55, .lifetime = 0.6f, .lunge = 140, .scale = 1.44f, .speed = 150},
|
||||||
.cooldown = 0.3f},
|
.cooldown = 0.3f},
|
||||||
},
|
},
|
||||||
.bot = {.rangeMax = 48},
|
.bot = {.rangeMax = 48},
|
||||||
@@ -556,13 +558,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.isInstant = true},
|
.isInstant = true},
|
||||||
.levels =
|
.levels =
|
||||||
{
|
{
|
||||||
{.normal = {.damage = 24, .lifetime = 0.38f, .speed = 950},
|
{.normal = {.damage = 12, .lifetime = 0.38f, .speed = 475},
|
||||||
.charged = {.area = 6, .damage = 46, .range = 125},
|
.charged = {.area = 6, .damage = 46, .range = 125},
|
||||||
.cooldown = 0.4f},
|
.cooldown = 0.4f},
|
||||||
{.normal = {.damage = 28, .lifetime = 0.38f, .speed = 1175},
|
{.normal = {.damage = 14, .lifetime = 0.38f, .speed = 588},
|
||||||
.charged = {.area = 7, .damage = 70, .range = 155},
|
.charged = {.area = 7, .damage = 70, .range = 155},
|
||||||
.cooldown = 0.4f},
|
.cooldown = 0.4f},
|
||||||
{.normal = {.damage = 32, .lifetime = 0.38f, .speed = 1400},
|
{.normal = {.damage = 16, .lifetime = 0.38f, .speed = 700},
|
||||||
.charged = {.area = 8, .damage = 93, .range = 185},
|
.charged = {.area = 8, .damage = 93, .range = 185},
|
||||||
.cooldown = 0.4f},
|
.cooldown = 0.4f},
|
||||||
},
|
},
|
||||||
@@ -606,13 +608,13 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.levels =
|
.levels =
|
||||||
{
|
{
|
||||||
{.normal = {.damage = 11, .lifetime = LIFETIME_INFINITE, .range = 33, .scale = 1, .speed = 518},
|
{.normal = {.damage = 11, .lifetime = LIFETIME_INFINITE, .range = 33, .scale = 1, .speed = 518},
|
||||||
.charged = {.damage = 35, .lifetime = LIFETIME_INFINITE, .range = 150, .scale = 1, .speed = 450},
|
.charged = {.damage = 35, .lifetime = FLAIL_LIFETIME, .range = 150, .scale = 1, .speed = 450},
|
||||||
.cooldown = 0.5f},
|
.cooldown = 0.5f},
|
||||||
{.normal = {.damage = 15, .lifetime = LIFETIME_INFINITE, .range = 41, .scale = 1.5f, .speed = 644},
|
{.normal = {.damage = 15, .lifetime = LIFETIME_INFINITE, .range = 41, .scale = 1.5f, .speed = 644},
|
||||||
.charged = {.damage = 44, .lifetime = LIFETIME_INFINITE, .range = 175, .scale = 1.5f, .speed = 450},
|
.charged = {.damage = 44, .lifetime = FLAIL_LIFETIME, .range = 175, .scale = 1.5f, .speed = 450},
|
||||||
.cooldown = 0.5f},
|
.cooldown = 0.5f},
|
||||||
{.normal = {.damage = 19, .lifetime = LIFETIME_INFINITE, .range = 47, .scale = 2, .speed = 738},
|
{.normal = {.damage = 19, .lifetime = LIFETIME_INFINITE, .range = 47, .scale = 2, .speed = 738},
|
||||||
.charged = {.damage = 51, .lifetime = LIFETIME_INFINITE, .range = 200, .scale = 2, .speed = 450},
|
.charged = {.damage = 51, .lifetime = FLAIL_LIFETIME, .range = 200, .scale = 2, .speed = 450},
|
||||||
.cooldown = 0.5f},
|
.cooldown = 0.5f},
|
||||||
},
|
},
|
||||||
.bot = {.rangeMax = 60},
|
.bot = {.rangeMax = 60},
|
||||||
@@ -1377,6 +1379,8 @@ struct Attack
|
|||||||
// When an arming attack came to rest (see is_armed), never until it has.
|
// When an arming attack came to rest (see is_armed), never until it has.
|
||||||
float restTime = LIFETIME_INFINITE;
|
float restTime = LIFETIME_INFINITE;
|
||||||
float scale = 1;
|
float scale = 1;
|
||||||
|
// Its owner's spread when it set off (see STAT_SPREAD), widening a cone.
|
||||||
|
float spreading = 1;
|
||||||
// A crawling bolt's side its surface is on: 1 for its right (as it heads), -1 for its left.
|
// A crawling bolt's side its surface is on: 1 for its right (as it heads), -1 for its left.
|
||||||
float side = 0;
|
float side = 0;
|
||||||
// A bouncing shot's slope under it, carried between ticks with whether it's on the ground (see
|
// A bouncing shot's slope under it, carried between ticks with whether it's on the ground (see
|
||||||
@@ -1394,6 +1398,9 @@ struct Attack
|
|||||||
Uint32 struck = 0;
|
Uint32 struck = 0;
|
||||||
int weapon = 0;
|
int weapon = 0;
|
||||||
int key = -1;
|
int key = -1;
|
||||||
|
// The weapon whose slot (key) keeps it out while it's tied to its owner (a mine, a flail ball or a grapple): its
|
||||||
|
// own, or the stealer's for a flail ball taken by an airblast.
|
||||||
|
int keyWeapon = -1;
|
||||||
// The hand it came from: the main hand (0) or a Dual Wield off-hand (1 up).
|
// The hand it came from: the main hand (0) or a Dual Wield off-hand (1 up).
|
||||||
int hand = 0;
|
int hand = 0;
|
||||||
AttackVariant variant = VARIANT_NORMAL;
|
AttackVariant variant = VARIANT_NORMAL;
|
||||||
@@ -1499,10 +1506,10 @@ inline bool is_cone(const AttackDef& def)
|
|||||||
return def.shape == SHAPE_CONE;
|
return def.shape == SHAPE_CONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How wide an attack's cone is (radians either side of its aim), or 0 for one without.
|
// How wide an attack's cone is (radians either side of its aim, wider with more spread), or 0 for one without.
|
||||||
inline float cone_get(Attack& attack)
|
inline float cone_get(Attack& attack)
|
||||||
{
|
{
|
||||||
return is_cone(*attack.def()) ? VACUUM_CONE : 0;
|
return is_cone(*attack.def()) ? VACUUM_CONE * attack.spreading : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How far an attack's cone reaches right now: a Vacuum's suck grows out as it spins up (its length, 0 to 1) and
|
// How far an attack's cone reaches right now: a Vacuum's suck grows out as it spins up (its length, 0 to 1) and
|
||||||
|
|||||||
Reference in new issue
Block a user