From ecf8b898816b1ea8acb732d88751405142c7ab8e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 5 Mar 2026 11:04:00 +0000 Subject: [PATCH] Auto commit: 2026-03-05T11:04:00.570Z --- core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 2928 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 2227 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 8618 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 1081 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 10234 bytes core/admin.py | 54 ++- core/forms.py | 28 ++ core/migrations/0001_initial.py | 113 +++++ core/migrations/0002_seed_story.py | 127 ++++++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 6525 bytes .../0002_seed_story.cpython-311.pyc | Bin 0 -> 5722 bytes core/models.py | 171 +++++++- core/templates/base.html | 52 ++- core/templates/core/character_create.html | 35 ++ core/templates/core/character_detail.html | 79 ++++ core/templates/core/index.html | 264 ++++++------ core/templates/core/inventory.html | 41 ++ core/templates/core/item_detail.html | 36 ++ core/templates/core/quest_detail.html | 32 ++ core/templates/core/quest_list.html | 36 ++ core/templates/core/story.html | 70 +++ core/urls.py | 18 +- core/views.py | 248 ++++++++++- static/css/custom.css | 391 ++++++++++++++++- staticfiles/css/custom.css | 398 +++++++++++++++++- 25 files changed, 2015 insertions(+), 178 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_seed_story.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_seed_story.cpython-311.pyc create mode 100644 core/templates/core/character_create.html create mode 100644 core/templates/core/character_detail.html create mode 100644 core/templates/core/inventory.html create mode 100644 core/templates/core/item_detail.html create mode 100644 core/templates/core/quest_detail.html create mode 100644 core/templates/core/quest_list.html create mode 100644 core/templates/core/story.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a0cd478c226e501b5189550e758662c560..5ed77b38ab866f82535ecad5a42ee1789023129c 100644 GIT binary patch literal 2928 zcmcJR%Wm676ozMb(S^2TTej@Pb|gEk-3m=Dw8^GLQJ_tWB1j{kFuEu=1Wk@@#-ykW z$t2!skq7WDeF7IoUZw)HFlJSttL$Vrt1OD1Ls^VWv4CKpl=zdJIsEjWxg6+^Qb{9l zeD&Lz^N&Kv-}o>(a#uH(p+v}QViTLX#G^W;#FlK?l{{IOJw;bMRad>7p7Zj0-Ye(@ zuc#L(kqDztV>)tJ;gk~c2F~Fjk)MdIydt&=Q8S~~QyF6593H1|c{6$-`fQ4*WDs*e z%uh`$rHFC{u>i#4)WmX%n9m?;K%AMHIF};kGKjN4EKNXSe%3IaMW%bzGwV7*XxL8Bam`~< z3Yf{QLu21zt{t3_OSJU<^X8%Nv1YUvwZo{%UNS2J)6Ew?KMFdg#hRASSu-Atoz5{Y z!8@^(;U>5R(oH^pEq>hn`Fv@s`vgB6lZ!*Mb6n(j84mPt02HAH z@t7(hdl`x{XU`&sGusG`9M=_^?d2nMFb=-&L0gMAK+_IC!!{I5ZNlL_bhtL|5JPX^uyU=^jI4eF zFrheDdkVN2Ivr+%+b^Iw+ieqiILZ#36K)@%+xoa$484KdxerBeXjS28mE(RFc|Qs* zs5e5hI5R`ozh(%gA3$HW>HGL93WiLW{vJ)&#!X{rtFb7`_EWQc;O|&IR9|P0%g`=z zP$!uia1|bS4keFLKq;c&e+NDTA_|-xK=FinuRXz+;#7v!FRl#n28&l{ya?j4jlEtk zBkv2}3~TeZ{A<8cI0FX%Z3Z%>^qg$`dHp+33RD`9|3j9EG~cbhs-37ky4q`){{#V3?fkJkF&04N(sNz3om{_O@*HjuJ`lzI}MP_!O}JlBv?M@m{wSbupN QDRrdOk+MD+AD6{{0h0q%oB#j- delta 168 zcmew$c7-uzIWI340}#~ttjM$n(vLwL7+``jJ_`XE(-~42QW$d>av7r-85vTTf*CZK zUxE~9GTvfMOv%m6^V4Ly#g~$mn3tZfmzKBJiZhlH>PO4oI c2T+U=h>K-`#0O?ZM#dWq3Ky`UA~v830EbZ}egFUf diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59e60bd35cbf5bc717d5cd47fd9e5d72a674ec47 GIT binary patch literal 2227 zcmb7E&2Q936rb7kdc7aP*#IRhEse;Bx=|sb5+y2Cze4$F$WlQ$bT5`;Pm*1{>t$?< z5T$JnNZm?l%Yh2iA~gq;g5ZFvR;nucKX{QPET1a%#BEV>s5tdKJL@!jRO;A%o;UMm z=Dj!X_xq`*CrhC1`Sf;mAxX$zsB~LuqqEk4&NZSFof@Q0E0hvFp(l+*Jy}WCg^EB) zf*dEhaGB^SMr+-5WEFP217FlCVrV6TRr)`y(zS=E>pZ5~N!Ls?se&NaeFh3ce|ND!8+30*^-hI;HJ#-a;15_eOpaLOG# zDQtJe)dcA9MGgIjebQZ>NwrM3AI^lC#1S20sM2Tl%RIGTPR~N5xnO%7)C6a$&5l9D zk3NQd7V;SUe$8yy+%%kBFyvl$@3+4laDR4xb$@9uyZ7CD?(gnBNKHyJRLcr-O+(e# zoN4HcWBOFv<`zt6>*~B>u*R%C7l;?D`Yf|KI=qkz&#;)mG#l*FT9{UgZ-dElDIZ9R z(opM6QG%?Z)J@$nu%B0y3yx|;GaT-It~3cSp#eGWdGQlS!ZL;w9~Mi zF*eUM$2R%cMU$Vmnh@}qW^y(b*2;nAC60v<3K2TiB8a8<@pHx2q2+h|?K^#WZ_8Q! z;@0PGdDb1C!&k1h<*FxFJE>$|d`v*BqgXoHkx1X*m7QK;pWnON>wVpq_rTi4mBY8D z+EJiXXOULHo163-psU5?BH2u$p4c59ro;(-k;P^zH{re#Lp|OII!Gf`x|KwL zDhVVwRSx6`SOA-i%!<)Kgu;BJ?B}XpHGvb7O|!}I+Jm$?T?1%ZfF!fQf|TVnf&BoL z)kf6@B#JW#Ig7sv#^D9=JT4QRrVd<4v}_QNg6`w@47SdBJ%fH>*VVlb z3qubIL+!$dR~Yek^!xG-Paf!`g>AqKAl6YV9qZ(U)D-QI|0g3RK{v`JDG>FV+L$$? z$VK30W>_J+MwKJr)kRbodS8TP(FSQ?6k~qAyt4CZ@o_k}{sN28`F{Y@-2k)z literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5fb7c2b603c587f8162d643eb10596d2858..3aa2b965e2f77ee6bc6fe3e68031e473915bf11a 100644 GIT binary patch literal 8618 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 delta 144 zcmZ4Ge33C>IWI340}#~ttjM$k(vLwL7+``jJ_`XE(-~42QW$d>av7r-85vTTf*CZK zUxE~9GTvg#%}+_qDfZK3y2Y82m6(^Fua}Zk#0-?2JfF#2>=%a(P(f)d0X Vi)9%m`zlCsUto{{q9Qh+5C96C9u)up diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..13127ffb914ad8ad338595611aa4415074c161b7 100644 GIT binary patch literal 1081 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..aff832d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,55 @@ from django.contrib import admin -# Register your models here. +from .models import ( + Character, + Choice, + InventoryItem, + Item, + Quest, + Scene, + StoryEntry, +) + + +@admin.register(Scene) +class SceneAdmin(admin.ModelAdmin): + list_display = ("title", "slug") + search_fields = ("title", "slug") + + +@admin.register(Choice) +class ChoiceAdmin(admin.ModelAdmin): + list_display = ("scene", "text", "required_skill", "difficulty") + list_filter = ("required_skill",) + search_fields = ("text",) + + +@admin.register(Character) +class CharacterAdmin(admin.ModelAdmin): + list_display = ("name", "background", "level") + list_filter = ("background",) + search_fields = ("name",) + + +@admin.register(Quest) +class QuestAdmin(admin.ModelAdmin): + list_display = ("title", "character", "status") + list_filter = ("status",) + + +@admin.register(Item) +class ItemAdmin(admin.ModelAdmin): + list_display = ("name", "slot", "power") + list_filter = ("slot",) + + +@admin.register(InventoryItem) +class InventoryItemAdmin(admin.ModelAdmin): + list_display = ("character", "item", "equipped") + list_filter = ("equipped",) + + +@admin.register(StoryEntry) +class StoryEntryAdmin(admin.ModelAdmin): + list_display = ("character", "scene", "outcome", "created_at") + list_filter = ("outcome",) diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..be0eb10 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,28 @@ +from django import forms + +from .models import Character, Choice + + +class CharacterCreateForm(forms.Form): + name = forms.CharField( + max_length=80, + widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "Имя ведьмака"}), + ) + background = forms.ChoiceField( + choices=Character.BACKGROUND_CHOICES, + widget=forms.Select(attrs={"class": "form-select"}), + ) + + +class ChoiceForm(forms.Form): + choice = forms.ModelChoiceField( + queryset=Choice.objects.none(), + empty_label=None, + widget=forms.RadioSelect, + ) + + def __init__(self, *args, **kwargs): + choice_queryset = kwargs.pop("choice_queryset", Choice.objects.none()) + super().__init__(*args, **kwargs) + self.fields["choice"].queryset = choice_queryset + self.fields["choice"].widget.attrs.update({"class": "choice-radio"}) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..e2356f4 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 5.2.7 on 2026-03-05 10:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Character', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=80)), + ('background', models.CharField(choices=[('warden', 'Warden of the North'), ('scour', 'Scour of the Roads'), ('alchemist', 'Herb-born Alchemist')], max_length=20)), + ('level', models.PositiveIntegerField(default=1)), + ('vigor', models.PositiveIntegerField(default=3)), + ('focus', models.PositiveIntegerField(default=3)), + ('alchemy', models.PositiveIntegerField(default=3)), + ('perk_points', models.PositiveIntegerField(default=1)), + ], + ), + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120)), + ('slot', models.CharField(choices=[('weapon', 'Weapon'), ('armor', 'Armor'), ('trinket', 'Trinket')], max_length=20)), + ('power', models.PositiveIntegerField(default=0)), + ('description', models.TextField()), + ], + ), + migrations.CreateModel( + name='Scene', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=120)), + ('slug', models.SlugField(unique=True)), + ('body', models.TextField()), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='InventoryItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('equipped', models.BooleanField(default=False)), + ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory', to='core.character')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='core.item')), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Quest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=140)), + ('summary', models.TextField()), + ('status', models.CharField(choices=[('active', 'Active'), ('complete', 'Complete'), ('failed', 'Failed')], default='active', max_length=20)), + ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quests', to='core.character')), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Choice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.CharField(max_length=200)), + ('required_skill', models.CharField(blank=True, choices=[('vigor', 'Vigor'), ('focus', 'Focus'), ('alchemy', 'Alchemy')], max_length=20)), + ('difficulty', models.PositiveIntegerField(blank=True, null=True)), + ('reward_item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reward_choices', to='core.item')), + ('fail_scene', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fail_from', to='core.scene')), + ('next_scene', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_choices', to='core.scene')), + ('scene', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='core.scene')), + ('success_scene', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='success_from', to='core.scene')), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.AddField( + model_name='character', + name='current_scene', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.scene'), + ), + migrations.CreateModel( + name='StoryEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('choice_text', models.CharField(max_length=200)), + ('outcome', models.CharField(choices=[('success', 'Success'), ('fail', 'Fail'), ('neutral', 'Neutral')], max_length=20)), + ('roll', models.PositiveIntegerField(blank=True, null=True)), + ('total', models.PositiveIntegerField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='story_entries', to='core.character')), + ('scene', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='story_entries', to='core.scene')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/core/migrations/0002_seed_story.py b/core/migrations/0002_seed_story.py new file mode 100644 index 0000000..63beceb --- /dev/null +++ b/core/migrations/0002_seed_story.py @@ -0,0 +1,127 @@ +from django.db import migrations + + +def seed_story(apps, schema_editor): + Scene = apps.get_model("core", "Scene") + Choice = apps.get_model("core", "Choice") + Item = apps.get_model("core", "Item") + + if Scene.objects.exists(): + return + + starter_item = Item.objects.create( + name="Серебряный знак Стылого Волка", + slot="trinket", + power=1, + description="Амулет, усиливающий чутьё на нечисть.", + ) + blade = Item.objects.create( + name="Клинок мокрых троп", + slot="weapon", + power=3, + description="Лёгкий меч, выкованный для болотных дуэлей.", + ) + + prologue = Scene.objects.create( + title="Угольный тракт", + slug="prologue", + body=( + "Над трактом висит туман, а трактирщик шепчет о пропавших караванах. " + "Вам предлагают золото и место у огня, если вы проверите болото." + ), + ) + crossroads = Scene.objects.create( + title="Перепутье болот", + slug="crossroads", + body=( + "Дорога дробится на три тропы. В одной слышен шёпот, в другой — скрежет стали, " + "третья уходит в заросли, где поблескивает огонёк." + ), + ) + lair = Scene.objects.create( + title="Логово на мели", + slug="lair", + body=( + "Вы находите гнездо твари раньше, чем она замечает вас. Пахнет железом и болотной " + "травой. Время решить, как закончится охота." + ), + ) + ambush = Scene.objects.create( + title="Засада в камышах", + slug="ambush", + body=( + "Тварь поднимается прямо из воды. Ваша спина касается холодного дерева, а туман " + "съедает пути отступления." + ), + ) + village = Scene.objects.create( + title="Поселение Гнилых крыш", + slug="village", + body=( + "В деревне тихо. Староста готов говорить только после того, как вы разделите " + "с ним кружку чёрного настоя." + ), + ) + + Choice.objects.create( + scene=prologue, + text="Принять заказ и выдвинуться к болоту.", + next_scene=crossroads, + ) + Choice.objects.create( + scene=prologue, + text="Сначала расспросить старосту о пропавших.", + next_scene=village, + ) + Choice.objects.create( + scene=crossroads, + text="Выслеживать тварь по следам (Фокус 12).", + required_skill="focus", + difficulty=12, + success_scene=lair, + fail_scene=ambush, + reward_item=blade, + ) + Choice.objects.create( + scene=crossroads, + text="Зажечь факелы и идти напролом.", + next_scene=ambush, + ) + Choice.objects.create( + scene=village, + text="Попросить алхимические сведения (Алхимия 12).", + required_skill="alchemy", + difficulty=12, + success_scene=crossroads, + fail_scene=prologue, + ) + Choice.objects.create( + scene=ambush, + text="Отступить к тракту и перегруппироваться.", + next_scene=prologue, + ) + Choice.objects.create( + scene=lair, + text="Проверить добычу и вернуться с трофеями.", + next_scene=prologue, + reward_item=starter_item, + ) + + +def unseed_story(apps, schema_editor): + Scene = apps.get_model("core", "Scene") + Choice = apps.get_model("core", "Choice") + Item = apps.get_model("core", "Item") + Choice.objects.all().delete() + Scene.objects.all().delete() + Item.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_story, unseed_story), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57141f39c57d0ade59b03ea23074401a6565f43e GIT binary patch literal 6525 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 Y^E~g$apmP}AN9BV%r5R7qr$xTKiHRXF8}}l literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0002_seed_story.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_story.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fde6ec26849aad0d0ddf58fa8b26251770b1c584 GIT binary patch literal 5722 zcmd5=-ER}w6(1+#&v=~gga9EEz7{Xw0Ik@4ph(@a>~;(32U4Yty6YO>As8GxJL4%t zGLjR5mO|RVE)uA#me5vdSL7i;QbHh!`X9_#mOPp#+Despsf_5;KJ|C*IF3VByIIFc+(tgxuYV?1h@I;H`a$xArS|#g}-+Y|RE*}7Mf^s#QXm2J!Vvw^H- zWUs-tFYBG!HR*w6oZrB5fQ2FXDtP(X>sfEsJ+*uF96*D?918F0wZ(W^Rc!Z(xRO$A z&-YHJ6LG~Bzn@VC^f%z4amSc7bH=Q3&&-+Q#)5g(cqAJSjRj-cn3s(^<_Ja}8;iyR zV^KCf!Np@^-k7$%88wkQt7M+h)YgxXrfbF%^CBM4nj?E;^P)Ly%;9d%xNl6Gzczn~ zt4Ff=F&>PVW9D_4Oc0qhKQ`vfVH%BuMO#e82H>EUOlNHOVEUY*+I39P;%Z`$x}Cn2 zO#Ox{UVu07@d^FF`>W<98Cr6aUapm zLjXR)m2&hm#$$6_Htt~@k((nl4&BBKeE$^%XgrGOn@Hy$2o`XUl|{LE0B4TazCkse zO!w=G{x9PEEey;!?(ydkdjj(mInEJ!auHs^3wSkM%{Yh92)udOybRMzfC~uAl#V4Q zSV-N+vpMn+xxpX?NENOxMJQ|2WQ@3H4FreV2RtXx@~{#l^vw~Nvy=;HH!s2l7#GZO zxP(!{oFleu_&!E+tL0n9f+%G zO;gh`rs@AA?i+xg@(Fhde!9Fz2H_$SLz2kgP2iCVEqf+L{bQeX^I;KO_hZ~)b2P{_a$zFQb&p&a@ZFg zkf8ek>;n{-g2OZ=pxBuTTrq~p=mNZ%kLVjI)X&Oe_bIV_Y9JM4+7^?sgsT6Im_9MD z@`o$d$qI4=)`u{Otc~#bhP24=7c~HX5j*@x*-#b0DuD@yok@USUQRSihXLMgDjHHq zkpTwso>vKEk^ZVl0abvPV4-ru4MES*dqNNqT2z!k41l06X|9r4nr{ny(wFnNNZ}CD z5q%3qd6QfL;|w_pa7p$F&mmm1J+Xn4x^`M$BG%tK(VAlp)|B))K8+|-yhu>>=xgW^ zaNl?ceXu^`lnKmSrV<)Pp5`1_X!?-pWpNHPwLhnvcoRfsd6Ym5f$QyvG^Elp`efC9F?zepsLv3^Bg z;H7}-tvG^oQRdMBj74PO4uJ_Dp_i_xLI_{9IetkP;3-cHi8&n%Y2hHk7o^-7huli; z&?}vqqDteX30W`>Q%M4K$J2`_ntAjNbo%QsLX(iAM$L%4S){o6dU2b4p!Px~vRoR| zlq09jzzX^!7@QS@XA>nMTZlq#G+ zzHvpMmm%ONIYX2qq-(jwEkUx}TgzT!3OWcOPG@Z+?-3YHpj^2S0SZqu6~YsFm+?oS z08jwc{sZBN?LL)`>)Nv*JPdo)4*aOp1MHBhe55B-g+;Zq$h93{iBqQ%aXpzCvL#KA z#}!R;qO$|1Vu@t=x=vNj#Z(qeR2Fs2Al|}0Cf7!BGfxSl<^gEuQ2(eE-bfrI;47A+ zi2h@O4CVL|K9GNm%A$!i$5$VxnNjKyHI+MhD;|v_(rZ<$<4&~RSTcTE85jzCRzg*2 zqN`m*WFp-l`VT;i-&M^NCq1z0RahdER#kdSROuyE^?}$yEfZ5S zipn!-yH6%#Oi}SsDX6VP+K99h*#M$Z`N&nr7jN~PP7f$O`bj;N(R-BhN?eDLo^xsS ztTq^nD?Rio*t7EC*t2inz5`KBp?U-3P-Jk(4y@d&dtrdSs2w;$2hPTafnVj#_o~ z`8Sc=u~JZw8pnc(_im{{XllRFS!~>qZ`@&p_F7U0aVA{gY}yIVCgN=JZy?!44(g(C2iI1hs{v=xjY zVhnk5N5NR%GB!{QZOwyS3(pAkL-RlL zm4COodsW&yd#OL718=9%9UyjX3=5@JAihS+V1{?X$_u{AHYnqqrFY%hvi z^5Pa#lnbJ4h_WU6MvoQ6mb}c;XC9+c33@(F?0n% y8w4rUx?C3vH%~{ str: + return self.title + + +class Item(models.Model): + SLOT_WEAPON = "weapon" + SLOT_ARMOR = "armor" + SLOT_TRINKET = "trinket" + SLOT_CHOICES = [ + (SLOT_WEAPON, "Weapon"), + (SLOT_ARMOR, "Armor"), + (SLOT_TRINKET, "Trinket"), + ] + + name = models.CharField(max_length=120) + slot = models.CharField(max_length=20, choices=SLOT_CHOICES) + power = models.PositiveIntegerField(default=0) + description = models.TextField() + + def __str__(self) -> str: + return self.name + + +class Choice(models.Model): + SKILL_VIGOR = "vigor" + SKILL_FOCUS = "focus" + SKILL_ALCHEMY = "alchemy" + SKILL_CHOICES = [ + (SKILL_VIGOR, "Vigor"), + (SKILL_FOCUS, "Focus"), + (SKILL_ALCHEMY, "Alchemy"), + ] + + scene = models.ForeignKey(Scene, related_name="choices", on_delete=models.CASCADE) + text = models.CharField(max_length=200) + next_scene = models.ForeignKey( + Scene, + related_name="incoming_choices", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + required_skill = models.CharField(max_length=20, choices=SKILL_CHOICES, blank=True) + difficulty = models.PositiveIntegerField(null=True, blank=True) + success_scene = models.ForeignKey( + Scene, + related_name="success_from", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + fail_scene = models.ForeignKey( + Scene, + related_name="fail_from", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + reward_item = models.ForeignKey( + Item, + related_name="reward_choices", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + + class Meta: + ordering = ["id"] + + def __str__(self) -> str: + return f"{self.scene.title}: {self.text}" + + +class Character(models.Model): + BACKGROUND_WARDEN = "warden" + BACKGROUND_SCOUR = "scour" + BACKGROUND_ALCHEMIST = "alchemist" + BACKGROUND_CHOICES = [ + (BACKGROUND_WARDEN, "Warden of the North"), + (BACKGROUND_SCOUR, "Scour of the Roads"), + (BACKGROUND_ALCHEMIST, "Herb-born Alchemist"), + ] + + name = models.CharField(max_length=80) + background = models.CharField(max_length=20, choices=BACKGROUND_CHOICES) + level = models.PositiveIntegerField(default=1) + vigor = models.PositiveIntegerField(default=3) + focus = models.PositiveIntegerField(default=3) + alchemy = models.PositiveIntegerField(default=3) + perk_points = models.PositiveIntegerField(default=1) + current_scene = models.ForeignKey( + Scene, on_delete=models.SET_NULL, null=True, blank=True + ) + + def __str__(self) -> str: + return self.name + + +class Quest(models.Model): + STATUS_ACTIVE = "active" + STATUS_COMPLETE = "complete" + STATUS_FAILED = "failed" + STATUS_CHOICES = [ + (STATUS_ACTIVE, "Active"), + (STATUS_COMPLETE, "Complete"), + (STATUS_FAILED, "Failed"), + ] + + character = models.ForeignKey( + Character, related_name="quests", on_delete=models.CASCADE + ) + title = models.CharField(max_length=140) + summary = models.TextField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE) + + class Meta: + ordering = ["id"] + + def __str__(self) -> str: + return self.title + + +class InventoryItem(models.Model): + character = models.ForeignKey( + Character, related_name="inventory", on_delete=models.CASCADE + ) + item = models.ForeignKey(Item, related_name="inventory_items", on_delete=models.CASCADE) + equipped = models.BooleanField(default=False) + + class Meta: + ordering = ["id"] + + def __str__(self) -> str: + return f"{self.character.name} - {self.item.name}" + + +class StoryEntry(models.Model): + OUTCOME_SUCCESS = "success" + OUTCOME_FAIL = "fail" + OUTCOME_NEUTRAL = "neutral" + OUTCOME_CHOICES = [ + (OUTCOME_SUCCESS, "Success"), + (OUTCOME_FAIL, "Fail"), + (OUTCOME_NEUTRAL, "Neutral"), + ] + + character = models.ForeignKey( + Character, related_name="story_entries", on_delete=models.CASCADE + ) + scene = models.ForeignKey(Scene, related_name="story_entries", on_delete=models.CASCADE) + choice_text = models.CharField(max_length=200) + outcome = models.CharField(max_length=20, choices=OUTCOME_CHOICES) + roll = models.PositiveIntegerField(null=True, blank=True) + total = models.PositiveIntegerField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return f"{self.character.name} - {self.choice_text}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..0a8feca 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,11 +1,16 @@ - + - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} + + {% block title %}Темная тропа{% endblock %} + {% if page_description %} + + {% elif project_description %} + {% endif %} + {% if project_description %} {% endif %} @@ -13,13 +18,52 @@ {% endif %} + + + + {% load static %} {% block head %}{% endblock %} - + + + {% block content %}{% endblock %} + +
+
+ +
+
+ diff --git a/core/templates/core/character_create.html b/core/templates/core/character_create.html new file mode 100644 index 0000000..9ef7dee --- /dev/null +++ b/core/templates/core/character_create.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+

Создать героя

+

Выберите происхождение и начните охоту за тайной Чёрной Трясины.

+
+
+
+ {% csrf_token %} +
+ + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors|striptags }}
+ {% endif %} +
+
+ + {{ form.background }} + {% if form.background.errors %} +
{{ form.background.errors|striptags }}
+ {% endif %} +
+
+ Подсказка: выбор происхождения задаёт стартовые навыки. +
+ +
+
+
+{% endblock %} diff --git a/core/templates/core/character_detail.html b/core/templates/core/character_detail.html new file mode 100644 index 0000000..aa5607b --- /dev/null +++ b/core/templates/core/character_detail.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

{{ character.name }}

+

Происхождение: {{ character.get_background_display }}

+
+ +
+ +
+
+

Характеристики

+
+
+

Уровень

+

Lv {{ character.level }}

+
+
+

Ви́гор

+

{{ character.vigor }}

+
+
+

Фокус

+

{{ character.focus }}

+
+
+

Алхимия

+

{{ character.alchemy }}

+
+
+

Перки

+

{{ character.perk_points }}

+
+
+

Откройте ветки перков в следующих итерациях.

+
+
+

Экипировка

+ {% if equipped %} +
    + {% for entry in equipped %} +
  • + {{ entry.item.name }} + {{ entry.item.get_slot_display }} +
  • + {% endfor %} +
+ {% else %} +

Экипировка пока не выбрана.

+ {% endif %} + Управлять лутом → +
+
+

Последние решения

+ {% if recent_entries %} +
    + {% for entry in recent_entries %} +
  • + {{ entry.scene.title }} + {{ entry.choice_text }} + {{ entry.get_outcome_display }} +
  • + {% endfor %} +
+ {% else %} +

Вы ещё не сделали выборов. Начните пролог.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..fe5e62e 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,133 @@ {% extends "base.html" %} +{% load static %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{{ page_title }}{% endblock %} {% block content %}
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+ Мрачное фэнтези · интерактивная проза +

Ваш выбор — клинок, который меняет судьбу.

+

+ Пройдите историю в духе «Ведьмака»: выбирайте действия прямо в диалоге, проходите проверки навыков, + собирайте добычу и отслеживайте квесты в удобном журнале. +

+ +
+ Выборы и последствия + Сцены с проверками + Лут и экипировка + Перки и развитие +
+
+
+
+

Статус похода

+ {% if character %} +
+
+

Герой

+

{{ character.name }}

+
+
+

Уровень

+

Lv {{ character.level }}

+
+
+

Ви́гор

+

{{ character.vigor }}

+
+
+

Фокус

+

{{ character.focus }}

+
+
+

Алхимия

+

{{ character.alchemy }}

+
+
+ Открыть текущую сцену + {% else %} +

+ Создайте персонажа, чтобы открыть сюжет, журнал заданий и инвентарь. +

+ Начать пролог + {% endif %} +
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
+ + +
+
+

Текущие задания

+ {% if character %} + {% if quests %} +
    + {% for quest in quests %} +
  • + {{ quest.title }} + {{ quest.get_status_display }} +
  • + {% endfor %} +
+ Открыть весь журнал → + {% else %} +

В журнале пока пусто — начните сюжет, чтобы получить первое задание.

+ {% endif %} + {% else %} +

Создайте героя, чтобы получать квесты и отслеживать последствия.

+ {% endif %} +
+
+

Инвентарь и экипировка

+ {% if character %} + {% if inventory %} +
    + {% for entry in inventory %} +
  • + {{ entry.item.name }} + {{ entry.item.get_slot_display }} +
  • + {% endfor %} +
+ Управлять лутом → + {% else %} +

Пока нет предметов. Пройдите пролог, чтобы получить добычу.

+ {% endif %} + {% else %} +

Лут и экипировка появятся после создания персонажа.

+ {% endif %} +
+
+

Как работает выбор

+

+ Каждая сцена — это диалог. Вы выбираете действие, и система бросает проверку навыка. + Успех открывает лучшие сцены и редкий лут, провал запускает опасные ветки. +

+
+
+

Бросок

+

d20 + навык

+
+
+

Порог

+

Сцена

+
+
+ Перейти к сцене → +
+
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html new file mode 100644 index 0000000..6185473 --- /dev/null +++ b/core/templates/core/inventory.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

Инвентарь

+

Сортируйте трофеи и экипируйте нужные предметы.

+
+ +
+ +
+ {% if items %} + {% for entry in items %} +
+

{{ entry.item.name }}

+

{{ entry.item.description }}

+
+ {{ entry.item.get_slot_display }} + {% if entry.equipped %} + Экипировано + {% endif %} +
+ Подробнее → +
+ {% endfor %} + {% else %} +
+

Инвентарь пуст

+

Пройдите пролог, чтобы получить первые трофеи.

+ Открыть сюжет → +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/item_detail.html b/core/templates/core/item_detail.html new file mode 100644 index 0000000..acb7f9b --- /dev/null +++ b/core/templates/core/item_detail.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

{{ inventory_item.item.name }}

+

{{ inventory_item.item.get_slot_display }}

+
+ +
+ +
+

Описание

+

{{ inventory_item.item.description }}

+
+
+

Сила

+

+{{ inventory_item.item.power }}

+
+
+

Статус

+ {% if inventory_item.equipped %} +

Экипировано

+ {% else %} +

В запасе

+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/templates/core/quest_detail.html b/core/templates/core/quest_detail.html new file mode 100644 index 0000000..3da8d97 --- /dev/null +++ b/core/templates/core/quest_detail.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

{{ quest.title }}

+

Статус: {{ quest.get_status_display }}

+
+ +
+ +
+

Описание

+

{{ quest.summary }}

+
+
+

Герой

+

{{ quest.character.name }}

+
+
+

Уровень

+

Lv {{ quest.character.level }}

+
+
+
+
+{% endblock %} diff --git a/core/templates/core/quest_list.html b/core/templates/core/quest_list.html new file mode 100644 index 0000000..8ee0afa --- /dev/null +++ b/core/templates/core/quest_list.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

Журнал заданий

+

Следите за основными и побочными ветками истории.

+
+ +
+ +
+ {% if quests %} + {% for quest in quests %} + + {% endfor %} + {% else %} +
+

Пока нет заданий

+

Пройдите первую сцену, чтобы получить квесты.

+ Открыть сюжет → +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/story.html b/core/templates/core/story.html new file mode 100644 index 0000000..9873603 --- /dev/null +++ b/core/templates/core/story.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +
+
+
+

Сюжет

+

Ваш путь: {{ character.name }} · Lv {{ character.level }}

+
+ +
+ + {% if not scene %} +
+

Сцены ещё не настроены

+

Добавьте сцены и варианты выбора в админке, чтобы запустить сюжет.

+ Перейти в админку → +
+ {% else %} +
+
+ Сцена +

{{ scene.title }}

+

{{ scene.body }}

+ {% if last_entry %} +
+
+ Ваш выбор: {{ last_entry.choice_text }} +
+
+ {{ last_entry.get_outcome_display }} + {% if last_entry.roll %} + Бросок: {{ last_entry.roll }} · Итог: {{ last_entry.total }} + {% endif %} +
+
+ {% endif %} +
+ + +
+ {% endif %} +
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..dac779c 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,23 @@ from django.urls import path -from .views import home +from .views import ( + character_create, + character_detail, + home, + inventory_view, + item_detail, + quest_detail, + quest_list, + story_view, +) urlpatterns = [ path("", home, name="home"), + path("character/create/", character_create, name="character_create"), + path("character//", character_detail, name="character_detail"), + path("story/", story_view, name="story"), + path("quests/", quest_list, name="quest_list"), + path("quests//", quest_detail, name="quest_detail"), + path("inventory/", inventory_view, name="inventory"), + path("inventory/item//", item_detail, name="item_detail"), ] diff --git a/core/views.py b/core/views.py index c9aed12..71b8c83 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,239 @@ -import os -import platform +import random -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from django.shortcuts import get_object_or_404, redirect, render + +from .forms import CharacterCreateForm, ChoiceForm +from .models import Character, Choice, InventoryItem, Item, Quest, Scene, StoryEntry + + +BACKGROUND_STATS = { + Character.BACKGROUND_WARDEN: {"vigor": 5, "focus": 3, "alchemy": 2}, + Character.BACKGROUND_SCOUR: {"vigor": 4, "focus": 4, "alchemy": 2}, + Character.BACKGROUND_ALCHEMIST: {"vigor": 2, "focus": 4, "alchemy": 5}, +} + + +def get_active_character(request): + character_id = request.session.get("active_character_id") + if character_id: + try: + return Character.objects.get(id=character_id) + except Character.DoesNotExist: + request.session.pop("active_character_id", None) + return None def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + character = get_active_character(request) + quests = character.quests.all()[:3] if character else [] + inventory = character.inventory.select_related("item")[:3] if character else [] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "page_title": "Темная тропа — интерактивная RPG", + "page_description": "Мрачное текстовое RPG-приключение в духе ведьмачьих саг: выбор, проверки навыков, журнал заданий и лут.", + "character": character, + "quests": quests, + "inventory": inventory, } return render(request, "core/index.html", context) + + +def character_create(request): + if request.method == "POST": + form = CharacterCreateForm(request.POST) + if form.is_valid(): + background = form.cleaned_data["background"] + stats = BACKGROUND_STATS.get(background, {"vigor": 3, "focus": 3, "alchemy": 3}) + starting_scene = Scene.objects.first() + character = Character.objects.create( + name=form.cleaned_data["name"], + background=background, + vigor=stats["vigor"], + focus=stats["focus"], + alchemy=stats["alchemy"], + current_scene=starting_scene, + ) + request.session["active_character_id"] = character.id + starter_item = Item.objects.first() + if starter_item: + InventoryItem.objects.create( + character=character, item=starter_item, equipped=True + ) + Quest.objects.create( + character=character, + title="Шепот Чёрной Трясины", + summary="Разузнать, кто тревожит болота и почему местные боятся ночи.", + ) + return redirect("character_detail", pk=character.id) + else: + form = CharacterCreateForm() + + return render( + request, + "core/character_create.html", + { + "form": form, + "page_title": "Создать героя", + "page_description": "Соберите персонажа и начните мрачное приключение.", + }, + ) + + +def character_detail(request, pk): + character = get_object_or_404(Character, pk=pk) + request.session["active_character_id"] = character.id + equipped = character.inventory.select_related("item").filter(equipped=True) + recent_entries = character.story_entries.select_related("scene")[:4] + + return render( + request, + "core/character_detail.html", + { + "character": character, + "equipped": equipped, + "recent_entries": recent_entries, + "page_title": f"{character.name} — профиль героя", + "page_description": "Профиль персонажа, характеристики и недавние выборы.", + }, + ) + + +def story_view(request): + character = get_active_character(request) + if not character: + return redirect("character_create") + + scene = character.current_scene or Scene.objects.first() + if not scene: + return render( + request, + "core/story.html", + { + "character": character, + "scene": None, + "form": None, + "page_title": "Сюжет", + "page_description": "Пока нет сцен. Добавьте их через админку.", + }, + ) + + last_entry_id = request.session.pop("last_entry_id", None) + last_entry = None + if last_entry_id: + last_entry = StoryEntry.objects.filter( + id=last_entry_id, character=character + ).select_related("scene").first() + + if request.method == "POST": + form = ChoiceForm(request.POST, choice_queryset=scene.choices.all()) + if form.is_valid(): + choice = form.cleaned_data["choice"] + outcome = StoryEntry.OUTCOME_NEUTRAL + roll = None + total = None + next_scene = choice.next_scene or scene + + if choice.required_skill and choice.difficulty: + roll = random.randint(1, 20) + skill_value = getattr(character, choice.required_skill, 0) + total = roll + skill_value + success = total >= choice.difficulty + outcome = StoryEntry.OUTCOME_SUCCESS if success else StoryEntry.OUTCOME_FAIL + next_scene = choice.success_scene if success else choice.fail_scene + next_scene = next_scene or choice.next_scene or scene + + entry = StoryEntry.objects.create( + character=character, + scene=scene, + choice_text=choice.text, + outcome=outcome, + roll=roll, + total=total, + ) + + if choice.reward_item and outcome != StoryEntry.OUTCOME_FAIL: + InventoryItem.objects.get_or_create( + character=character, item=choice.reward_item + ) + + character.current_scene = next_scene + character.save(update_fields=["current_scene"]) + request.session["last_entry_id"] = entry.id + return redirect("story") + else: + form = ChoiceForm(choice_queryset=scene.choices.all()) + + return render( + request, + "core/story.html", + { + "character": character, + "scene": scene, + "form": form, + "last_entry": last_entry, + "page_title": "Сюжет", + "page_description": "Выбирайте действия и наблюдайте последствия.", + }, + ) + + +def quest_list(request): + character = get_active_character(request) + quests = character.quests.all() if character else [] + return render( + request, + "core/quest_list.html", + { + "character": character, + "quests": quests, + "page_title": "Журнал заданий", + "page_description": "Список текущих и завершённых заданий.", + }, + ) + + +def quest_detail(request, pk): + character = get_active_character(request) + quest = get_object_or_404(Quest, pk=pk) + return render( + request, + "core/quest_detail.html", + { + "character": character, + "quest": quest, + "page_title": quest.title, + "page_description": "Детали задания и его статус.", + }, + ) + + +def inventory_view(request): + character = get_active_character(request) + items = ( + character.inventory.select_related("item") if character else InventoryItem.objects.none() + ) + return render( + request, + "core/inventory.html", + { + "character": character, + "items": items, + "page_title": "Инвентарь", + "page_description": "Лут, экипировка и свойства предметов.", + }, + ) + + +def item_detail(request, pk): + character = get_active_character(request) + inventory_item = get_object_or_404(InventoryItem, pk=pk) + return render( + request, + "core/item_detail.html", + { + "character": character, + "inventory_item": inventory_item, + "page_title": inventory_item.item.name, + "page_description": "Подробности предмета и его эффектов.", + }, + ) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..2cb68e6 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,391 @@ /* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +:root { + --noir-900: #0b0f12; + --noir-800: #12171c; + --noir-700: #1a2128; + --ash-200: #d7d2c8; + --ash-100: #f2efe8; + --accent-gold: #d4a84b; + --accent-crimson: #b7423b; + --accent-teal: #4aa3a1; + --glass: rgba(20, 26, 32, 0.75); +} + +body { + font-family: "Inter", system-ui, -apple-system, sans-serif; + background: radial-gradient(circle at top, #1a2026, #0b0f12 55%); + color: var(--ash-100); + min-height: 100vh; +} + +h1, h2, h3, .navbar-brand { + font-family: "Cormorant Garamond", serif; + letter-spacing: 0.02em; +} + +.site-nav { + background: linear-gradient(120deg, rgba(14, 18, 22, 0.95), rgba(20, 26, 32, 0.75)); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + position: sticky; + top: 0; + z-index: 100; +} + +.navbar-brand { + font-weight: 700; + font-size: 1.4rem; + color: var(--ash-100); +} + +.brand-tag { + display: block; + font-size: 0.75rem; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.55); + letter-spacing: 0.16em; +} + +.nav-link { + font-weight: 500; + color: rgba(255, 255, 255, 0.8); +} + +.nav-link:hover, +.nav-link:focus { + color: var(--accent-gold); +} + +.nav-link-admin { + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 999px; + padding: 0.35rem 1rem; +} + +.hero { + padding: 5rem 0 3rem; + position: relative; + overflow: hidden; +} + +.hero::before { + content: ""; + position: absolute; + inset: -20% 0 0; + background: radial-gradient(circle, rgba(212, 168, 75, 0.15), transparent 55%); + z-index: 0; +} + +.hero-grid { + display: grid; + gap: 2.5rem; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + position: relative; + z-index: 1; +} + +.hero-text h1 { + font-size: clamp(2.6rem, 3vw + 1.6rem, 3.8rem); + margin-bottom: 1rem; +} + +.hero-lead { + color: rgba(255, 255, 255, 0.82); + font-size: 1.1rem; + line-height: 1.7; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin: 2rem 0 1.5rem; +} + +.hero-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.hero-tags span { + background: rgba(255, 255, 255, 0.08); + padding: 0.35rem 0.9rem; + border-radius: 999px; + font-size: 0.85rem; +} + +.glass-card { + background: var(--glass); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 2rem; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); + backdrop-filter: blur(16px); +} + +.hero-card { + position: relative; +} + +.hero-ornament { + position: absolute; + width: 140px; + height: 140px; + border-radius: 24px; + background: linear-gradient(135deg, rgba(74, 163, 161, 0.35), rgba(183, 66, 59, 0.25)); + right: -30px; + bottom: -30px; + filter: blur(2px); +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.3em; + font-size: 0.7rem; + color: var(--accent-gold); +} + +.section-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + padding-bottom: 3rem; +} + +.section-card { + background: rgba(18, 23, 28, 0.75); + border-radius: 18px; + padding: 1.8rem; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} + +.stat-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: rgba(255, 255, 255, 0.55); + margin-bottom: 0.35rem; +} + +.stat-value { + font-size: 1.3rem; + font-weight: 600; +} + +.muted { + color: rgba(255, 255, 255, 0.68); +} + +.text-link { + color: var(--accent-gold); + text-decoration: none; + font-weight: 600; +} + +.text-link:hover { + color: var(--accent-teal); +} + +.btn-primary { + background: linear-gradient(120deg, var(--accent-gold), #f0c76a); + border: none; + color: #1c1406; + font-weight: 600; + box-shadow: 0 12px 30px rgba(212, 168, 75, 0.35); +} + +.btn-outline-light { + border-color: rgba(255, 255, 255, 0.35); +} + +.page-head { + margin: 3rem 0 2rem; +} + +.split-head { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + justify-content: space-between; + align-items: center; +} + +.head-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.form-card { + max-width: 540px; +} + +.form-grid { + display: grid; + gap: 1.2rem; +} + +.form-error { + color: var(--accent-crimson); + font-size: 0.85rem; + margin-top: 0.35rem; +} + +.form-hint { + background: rgba(255, 255, 255, 0.06); + padding: 0.8rem 1rem; + border-radius: 12px; + font-size: 0.9rem; +} + +.profile-grid, +.story-grid, +.quest-grid, +.inventory-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.story-layout { + padding-bottom: 3rem; +} + +.story-scene h2 { + margin: 0.8rem 0 1rem; +} + +.story-body { + font-size: 1.05rem; + line-height: 1.75; +} + +.choice-form { + display: grid; + gap: 1.2rem; +} + +.choice-list { + display: grid; + gap: 0.8rem; +} + +.choice-item { + display: flex; + gap: 0.75rem; + align-items: flex-start; + padding: 0.8rem 1rem; + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.choice-item input[type="radio"] { + margin-top: 0.25rem; +} + +.result-card { + margin-top: 1.5rem; + padding: 1rem; + border-radius: 14px; + background: rgba(255, 255, 255, 0.06); +} + +.result-meta { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; + margin-top: 0.5rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +.info-list, +.timeline { + list-style: none; + padding: 0; + margin: 1rem 0; + display: grid; + gap: 0.75rem; +} + +.info-list li, +.timeline li { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + background: rgba(255, 255, 255, 0.04); + padding: 0.7rem 1rem; + border-radius: 12px; +} + +.timeline-title { + font-weight: 600; +} + +.timeline-meta { + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + +.badge-outcome { + padding: 0.35rem 0.7rem; + border-radius: 999px; + font-size: 0.75rem; +} + +.badge-success { + background: rgba(74, 163, 161, 0.25); + color: var(--accent-teal); +} + +.badge-fail { + background: rgba(183, 66, 59, 0.25); + color: var(--accent-crimson); +} + +.badge-neutral { + background: rgba(212, 168, 75, 0.2); + color: var(--accent-gold); +} + +.badge-status { + background: rgba(255, 255, 255, 0.1); + color: var(--ash-200); +} + +.narrow-section { + padding-bottom: 3rem; +} + +.quest-meta, +.item-meta { + display: flex; + gap: 1rem; + align-items: center; + flex-wrap: wrap; + margin-top: 1rem; +} + +.site-footer { + margin-top: 4rem; + padding: 2.5rem 0 3rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(8, 10, 12, 0.8); +} + +.footer-inner { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; + color: rgba(255, 255, 255, 0.6); } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..2cb68e6 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,391 @@ - +/* Custom styles for the application */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --noir-900: #0b0f12; + --noir-800: #12171c; + --noir-700: #1a2128; + --ash-200: #d7d2c8; + --ash-100: #f2efe8; + --accent-gold: #d4a84b; + --accent-crimson: #b7423b; + --accent-teal: #4aa3a1; + --glass: rgba(20, 26, 32, 0.75); } + body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; + font-family: "Inter", system-ui, -apple-system, sans-serif; + background: radial-gradient(circle at top, #1a2026, #0b0f12 55%); + color: var(--ash-100); min-height: 100vh; - text-align: center; +} + +h1, h2, h3, .navbar-brand { + font-family: "Cormorant Garamond", serif; + letter-spacing: 0.02em; +} + +.site-nav { + background: linear-gradient(120deg, rgba(14, 18, 22, 0.95), rgba(20, 26, 32, 0.75)); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + position: sticky; + top: 0; + z-index: 100; +} + +.navbar-brand { + font-weight: 700; + font-size: 1.4rem; + color: var(--ash-100); +} + +.brand-tag { + display: block; + font-size: 0.75rem; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.55); + letter-spacing: 0.16em; +} + +.nav-link { + font-weight: 500; + color: rgba(255, 255, 255, 0.8); +} + +.nav-link:hover, +.nav-link:focus { + color: var(--accent-gold); +} + +.nav-link-admin { + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 999px; + padding: 0.35rem 1rem; +} + +.hero { + padding: 5rem 0 3rem; + position: relative; overflow: hidden; +} + +.hero::before { + content: ""; + position: absolute; + inset: -20% 0 0; + background: radial-gradient(circle, rgba(212, 168, 75, 0.15), transparent 55%); + z-index: 0; +} + +.hero-grid { + display: grid; + gap: 2.5rem; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + position: relative; + z-index: 1; +} + +.hero-text h1 { + font-size: clamp(2.6rem, 3vw + 1.6rem, 3.8rem); + margin-bottom: 1rem; +} + +.hero-lead { + color: rgba(255, 255, 255, 0.82); + font-size: 1.1rem; + line-height: 1.7; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin: 2rem 0 1.5rem; +} + +.hero-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.hero-tags span { + background: rgba(255, 255, 255, 0.08); + padding: 0.35rem 0.9rem; + border-radius: 999px; + font-size: 0.85rem; +} + +.glass-card { + background: var(--glass); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 2rem; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); + backdrop-filter: blur(16px); +} + +.hero-card { position: relative; } + +.hero-ornament { + position: absolute; + width: 140px; + height: 140px; + border-radius: 24px; + background: linear-gradient(135deg, rgba(74, 163, 161, 0.35), rgba(183, 66, 59, 0.25)); + right: -30px; + bottom: -30px; + filter: blur(2px); +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.3em; + font-size: 0.7rem; + color: var(--accent-gold); +} + +.section-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + padding-bottom: 3rem; +} + +.section-card { + background: rgba(18, 23, 28, 0.75); + border-radius: 18px; + padding: 1.8rem; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} + +.stat-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: rgba(255, 255, 255, 0.55); + margin-bottom: 0.35rem; +} + +.stat-value { + font-size: 1.3rem; + font-weight: 600; +} + +.muted { + color: rgba(255, 255, 255, 0.68); +} + +.text-link { + color: var(--accent-gold); + text-decoration: none; + font-weight: 600; +} + +.text-link:hover { + color: var(--accent-teal); +} + +.btn-primary { + background: linear-gradient(120deg, var(--accent-gold), #f0c76a); + border: none; + color: #1c1406; + font-weight: 600; + box-shadow: 0 12px 30px rgba(212, 168, 75, 0.35); +} + +.btn-outline-light { + border-color: rgba(255, 255, 255, 0.35); +} + +.page-head { + margin: 3rem 0 2rem; +} + +.split-head { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + justify-content: space-between; + align-items: center; +} + +.head-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.form-card { + max-width: 540px; +} + +.form-grid { + display: grid; + gap: 1.2rem; +} + +.form-error { + color: var(--accent-crimson); + font-size: 0.85rem; + margin-top: 0.35rem; +} + +.form-hint { + background: rgba(255, 255, 255, 0.06); + padding: 0.8rem 1rem; + border-radius: 12px; + font-size: 0.9rem; +} + +.profile-grid, +.story-grid, +.quest-grid, +.inventory-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.story-layout { + padding-bottom: 3rem; +} + +.story-scene h2 { + margin: 0.8rem 0 1rem; +} + +.story-body { + font-size: 1.05rem; + line-height: 1.75; +} + +.choice-form { + display: grid; + gap: 1.2rem; +} + +.choice-list { + display: grid; + gap: 0.8rem; +} + +.choice-item { + display: flex; + gap: 0.75rem; + align-items: flex-start; + padding: 0.8rem 1rem; + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.choice-item input[type="radio"] { + margin-top: 0.25rem; +} + +.result-card { + margin-top: 1.5rem; + padding: 1rem; + border-radius: 14px; + background: rgba(255, 255, 255, 0.06); +} + +.result-meta { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; + margin-top: 0.5rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +.info-list, +.timeline { + list-style: none; + padding: 0; + margin: 1rem 0; + display: grid; + gap: 0.75rem; +} + +.info-list li, +.timeline li { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + background: rgba(255, 255, 255, 0.04); + padding: 0.7rem 1rem; + border-radius: 12px; +} + +.timeline-title { + font-weight: 600; +} + +.timeline-meta { + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + +.badge-outcome { + padding: 0.35rem 0.7rem; + border-radius: 999px; + font-size: 0.75rem; +} + +.badge-success { + background: rgba(74, 163, 161, 0.25); + color: var(--accent-teal); +} + +.badge-fail { + background: rgba(183, 66, 59, 0.25); + color: var(--accent-crimson); +} + +.badge-neutral { + background: rgba(212, 168, 75, 0.2); + color: var(--accent-gold); +} + +.badge-status { + background: rgba(255, 255, 255, 0.1); + color: var(--ash-200); +} + +.narrow-section { + padding-bottom: 3rem; +} + +.quest-meta, +.item-meta { + display: flex; + gap: 1rem; + align-items: center; + flex-wrap: wrap; + margin-top: 1rem; +} + +.site-footer { + margin-top: 4rem; + padding: 2.5rem 0 3rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(8, 10, 12, 0.8); +} + +.footer-inner { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; + color: rgba(255, 255, 255, 0.6); +}