new DRAMA & BILLBOARD apps to start provenance system; new billboard.html & _scroll.html templates; admin area now displays game event log; new CLAUDE.md file to free up Claude Code's memory.md space; minor additions to apps.epic.views to ensure new systems just described adhere to existing game views
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
88
src/apps/drama/models.py
Normal file
88
src/apps/drama/models.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class GameEvent(models.Model):
|
||||
# Gate phase
|
||||
ROOM_CREATED = "room_created"
|
||||
SLOT_RESERVED = "slot_reserved"
|
||||
SLOT_FILLED = "slot_filled"
|
||||
SLOT_RETURNED = "slot_returned"
|
||||
SLOT_RELEASED = "slot_released"
|
||||
INVITE_SENT = "invite_sent"
|
||||
# Role Select phase
|
||||
ROLE_SELECT_STARTED = "role_select_started"
|
||||
ROLE_SELECTED = "role_selected"
|
||||
ROLES_REVEALED = "roles_revealed"
|
||||
|
||||
VERB_CHOICES = [
|
||||
(ROOM_CREATED, "Room created"),
|
||||
(SLOT_RESERVED, "Gate slot reserved"),
|
||||
(SLOT_FILLED, "Gate slot filled"),
|
||||
(SLOT_RETURNED, "Gate slot returned"),
|
||||
(SLOT_RELEASED, "Gate slot released"),
|
||||
(INVITE_SENT, "Invite sent"),
|
||||
(ROLE_SELECT_STARTED, "Role select started"),
|
||||
(ROLE_SELECTED, "Role selected"),
|
||||
(ROLES_REVEALED, "Roles revealed"),
|
||||
]
|
||||
|
||||
room = models.ForeignKey(
|
||||
"epic.Room", on_delete=models.CASCADE, related_name="events",
|
||||
)
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name="game_events",
|
||||
)
|
||||
verb = models.CharField(max_length=30, choices=VERB_CHOICES)
|
||||
data = models.JSONField(default=dict)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["timestamp"]
|
||||
|
||||
def to_prose(self):
|
||||
"""Return a human-readable action description (actor rendered separately in template)."""
|
||||
d = self.data
|
||||
if self.verb == self.SLOT_FILLED:
|
||||
_token_names = {
|
||||
"coin": "Coin-on-a-String", "Free": "Free Token",
|
||||
"tithe": "Tithe Token", "pass": "Backstage Pass", "carte": "Carte Blanche",
|
||||
}
|
||||
code = d.get("token_type", "token")
|
||||
token = d.get("token_display") or _token_names.get(code, code)
|
||||
days = d.get("renewal_days", 7)
|
||||
slot = d.get("slot_number", "?")
|
||||
return f"deposits a {token} for slot {slot} ({days} days)"
|
||||
if self.verb == self.SLOT_RESERVED:
|
||||
return "reserves a seat"
|
||||
if self.verb == self.SLOT_RETURNED:
|
||||
return "withdraws from the gate"
|
||||
if self.verb == self.SLOT_RELEASED:
|
||||
return f"releases slot {d.get('slot_number', '?')}"
|
||||
if self.verb == self.ROOM_CREATED:
|
||||
return "opens this room"
|
||||
if self.verb == self.INVITE_SENT:
|
||||
return "sends an invitation"
|
||||
if self.verb == self.ROLE_SELECT_STARTED:
|
||||
return "Role selection begins"
|
||||
if self.verb == self.ROLE_SELECTED:
|
||||
_role_names = {
|
||||
"PC": "Player", "BC": "Builder", "SC": "Shepherd",
|
||||
"AC": "Alchemist", "NC": "Narrator", "EC": "Economist",
|
||||
}
|
||||
code = d.get("role", "?")
|
||||
role = d.get("role_display") or _role_names.get(code, code)
|
||||
return f"starts as {role}"
|
||||
if self.verb == self.ROLES_REVEALED:
|
||||
return "All roles assigned"
|
||||
return self.verb
|
||||
|
||||
def __str__(self):
|
||||
actor = self.actor.email if self.actor else "system"
|
||||
return f"[{self.timestamp:%Y-%m-%d %H:%M}] {actor} → {self.verb}"
|
||||
|
||||
|
||||
def record(room, verb, actor=None, **data):
|
||||
"""Record a game event in the drama log."""
|
||||
return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)
|
||||
Reference in New Issue
Block a user