Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf8b89881 |
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,55 @@
|
|||||||
from django.contrib import admin
|
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",)
|
||||||
|
|||||||
28
core/forms.py
Normal file
28
core/forms.py
Normal file
@ -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"})
|
||||||
113
core/migrations/0001_initial.py
Normal file
113
core/migrations/0001_initial.py
Normal file
@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
127
core/migrations/0002_seed_story.py
Normal file
127
core/migrations/0002_seed_story.py
Normal file
@ -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),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0002_seed_story.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_seed_story.cpython-311.pyc
Normal file
Binary file not shown.
171
core/models.py
171
core/models.py
@ -1,3 +1,172 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
class Scene(models.Model):
|
||||||
|
title = models.CharField(max_length=120)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
body = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["id"]
|
||||||
|
|
||||||
|
def __str__(self) -> 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}"
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{% if project_description %}
|
<title>{% block title %}Темная тропа{% endblock %}</title>
|
||||||
|
{% if page_description %}
|
||||||
|
<meta name="description" content="{{ page_description }}">
|
||||||
|
{% elif project_description %}
|
||||||
<meta name="description" content="{{ project_description }}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
|
{% endif %}
|
||||||
|
{% if project_description %}
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<meta property="twitter:description" content="{{ project_description }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -13,13 +18,52 @@
|
|||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<meta property="og:image" content="{{ project_image_url }}">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="bg-noir text-light">
|
||||||
|
<header class="site-nav">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'home' %}">
|
||||||
|
Темная тропа
|
||||||
|
<span class="brand-tag">Dark Path RPG</span>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="mainNav">
|
||||||
|
<ul class="navbar-nav ms-auto align-items-lg-center">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'story' %}">Сюжет</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'quest_list' %}">Журнал</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'inventory' %}">Инвентарь</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'character_create' %}">Новый герой</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link nav-link-admin" href="/admin/" target="_blank" rel="noopener">Админка</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Темная тропа</strong> — интерактивная история в духе ведьмачьих хроник.
|
||||||
|
</div>
|
||||||
|
<div class="footer-meta">Собрано на Django · Развивайте мир через админку</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
35
core/templates/core/character_create.html
Normal file
35
core/templates/core/character_create.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container narrow-section">
|
||||||
|
<div class="page-head">
|
||||||
|
<h1>Создать героя</h1>
|
||||||
|
<p class="muted">Выберите происхождение и начните охоту за тайной Чёрной Трясины.</p>
|
||||||
|
</div>
|
||||||
|
<div class="glass-card form-card">
|
||||||
|
<form method="post" class="form-grid">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="{{ form.name.id_for_label }}">Имя героя</label>
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="form-error">{{ form.name.errors|striptags }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="{{ form.background.id_for_label }}">Происхождение</label>
|
||||||
|
{{ form.background }}
|
||||||
|
{% if form.background.errors %}
|
||||||
|
<div class="form-error">{{ form.background.errors|striptags }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-hint">
|
||||||
|
<strong>Подсказка:</strong> выбор происхождения задаёт стартовые навыки.
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Сотворить героя</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
79
core/templates/core/character_detail.html
Normal file
79
core/templates/core/character_detail.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>{{ character.name }}</h1>
|
||||||
|
<p class="muted">Происхождение: {{ character.get_background_display }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'story' %}">Продолжить сюжет</a>
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'inventory' %}">Инвентарь</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="profile-grid">
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Характеристики</h3>
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Уровень</p>
|
||||||
|
<p class="stat-value">Lv {{ character.level }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Ви́гор</p>
|
||||||
|
<p class="stat-value">{{ character.vigor }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Фокус</p>
|
||||||
|
<p class="stat-value">{{ character.focus }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Алхимия</p>
|
||||||
|
<p class="stat-value">{{ character.alchemy }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Перки</p>
|
||||||
|
<p class="stat-value">{{ character.perk_points }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="muted">Откройте ветки перков в следующих итерациях.</p>
|
||||||
|
</div>
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Экипировка</h3>
|
||||||
|
{% if equipped %}
|
||||||
|
<ul class="info-list">
|
||||||
|
{% for entry in equipped %}
|
||||||
|
<li>
|
||||||
|
<span>{{ entry.item.name }}</span>
|
||||||
|
<span class="badge bg-dark-subtle text-light">{{ entry.item.get_slot_display }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Экипировка пока не выбрана.</p>
|
||||||
|
{% endif %}
|
||||||
|
<a class="text-link" href="{% url 'inventory' %}">Управлять лутом →</a>
|
||||||
|
</div>
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Последние решения</h3>
|
||||||
|
{% if recent_entries %}
|
||||||
|
<ul class="timeline">
|
||||||
|
{% for entry in recent_entries %}
|
||||||
|
<li>
|
||||||
|
<span class="timeline-title">{{ entry.scene.title }}</span>
|
||||||
|
<span class="timeline-meta">{{ entry.choice_text }}</span>
|
||||||
|
<span class="badge badge-outcome badge-{{ entry.outcome }}">{{ entry.get_outcome_display }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Вы ещё не сделали выборов. Начните пролог.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -1,145 +1,133 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
: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);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<div class="card">
|
<section class="hero">
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="container hero-grid">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="hero-text">
|
||||||
<span class="sr-only">Loading…</span>
|
<span class="eyebrow">Мрачное фэнтези · интерактивная проза</span>
|
||||||
</div>
|
<h1>Ваш выбор — клинок, который меняет судьбу.</h1>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
<p class="hero-lead">
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
Пройдите историю в духе «Ведьмака»: выбирайте действия прямо в диалоге, проходите проверки навыков,
|
||||||
<p class="runtime">
|
собирайте добычу и отслеживайте квесты в удобном журнале.
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
|
||||||
</p>
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
{% if character %}
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'story' %}">Продолжить сюжет</a>
|
||||||
|
<a class="btn btn-outline-light btn-lg" href="{% url 'character_detail' character.id %}">Профиль героя</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'character_create' %}">Создать героя</a>
|
||||||
|
<a class="btn btn-outline-light btn-lg" href="{% url 'story' %}">Посмотреть пролог</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hero-tags">
|
||||||
|
<span>Выборы и последствия</span>
|
||||||
|
<span>Сцены с проверками</span>
|
||||||
|
<span>Лут и экипировка</span>
|
||||||
|
<span>Перки и развитие</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-card">
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Статус похода</h3>
|
||||||
|
{% if character %}
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Герой</p>
|
||||||
|
<p class="stat-value">{{ character.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Уровень</p>
|
||||||
|
<p class="stat-value">Lv {{ character.level }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Ви́гор</p>
|
||||||
|
<p class="stat-value">{{ character.vigor }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Фокус</p>
|
||||||
|
<p class="stat-value">{{ character.focus }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Алхимия</p>
|
||||||
|
<p class="stat-value">{{ character.alchemy }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-light w-100" href="{% url 'story' %}">Открыть текущую сцену</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">
|
||||||
|
Создайте персонажа, чтобы открыть сюжет, журнал заданий и инвентарь.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-outline-light w-100" href="{% url 'character_create' %}">Начать пролог</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="hero-ornament"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container section-grid">
|
||||||
|
<div class="section-card">
|
||||||
|
<h2>Текущие задания</h2>
|
||||||
|
{% if character %}
|
||||||
|
{% if quests %}
|
||||||
|
<ul class="info-list">
|
||||||
|
{% for quest in quests %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'quest_detail' quest.id %}">{{ quest.title }}</a>
|
||||||
|
<span class="badge bg-dark-subtle text-light">{{ quest.get_status_display }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<a class="text-link" href="{% url 'quest_list' %}">Открыть весь журнал →</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">В журнале пока пусто — начните сюжет, чтобы получить первое задание.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Создайте героя, чтобы получать квесты и отслеживать последствия.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="section-card">
|
||||||
|
<h2>Инвентарь и экипировка</h2>
|
||||||
|
{% if character %}
|
||||||
|
{% if inventory %}
|
||||||
|
<ul class="info-list">
|
||||||
|
{% for entry in inventory %}
|
||||||
|
<li>
|
||||||
|
<span>{{ entry.item.name }}</span>
|
||||||
|
<span class="badge bg-dark-subtle text-light">{{ entry.item.get_slot_display }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<a class="text-link" href="{% url 'inventory' %}">Управлять лутом →</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Пока нет предметов. Пройдите пролог, чтобы получить добычу.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Лут и экипировка появятся после создания персонажа.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="section-card">
|
||||||
|
<h2>Как работает выбор</h2>
|
||||||
|
<p class="muted">
|
||||||
|
Каждая сцена — это диалог. Вы выбираете действие, и система бросает проверку навыка.
|
||||||
|
Успех открывает лучшие сцены и редкий лут, провал запускает опасные ветки.
|
||||||
|
</p>
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Бросок</p>
|
||||||
|
<p class="stat-value">d20 + навык</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Порог</p>
|
||||||
|
<p class="stat-value">Сцена</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="text-link" href="{% url 'story' %}">Перейти к сцене →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
41
core/templates/core/inventory.html
Normal file
41
core/templates/core/inventory.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>Инвентарь</h1>
|
||||||
|
<p class="muted">Сортируйте трофеи и экипируйте нужные предметы.</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'story' %}">К сюжету</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="inventory-grid">
|
||||||
|
{% if items %}
|
||||||
|
{% for entry in items %}
|
||||||
|
<article class="glass-card item-card">
|
||||||
|
<h3>{{ entry.item.name }}</h3>
|
||||||
|
<p class="muted">{{ entry.item.description }}</p>
|
||||||
|
<div class="item-meta">
|
||||||
|
<span class="badge bg-dark-subtle text-light">{{ entry.item.get_slot_display }}</span>
|
||||||
|
{% if entry.equipped %}
|
||||||
|
<span class="badge badge-outcome badge-success">Экипировано</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<a class="text-link" href="{% url 'item_detail' entry.id %}">Подробнее →</a>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Инвентарь пуст</h3>
|
||||||
|
<p class="muted">Пройдите пролог, чтобы получить первые трофеи.</p>
|
||||||
|
<a class="text-link" href="{% url 'story' %}">Открыть сюжет →</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
36
core/templates/core/item_detail.html
Normal file
36
core/templates/core/item_detail.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container narrow-section">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>{{ inventory_item.item.name }}</h1>
|
||||||
|
<p class="muted">{{ inventory_item.item.get_slot_display }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'inventory' %}">Назад к инвентарю</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Описание</h3>
|
||||||
|
<p class="muted">{{ inventory_item.item.description }}</p>
|
||||||
|
<div class="quest-meta">
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Сила</p>
|
||||||
|
<p class="stat-value">+{{ inventory_item.item.power }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Статус</p>
|
||||||
|
{% if inventory_item.equipped %}
|
||||||
|
<p class="stat-value">Экипировано</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="stat-value">В запасе</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
32
core/templates/core/quest_detail.html
Normal file
32
core/templates/core/quest_detail.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container narrow-section">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>{{ quest.title }}</h1>
|
||||||
|
<p class="muted">Статус: {{ quest.get_status_display }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'quest_list' %}">Назад к журналу</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Описание</h3>
|
||||||
|
<p class="muted">{{ quest.summary }}</p>
|
||||||
|
<div class="quest-meta">
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Герой</p>
|
||||||
|
<p class="stat-value">{{ quest.character.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="stat-label">Уровень</p>
|
||||||
|
<p class="stat-value">Lv {{ quest.character.level }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
36
core/templates/core/quest_list.html
Normal file
36
core/templates/core/quest_list.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>Журнал заданий</h1>
|
||||||
|
<p class="muted">Следите за основными и побочными ветками истории.</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'story' %}">Вернуться к сюжету</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="quest-grid">
|
||||||
|
{% if quests %}
|
||||||
|
{% for quest in quests %}
|
||||||
|
<article class="glass-card quest-card">
|
||||||
|
<h3>{{ quest.title }}</h3>
|
||||||
|
<p class="muted">{{ quest.summary }}</p>
|
||||||
|
<span class="badge badge-status badge-{{ quest.status }}">{{ quest.get_status_display }}</span>
|
||||||
|
<a class="text-link" href="{% url 'quest_detail' quest.id %}">Подробнее →</a>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Пока нет заданий</h3>
|
||||||
|
<p class="muted">Пройдите первую сцену, чтобы получить квесты.</p>
|
||||||
|
<a class="text-link" href="{% url 'story' %}">Открыть сюжет →</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
70
core/templates/core/story.html
Normal file
70
core/templates/core/story.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="container story-layout">
|
||||||
|
<div class="page-head split-head">
|
||||||
|
<div>
|
||||||
|
<h1>Сюжет</h1>
|
||||||
|
<p class="muted">Ваш путь: {{ character.name }} · Lv {{ character.level }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="head-actions">
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'quest_list' %}">Журнал</a>
|
||||||
|
<a class="btn btn-outline-light" href="{% url 'inventory' %}">Инвентарь</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not scene %}
|
||||||
|
<div class="glass-card">
|
||||||
|
<h3>Сцены ещё не настроены</h3>
|
||||||
|
<p class="muted">Добавьте сцены и варианты выбора в админке, чтобы запустить сюжет.</p>
|
||||||
|
<a class="text-link" href="/admin/" target="_blank" rel="noopener">Перейти в админку →</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="story-grid">
|
||||||
|
<section class="glass-card story-scene">
|
||||||
|
<span class="eyebrow">Сцена</span>
|
||||||
|
<h2>{{ scene.title }}</h2>
|
||||||
|
<p class="story-body">{{ scene.body }}</p>
|
||||||
|
{% if last_entry %}
|
||||||
|
<div class="result-card">
|
||||||
|
<div>
|
||||||
|
<strong>Ваш выбор:</strong> {{ last_entry.choice_text }}
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">
|
||||||
|
<span class="badge badge-outcome badge-{{ last_entry.outcome }}">{{ last_entry.get_outcome_display }}</span>
|
||||||
|
{% if last_entry.roll %}
|
||||||
|
<span>Бросок: {{ last_entry.roll }} · Итог: {{ last_entry.total }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="glass-card story-choices">
|
||||||
|
<h3>Выберите действие</h3>
|
||||||
|
<form method="post" class="choice-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form %}
|
||||||
|
<div class="choice-list">
|
||||||
|
{% for field in form.choice %}
|
||||||
|
<label class="choice-item">
|
||||||
|
{{ field.tag }}
|
||||||
|
<span class="choice-text">{{ field.choice_label }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if form.choice.errors %}
|
||||||
|
<div class="form-error">{{ form.choice.errors|striptags }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Подтвердить</button>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">Для этой сцены нет вариантов выбора.</p>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
18
core/urls.py
18
core/urls.py
@ -1,7 +1,23 @@
|
|||||||
from django.urls import path
|
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 = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("character/create/", character_create, name="character_create"),
|
||||||
|
path("character/<int:pk>/", character_detail, name="character_detail"),
|
||||||
|
path("story/", story_view, name="story"),
|
||||||
|
path("quests/", quest_list, name="quest_list"),
|
||||||
|
path("quests/<int:pk>/", quest_detail, name="quest_detail"),
|
||||||
|
path("inventory/", inventory_view, name="inventory"),
|
||||||
|
path("inventory/item/<int:pk>/", item_detail, name="item_detail"),
|
||||||
]
|
]
|
||||||
|
|||||||
248
core/views.py
248
core/views.py
@ -1,25 +1,239 @@
|
|||||||
import os
|
import random
|
||||||
import platform
|
|
||||||
|
|
||||||
from django import get_version as django_version
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.shortcuts import render
|
|
||||||
from django.utils import timezone
|
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):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
character = get_active_character(request)
|
||||||
host_name = request.get_host().lower()
|
quests = character.quests.all()[:3] if character else []
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
inventory = character.inventory.select_related("item")[:3] if character else []
|
||||||
now = timezone.now()
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
"page_title": "Темная тропа — интерактивная RPG",
|
||||||
"agent_brand": agent_brand,
|
"page_description": "Мрачное текстовое RPG-приключение в духе ведьмачьих саг: выбор, проверки навыков, журнал заданий и лут.",
|
||||||
"django_version": django_version(),
|
"character": character,
|
||||||
"python_version": platform.python_version(),
|
"quests": quests,
|
||||||
"current_time": now,
|
"inventory": inventory,
|
||||||
"host_name": host_name,
|
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
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": "Подробности предмета и его эффектов.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@ -1,4 +1,391 @@
|
|||||||
/* Custom styles for the application */
|
/* Custom styles for the application */
|
||||||
body {
|
:root {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
--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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,391 @@
|
|||||||
|
/* Custom styles for the application */
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--noir-900: #0b0f12;
|
||||||
--bg-color-end: #2575fc;
|
--noir-800: #12171c;
|
||||||
--text-color: #ffffff;
|
--noir-700: #1a2128;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--ash-200: #d7d2c8;
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--ash-100: #f2efe8;
|
||||||
|
--accent-gold: #d4a84b;
|
||||||
|
--accent-crimson: #b7423b;
|
||||||
|
--accent-teal: #4aa3a1;
|
||||||
|
--glass: rgba(20, 26, 32, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||||
font-family: 'Inter', sans-serif;
|
background: radial-gradient(circle at top, #1a2026, #0b0f12 55%);
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
color: var(--ash-100);
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
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;
|
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;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user