diff --git a/src/effects.cpp b/src/effects.cpp index a59a0fd..17c5721 100644 --- a/src/effects.cpp +++ b/src/effects.cpp @@ -14,12 +14,6 @@ constexpr float FLASH_LIFE = 0.05f; constexpr float FLASH_WIDTH = 6; constexpr float RING_LIFE = 0.3f; constexpr int RING_WIDTH = 2; -constexpr float SPLAT_CORE = 6; -constexpr float SPLAT_FADE = 0.6f; -constexpr float SPLAT_LIFE = 2; -constexpr float SPLAT_REACH = 10; -constexpr float SPLAT_SIZE_MAX = 3.5f; -constexpr float SPLAT_SIZE_MIN = 1.5f; constexpr float AFTERIMAGE_LIFE = 0.12f; constexpr float NUMBER_GAP = 8; constexpr float POPUP_FADE = 0.4f; @@ -133,19 +127,6 @@ void Effects::ring_spawn(Vec2 position, float radius, Color color) effect_add(rings, Ring{color, position, RING_LIFE, radius}); } -// A big blob where it landed, with smaller ones flung round it. -void Effects::splat_spawn(Vec2 position, Color color) -{ - Splat splat = {.color = color, .position = position, .life = SPLAT_LIFE}; - splat.sizes[0] = SPLAT_CORE; - for (int i = 1; i < SPLAT_BLOBS; i++) - { - splat.blobs[i] = vec2_polar(random_get(0, 2 * PI), random_get(SPLAT_CORE, SPLAT_REACH)); - splat.sizes[i] = random_get(SPLAT_SIZE_MIN, SPLAT_SIZE_MAX); - } - effect_add(splats, splat); -} - void Effects::dps_record(int amount) { hits.push_back({ticks * TICK, amount}); @@ -187,10 +168,6 @@ void Effects::update() ring.life -= TICK; std::erase_if(rings, [](Ring& ring) { return ring.life <= 0; }); - for (Splat& splat : splats) - splat.life -= TICK; - std::erase_if(splats, [](Splat& splat) { return splat.life <= 0; }); - for (Afterimage& afterimage : afterimages) afterimage.life -= TICK; std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; }); @@ -232,14 +209,6 @@ void Effects::draw(Game& game) renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color); } - // Each splat holds, then fades out over its last moments. - for (Splat& splat : splats) - { - Color color = color_fade(splat.color, std::min(1.0f, splat.life / SPLAT_FADE)); - for (int i = 0; i < SPLAT_BLOBS; i++) - renderer.circle_draw(splat.position + splat.blobs[i], splat.sizes[i], color); - } - // Each ring eases out to its full radius as it fades. for (Ring& ring : rings) { @@ -290,6 +259,5 @@ void Effects::clear() particles.clear(); popups.clear(); rings.clear(); - splats.clear(); shake = 0; } diff --git a/src/effects.h b/src/effects.h index 04d7911..0ddcc56 100644 --- a/src/effects.h +++ b/src/effects.h @@ -58,18 +58,6 @@ struct Ring float radius = 0; }; -constexpr int SPLAT_BLOBS = 6; - -// A tomato's splat: blobs of colour round where it landed, fading out. -struct Splat -{ - Color color; - Vec2 position; - Vec2 blobs[SPLAT_BLOBS]; - float sizes[SPLAT_BLOBS]; - float life = 0; -}; - // A fading copy of a sprite left behind by something moving fast. struct Afterimage { @@ -85,7 +73,6 @@ public: void afterimage_spawn(Pose pose); void flash_spawn(Vec2 position, float angle, float size, Color color); void ring_spawn(Vec2 position, float radius, Color color); - void splat_spawn(Vec2 position, Color color); void afterimage_draw(Game& game); void number_spawn(Vec2 position, int value, int target, Color color); void dps_record(int amount); @@ -104,7 +91,6 @@ private: std::vector particles; std::vector popups; std::vector rings; - std::vector splats; float shake = 0; int ticks = 0; }; diff --git a/src/fighter.cpp b/src/fighter.cpp index d067052..984e971 100644 --- a/src/fighter.cpp +++ b/src/fighter.cpp @@ -397,7 +397,7 @@ void Fighter::rope_update(Attack& hook) // The controls' slot picks a weapon slot, wrapping around, and the level buttons level the weapon copy in it. The // weapon switched away from becomes the off-hand weapon for Dual Wield (a copy of the same weapon counts when weapons -// stack), and a thrown flail ball is dropped. +// stack). A thrown flail ball or grapple out stays out. void Fighter::slot_update(Sim& sim) { int previous = slot; @@ -408,10 +408,7 @@ void Fighter::slot_update(Sim& sim) if (slot != previous) sim.cues->sound_play(SOUND_SELECT); if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot)) - { slotSwitches[previous] = ++switches; - sim.weapons.attacks_release(*this, ATTACK_FLAIL); - } int step = (int)is_pressed(CONTROL_LEVEL_UP) - (int)is_pressed(CONTROL_LEVEL_DOWN); if (step != 0 && held >= 0 && sim.match.state == MATCH_PRACTICE) @@ -605,7 +602,8 @@ void Fighter::charge_update(Sim& sim) if (is_released(CONTROL_ATTACK)) sim.weapons.wisps_release(*this); - if (hook && (isLeftPressed || isRightPressed)) + // A latched grapple lets go when clicked with the grapple in hand; another weapon fires as it hangs on. + if (hook && hook->key == weapon_key(slot) && (isLeftPressed || isRightPressed)) { sim.weapons.attacks_release(*this, ATTACK_HOOK); isFireBlocked = isLeftPressed; diff --git a/src/match.cpp b/src/match.cpp index e6e6003..778dfc6 100644 --- a/src/match.cpp +++ b/src/match.cpp @@ -86,6 +86,8 @@ Match::Match() void Match::update(Sim& sim) { timer -= TICK; + for (Tomato& tomato : tomatoes) + tomato.age += TICK; // Offline, the menu's Play starts a game, and leaving the results goes back to the sandbox. bool isPlay = !isOnline && isPlayRequested; bool isSandbox = !isOnline && state == MATCH_OVER && isSandboxRequested; @@ -429,7 +431,8 @@ void Match::tomatoes_update(Sim& sim) if (i == winner || isGone[i] || tomatoCooldowns[i] > 0 || !isThrown) continue; tomatoCooldowns[i] = TOMATO_COOLDOWN; - sim.cues->tomato_splat(fighter.controls.cursor, i); + tomatoes[tomatoCount % TOMATO_MAX] = {.position = fighter.controls.cursor, .owner = i, .serial = tomatoCount}; + tomatoCount++; } } diff --git a/src/match.h b/src/match.h index 64e2a3c..0961651 100644 --- a/src/match.h +++ b/src/match.h @@ -175,6 +175,18 @@ struct Queue bool isActive = false; }; +constexpr int TOMATO_MAX = 32; + +// A tomato splatted after a round: where it landed, who threw it, how long ago, and its number among every one thrown +// in the match (which also keeps its look the same on every screen). +struct Tomato +{ + Vec2 position; + float age = 0; + int owner = -1; + int serial = 0; +}; + // Best-of-three deathmatch, or a single race, played by the rules. Practice is the sandbox with the dummy and the menu. // Each round opens with a long countdown to ready up in (cut short once every fighter is ready), at the round's level // and health. Countdown is how long it ran from: longer on rounds where fighters choose items. On the rounds items are @@ -192,8 +204,11 @@ public: float clock = 0; float countdown = READY_TIME; float finishTimes[MAX_FIGHTERS] = {}; - // How long until each fighter can splat another tomato on the results. + // How long until each fighter can splat another tomato, and the latest splats, kept with the game so everyone sees + // everyone's. float tomatoCooldowns[MAX_FIGHTERS] = {}; + Tomato tomatoes[TOMATO_MAX]; + int tomatoCount = 0; float timer = 0; int round = 0; int winner = -1; diff --git a/src/scene.cpp b/src/scene.cpp index 8d3c34b..3690ad0 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -69,6 +69,16 @@ constexpr float WHIP_INNER = 2; constexpr float WHIP_JITTER = 0.5f; constexpr float WHIP_OUTER = 5; constexpr float WISP_FRAME_STAGGER = 0.3f; +constexpr int TOMATO_BLOBS = 6; +constexpr float TOMATO_BLOB_MAX = 3.5f; +constexpr float TOMATO_BLOB_MIN = 1.5f; +constexpr float TOMATO_CORE = 6; +constexpr float TOMATO_FADE = 0.6f; +// How old a tomato can be when this screen first sees it and still splat with juice and a sound. +constexpr float TOMATO_FRESH = 0.5f; +constexpr float TOMATO_LIFE = 2; +constexpr Uint32 TOMATO_LOOK_STEP = 2654435761u; +constexpr float TOMATO_REACH = 10; constexpr Burst TOMATO_BURST = {.gravity = 400, .life = 0.5f, .size = 2, .speed = 140, .spread = 1.2f, .count = 12}; // A random offset for an empowered attack or fighter to shake by, wider the more its colour fills in, like a damage @@ -391,17 +401,6 @@ void Scene::match_start() game.menu.isOpen = false; } -// A tomato thrown on the results: a splat in the thrower's colour where it landed, spraying juice. -void Scene::tomato_splat(Vec2 position, int owner) -{ - if (isMuted) - return; - Color color = game.color_get(owner, true); - game.effects.splat_spawn(position, color); - game.effects.burst(TOMATO_BURST, position, -PI / 2, color); - game.audio.play(SOUND_SPLAT); -} - // Readying with empty slots dealt them: this screen's own fighters hear the items go in. void Scene::deal_play(int fighter) { @@ -425,6 +424,39 @@ void Scene::update() { for (int i = 0; i < game.sim.fighterCount; i++) animator_update(game.sim.fighters[i], game.sprites.fighters[i]); + + // Each new tomato (this screen's, or someone else's once their click arrives) splats with juice and a sound, once. + Match& match = game.sim.match; + tomatoesSeen = std::min(tomatoesSeen, match.tomatoCount); + for (; tomatoesSeen < match.tomatoCount; tomatoesSeen++) + { + Tomato& tomato = match.tomatoes[tomatoesSeen % TOMATO_MAX]; + if (tomato.serial != tomatoesSeen || tomato.age > TOMATO_FRESH) + continue; + game.effects.burst(TOMATO_BURST, tomato.position, -PI / 2, game.color_get(tomato.owner, true)); + game.audio.play(SOUND_SPLAT); + } +} + +// The match's tomatoes as splats in their throwers' colours: a big blob where each landed with smaller ones flung round +// it (the same on every screen), holding and then fading out. +void Scene::tomatoes_draw() +{ + for (Tomato& tomato : game.sim.match.tomatoes) + { + if (tomato.owner < 0 || tomato.age >= TOMATO_LIFE) + continue; + Color color = + color_fade(game.color_get(tomato.owner, true), std::min(1.0f, (TOMATO_LIFE - tomato.age) / TOMATO_FADE)); + game.renderer.circle_draw(tomato.position, TOMATO_CORE, color); + Rng look = {.state = (Uint32)tomato.serial * TOMATO_LOOK_STEP + 1}; + for (int i = 1; i < TOMATO_BLOBS; i++) + { + Vec2 offset = vec2_polar(look.random_get(0, 2 * PI), look.random_get(TOMATO_CORE, TOMATO_REACH)); + game.renderer.circle_draw(tomato.position + offset, look.random_get(TOMATO_BLOB_MIN, TOMATO_BLOB_MAX), + color); + } + } } // The game between its last two ticks: the world, the bots' map (when debugging), afterimages, blasts under the @@ -438,6 +470,7 @@ void Scene::draw() for (int i = 0; i < game.sim.fighterCount; i++) fighter_draw(game.sim.fighters[i]); attacks_draw(); + tomatoes_draw(); if (game.settings.isCollisionShown) collision_draw(); } diff --git a/src/scene.h b/src/scene.h index 390f120..ab27d43 100644 --- a/src/scene.h +++ b/src/scene.h @@ -36,14 +36,16 @@ public: void level_step(bool isUp) override; void match_start() override; void deal_play(int fighter) override; - void tomato_splat(Vec2 position, int owner) override; void sandbox_return() override; private: Game& game; int chargeVoices[MAX_FIGHTERS]; + // How many of the match's tomatoes have splatted on this screen. + int tomatoesSeen = 0; void world_draw(); + void tomatoes_draw(); void bot_draw(); void fighter_draw(Fighter& fighter); const char* animation_get(Fighter& fighter); diff --git a/src/sim.h b/src/sim.h index f77544b..023628d 100644 --- a/src/sim.h +++ b/src/sim.h @@ -41,8 +41,6 @@ public: {} virtual void deal_play(int fighter) {} - virtual void tomato_splat(Vec2 position, int owner) - {} virtual void sandbox_return() {} }; diff --git a/src/weapon.cpp b/src/weapon.cpp index 7ee2f58..14df0b4 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -908,6 +908,17 @@ void Weapons::attack_move(Sim& sim, Attack& attack) float progress = attack.time / attack.duration; float range = attack.stats.range; + // A mine, flail ball or grapple left out goes once its slot no longer holds the weapon that put it there (which + // could no longer set it off, reel it in or let it go), a mine fizzling with a puff. + bool isLeftOut = def.kind == ATTACK_MINE || def.kind == ATTACK_FLAIL || def.kind == ATTACK_HOOK; + if (isLeftOut && !attack.isExplosion && fighter.weaponLoadout[attack.key] != attack.weapon) + { + attack.isDead = true; + if (def.kind == ATTACK_MINE) + sim.cues->attack_burst_spawn(SMOKE_BURST, attack.position, 0, attack); + return; + } + switch (def.kind) { case ATTACK_SWING: diff --git a/src/world.cpp b/src/world.cpp index d06af49..981f14e 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -211,9 +211,9 @@ static bool is_solid(Tile tile) return tile == TILE_SOLID || tile == TILE_BLOCK; } -// The arena with the practice dummy standing on the first surface below the middle of the map, and the player -// spawning right underneath it, on the next surface down. Its marked spawns are joined by more spread over its ground, -// for a lobby's sandbox full of players. +// The arena with the practice dummy standing on the first surface below the middle of the map. Fighters spawn on the +// arena's marked spawns (the first player, solo or hosting, on 1), joined by more spread over its ground for a lobby's +// sandbox full of players. void World::sandbox_load() { arena_load(); @@ -225,15 +225,6 @@ void World::sandbox_load() float standY = standRow * TILE_SIZE; float centerX = width * TILE_SIZE / 2.0f; dummies.push_back({.box = {centerX - DUMMY_WIDTH / 2, standY - DUMMY_HEIGHT, DUMMY_WIDTH, DUMMY_HEIGHT}}); - - int groundRow = standRow; - while (groundRow < height && tile_get(column, groundRow) != TILE_EMPTY) - groundRow++; - while (groundRow < height && tile_get(column, groundRow) == TILE_EMPTY) - groundRow++; - float groundY = groundRow * TILE_SIZE; - Ramp* ramp = ramp_find({centerX, groundY + TILE_SIZE / 2.0f}); - spawns[0] = {centerX, ramp ? ramp->y_at(centerX) : groundY}; spawns_spread(SPAWN_MARKS); }