Files
toptrumps/cards/models.py
T
Ross bf9e90d754 .
2025-12-05 10:46:16 +00:00

202 lines
6.7 KiB
Python

from django.db import models
class Dinosaur(models.Model):
ERA_CHOICES = [
('triassic', 'Triassic'),
('jurassic', 'Jurassic'),
('cretaceous', 'Cretaceous'),
]
DIET_CHOICES = [
('herbivore', 'Herbivore'),
('carnivore', 'Carnivore'),
('omnivore', 'Omnivore'),
]
name = models.CharField(max_length=120)
era = models.CharField(max_length=32, choices=ERA_CHOICES, default='jurassic')
diet = models.CharField(max_length=32, choices=DIET_CHOICES, default='herbivore')
description = models.TextField(blank=True)
image = models.ImageField(upload_to='dinosaurs/', blank=True, null=True)
background = models.ForeignKey(
'cards.BackgroundImage',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='dinosaurs',
help_text='Optional per-card background image'
)
# Stats stored in real-world units now:
# - `speed` in km/h
# - `weight` in kg
# - `height` in metres
# - `intelligence` remains a 0-100 score
speed = models.PositiveIntegerField(default=30, help_text='Speed in km/h')
weight = models.PositiveIntegerField(default=1000, help_text='Weight in kg')
intelligence = models.PositiveSmallIntegerField(default=50, help_text='Score 0-100')
height = models.DecimalField(max_digits=5, decimal_places=1, default=1.5, help_text='Height in metres')
facts = models.TextField(blank=True, help_text='One fact per line; shown as bullet points on the card')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at', 'name']
def __str__(self):
return self.name
@property
def facts_list(self):
"""Return facts as a list of non-empty lines."""
if not self.facts:
return []
return [f.strip() for f in self.facts.splitlines() if f.strip()]
# -- Unit helpers / display properties -------------------------------------------------
# These map the 0-100 top-trump style values to human-friendly units. The numeric
# mappings are conservative defaults and can be adjusted if you want different ranges.
# Ranges used to convert between stored unit values and the 0-100 bar/star scale.
SPEED_MAX_KMH = 60.0
WEIGHT_MAX_KG = 15000.0
HEIGHT_MAX_M = 12.0
# Display helpers (use stored unit values directly)
@property
def speed_display(self):
return f"{self.speed} km/h"
@property
def weight_display(self):
kg = int(self.weight)
if kg >= 1000:
tonnes = round(kg / 1000.0, 1)
return f"{tonnes} t"
return f"{kg} kg"
@property
def height_display(self):
# height is a DecimalField; format to one decimal place
return f"{float(self.height):.1f} m"
@property
def intelligence_display(self):
return f"{self.intelligence}%"
# Percent helpers: convert stored units back to a 0-100 scale for bars/stars
def speed_percent(self):
return min(100, int(round(self.speed / float(self.SPEED_MAX_KMH) * 100)))
def weight_percent(self):
return min(100, int(round(self.weight / float(self.WEIGHT_MAX_KG) * 100)))
def height_percent(self):
return min(100, int(round(float(self.height) / float(self.HEIGHT_MAX_M) * 100)))
def intelligence_percent(self):
return min(100, int(self.intelligence or 0))
# -- Star rating helpers -------------------------------------------------------------
def _star_rating_from_percent(self, percent):
"""Convert a 0-100 percent into a 0-5 star score in 0.5 increments."""
if percent is None:
return 0.0
val = float(percent) * 0.05 # scale 0-100 -> 0-5
# round to nearest 0.5
return round(val * 2) / 2.0
def _stars_string(self, percent):
"""Return a simple star string like '★★★★½' and trailing empty stars ''."""
stars = self._star_rating_from_percent(percent)
full = int(stars)
half = 1 if (stars - full) >= 0.5 else 0
empty = 5 - full - half
return "" * full + ("½" if half else "") + "" * empty
def _star_types_from_percent(self, percent):
"""Return a list of five items: 'full', 'half', or 'empty'."""
stars = self._star_rating_from_percent(percent)
full = int(stars)
half = 1 if (stars - full) >= 0.5 else 0
types = ['full'] * full
if half:
types.append('half')
types.extend(['empty'] * (5 - len(types)))
return types
# Expose star strings and original values for templates
@property
def speed_stars(self):
return self._stars_string(self.speed_percent())
@property
def speed_star_types(self):
return self._star_types_from_percent(self.speed_percent())
@property
def speed_orig(self):
return self.speed
@property
def weight_stars(self):
return self._stars_string(self.weight_percent())
@property
def weight_star_types(self):
return self._star_types_from_percent(self.weight_percent())
@property
def weight_orig(self):
return self.weight
@property
def height_stars(self):
return self._stars_string(self.height_percent())
@property
def height_star_types(self):
return self._star_types_from_percent(self.height_percent())
@property
def height_orig(self):
return self.height
@property
def intelligence_stars(self):
return self._stars_string(self.intelligence_percent())
@property
def intelligence_star_types(self):
return self._star_types_from_percent(self.intelligence_percent())
@property
def intelligence_orig(self):
return self.intelligence
class BackgroundImage(models.Model):
"""Background images used behind card renders.
Admins can upload multiple backgrounds and mark one as active. Templates
will use BackgroundImage.get_active() to obtain the active background.
"""
title = models.CharField(max_length=200, blank=True)
image = models.ImageField(upload_to='backgrounds/')
is_active = models.BooleanField(default=False, help_text='Mark as the active background')
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-is_active', '-uploaded_at']
def __str__(self):
return self.title or f"Background #{self.pk}"
@classmethod
def get_active(cls):
"""Return the active BackgroundImage if set, otherwise the most recent one or None."""
active = cls.objects.filter(is_active=True).order_by('-uploaded_at').first()
if active:
return active
return cls.objects.order_by('-uploaded_at').first()