game room voice: gate on >1 cost-current depositor over the 7d initial period (not grace), not the phase — solo/CARTE-all-6 stays off — TDD
Refines the room voice activation per user-spec: voice mirrors my_sea's window but over the seat's 7d INITIAL cost period (GateSlot.cost_current — within [filled_at, filled_at+7d), NOT the renewal grace). voice_active now needs the gate CLOSED (table_status set = ROLE_SELECT onset) + the viewer seated + MORE THAN ONE distinct gamer holding a FILLED, cost-current seat. A sole depositor — including a CARTE owner occupying all 6 seats (one gamer) — keeps voice OFF (no one to talk to); a 2nd qualifying gamer flips it on; it toggles back off once the cost period lapses into grace. Replaces the earlier phase-set gate (ROLE/SIG/SKY only) — voice now spans the whole 7d window incl. IN_GAME (the phase ceiling at SEA_SELECT made the 7d expiry unreachable). TDD: RoomVoiceActivationTest reworked (13 ITs) — active w. 6 depositors across ROLE/SIG/SKY + persists in IN_GAME; inactive for a single depositor (CARTE-all-6), flips on once a 2nd gamer qualifies, inactive when filled 8d ago (grace), inactive before the gate closes, inactive for a non-seated viewer; room-id / muted-at passthrough + active-btn markup unchanged. 736 epic+drama ITs green. [[project-my-sea-invite-voice-blueprint]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4636,13 +4636,15 @@ class CarteSeatSwitchSkySeaTest(TestCase):
|
||||
|
||||
|
||||
class RoomVoiceActivationTest(TestCase):
|
||||
"""Voice activation for the epic game room. Voice is live for a SEATED
|
||||
gamer across the whole character-creation arc — from ROLE_SELECT onset
|
||||
(all tokens committed, seats pre-created) continuously through SKY_SELECT
|
||||
(which hosts the in-page DRAW SEA felt), turning off at IN_GAME. The
|
||||
burger fan's #id_voice_btn (shared `_burger.html`) lights `.active` +
|
||||
carries `data-room-id` = the room UUID, which the already-room-aware
|
||||
RoomVoiceConsumer (`TableSeat` membership gate) + voice-mesh.js consume."""
|
||||
"""Voice activation for the epic game room. Voice mirrors my_sea's window but
|
||||
over the seat's 7d INITIAL cost period (GateSlot.cost_current, NOT the renewal
|
||||
grace): live only when the gate is CLOSED (table_status set), the viewer is
|
||||
seated, AND MORE THAN ONE distinct gamer holds a token-backed, cost-current
|
||||
seat — a sole depositor (incl. a CARTE owner occupying all 6 seats = one
|
||||
gamer) has no one to talk to, so voice stays off. The burger fan's
|
||||
#id_voice_btn (shared `_burger.html`) lights `.active` + carries `data-room-id`
|
||||
= the room UUID, consumed by the already-room-aware RoomVoiceConsumer +
|
||||
voice-mesh.js. setUp = 6 distinct depositors (filled_at None → cost-current)."""
|
||||
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="vfounder@test.io")
|
||||
@@ -4683,15 +4685,42 @@ class RoomVoiceActivationTest(TestCase):
|
||||
self.room.save()
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], True)
|
||||
|
||||
def test_voice_inactive_in_game(self):
|
||||
def test_voice_persists_in_game_with_multiple_depositors(self):
|
||||
# The 7d window spans gameplay — voice stays live in IN_GAME as long as
|
||||
# >1 cost-current depositor remains (no phase ceiling at SEA_SELECT).
|
||||
self.room.table_status = Room.IN_GAME
|
||||
self.room.save()
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], False)
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], True)
|
||||
|
||||
def test_voice_inactive_for_non_seated_viewer(self):
|
||||
outsider = User.objects.create(email="outsider@test.io")
|
||||
self.assertIs(self._ctx(outsider)["voice_active"], False)
|
||||
|
||||
def test_voice_inactive_with_a_single_depositor(self):
|
||||
# CARTE owner of all 6 seats = ONE depositor → no one to talk to.
|
||||
self.room.gate_slots.filter(status=GateSlot.FILLED).update(gamer=self.founder)
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], False)
|
||||
|
||||
def test_voice_flips_on_once_a_second_gamer_qualifies(self):
|
||||
# Collapse to one depositor (off), then hand a slot to a 2nd gamer (on).
|
||||
self.room.gate_slots.filter(status=GateSlot.FILLED).update(gamer=self.founder)
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], False)
|
||||
self.room.gate_slots.filter(slot_number=2).update(gamer=self.gamers[1])
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], True)
|
||||
|
||||
def test_voice_inactive_when_cost_period_lapsed_into_grace(self):
|
||||
# Filled 8d ago → past the 7d initial period (renewal grace), so the
|
||||
# seats aren't cost-current → no active depositors → voice off.
|
||||
self.room.gate_slots.filter(status=GateSlot.FILLED).update(
|
||||
filled_at=timezone.now() - timedelta(days=8))
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], False)
|
||||
|
||||
def test_voice_inactive_before_the_gate_closes(self):
|
||||
# Still gathering (table_status None) → voice off even with depositors.
|
||||
self.room.table_status = None
|
||||
self.room.save()
|
||||
self.assertIs(self._ctx(self.founder)["voice_active"], False)
|
||||
|
||||
def test_voice_muted_at_empty_when_unset(self):
|
||||
self.assertEqual(self._ctx(self.founder)["voice_muted_at"], "")
|
||||
|
||||
|
||||
@@ -820,18 +820,30 @@ def room_view(request, room_id):
|
||||
seen_seated.add(seat.gamer_id)
|
||||
seated_others.append(seat.gamer)
|
||||
ctx["seated_others"] = seated_others
|
||||
# Voice (Phase C, room path) — the burger fan's #id_voice_btn lights for a
|
||||
# SEATED gamer across the whole character-creation arc: ROLE_SELECT onset
|
||||
# (all tokens committed, seats pre-created by pick_roles) continuously through
|
||||
# SKY_SELECT (which hosts the in-page DRAW SEA felt), going dark at IN_GAME.
|
||||
# The room UUID is the WebRTC mesh key — RoomVoiceConsumer._can_join already
|
||||
# gates an epic room on TableSeat membership (no consumer change), and
|
||||
# voice-mesh.js / burger-btn.js bind the active click → join/toggle-mute.
|
||||
# voice_muted_at carries the persisted mute so an in-arc nav/refresh rejoins
|
||||
# MUTED (mirrors my_sea). [[project-my-sea-invite-voice-blueprint]]
|
||||
VOICE_PHASES = {Room.ROLE_SELECT, Room.SIG_SELECT, Room.SKY_SELECT}
|
||||
# Voice (Phase C, room path) — the burger fan's #id_voice_btn. Mirrors my_sea's
|
||||
# voice window, but over the seat's 7d INITIAL cost period (GateSlot.cost_current
|
||||
# — NOT the renewal grace, mirroring "lasts a day, off in grace" at 7d scale).
|
||||
# It needs MORE THAN ONE distinct gamer holding a token-backed, cost-current
|
||||
# seat: a SOLE depositor — including a CARTE owner occupying all 6 seats (one
|
||||
# gamer) — has no one to talk to, so voice stays OFF; a 2nd qualifying gamer
|
||||
# flips it on, and it toggles back off once the cost period lapses into grace.
|
||||
# Gated on the gate being CLOSED (table_status set = ROLE_SELECT onset) + the
|
||||
# viewer being seated. The room UUID is the WebRTC mesh key —
|
||||
# RoomVoiceConsumer._can_join already gates an epic room on TableSeat
|
||||
# membership (no consumer change); voice_muted_at carries the persisted mute so
|
||||
# an in-period nav/refresh rejoins MUTED. [[project-my-sea-invite-voice-blueprint]]
|
||||
active_depositors = {
|
||||
slot.gamer_id
|
||||
for slot in room.gate_slots.filter(
|
||||
status=GateSlot.FILLED, gamer__isnull=False)
|
||||
if slot.cost_current
|
||||
}
|
||||
is_seated = room.table_seats.filter(gamer=request.user).exists()
|
||||
ctx["voice_active"] = is_seated and room.table_status in VOICE_PHASES
|
||||
ctx["voice_active"] = (
|
||||
room.table_status is not None
|
||||
and is_seated
|
||||
and len(active_depositors) > 1
|
||||
)
|
||||
ctx["voice_room_id"] = str(room.id)
|
||||
ctx["voice_muted_at"] = (
|
||||
request.user.voice_muted_at.isoformat()
|
||||
|
||||
Reference in New Issue
Block a user