adding missing files

This commit is contained in:
2025-11-03 21:43:13 -06:00
parent efdf3570c5
commit 0443d1553f
36 changed files with 1765 additions and 0 deletions

16
app/blueprints/ajax.py Normal file
View File

@@ -0,0 +1,16 @@
from flask import Blueprint, request, url_for, render_template, session,flash
from app.services.appwrite_client import AppWriteClient
ajax_bp = Blueprint("ajax", __name__, url_prefix="/ajax")
@ajax_bp.route("/races", methods=["GET", "POST"])
def races():
race = request.args.get("race")
return render_template(f"ajax/race_{race}.html")
@ajax_bp.route("/prof", methods=["GET", "POST"])
def professions():
prof = request.args.get("prof")
return render_template(f"ajax/prof_{prof}.html")

112
app/blueprints/auth.py Normal file
View File

@@ -0,0 +1,112 @@
# app/auth/routes.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, session,make_response
from app.services.appwrite_client import AppWriteClient
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
email = request.form.get("email", "").strip()
password = request.form.get("password", "")
name = (request.form.get("name") or "").strip() or None
if not email or not password:
flash("Email and password are required.", "error")
return redirect(url_for("auth.register"))
try:
aw = AppWriteClient()
aw.create_new_user(email,password,name)
login_valid, error = aw.log_user_in(email,password)
if login_valid:
flash("Account created and you are now logged in.", "success")
return redirect(url_for("main.dashboard"))
else:
flash(str(error), "error")
except Exception as e:
flash(str(e), "error")
return redirect(url_for("auth.register"))
return render_template("auth/register.html")
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get("email", "").strip()
password = request.form.get("password", "")
if not email or not password:
flash("Email and password are required.", "error")
return redirect(url_for("auth.login"))
aw = AppWriteClient()
login_valid, error = aw.log_user_in(email,password)
try:
if login_valid:
username = session.get("user",{}).get("name","User")
flash(f"Welcome Back {username}", "success")
return redirect(url_for("main.dashboard"))
else:
flash(str(error), "error")
except Exception as e:
flash(str(e), "error")
return redirect(url_for("auth.login"))
# Get method
return render_template("auth/login.html")
@auth_bp.route("/logout", methods=["GET", "POST"])
def logout():
aw = AppWriteClient()
aw.log_user_out()
session.clear()
flash("Signed out.", "success")
return redirect(url_for("auth.login"))
@auth_bp.route("/send", methods=["POST"])
def send():
"""
Sends a verification email to the currently logged-in user.
Appwrite will redirect the user back to /verify/callback with userId & secret.
"""
try:
aw = AppWriteClient()
aw.send_email_verification()
flash("Verification email sent. Please check your inbox.", "info")
except Exception as e:
flash(f"Could not send verification email: {e}", "error")
# Go back to where the user came from, or your dashboard
return redirect(request.referrer or url_for("main.dashboard"))
@auth_bp.route("/callback", methods=["GET"])
def callback():
"""
Completes verification after user clicks the email link.
Requires the user to be logged in (Appwrite Account endpoints need a session).
If not logged in, we stash the link params and send them to log in first.
"""
aw = AppWriteClient()
user_id = request.args.get("userId")
secret = request.args.get("secret")
if not user_id or not secret:
flash("Invalid verification link.", "error")
return redirect(url_for("auth.login"))
try:
# If we don't currently have an Appwrite session, ask them to log in, then resume.
if not aw.verify_email(user_id,secret):
session["pending_verification"] = {"userId": user_id, "secret": secret}
flash("Please log in to complete email verification.", "warning")
return redirect(url_for("auth.login"))
# We have a session; complete verification
flash("Email verified! You're all set.", "success")
return redirect(url_for("main.dashboard"))
except Exception as e:
flash(f"Verification failed: {e}", "error")
return redirect(url_for("auth.login"))

46
app/blueprints/char.py Normal file
View File

@@ -0,0 +1,46 @@
import os
import json
import random
from flask import Blueprint, redirect, url_for, render_template, g, request, flash
from typing import cast
from app.utils.session_user import SessionUser
from app.services.appwrite_client import AppWriteClient
from app.services.appwrite_db import AppwriteTables, Env
from app.services.coc_api import CoCApi
char_bp = Blueprint("char", __name__, url_prefix="/char")
cocapi = CoCApi()
def get_current_user() -> SessionUser:
return cast(SessionUser, g.current_user)
@char_bp.route("/create", methods=["GET", "POST"])
def create_char():
if request.method == "POST":
name = request.form.get("character_name")
race_id = request.form.get("race_dropdown")
profession_id = request.form.get("profession_dropdown")
origin_story = request.form.get("origin_story")
uuid = cocapi.create_char(name=name,origin_story=origin_story,race_id=race_id,profession_id=profession_id)
redirect(url_for("main.dashboard"))
ai_dumps_path = os.path.join("app","game_data","ai_dumps.json")
with open(ai_dumps_path,"r") as f:
data = json.load(f)
origin_stories = data.get("origin_stories",[])
if len(origin_stories) > 0:
starter_text = random.choice(origin_stories)
else:
starter_text = "I was born in a small, secluded village on the edge of a vast and mysterious forest, where whispers of ancient magic still lingered in the air."
template_data = {
"starter_text":starter_text
}
return render_template("char/create_char.html", data=template_data)

27
app/blueprints/main.py Normal file
View File

@@ -0,0 +1,27 @@
import os
from flask import Blueprint, redirect, url_for, render_template, g, session,flash
from typing import cast
from app.utils.session_user import SessionUser
from app.services.appwrite_client import AppWriteClient
from app.services.appwrite_db import AppwriteTables
from app.services.coc_api import CoCApi
main_bp = Blueprint("main", __name__, url_prefix="/main")
cocapi = CoCApi()
def get_current_user() -> SessionUser:
return cast(SessionUser, g.current_user)
@main_bp.route("/dashboard")
def dashboard():
db_tables = AppwriteTables()
user = get_current_user()
results = db_tables.get_characters_for_user_id(user.id)
if len(results) == 0:
return redirect(url_for("char.create_char"))
else:
char=results
return render_template("main/dashboard.html", profile=g.current_user, jwt_info=char)

10
app/blueprints/public.py Normal file
View File

@@ -0,0 +1,10 @@
from flask import Blueprint, redirect, url_for, render_template, session,flash
from app.services.appwrite_client import AppWriteClient
public_bp = Blueprint("public", __name__, url_prefix="/")
@public_bp.route("/")
def home():
return render_template("public/home.html")

View File

@@ -0,0 +1,19 @@
{
"origin_stories":[
"I was born in a small village on the edge of a vast and mysterious forest.",
"The day I was born, the stars aligned in a peculiar pattern above our town.",
"My earliest memories are of wandering through the ruins of an ancient city.",
"In the depths of my childhood, I stumbled upon a hidden text that changed everything.",
"I was raised by a family of nomads who taught me the ways of the wind and the sun.",
"The village elder predicted my arrival on the day I was born, as if I were already known.",
"I've always felt like there's something missing in my memories, like pieces are waiting to be filled in.",
"As a child, I would often sneak into the local library at night, devouring forbidden knowledge from ancient tomes.",
"My family's past is shrouded in mystery, but one thing is certain: we've always been on the move.",
"I was found as a baby in the heart of a dense jungle, with no memory of who I am or where I came from.",
"The village priestess foretold my destiny on the day of my birth, speaking words that only I could understand.",
"Growing up, I would often experience strange and vivid dreams that felt more real than reality itself.",
"I was born with a rare gift: the ability to communicate with animals in ways others cannot.",
"The village elder's prophecies spoke of me as a bringer of change, but I'm still unsure what that means."
]
}

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from enum import StrEnum
from typing import Final, Optional
from flask import current_app
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
from appwrite.query import Query
from appwrite.id import ID
from app.utils.logging import get_logger
from app.utils.settings import get_settings, Environment
settings = get_settings()
logger = get_logger(__file__)
class Env(StrEnum):
PROD = "prod"
DEV = "dev"
# --- Database schemas (strongly-typed namespaces) ----------------------------
@dataclass(frozen=True)
class Database:
"""Schema for a single database: each attribute is a table name (or ID)."""
id: str
characters: str
inventory: str
# add more tables here as you grow your schema
@dataclass(frozen=True)
class Databases:
"""Top-level namespace exposing prod/dev as attributes."""
prod: Database
dev: Database
DB: Final[Databases] = Databases(
prod=Database(
id="SETME", # actual DB / ID
characters="SETME", # actual table / ID
inventory="inventory",
),
dev=Database(
id="69041f9600177b675485",
characters="69050f830024afb0d253",
inventory="inventory",
),
)
class AppwriteTables:
def __init__(self):
print()
self.client = (Client()
.set_endpoint(settings.appwrite_endpoint)
.set_project(settings.appwrite_project_id)
.set_key(settings.appwrite_api_key)
)
self.tables_db = TablesDB(self.client)
self.env = Env.DEV
if settings.env == Environment.PROD:
self.env = Env.PROD
@property
def db(self) -> Database:
# Gives autocompletion for .character, .users, etc.
return DB.prod if self.env is Env.PROD else DB.dev
def get_characters_for_user_id(self, user_id: str) -> Optional[dict]:
try:
result = self.tables_db.list_rows(
self.db.id,
self.db.characters,
[
Query.equal('player_id', [str(user_id)]),
]
)
except Exception as e:
logger.error(f"Unable to list rows for char. User id: {user_id}")
return {}
return result.get("rows",{})

232
app/services/coc_api.py Normal file
View File

@@ -0,0 +1,232 @@
from __future__ import annotations
import json
import logging
from typing import Any, Dict, Optional
import requests
from requests import Response, Session
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from urllib.parse import urljoin
from app.services.appwrite_client import AppWriteClient
from app.utils.logging import get_logger
logger = get_logger(__file__)
class CoCApi:
"""
Centralized API client for Code of Conquest.
All HTTP interactions go through _request() for consistent behavior and logging.
"""
def __init__(
self,
base_url: str = "http://localhost:8000",
default_timeout: tuple[float, float] = (5.0, 10.0),
max_retries: int = 3,
) -> None:
"""
:param base_url: Base URL for the API (no trailing slash needed).
:param default_timeout: (connect_timeout, read_timeout)
:param max_retries: Number of retries for transient network/server errors.
"""
self.base_url = base_url.rstrip("/")
self.default_timeout = default_timeout
self._aw = AppWriteClient()
# Base headers for JSON APIs.
self._base_headers: Dict[str, str] = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "CoC-Client/1.0",
}
# Pre-configured Session with retries.
self._session = self._build_session(max_retries=max_retries)
# ---------- Public convenience methods ----------
def create_char(self, name:str, origin_story:str, race_id:str, profession_id:str) -> Dict[str, Any]:
payload = {
"name":name,
"origin_story":origin_story,
"race_id":race_id,
"profession_id":profession_id
}
result = self.post("/char/new",payload=payload)
player_uuid = result.get("result")
return player_uuid
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return self._request("GET", path, params=params)
def post(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return self._request("POST", path, json_body=payload)
def patch(self, path: str, json_body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return self._request("PATCH", path, json_body=json_body)
# ---------- Internal helpers ----------
def _build_session(self, max_retries: int) -> Session:
"""
Create a Session with sane retries for transient network/server failures.
We retry idempotent methods and some 5xx responses.
"""
session = requests.Session()
retries = Retry(
total=max_retries,
connect=max_retries,
read=max_retries,
backoff_factor=0.5,
status_forcelist=(502, 503, 504),
allowed_methods=frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]),
raise_on_status=False,
)
adapter = HTTPAdapter(max_retries=retries)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def _mint_jwt(self) -> str:
"""Mint a JWT from AppWrite; empty string if unavailable (dont block requests)."""
try:
token = self._aw.mint_jwt() # expected to return dict-like
return token.get("jwt", "") if token else ""
except Exception as e:
logger.warning("Failed to mint JWT", extra={"error": str(e)})
return ""
def _auth_headers(self) -> Dict[str, str]:
"""
Build per-call headers with Authorization if a JWT is available.
Avoid mutating shared headers.
"""
headers = dict(self._base_headers)
jwt = self._mint_jwt()
if jwt:
headers["Authorization"] = f"Bearer {jwt}"
return headers
def _resolve_url(self, path_or_url: str) -> str:
"""
Accept either a full URL or a path (e.g., '/char/'). Join with base_url when needed.
"""
if path_or_url.lower().startswith(("http://", "https://")):
return path_or_url
return urljoin(self.base_url + "/", path_or_url.lstrip("/"))
def _safe_json(self, resp: Response) -> Dict[str, Any]:
"""
Attempt to parse JSON. If body is empty or not JSON, return {}.
"""
# 204 No Content or truly empty payloads
if resp.status_code == 204 or not resp.content:
return {}
try:
return resp.json()
except ValueError:
# Non-JSON payload; log preview and return empty.
preview = ""
try:
preview = resp.text[:400]
except Exception:
pass
logger.warning(
"Non-JSON response body",
extra={
"url": resp.request.url if resp.request else None,
"status": resp.status_code,
"body_preview": preview,
},
)
return {}
def _request(
self,
method: str,
path: str,
*,
params: Optional[Dict[str, Any]] = None,
json_body: Optional[Dict[str, Any]] = None,
timeout: Optional[tuple[float, float]] = None,
) -> Dict[str, Any]:
"""
Central request executor. Never raises to the caller.
Returns parsed JSON on success or {} on any error.
"""
url = self._resolve_url(path)
headers = self._auth_headers()
to = timeout or self.default_timeout
try:
resp = self._session.request(
method=method.upper(),
url=url,
headers=headers,
params=params,
json=json_body,
timeout=to,
)
# Log and return {} on non-2xx
if not (200 <= resp.status_code < 300):
# Truncate body in logs to avoid huge entries
preview = ""
try:
preview = resp.text[:400]
except Exception:
pass
logger.warning(
"HTTP request failed",
extra={
"method": method.upper(),
"url": resp.request.url if resp.request else url,
"status": resp.status_code,
"params": params,
"json_body": json_body,
"body_preview": preview,
},
)
return {}
# Success path: parse JSON safely
return self._safe_json(resp)
except requests.exceptions.RequestException as e:
# Network/timeout/connection errors
logger.warning(
"Network error during HTTP request",
extra={
"method": method.upper(),
"url": url,
"params": params,
"json_body": json_body,
"error_type": type(e).__name__,
"error": str(e),
},
exc_info=True,
)
return {}
except Exception as e:
# Absolute last-resort guardrail
logger.error(
"Unexpected error during HTTP request",
extra={
"method": method.upper(),
"url": url,
"params": params,
"json_body": json_body,
"error_type": type(e).__name__,
"error": str(e),
},
exc_info=True,
)
return {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,18 @@
<h3>Archanist</h3>
<p>
The Archanists are masters of the arcane arts, wielding the fundamental forces of reality to achieve their goals.
They possess a deep understanding of the underlying fabric of existence, allowing them to manipulate the threads
of fate, bend time and space to their will, and summon the raw power of the cosmos. Through tireless study,
experimentation, and mystical communion with the celestial forces, Archanists have developed a unique blend of
magical prowess, strategic insight, and philosophical clarity. They are often sought as counselors, diplomats, and
problem-solvers by those in need of guidance or solution to complex challenges.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 8</li>
<li>Magic Power: 14</li>
<li>Primary Stat: INT</li>
</ul>
</div>

View File

@@ -0,0 +1,19 @@
<h3>Assassins</h3>
<p>
The Assassins are stealthy and deadly agents, trained to stalk the shadows and strike without warning. They
possess a unique blend of physical agility, mental focus, and cunning strategy, allowing them to move unseen,
gather information, and eliminate targets with precision and silence. Through years of rigorous training in the
art of espionage, deception, and murder, Assassins have honed their skills to the point where they can blend into
the background, become one with the darkness, and disappear into the night like specters. They are often hired as
mercenaries, bodyguards, or spies, but those who employ them do so at their own peril.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 10</li>
<li>Magic Power: 10</li>
<li>Primary Stat: DEX</li>
</ul>
</div>

View File

@@ -0,0 +1,19 @@
<h3>Bloodborn</h3>
<p>
The Bloodborn are unbridled warriors, born of the darkest depths of human nature and forged in the fire of
unforgiving violence. They possess a primal connection to their own rage and fury, allowing them to tap into a
deep wellspring of strength, speed, and ferocity when faced with adversity. Through generations of brutal training
and unrelenting combat, Bloodborn have learned to channel their inner turmoil into a maelstrom of aggression,
unleashing devastating attacks that leave foes reeling in terror. They are often seen as outcasts, driven by their
own unyielding passions and instincts, but those who dare oppose them do so at the risk of facing an unrelenting
storm of bloodlust and fury.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 14</li>
<li>Magic Power: 5</li>
<li>Primary Stat: STR</li>
</ul>
</div>

View File

@@ -0,0 +1,17 @@
<h3>Clerics</h3>
<p>
The Clerics are devoted servants of a higher power, channeling the divine energies to heal, protect, and uplift
those around them. They possess a deep understanding of the mysteries of life and death, allowing them to tend to
wounds, calm fears, and bring solace to the afflicted. Through their sacred vows and mystical communion with the
divine, Clerics have developed a unique blend of spiritual insight, empathetic compassion, and healing artistry.
As agents of hope and redemption, they often walk among the sick, the dying, and the grieving, offering comfort,
guidance, and the promise of a brighter tomorrow.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 8</li>
<li>Magic Power: 14</li>
<li>Primary Stat: WIS</li>
</ul>
</div>

View File

@@ -0,0 +1,17 @@
<h3>Guardians</h3>
<p>
The Guardians are holy warriors, sworn to defend the innocent and uphold the principles of justice, honor, and
righteousness. They possess a strong sense of duty, unshakeable conviction, and unwavering compassion, guiding
them as they stride into battle with valor in their hearts. Through their sacred oath and spiritual connection to
a higher power, Guardians have developed a unique blend of martial prowess, moral authority, and divine
intervention. As champions of the faithful, they are often called upon to vanquish evil, protect the vulnerable,
and defend the sacred realms against darkness and despair.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 14</li>
<li>Magic Power: 5</li>
<li>Primary Stat: STR</li>
</ul>
</div>

View File

@@ -0,0 +1,17 @@
<h3>Hexists</h3>
<p>
The Hexists are masters of dark and malevolent magic, delighting in the suffering and terror they inspire in
others. They possess a twisted understanding of the arcane forces, allowing them to manipulate reality itself to
inflict cruel and capricious punishment upon their enemies. Through their mastery of dark arts and infernal pacts,
Hexists have developed a unique blend of magical cunning, sadistic glee, and corrupted ambition. As agents of
chaos and despair, they often seek to undermine the fabric of society, sow discord, and revel in the misery of
others.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 8</li>
<li>Magic Power: 14</li>
<li>Primary Stat: INT</li>
</ul>
</div>

View File

@@ -0,0 +1,16 @@
<h3>Rangers</h3>
<p>
The Rangers are skilled trackers, hunters, and guardians of the natural world. They possess a deep understanding
of the land, its rhythms, and its creatures, allowing them to move unseen, stalk their prey with precision, and
track down even the most elusive foes. Through their connection to the wild, Rangers have developed a unique blend
of survival skills, stealth, and primal intuition. As champions of the wilderness, they often walk a fine line
between hunter and protector, defending the innocent and vanquishing those who would desecrate the land.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 10</li>
<li>Magic Power: 10</li>
<li>Primary Stat: DEX</li>
</ul>
</div>

View File

@@ -0,0 +1,17 @@
<h3>Warlock</h3>
<p>
The Warlocks are adepts of dark and forbidden magic, forging pacts with malevolent forces to wield power beyond
mortal comprehension. They possess a mastery of arcane energies, allowing them to summon eldritch powers,
manipulate reality, and bend the fabric of existence to their will. Through their pact with an otherworldly
patron, Warlocks have developed a unique blend of magical prowess, psychological manipulation, and shadowy
intrigue. As agents of chaos and darkness, they often walk a delicate balance between power and corruption, using
their mastery of the arcane to further their own sinister agendas.
</p>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes:</h4>
<ul>
<li>Health: 8</li>
<li>Magic Power: 14</li>
<li>Primary Stat: WIS</li>
</ul>
</div>

View File

@@ -0,0 +1,30 @@
<h3>Avaline</h3>
<p>
The Avaline are a divine and majestic people, born from the celestial realm to serve as radiant warriors of the
sky. Their physical form is one of elegance and power, with slender bodies, delicate wings, and eyes that shine
like stars. With their connection to the divine, they possess exceptional martial prowess, wielding blades that
seem to cut through the very fabric of reality. The Avaline are drawn to conflict, not for the sake of bloodshed,
but to vanquish darkness and restore balance to a world in need. Their presence on the battlefield is a beacon of
hope, inspiring allies and striking fear into the hearts of their enemies.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/avaline.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +5</li>
<li>Dexterity (Dex): 0</li>
<li>Intelligence (Int): +3</li>
<li>Wisdom (Wis): 1</li>
<li>Luck (Luk): -2</li>
<li>Charisma (Cha): -1</li>
<li>Constitution (Con): 0</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<h3>Beastfolk</h3>
<p>
Beastfolk are feral and resilient tribes that have adapted to life in the wilderness, developing a deep connection
with the natural world and its creatures. They possess a unique physical form, blending human and animal
characteristics, allowing them to move unseen, communicate through scent and sound, and track their prey with
ease. Beastfolk are fiercely protective of their territory and those they care about, making them formidable
allies or fierce opponents. Despite their primal appearance, they are not mindless beasts, but rather complex
individuals with a rich culture and history.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/beastfolk.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +3</li>
<li>Dexterity (Dex): +5</li>
<li>Intelligence (Int): +1</li>
<li>Wisdom (Wis): -2</li>
<li>Luck (Luk): -1</li>
<li>Charisma (Cha): 0</li>
<li>Constitution (Con): +1</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<h3>Draconian</h3>
<p>
The Draconians are a proud and ancient people, born from the union of dragons and terrans. Their physical form is a
testament to their draconic heritage, with scales that shimmer like polished gemstones, wings that soar through
the skies, and eyes that burn with inner fire. As half-dragon, they possess a unique blend of ferocity,
intelligence, and charisma, making them formidable diplomats, warriors, and leaders. The Draconians walk the line
between two worlds, bridging the gap between humans and dragons, and often serving as mediators and ambassadors
between their respective cultures. Their presence is often accompanied by a hint of smoke and flame, signifying
their connection to the primal forces of nature.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/draconian.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +1</li>
<li>Dexterity (Dex): +3</li>
<li>Intelligence (Int): 0</li>
<li>Wisdom (Wis): -2</li>
<li>Luck (Luk): 5</li>
<li>Charisma (Cha): +1</li>
<li>Constitution (Con): -1</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<h3>Dwarf</h3>
<p>
The Dwarves are a sturdy and proud people, born from the depths of the earth to forge a legacy of resilience and
determination. Their physical form is a testament to their craft and industry, with stout bodies, strong limbs,
and eyes that shine like polished iron. As skilled artisans and master craftsmen, they possess an unyielding
dedication to their work, honing their skills in the depths of mountains and caverns. Dwarves are a people of
tradition and heritage, standing guard over ancient secrets, hidden treasures, and forgotten knowledge. Their
connection to the earth is deep and abiding, granting them a profound understanding of the natural world and its
rhythms.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/dwarf.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +5</li>
<li>Dexterity (Dex): -2</li>
<li>Intelligence (Int): 0</li>
<li>Wisdom (Wis): -1</li>
<li>Luck (Luk): +1</li>
<li>Charisma (Cha): +1</li>
<li>Constitution (Con): +3</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<h3>Elves</h3>
<p>
Elves are an ancient and enigmatic people, known for their striking physical beauty, exceptional magical
abilities, and unparalleled connection with nature. They possess a deep understanding of the natural world,
allowing them to communicate with animals, manipulate plants, and wield the elements with precision. Elves are
highly attuned to their surroundings, making them formidable hunters, skilled archers, and gifted warriors.
Despite their refined features, elves are not immune to the harsh realities of life, and they have developed a
rich culture that reflects their struggles against the forces of darkness.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/elf.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): -2</li>
<li>Dexterity (Dex): +5</li>
<li>Intelligence (Int): +3</li>
<li>Wisdom (Wis): 0</li>
<li>Luck (Luk): 1</li>
<li>Charisma (Cha): 1</li>
<li>Constitution (Con): -1</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<h3>Hellions</h3>
<p>
The Hellions are a malevolent and enigmatic people, born from the dark recesses of the Shadow Realm to bring
terror and chaos into the world. Their physical form is a twisted mockery of humanity, with bodies that seem to
shift and writhe like living shadows, eyes that burn with an otherworldly green fire, and skin that seems to
absorb the light around them. As half-demon, they possess a unique blend of dark magic, cunning, and charisma,
making them formidable manipulators, spies, and assassins. Hellions are drawn to the darker aspects of life,
reveling in the fear and suffering of others, and often serving as agents of chaos and destruction for their
masters. Their presence is accompanied by an aura of malevolent energy, casting a pall of dread over those around
them.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/hellion.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +3</li>
<li>Dexterity (Dex): +1</li>
<li>Intelligence (Int): +1</li>
<li>Wisdom (Wis): 0</li>
<li>Luck (Luk): -2</li>
<li>Charisma (Cha): +5</li>
<li>Constitution (Con): -1</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<h3>Terrans</h3>
<p>
The Terrans are a sturdy and resilient people, born from the rich soil of the land to thrive in the everyday
world. Their physical form is unremarkable, yet serviceable, with bodies that adapt to their surroundings, eyes
that see clearly through the mundane, and hearts that beat with a deep connection to the earth. As ordinary
individuals, they possess an extraordinary capacity for empathy, compassion, and determination, making them
formidable mediators, leaders, and guardians of the common good. Terrans are not remarkable in any one way, yet
their collective strength lies in their ability to work together, support each other, and build a better world
through incremental progress and everyday heroism.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/terran.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +1</li>
<li>Dexterity (Dex): +3</li>
<li>Intelligence (Int): 0</li>
<li>Wisdom (Wis): -2</li>
<li>Luck (Luk): +5</li>
<li>Charisma (Cha): +1</li>
<li>Constitution (Con): -1</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,33 @@
<h3>Vorgath</h3>
<p>
The Vorgath are a twisted and malevolent people, born from the darkest recesses of existence to bring unrelenting
destruction into the world. Their physical form is an affront to nature, with bodies that seem to be perpetually
corrupted, eyes that blaze with an otherworldly intensity, and skin that appears to writhe like living darkness.
As abominations of the cosmos, they possess a unique blend of dark energy, unnatural resilience, and malevolent
willpower, making them formidable enemies, unyielding in their pursuit of chaos and despair. Vorgath are driven by
an insatiable hunger for destruction, reveling in the suffering of others, and often serving as agents of darkness
and terror for their masters. Their presence is accompanied by an aura of unrelenting malevolence, striking fear
into the hearts of all who behold them.
</p>
<div class="row align-items-center justify-content-center">
<div class="col-12 col-md-6">
<img src="{{ url_for('static', filename='images/races/vorgath.jpg') }}" style="width: 300px;" alt="Race Image"/>
</div>
<div class="col-12 col-md-6" id="attributes">
<h4>Attributes: + / - from 10</h4>
<ul>
<li>Strength (Str): +5</li>
<li>Dexterity (Dex): +3</li>
<li>Intelligence (Int): 1</li>
<li>Wisdom (Wis): 0</li>
<li>Luck (Luk): -1</li>
<li>Charisma (Cha): -2</li>
<li>Constitution (Con): +3</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,136 @@
{% extends "bases/main_base.html" %}
{% block title %}Admin Panel — Dashboard{% endblock %}
{% block body %}
<div class="container-fluid vh-100">
<div class="row vh-100 align-items-center justify-content-center">
<div class="col-12 col-md-6">
<div class="card w-400 mw-full mx-auto" style="padding: 20px;">
<h5 class="card-title text-center">Create Character</h5>
<form action="" method="POST">
<div class="form-group">
<label for="text-input" class="required">Name</label>
<input type="text" class="form-control mb-4" id="character_name" name="character_name" placeholder="Name your Character" required >
</div>
<div class="form-group">
<label for="race_dropdown">Choose Race</label>
<select class="form-control mb-4" id="race_dropdown" name="race_dropdown" required >
<option value="" disabled selected>Select a Race...</option>
<option value="avaline">Avaline - Divine, Strong, Intelligent</option>
<option value="beastfolk">Beastfolk - Half Beast, Half Terran</option>
<option value="draconian">Draconian - Half Dragon, Half Terran</option>
<option value="dwarf">Dwarf - Stout, Strong, Short</option>
<option value="elf">Elf - Tall, Slender, Agile</option>
<option value="hellion">Hellion - Dark, Shadowy Humanoid</option>
<option value="terran">Terran - Some call them Human.</option>
<option value="vorgath">Vorgath - Monstrous Evil Humanoid</option>
</select>
</div>
<div class="form-group">
<label for="profession_dropdown">Choose Profession</label>
<select class="form-control mb-4" id="profession_dropdown" name="profession_dropdown" required >
<option value="" disabled selected>Select a Profession...</option>
<option value="archanist">Archanist - Magic</option>
<option value="assassin">Assassin - Physical</option>
<option value="bloodborn">Bloodborn - Physical</option>
<option value="cleric">Cleric - Magic</option>
<option value="guardian">Guardian - Physical</option>
<option value="hexist">Hexist - Magic</option>
<option value="ranger">Ranger - Physical</option>
<option value="warlock">Warlock - Magic</option>
</select>
</div>
<div class="form-group">
<label for="origin_story">Origin Story</label>
<textarea class="form-control mb-4" id="origin_story" name="origin_story" rows="10" cols="40" required >{{data.starter_text}}</textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">
Submit
</button>
</form>
</div>
</div>
<div id="text_holder" class="col-12 col-md-6">
<p>
To start playing, we need to help you create your unique character.
</p>
<p>The following fields are required:</p>
<ul>
<li>Name: This is your character's identity in the game.</li>
<li>Race: Choose from our selection of options.</li>
<li>Profession: Select from our list of profession (commonly called classes).</li>
<li>Origin Story: This section is optional, but can provide additional context for your character.</li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Reference to the dropdown and image
const racedropdown = document.getElementById('race_dropdown');
const profdropdown = document.getElementById('profession_dropdown');
const contentArea = document.getElementById('text_holder');
racedropdown.addEventListener('change', async function () {
const value = this.value;
const url = `/ajax/races?race=${value}`;
try {
// Fetch HTML fragment from the server
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
// Replace existing content
contentArea.innerHTML = html;
} catch (err) {
console.error("Error loading content:", err);
contentArea.innerHTML = `
<div class="alert alert-danger">
Failed to load content. Please try again.
</div>`;
}
});
profdropdown.addEventListener('change', async function () {
const value = this.value;
const url = `/ajax/prof?prof=${value}`;
try {
// Fetch HTML fragment from the server
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
// Replace existing content
contentArea.innerHTML = html;
} catch (err) {
console.error("Error loading content:", err);
contentArea.innerHTML = `
<div class="alert alert-danger">
Failed to load content. Please try again.
</div>`;
}
});
</script>
{% endblock %}