feat: add progression models and service structure
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
Service chargé de valider la réalisation d'une tâche.
|
||||||
+38
-2
@@ -1,3 +1,39 @@
|
|||||||
from django.db import models
|
# apps/accounts/models.py
|
||||||
|
|
||||||
# Create your models here.
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PlayerProfile(models.Model):
|
||||||
|
class Visibility(models.TextChoices):
|
||||||
|
PRIVATE = "private", "Privé"
|
||||||
|
LOCAL = "local", "Instance locale"
|
||||||
|
FEDERATED = "federated", "Public et fédéré"
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.OneToOneField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="player_profile",
|
||||||
|
)
|
||||||
|
handle = models.SlugField(max_length=50, unique=True)
|
||||||
|
display_name = models.CharField(max_length=100)
|
||||||
|
bio = models.TextField(blank=True)
|
||||||
|
avatar = models.ImageField(upload_to="avatars/", blank=True)
|
||||||
|
|
||||||
|
visibility = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=Visibility.choices,
|
||||||
|
default=Visibility.PRIVATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Identifiant stable exposé à la fédération
|
||||||
|
public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -1,3 +1,73 @@
|
|||||||
|
# apps/achivements/models.py
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
class Achievement(models.Model):
|
||||||
|
class RuleType(models.TextChoices):
|
||||||
|
TASK_COUNT = "task_count", "Nombre de réalisations"
|
||||||
|
SKILL_XP = "skill_xp", "XP dans une compétence"
|
||||||
|
TREE_XP = "tree_xp", "XP dans un arbre"
|
||||||
|
TOTAL_XP = "total_xp", "XP total"
|
||||||
|
DISTINCT_TASKS = "distinct_tasks", "Tâches différentes"
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
slug = models.SlugField(max_length=100, unique=True)
|
||||||
|
name = models.CharField(max_length=160)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
icon = models.CharField(max_length=80, blank=True)
|
||||||
|
|
||||||
|
rule_type = models.CharField(max_length=32, choices=RuleType.choices)
|
||||||
|
rule_config = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
xp_reward = models.PositiveIntegerField(default=0)
|
||||||
|
is_secret = models.BooleanField(default=False)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerAchievement(models.Model):
|
||||||
|
player = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="achievements",
|
||||||
|
)
|
||||||
|
achievement = models.ForeignKey(
|
||||||
|
Achievement,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="players",
|
||||||
|
)
|
||||||
|
unlocked_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
progress_snapshot = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("player", "achievement"),
|
||||||
|
name="unique_player_achievement",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Title(models.Model):
|
||||||
|
slug = models.SlugField(max_length=100, unique=True)
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
achievement = models.OneToOneField(
|
||||||
|
Achievement,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="title",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerTitle(models.Model):
|
||||||
|
player = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="titles",
|
||||||
|
)
|
||||||
|
title = models.ForeignKey(Title, on_delete=models.CASCADE)
|
||||||
|
unlocked_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_equipped = models.BooleanField(default=False)
|
||||||
|
|||||||
+141
-1
@@ -1,3 +1,143 @@
|
|||||||
|
# apps/progression/models.py
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
class TaskCompletion(models.Model):
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
PENDING = "pending", "En attente"
|
||||||
|
ACCEPTED = "accepted", "Acceptée"
|
||||||
|
REJECTED = "rejected", "Refusée"
|
||||||
|
CANCELLED = "cancelled", "Annulée"
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
player = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="task_completions",
|
||||||
|
)
|
||||||
|
task = models.ForeignKey(
|
||||||
|
"skills.Task",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="completions",
|
||||||
|
)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=Status.choices,
|
||||||
|
default=Status.ACCEPTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_at = models.DateTimeField()
|
||||||
|
submitted_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
note = models.TextField(blank=True)
|
||||||
|
proof_url = models.URLField(blank=True)
|
||||||
|
|
||||||
|
# Évite les doubles créations lors d’un retry de l’API.
|
||||||
|
idempotency_key = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Snapshot au moment de la validation
|
||||||
|
task_name_snapshot = models.CharField(max_length=160)
|
||||||
|
xp_snapshot = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
validated_by = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="validated_completions",
|
||||||
|
)
|
||||||
|
validated_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("player", "idempotency_key"),
|
||||||
|
condition=models.Q(idempotency_key__isnull=False),
|
||||||
|
name="unique_player_idempotency_key",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=("player", "-completed_at")),
|
||||||
|
models.Index(fields=("task", "-completed_at")),
|
||||||
|
]
|
||||||
|
|
||||||
|
class XPTransaction(models.Model):
|
||||||
|
class Reason(models.TextChoices):
|
||||||
|
TASK = "task", "Réalisation d’une tâche"
|
||||||
|
ACHIEVEMENT = "achievement", "Succès"
|
||||||
|
BONUS = "bonus", "Bonus"
|
||||||
|
CORRECTION = "correction", "Correction"
|
||||||
|
REVERSAL = "reversal", "Annulation"
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
player = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="xp_transactions",
|
||||||
|
)
|
||||||
|
skill = models.ForeignKey(
|
||||||
|
"skills.Skill",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="xp_transactions",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Signé : positif ou négatif
|
||||||
|
amount = models.IntegerField()
|
||||||
|
reason = models.CharField(max_length=20, choices=Reason.choices)
|
||||||
|
|
||||||
|
completion = models.ForeignKey(
|
||||||
|
TaskCompletion,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="xp_transactions",
|
||||||
|
)
|
||||||
|
reversed_transaction = models.OneToOneField(
|
||||||
|
"self",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="reversal",
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=("player", "skill")),
|
||||||
|
models.Index(fields=("player", "-created_at")),
|
||||||
|
]
|
||||||
|
|
||||||
|
class PlayerSkillProgress(models.Model):
|
||||||
|
player = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="skill_progress",
|
||||||
|
)
|
||||||
|
skill = models.ForeignKey(
|
||||||
|
"skills.Skill",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="player_progress",
|
||||||
|
)
|
||||||
|
xp = models.PositiveBigIntegerField(default=0)
|
||||||
|
level = models.PositiveIntegerField(default=0)
|
||||||
|
unlocked_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("player", "skill"),
|
||||||
|
name="unique_player_skill_progress",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|||||||
+98
-1
@@ -1,3 +1,100 @@
|
|||||||
|
# apps/skills/models.py
|
||||||
|
|
||||||
|
import uuid
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
class SkillTree(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
slug = models.SlugField(max_length=80, unique=True)
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
icon = models.CharField(max_length=80, blank=True)
|
||||||
|
color = models.CharField(max_length=7, blank=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
sort_order = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Skill(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
tree = models.ForeignKey(
|
||||||
|
SkillTree,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="skills",
|
||||||
|
)
|
||||||
|
slug = models.SlugField(max_length=80)
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
icon = models.CharField(max_length=80, blank=True)
|
||||||
|
|
||||||
|
# Utilisable pour afficher le graphe
|
||||||
|
position_x = models.IntegerField(default=0)
|
||||||
|
position_y = models.IntegerField(default=0)
|
||||||
|
sort_order = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
prerequisites = models.ManyToManyField(
|
||||||
|
"self",
|
||||||
|
symmetrical=False,
|
||||||
|
blank=True,
|
||||||
|
related_name="unlocks",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("tree", "slug"),
|
||||||
|
name="unique_skill_slug_per_tree",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
class Task(models.Model):
|
||||||
|
class ValidationMode(models.TextChoices):
|
||||||
|
SELF = "self", "Auto-validation"
|
||||||
|
PEER = "peer", "Validation par un tiers"
|
||||||
|
PROOF = "proof", "Preuve demandée"
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
slug = models.SlugField(max_length=100, unique=True)
|
||||||
|
name = models.CharField(max_length=160)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
base_xp = models.PositiveIntegerField()
|
||||||
|
validation_mode = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=ValidationMode.choices,
|
||||||
|
default=ValidationMode.SELF,
|
||||||
|
)
|
||||||
|
|
||||||
|
cooldown_seconds = models.PositiveIntegerField(default=0)
|
||||||
|
daily_limit = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
is_repeatable = models.BooleanField(default=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
skills = models.ManyToManyField(
|
||||||
|
Skill,
|
||||||
|
through="TaskSkillReward",
|
||||||
|
related_name="tasks",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSkillReward(models.Model):
|
||||||
|
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||||
|
skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# Permet, par exemple, de répartir 10 XP entre maison et écologie.
|
||||||
|
xp = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("task", "skill"),
|
||||||
|
name="unique_reward_per_task_skill",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Service chargé de valider la réalisation d'une tâche.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Service chargé de valider la réalisation d'une tâche.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Service chargé de valider la réalisation d'une tâche.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Service chargé de valider la réalisation d'une tâche.
|
||||||
Reference in New Issue
Block a user