'Playful Planning', 'deck_config' => [ 'name' => 'Fibonacci', 'cards' => [ ['key' => '3', 'label' => '3'], ['key' => '5', 'label' => '5'], ], ], ...$attributes, ]); } test('a participant can throw a realtime paper ball at another player', function () { $room = createEmoteRoom(['last_activity_at' => now()->subDay()]); $sender = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $target = $room->players()->create([ 'name' => 'Anna', 'is_moderator' => false, ]); $room->refresh(); $activity = $room->last_activity_at; $stateVersion = $room->state_version; Event::fake([PaperBallThrown::class]); $this->withSession(["room_players.{$room->id}" => $sender->id]) ->postJson(route('room.emote.paper-ball.store', $room), [ 'target_player_id' => $target->id, ]) ->assertNoContent(); Event::assertDispatched(PaperBallThrown::class, function (PaperBallThrown $event) use ($room, $sender, $target): bool { return $event->roomId === $room->id && $event->fromPlayerId === $sender->id && $event->targetPlayerId === $target->id && $event->chargeLevel === 0 && $event->broadcastOn()->name === "rooms.{$room->id}" && $event->broadcastAs() === 'room.emote.thrown' && array_keys($event->broadcastWith()) === [ 'id', 'from_player_id', 'target_player_id', 'charge_level', ]; }); $room->refresh(); expect($room->last_activity_at->equalTo($activity))->toBeTrue() ->and($room->state_version)->toBe($stateVersion); }); test('paper ball charge percentages map to the expected level', function (int $chargePercentage, int $expectedLevel) { $room = createEmoteRoom(); $sender = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $target = $room->players()->create([ 'name' => 'Anna', 'is_moderator' => false, ]); Event::fake([PaperBallThrown::class]); $this->withSession(["room_players.{$room->id}" => $sender->id]) ->postJson(route('room.emote.paper-ball.store', $room), [ 'target_player_id' => $target->id, 'charge_percentage' => $chargePercentage, ]) ->assertNoContent(); Event::assertDispatched( PaperBallThrown::class, fn (PaperBallThrown $event): bool => $event->chargeLevel === $expectedLevel, ); })->with([ 'uncharged' => [0, 0], 'just below level one' => [99, 0], 'level one threshold' => [100, 1], 'top of level one' => [199, 1], 'level two threshold' => [200, 2], 'top of level two' => [299, 2], 'level three threshold and cap' => [300, 3], ]); test('paper ball charge percentages outside the supported range are rejected', function (int $chargePercentage) { $room = createEmoteRoom(); $sender = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $target = $room->players()->create([ 'name' => 'Anna', 'is_moderator' => false, ]); Event::fake([PaperBallThrown::class]); $this->withSession(["room_players.{$room->id}" => $sender->id]) ->postJson(route('room.emote.paper-ball.store', $room), [ 'target_player_id' => $target->id, 'charge_percentage' => $chargePercentage, ]) ->assertUnprocessable() ->assertJsonValidationErrors('charge_percentage'); Event::assertNotDispatched(PaperBallThrown::class); })->with([ 'below zero' => -1, 'above the cap' => 301, ]); test('a participant cannot throw a paper ball at themselves', function () { Event::fake([PaperBallThrown::class]); $room = createEmoteRoom(); $player = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $this->withSession(["room_players.{$room->id}" => $player->id]) ->postJson(route('room.emote.paper-ball.store', $room), [ 'target_player_id' => $player->id, ]) ->assertUnprocessable() ->assertJsonValidationErrors('target_player_id'); Event::assertNotDispatched(PaperBallThrown::class); }); test('a participant cannot target a player from another room', function () { Event::fake([PaperBallThrown::class]); $room = createEmoteRoom(); $sender = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $otherRoom = createEmoteRoom(); $otherPlayer = $otherRoom->players()->create([ 'name' => 'Nina', 'is_moderator' => true, ]); $this->withSession(["room_players.{$room->id}" => $sender->id]) ->postJson(route('room.emote.paper-ball.store', $room), [ 'target_player_id' => $otherPlayer->id, ]) ->assertUnprocessable() ->assertJsonValidationErrors('target_player_id'); Event::assertNotDispatched(PaperBallThrown::class); });