Add very basic battle effect version

This commit is contained in:
Gabriel Augendre 2023-01-16 18:02:14 +01:00
parent 98184c72d5
commit 5c9718d14a
14 changed files with 320 additions and 16 deletions

View file

@ -18,6 +18,7 @@
<excludeFolder url="file://$MODULE_DIR$/src/public" />
<excludeFolder url="file://$MODULE_DIR$/src/test_reports" />
<excludeFolder url="file://$MODULE_DIR$/src/character/tests/test_reports" />
<excludeFolder url="file://$MODULE_DIR$/src/common/static/vendor" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11 (charasheet)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />

View file

@ -4,7 +4,7 @@
[tool.pytest.ini_options]
addopts = """
--html=test_reports/pytest_result/pytest.html --color=yes --durations 20
--no-cov-on-fail --strict-markers
--no-cov-on-fail --strict-markers --reuse-db
--driver=Firefox
-W error
-W "ignore:capabilities and desired_capabilities have been deprecated:DeprecationWarning:pytest_selenium.pytest_selenium"
@ -68,3 +68,6 @@ flake8-bandit = [
"-S106", # Possible hardcoded password.
"-S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes.
]
flake8-bugbear = [
"-B011", # Do not call assert False since python -O removes these calls.
]

View file

@ -22,3 +22,18 @@ img.profile-pic {
max-height: 240px;
object-fit: cover;
}
#effects-list {
display: flex;
flex-direction: row;
}
.effect {
width: 100px;
margin-right: 3em;
align-self: flex-end;
}
.effect .bar {
background-color: red;
}

View file

@ -1,5 +1,6 @@
import pytest
from django.core.management import call_command
from selenium.webdriver.remote.webdriver import WebDriver
@pytest.fixture(scope="session", autouse=True)
@ -7,12 +8,25 @@ def collectstatic():
call_command("collectstatic", "--clear", "--noinput", "--verbosity=0")
@pytest.fixture
def live_server(settings, live_server):
settings.STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
return live_server
@pytest.fixture
def firefox_options(firefox_options):
firefox_options.add_argument("-headless")
return firefox_options
@pytest.fixture
def selenium(selenium: WebDriver) -> WebDriver:
selenium.implicitly_wait(3)
selenium.set_window_size(3860, 2140)
return selenium
@pytest.fixture(autouse=True)
def settings(settings):
settings.DEBUG_TOOLBAR = False

View file

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.db.models import Q
from character.models import Character
from party.models import Party
from party.models import BattleEffect, Party
class PartyForm(forms.ModelForm):
@ -38,3 +38,9 @@ class PartyForm(forms.ModelForm):
ValidationError(f"{character} is already a group member."),
)
return invited
class BattleEffectForm(forms.ModelForm):
class Meta:
model = BattleEffect
fields = ["name", "target", "description", "remaining_rounds"]

View file

@ -0,0 +1,79 @@
# Generated by Django 4.1.5 on 2023-01-16 16:21
import django.db.models.deletion
import django_extensions.db.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("party", "0002_party_invited_characters"),
]
operations = [
migrations.CreateModel(
name="BattleEffect",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created"
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified"
),
),
("name", models.CharField(max_length=100, verbose_name="nom")),
("target", models.CharField(max_length=100, verbose_name="cible")),
(
"description",
models.TextField(blank=True, verbose_name="description"),
),
(
"remaining_rounds",
models.SmallIntegerField(
default=-1,
help_text="-1 pour un effet permanent",
verbose_name="nombre de tours restants",
),
),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="effects",
to=settings.AUTH_USER_MODEL,
verbose_name="créé par",
),
),
(
"party",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="effects",
to="party.party",
verbose_name="groupe",
),
),
],
options={
"get_latest_by": "modified",
"abstract": False,
},
),
]

View file

@ -1 +1 @@
0002_party_invited_characters
0003_battleeffect

View file

@ -14,6 +14,12 @@ class PartyQuerySet(models.QuerySet):
def played_by(self, user):
return self.filter(characters__in=Character.objects.filter(player=user))
def played_or_mastered_by(self, user):
return self.filter(
Q(game_master=user)
| Q(characters__in=Character.objects.filter(player=user))
).distinct()
def related_to(self, user):
return self.filter(
Q(game_master=user)
@ -61,3 +67,36 @@ class Party(UniquelyNamedModel, TimeStampedModel, models.Model):
def reset_stats(self):
for character in self.characters.all():
character.reset_stats()
class BattleEffectManager(models.Manager):
def decrease_all_remaining_rounds(self):
pass
class BattleEffect(TimeStampedModel, models.Model):
name = models.CharField(max_length=100, blank=False, null=False, verbose_name="nom")
target = models.CharField(
max_length=100, blank=False, null=False, verbose_name="cible"
)
description = models.TextField(blank=True, null=False, verbose_name="description")
remaining_rounds = models.SmallIntegerField(
blank=False,
default=-1,
verbose_name="nombre de tours restants",
help_text="-1 pour un effet permanent",
)
party = models.ForeignKey(
"party.Party",
on_delete=models.CASCADE,
related_name="effects",
verbose_name="groupe",
)
created_by = models.ForeignKey(
"common.User",
on_delete=models.CASCADE,
related_name="effects",
verbose_name="créé par",
)
objects = BattleEffectManager()

View file

@ -8,7 +8,8 @@
<h1>{{ party.name }}</h1>
<p>MJ : {{ party.game_master.get_full_name|default:party.game_master.username }}</p>
<p>
<a href="{% url "party:reset_stats" pk=party.pk %}" id="reset-stats">Réinitialiser les stats</a>
<a href="{% url "party:reset_stats" pk=party.pk %}" id="reset-stats">Réinitialiser
les stats</a>
</p>
<h2>Personnages</h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
@ -28,4 +29,6 @@
</div>
{% endif %}
{% endwith %}
<h2>Combat</h2>
{% include "party/snippets/effects.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% load django_bootstrap5 %}
<form>
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-primary" type="submit" hx-post="{% url "party:add_effect" pk=party.pk %}"
hx-target="#effects" hx-swap="outerHTML"
>Enregistrer</button>
</form>

View file

@ -0,0 +1,22 @@
<div id="effects">
<div id="effects-form">
<button
hx-get="{% url "party:add_effect" pk=party.pk %}"
hx-target="#effects-form"
hx-swap="innerHTML"
type="button"
id="add-effect"
class="btn btn-primary"><i class="fa-solid fa-plus"></i> Ajouter un effet
</button>
</div>
<div id="effects-list">
{% for effect in party.effects.all %}
<div class="effect">
<div style="height: calc({{ effect.remaining_rounds }}px * 20)" class="bar"></div>
<div class="name">{{ effect.name }}</div>
<div class="target">sur : {{ effect.target }}</div>
<div class="description text-secondary small">{{ effect.description }}</div>
</div>
{% endfor %}
</div>
</div>

View file

@ -1,8 +1,8 @@
import pytest
from django.core.management import call_command
from django.urls import reverse
from model_bakery import baker
from pytest_django.live_server_helper import LiveServer
from selenium.webdriver import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
from selenium.webdriver.support.select import Select
@ -10,13 +10,13 @@ from selenium.webdriver.support.select import Select
from character.models import Character, Profile
from character.tests.test_interactions import create_hurt_character, login
from common.models import User
from party.models import Party
from party.models import BattleEffect, Party
@pytest.mark.django_db
def test_add_character_to_existing_group(selenium: WebDriver, live_server: LiveServer):
call_command("loaddata", "initial_data")
def test_add_character_to_existing_group(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
username, password = "gm", "password"
gm = User.objects.create_user(username, password=password)
player = User.objects.create_user("player")
@ -40,10 +40,8 @@ def test_add_character_to_existing_group(selenium: WebDriver, live_server: LiveS
@pytest.mark.django_db
def test_gm_observe_invited_character_in_group(
selenium: WebDriver, live_server: LiveServer
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
call_command("loaddata", "initial_data")
username, password = "gm", "password"
gm = User.objects.create_user(username, password=password)
player = User.objects.create_user("player")
@ -66,10 +64,8 @@ def test_gm_observe_invited_character_in_group(
@pytest.mark.django_db
def test_gm_observe_invited_character_in_two_groups(
selenium: WebDriver, live_server: LiveServer
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
call_command("loaddata", "initial_data")
username, password = "gm", "password"
gm = User.objects.create_user(username, password=password)
player = User.objects.create_user("player")
@ -118,3 +114,102 @@ def test_reset_stats_view(
assert character.mana_remaining == character.mana_max
assert character.recovery_points_remaining == character.recovery_points_max
assert character.luck_points_remaining == character.luck_points_max
@pytest.mark.django_db
def test_player_can_add_effect_to_group(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""Any member of a group can add effects to the group."""
user, password = "player", "password"
player = User.objects.create_user(user, password=password)
character = baker.make(Character, player=player)
party = baker.make(Party)
party.characters.add(character)
assert BattleEffect.objects.count() == 0
login(selenium, live_server, user, password)
url = reverse("party:details", kwargs={"pk": party.pk})
selenium.get(live_server.url + url)
selenium.find_element(By.ID, "add-effect").click()
selenium.find_element(By.ID, "id_name").send_keys("Agrandissement")
selenium.find_element(By.ID, "id_target").send_keys("Joueur 4")
selenium.find_element(By.ID, "id_description").send_keys(
"Le Magicien ou une cible volontaire (au contact) voit sa taille augmenter de "
"50% pendant [5 + Mod. d'INT] tours. Il gagne +2 aux DM au contact et aux "
"tests de FOR. Pataud, il subit un malus de -2 aux tests de DEX."
)
selenium.find_element(By.ID, "id_remaining_rounds").send_keys(Keys.DELETE, "8")
selenium.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
assert BattleEffect.objects.count() == 1
# Todo: assert effect is displayed
@pytest.mark.django_db
def test_gm_can_add_effect_to_group(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""The GM of a group can add effects to the group."""
user, password = "gm", "password"
gm = User.objects.create_user(user, password=password)
party = baker.make(Party, game_master=gm)
assert BattleEffect.objects.count() == 0
login(selenium, live_server, user, password)
url = reverse("party:details", kwargs={"pk": party.pk})
selenium.get(live_server.url + url)
selenium.find_element(By.ID, "add-effect").click()
selenium.find_element(By.ID, "id_name").send_keys("Brûlé")
selenium.find_element(By.ID, "id_target").send_keys("Boss 2")
selenium.find_element(By.ID, "id_description").send_keys(
"Le Magicien choisit une cible située à moins de 30 mètres. Si son attaque "
"magique réussit, la cible encaisse [1d6 + Mod. d'INT] DM et la flèche "
"enflamme ses vêtements. Chaque tour de combat suivant, le feu inflige 1d6 "
"dégâts supplémentaires. Sur un résultat de 1 à 2, les flammes s'éteignent et "
"le sort prend fin."
)
selenium.find_element(By.ID, "id_remaining_rounds").send_keys(Keys.DELETE, "-1")
selenium.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
assert BattleEffect.objects.count() == 1
# Todo: assert effect is displayed
@pytest.mark.django_db
def test_gm_can_change_remaining_rounds(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""The GM of a group can increase or decrease the remaining rounds of effects."""
@pytest.mark.django_db
def test_gm_can_update_existing_effect(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""The GM of a group can update existing effect, except group and creator."""
@pytest.mark.django_db
def test_gm_can_delete_any_existing_effect(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""The GM of a group can delete any existing effect, running or terminated."""
@pytest.mark.django_db
def test_player_cant_change_existing_running_effect(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""Members of the group can only view existing running effects, no update."""
@pytest.mark.django_db
def test_player_can_delete_terminated_effect(
selenium: WebDriver, live_server: LiveServer, initial_data: None
):
"""Members of the group can delete terminated effects."""

View file

@ -10,6 +10,7 @@ urlpatterns = [
path("<int:pk>/change/", views.party_change, name="change"),
path("<int:pk>/delete/", views.party_delete, name="delete"),
path("<int:pk>/reset_stats/", views.party_reset_stats, name="reset_stats"),
path("<int:pk>/add_effect/", views.party_add_effect, name="add_effect"),
path("<int:pk>/leave/<int:character_pk>/", views.party_leave, name="leave"),
path("<int:pk>/join/<int:character_pk>/", views.party_join, name="join"),
path("<int:pk>/refuse/<int:character_pk>/", views.party_refuse, name="refuse"),

View file

@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from character.models import Character, HarmfulState
from party.forms import PartyForm
from party.forms import BattleEffectForm, PartyForm
from party.models import Party
@ -68,6 +68,24 @@ def party_reset_stats(request, pk):
return render(request, "party/party_reset_stats.html", context)
@login_required
def party_add_effect(request, pk):
party = get_object_or_404(Party.objects.played_or_mastered_by(request.user), pk=pk)
context = {"party": party}
if request.method == "GET":
form = BattleEffectForm()
else:
form = BattleEffectForm(request.POST or None)
if form.is_valid():
effect = form.save(commit=False)
effect.party = party
effect.created_by = request.user
effect.save()
return render(request, "party/snippets/effects.html", context)
context["form"] = form
return render(request, "party/snippets/add_effect_form.html", context)
@login_required
def party_change(request, pk):
party = get_object_or_404(Party.objects.managed_by(request.user), pk=pk)