Skip to content

Commit

Permalink
Merge pull request #31 from mmacy/char-mgmt-2
Browse files Browse the repository at this point in the history
[tui] new character modal screen (WIP)
  • Loading branch information
mmacy committed Dec 8, 2023
2 parents 505dad5 + 08c01c7 commit f26a25a
Show file tree
Hide file tree
Showing 17 changed files with 4,916 additions and 34 deletions.
4,754 changes: 4,754 additions & 0 deletions docs/tui-layouts.excalidraw

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions osrgame/osrgame/osrgame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from screen_character import CharacterScreen
from screen_welcome import WelcomeScreen
from screen_explore import ExploreScreen
from screen_modal_new_char import NewCharacterModalScreen
from screen_adventure_browser import AdventureBrowserScreen

from osrlib.adventure import Adventure
Expand All @@ -19,21 +20,21 @@ class OSRConsole(App):
player_party = None
adventure = None
dungeon_master = None
openai_model = OpenAIModelVersion.GPT4TURBO
openai_model = OpenAIModelVersion.GPT35TURBO

CSS_PATH = "screen.tcss"

BINDINGS = [
("escape", "previous_screen", "Previous screen"),
("escape", "previous_screen", "Back"),
("q", "quit", "Quit"),
]

SCREENS = {
"screen_adventure_browser": AdventureBrowserScreen(),
#"screen_adventure_creator": AdventureCreator(),
"screen_character": CharacterScreen(),
"screen_explore": ExploreScreen(),
"screen_welcome": WelcomeScreen(),
"screen_modal_new_char": NewCharacterModalScreen(),
}

def compose(self) -> ComposeResult:
Expand Down
35 changes: 34 additions & 1 deletion osrgame/osrgame/screen.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,40 @@ ExploreScreen {
background: $surface;
}

NewCharacterModalScreen {
align: center middle;
}

CharacterClassRadioButtons {
column-span: 2;
align: center middle;
}

RadioSet {
width: 100%;
}

#character_name {
column-span: 2;
}

#new_character_grid {
align: center middle;
grid-size: 1;
grid-gutter: 1 2;
grid-rows: 0.05fr 0.07fr 0.2fr 0.09fr;
padding: 0 1;
width: 30%;
height: 75%;
border: thick $background 70%;
background: $surface;
}

#stat-block {
padding: 1;
column-span: 2;
}

#log {
column-span: 2;
}
Expand Down Expand Up @@ -67,7 +97,10 @@ ExploreScreen {

.button {
margin: 1;
background: $surface-lighten-1;
}

.char_create_buttons {
align: center bottom;
}

.header-footer {
Expand Down
2 changes: 1 addition & 1 deletion osrgame/osrgame/screen_adventure_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AdventureBrowserScreen(Screen):
def compose(self) -> ComposeResult:
"""Compose our UI."""
# Set the path to the current user's home directory.
path = get_data_dir_path("osrlib")# / "adventures"
path = get_data_dir_path("osrlib") / "adventures"
yield Header()
with Container():
yield JsonFilteredDirectoryTree(path, id="tree-view")
Expand Down
27 changes: 22 additions & 5 deletions osrgame/osrgame/screen_character.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from typing import Any, Coroutine
from textual import events
from textual import events, on
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Button, Header, Footer, Log
from textual.events import Event

from widgets import CharacterStatsBox, AbilityTable, ItemTable, SavingThrowTable, CharacterScreenButtons
from widgets import (
CharacterStatsBox,
AbilityTable,
ItemTable,
SavingThrowTable,
CharacterScreenButtons,
)


class CharacterScreen(Screen):
BINDINGS = [
("k", "clear_log", "Clear log"),
("escape", "app.pop_screen", "Pop screen"),
("escape", "app.pop_screen", "Back"),
("n", "next_character", "Next character"),
("ctrl+n", "new_character", "New character"),
]
Expand All @@ -38,6 +44,10 @@ def on_mount(self) -> None:
self.query_one(SavingThrowTable).update_table()
self.query_one(ItemTable).items = self.app.adventure.active_party.active_character.inventory.all_items

@on(Button.Pressed, "#btn_new_character")
def default_button_pressed(self) -> None:
self.action_new_character()

def on_button_pressed(self, event: Button.Pressed) -> None:
pc = self.app.adventure.active_party.active_character
if event.button.id == "btn_roll_abilities":
Expand All @@ -52,16 +62,23 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
self.query_one(Log).write_line(roll_string)
self.query_one(CharacterStatsBox).pc_hp = pc.character_class.max_hp

elif event.button.id == "btn_save_character":
pc.save_character()
self.query_one(Log).write_line("Character saved.")

def action_clear_log(self) -> None:
"""An action to clear the log."""
self.query_one(Log).clear()

def action_new_character(self) -> None:
"""An action to create a new character."""
self.app.push_screen("screen_modal_new_char")

def action_next_character(self) -> None:
"""An action to switch to the next character in the party."""
self.app.adventure.active_party.set_next_character_as_active()
self.on_mount()


def on_event(self, event: Event) -> Coroutine[Any, Any, None]:
"""Handle events."""
# HACK: This is a hack to get the screen to update when the user switches to it.
Expand All @@ -72,4 +89,4 @@ def on_event(self, event: Event) -> Coroutine[Any, Any, None]:
def reroll(self):
"""Rolls the ability scores of the active character."""
self.app.adventure.active_party.active_character.roll_abilities()
self.query_one(AbilityTable).update_table()
self.query_one(AbilityTable).update_table()
2 changes: 1 addition & 1 deletion osrgame/osrgame/screen_explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def action_summarize(self) -> None:
)
dm_response = self.dungeon_master.player_message(formatted_message)
self.query_one("#dm_log").write_line(
"> " + str(self.dungeon_master.adventure.active_dungeon.current_location)
""#"> " + str(self.dungeon_master.adventure.active_dungeon.current_location)
)
self.query_one("#dm_log").write_line(wrap_text(dm_response))
self.query_one("#dm_log").write_line("---")
Expand Down
40 changes: 40 additions & 0 deletions osrgame/osrgame/screen_modal_new_char.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from textual.app import App, ComposeResult
from textual.containers import Grid, Horizontal
from textual.screen import ModalScreen
from textual.widgets import Button, Input, RadioSet, Static, Placeholder
from textual import on
from widgets import CharacterClassRadioButtons
from osrlib.enums import CharacterClassType

class NewCharacterModalScreen(ModalScreen):
"""A modal screen for creating a new character."""

CSS_PATH = "screen.tcss"

def compose(self) -> ComposeResult:
yield Grid(
Static("Create New Character", id="title"),
Input(id="character_name", placeholder="Enter character name"),
CharacterClassRadioButtons(),
Horizontal(
Button("Create", id="btn_char_create", variant="primary", classes="button"),
Button("Cancel", id="btn_char_cancel", variant="default", classes="button"),
classes="char_create_buttons",
),
id="new_character_grid",
)

def on_mount(self) -> None:
self.set_focus(self.query_one("#character_name"))

@on(Button.Pressed, "#btn_char_cancel")
def cancel_button_pressed(self) -> None:
self.app.pop_screen()

@on(Button.Pressed, "#btn_char_create")
def create_button_pressed(self) -> None:
character_name = self.query_one("#character_name").value
character_class_value = self.query_one(RadioSet).pressed_button.label.plain
character_class = CharacterClassType[character_class_value.upper()]
# Implement the character creation logic here
self.app.pop_screen()
4 changes: 0 additions & 4 deletions osrgame/osrgame/screen_welcome.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
from textual import on
from textual.app import ComposeResult
from textual.screen import Screen
from textual.app import ComposeResult
from textual.widgets import Header, Footer, Button
from widgets import WelcomeScreenButtons
from osrlib.dungeon_master import DungeonMaster
from osrlib.game_manager import logger
from osrlib.enums import OpenAIModelVersion


class WelcomeScreen(Screen):
Expand Down
14 changes: 11 additions & 3 deletions osrgame/osrgame/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
from textual.app import ComposeResult
from textual.containers import Container
from textual.reactive import reactive
from textual.widgets import Button, DataTable, Log, Static, DirectoryTree

from textual.widgets import Button, DataTable, Log, Static, DirectoryTree, RadioSet, RadioButton

from osrlib.enums import CharacterClassType
from osrlib.item import Item
from osrlib.utils import format_modifiers


class CharacterClassRadioButtons(Container):
def compose(self) -> ComposeResult:
with RadioSet(id="character_class") as radio_set:
for character_class in CharacterClassType:
yield RadioButton(character_class.value, value=character_class.name)


class WelcomeScreenButtons(Container):
def compose(self) -> ComposeResult:
yield Button("Random adventure", id="btn-adventure-default", classes="button")
Expand All @@ -20,6 +27,7 @@ def compose(self) -> ComposeResult:

class CharacterScreenButtons(Container):
def compose(self) -> ComposeResult:
yield Button("New character", id="btn_new_character", classes="button")
yield Button("Roll abilities", id="btn_roll_abilities", classes="button")
yield Button("Roll HP", id="btn_roll_hp", classes="button")
yield Button("Save character", id="btn_save_character", classes="button")
Expand Down Expand Up @@ -104,7 +112,7 @@ def update_table(self):
pc.name,
pc.character_class.class_type.value,
Text(str(pc.level), justify="center"),
Text(f"{str(pc.hit_points)}/{str(pc.max_hit_points)}", justify="center"),
Text("DEAD" if pc.hit_points <= 0 else f"{pc.hit_points}/{pc.max_hit_points}", justify="center"),
Text(str(pc.armor_class), justify="center"),
Text(str(pc.xp) + "/" + str(pc.xp_needed_for_next_level), justify="center"),
]
Expand Down
2 changes: 1 addition & 1 deletion osrgame/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion osrlib/osrlib/adventure.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def save_adventure(self, file_path: str = None) -> str:
now = datetime.datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S") # YYYYMMDD_HHMMSS
filename = f"{self.name}_{timestamp}.json".replace(" ", "_").lower()
save_dir = get_data_dir_path("osrlib")# / "adventures"
save_dir = get_data_dir_path("osrlib") / "adventures"
create_dir_tree_if_not_exist(save_dir)
file_path = save_dir / filename

Expand Down
37 changes: 37 additions & 0 deletions osrlib/osrlib/player_character.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""This module contains the PlayerCharacter class."""
from enum import Enum
import datetime, json
import osrlib.ability
from osrlib.ability import (
AbilityType,
Expand All @@ -18,6 +19,7 @@
from osrlib.inventory import Inventory
from osrlib.dice_roller import roll_dice, DiceRoll
from osrlib.game_manager import logger
from osrlib.utils import get_data_dir_path, create_dir_tree_if_not_exist


class Alignment(Enum):
Expand Down Expand Up @@ -307,3 +309,38 @@ def from_dict(cls, data_dict: dict):
pc.inventory = Inventory.from_dict(data_dict["inventory"], pc)

return pc

def save_character(self, file_path: str = None) -> str:
"""
Saves the character to a JSON file.
Args:
file_path (str, optional): The path where the file will be saved.
If None, saves in the default data directory.
Returns:
str: The path where the file was saved.
Raises:
OSError: If the file cannot be written.
"""
character_data = self.to_dict()
json_data = json.dumps(character_data, indent=4)

if file_path is None:
now = datetime.datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S") # YYYYMMDD_HHMMSS
filename = f"{self.name}_{timestamp}.json".replace(" ", "_").lower()
save_dir = get_data_dir_path("osrlib") / "characters"
create_dir_tree_if_not_exist(save_dir)
file_path = save_dir / filename

try:
with open(file_path, "w") as file:
file.write(json_data)
logger.debug(f"Character saved to {file_path}")
except OSError as e:
logger.error(f"Failed to save character to {file_path}: {e}")
raise

return str(file_path)
7 changes: 1 addition & 6 deletions osrlib/osrlib/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os, platform, textwrap
import os, platform, textwrap, re
from pathlib import Path

from osrlib.ability import ModifierType
Expand Down Expand Up @@ -50,11 +50,6 @@ def wrap_text(text: str, width: int = 100) -> str:
return textwrap.fill(text, width)


import re

# import unicodedata # Uncomment if you need Unicode normalization


def sanitize_path_element(path_element: str, replace_space: str = "_") -> str:
"""
Sanitize a string to ensure it's a valid path element for file and directory names.
Expand Down
2 changes: 1 addition & 1 deletion osrlib/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "osrlib"
version = "0.1.44"
version = "0.1.46"
description = "Turn-based dungeon-crawler game engine for OSR-style RPGs."
authors = ["Marsh Macy <[email protected]>"]
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions tests/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f26a25a

Please sign in to comment.