diff --git a/app/blueprints/ajax.py b/app/blueprints/ajax.py new file mode 100644 index 0000000..afdb516 --- /dev/null +++ b/app/blueprints/ajax.py @@ -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") \ No newline at end of file diff --git a/app/blueprints/auth.py b/app/blueprints/auth.py new file mode 100644 index 0000000..6633381 --- /dev/null +++ b/app/blueprints/auth.py @@ -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")) \ No newline at end of file diff --git a/app/blueprints/char.py b/app/blueprints/char.py new file mode 100644 index 0000000..389d654 --- /dev/null +++ b/app/blueprints/char.py @@ -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) + diff --git a/app/blueprints/main.py b/app/blueprints/main.py new file mode 100644 index 0000000..83f34e7 --- /dev/null +++ b/app/blueprints/main.py @@ -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) diff --git a/app/blueprints/public.py b/app/blueprints/public.py new file mode 100644 index 0000000..1cb9689 --- /dev/null +++ b/app/blueprints/public.py @@ -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") \ No newline at end of file diff --git a/app/game_data/ai_dumps.json b/app/game_data/ai_dumps.json new file mode 100644 index 0000000..c4b70d2 --- /dev/null +++ b/app/game_data/ai_dumps.json @@ -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." +] + +} \ No newline at end of file diff --git a/app/services/appwrite_db.py b/app/services/appwrite_db.py new file mode 100644 index 0000000..fbd7271 --- /dev/null +++ b/app/services/appwrite_db.py @@ -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",{}) + diff --git a/app/services/coc_api.py b/app/services/coc_api.py new file mode 100644 index 0000000..7da6976 --- /dev/null +++ b/app/services/coc_api.py @@ -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 (don’t 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 {} diff --git a/app/static/images/races/avaline.jpg b/app/static/images/races/avaline.jpg new file mode 100644 index 0000000..d0bc58b Binary files /dev/null and b/app/static/images/races/avaline.jpg differ diff --git a/app/static/images/races/beastfolk.jpg b/app/static/images/races/beastfolk.jpg new file mode 100644 index 0000000..6325edf Binary files /dev/null and b/app/static/images/races/beastfolk.jpg differ diff --git a/app/static/images/races/draconian.jpg b/app/static/images/races/draconian.jpg new file mode 100644 index 0000000..1418a9a Binary files /dev/null and b/app/static/images/races/draconian.jpg differ diff --git a/app/static/images/races/dwarf.jpg b/app/static/images/races/dwarf.jpg new file mode 100644 index 0000000..075244e Binary files /dev/null and b/app/static/images/races/dwarf.jpg differ diff --git a/app/static/images/races/elf.jpg b/app/static/images/races/elf.jpg new file mode 100644 index 0000000..6aa8df4 Binary files /dev/null and b/app/static/images/races/elf.jpg differ diff --git a/app/static/images/races/hellion.jpg b/app/static/images/races/hellion.jpg new file mode 100644 index 0000000..55bed25 Binary files /dev/null and b/app/static/images/races/hellion.jpg differ diff --git a/app/static/images/races/terran.jpg b/app/static/images/races/terran.jpg new file mode 100644 index 0000000..74018f0 Binary files /dev/null and b/app/static/images/races/terran.jpg differ diff --git a/app/static/images/races/vorgath.jpg b/app/static/images/races/vorgath.jpg new file mode 100644 index 0000000..24dabf6 Binary files /dev/null and b/app/static/images/races/vorgath.jpg differ diff --git a/app/templates/ajax/prof_archanist.html b/app/templates/ajax/prof_archanist.html new file mode 100644 index 0000000..f7db600 --- /dev/null +++ b/app/templates/ajax/prof_archanist.html @@ -0,0 +1,18 @@ +
+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. +
+ ++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. + +
+ ++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. +
+ ++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. +
++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. +
++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. +
++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. +
++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. +
++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. + +
+ +
+ +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. +
+ +
+ +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. + +
+
+ +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. + +
+
+ +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. +
+ +
+ +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. + +
+ +
+ +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. + +
+ +
+ +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. + +
+ +
+ + To start playing, we need to help you create your unique character. +
+ +The following fields are required:
+