diff --git a/src/tilemapper/battlemap.py b/src/tilemapper/battlemap.py index eb9730c..16c3c94 100644 --- a/src/tilemapper/battlemap.py +++ b/src/tilemapper/battlemap.py @@ -1,13 +1,12 @@ # import logging +import tomllib from dataclasses import dataclass from functools import cached_property from io import StringIO from pathlib import Path from textwrap import indent -from typing import List, Union, get_type_hints from types import SimpleNamespace - -import tomllib +from typing import List, Union, get_type_hints from PIL import Image @@ -40,6 +39,13 @@ class BattleMap(BattleMapType): map_string: str = "" metadata: SimpleNamespace = None tileset: TileSet = None + + render_header: bool = True + render_border: bool = True + render_coordinates: bool = True + render_legend: bool = True + render_notes: bool = True + width: int = 0 height: int = 0 @@ -62,7 +68,7 @@ class BattleMap(BattleMapType): line = line.lstrip(" ") if in_map: if line and line[0] == "#": - map_data += line[1:] + "\n" + map_data += line[1:].lstrip() + "\n" continue in_map = False metadata += line + "\n" @@ -82,6 +88,8 @@ class BattleMap(BattleMapType): del self.legend if hasattr(self, "coordinates"): del self.coordinates + if hasattr(self, "notes"): + del self.notes return self.grid def validate_map_string(self, data: str) -> bool: @@ -127,31 +135,46 @@ class BattleMap(BattleMapType): @cached_property def header(self) -> str: - lines = [f"BattleMap: {self.metadata.map['name']} ({self.width}x{self.height})"] - lines.append(('-' * len(lines[0]))) + lines = ["", f"BattleMap: {self.metadata.map['name']} ({self.width}x{self.height})"] + lines.append(("-" * len(lines[0]))) for key, value in self.metadata.map.items(): if key in ("name", "description"): continue lines.append(f"{key.title()}: {value}") + lines.append("") return "\n".join(lines) @cached_property def footer(self) -> str: lines = [] - if 'description' in self.metadata.map: + if "description" in self.metadata.map: lines.append(f"{self.metadata.map['description']}") lines.append("") lines.append("Legend:") lines.append(self.legend) return "\n".join(lines) + @cached_property + def notes(self) -> str: + lines = [] + for number in dir(self.metadata): + if not number.isdigit(): + continue + location = getattr(self.metadata, number) + lines.append(f"[{number}]{location.get('name', '')}") + if hasattr(location, "notes"): + for note in location.notes: + lines.append(f" {note}") + lines.append("") + return "\n".join(lines) + @cached_property def legend(self) -> str: output = "" locations = False for char in sorted(set(list(self.map_string)), key=str.lower): if char in self.tileset.config.legend: - if char in "0123456789": + if char.isdigit(): locations = True continue output += f"{char} - {self.tileset.config.legend[char]}\n" @@ -182,24 +205,42 @@ class BattleMap(BattleMapType): return self.as_string() def __repr__(self) -> str: - lines = "" - left_coords = self.left_coordinates - i = 0 - for line in str(self).splitlines(): - lines += f"{left_coords[i].rjust(2, ' ')} │ {line}".ljust(self.width, " ") + " │\n" - i = i + 1 - top_break = "┌" + ("─" * (self.width + 2)) + "┐" - bot_break = "└" + ("─" * (self.width + 2)) + "┘" - return "\n".join( - [ - "", - self.header, - "", - indent(self.top_coordinates, " " * 5), - indent(top_break, " " * 3), - lines.rstrip(), - indent(bot_break, " " * 3), - "", - indent(self.footer, " ") - ] - ) + lines = [""] + + map_lines = "" + if self.render_coordinates: + left_coords = self.left_coordinates + i = 0 + border = "│" if self.render_border else " " + for line in str(self).splitlines(): + map_lines += f"{left_coords[i].rjust(2, ' ')}{border}{line}".ljust(self.width, " ") + f"{border}\n" + i = i + 1 + elif self.render_border: + for line in str(self).splitlines(): + map_lines += f"│{line}".ljust(self.width, " ") + "│\n" + else: + map_lines = str(self) + + top_break = bot_break = "" + if self.render_border: + top_break = "┌" + ("─" * self.width) + "┐" + bot_break = "└" + ("─" * self.width) + "┘" + + lines = [self.header] if self.render_header else [] + if self.render_coordinates: + lines.append(indent(self.top_coordinates, " " * 3)) + if top_break: + lines.append(indent(top_break, " " * 2) if self.render_coordinates else top_break) + lines.append(map_lines.rstrip()) + if bot_break: + lines.append(indent(bot_break, " " * 2) if self.render_coordinates else bot_break) + lines.append("") + + if self.render_legend: + lines.append(indent(self.footer, " ")) + lines.append("") + + if self.render_notes: + lines.append(self.notes) + + return indent("\n".join(lines), " ") diff --git a/src/tilemapper/cli.py b/src/tilemapper/cli.py index dcdd604..0ebafee 100644 --- a/src/tilemapper/cli.py +++ b/src/tilemapper/cli.py @@ -54,31 +54,60 @@ def convert(source: typer.FileText = typer.Argument(help="The donjon.sh .json fi @app.command() def render( source: typer.FileText = typer.Argument( - help="The battle map text file to load.", default=INSTALL_DIR / "examples" / "five_room_dungeon.txt" + help="The battle map text file to load.", default=INSTALL_DIR / "examples" / "five_room_dungeon.toml" ), outfile: Path = typer.Option(help="The file to create. If not specified, print to STDOUT", default=None), tileset: str = typer.Option( help="The name of the tile set to use (run mapper list to see what's available).", default="colorized" ), + header: bool = typer.Option(help="If True, include the map header.", default=True), + border: bool = typer.Option(help="If True, draw the map border.", default=True), + coordinates: bool = typer.Option(help="If True, draw the coordinate outside the border.", default=True), + legend: bool = typer.Option(help="If True, include the legend.", default=True), + notes: bool = typer.Option(help="If True, include the notes.", default=True), + map_only: bool = typer.Option( + help="Equivalent to --no-header --no-border --no-coordinates --no-legend --no-notes", default=False + ), ): """ Create a rendered battle map using a tile set. Will generate a PNG file if the tile set supports it, otherwise text output. """ - render_map(source, outfile, tileset) + render_map( + source=source, + outfile=outfile, + tileset=tileset, + header=header and not map_only, + border=border and not map_only, + coordinates=coordinates and not map_only, + legend=legend and not map_only, + notes=notes and not map_only, + ) def render_map( source: typer.FileText = INSTALL_DIR / "examples" / "five_room_dungeon.txt", outfile: Union[Path, None] = None, tileset: str = "colorized", + header: bool = True, + border: bool = True, + coordinates: bool = True, + legend: bool = True, + notes: bool = True, ): manager = app_state["tileset_manager"] if tileset not in manager.available: raise RuntimeError(f"Could not locate the tile set {tileset} in {manager.config_dir}.") + render_options = { + "render_header": header, + "render_border": border, + "render_coordinates": coordinates, + "render_notes": notes, + "render_legend": legend, + } try: - bmap = battlemap.BattleMap(source=source, tileset=manager.load(tileset)) + bmap = battlemap.BattleMap(source=source, tileset=manager.load(tileset), **render_options) bmap.load() except battlemap.UnsupportedTileException as e: logging.error(e) diff --git a/test/test_mapper.py b/test/test_mapper.py index 6c6e081..0313a34 100644 --- a/test/test_mapper.py +++ b/test/test_mapper.py @@ -35,6 +35,10 @@ def sample_map(): [1] name = "room 1" + description = "A large empty room." + notes = [ + "Locked door DC 15" + ] [2] name = "room 2" [3] @@ -62,13 +66,13 @@ def test_renderer(manager, sample_map): srclines = sample_map.splitlines()[1:] strlines = str(test_map).splitlines() for i in range(len(strlines)): - assert strlines[i] == srclines[i].lstrip(' #').ljust(21, " ") + assert strlines[i] == srclines[i].lstrip(" #").ljust(21, " ") def test_grid_coordiates(manager): coord_length = len(X_COORDS) map_size = 2 * coord_length + 1 - bigmap = StringIO((' # ' + ("." * map_size) + "\n") * map_size) + bigmap = StringIO((" # " + ("." * map_size) + "\n") * map_size) test_map = battlemap.BattleMap(source=bigmap, tileset=manager.load("ascii")) test_map.load() assert test_map.grid.data[-1][-1].coordinates == (f"{map_size - 1}", X_COORDS[0] * 3)