mirror of
https://github.com/Crocmagnon/charasheet.git
synced 2024-12-22 13:51:49 +01:00
Add very basic battle effect version
This commit is contained in:
parent
98184c72d5
commit
5c9718d14a
14 changed files with 320 additions and 16 deletions
|
@ -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" />
|
||||
|
|
|
@ -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.
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
79
src/party/migrations/0003_battleeffect.py
Normal file
79
src/party/migrations/0003_battleeffect.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1 +1 @@
|
|||
0002_party_invited_characters
|
||||
0003_battleeffect
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 %}
|
||||
|
|
8
src/party/templates/party/snippets/add_effect_form.html
Normal file
8
src/party/templates/party/snippets/add_effect_form.html
Normal 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>
|
22
src/party/templates/party/snippets/effects.html
Normal file
22
src/party/templates/party/snippets/effects.html
Normal 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>
|
|
@ -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."""
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue