Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Apr 1, 2024
1 parent 8c96047 commit 5755e95
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 31 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ This web application was made as a companion for some scenarios for [Run. Die. R
- Rocks from [Isometric 64x64 Outside Tileset by Yar](https://opengameart.org/content/isometric-64x64-outside-tileset) - [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)
- [Map - Small Town](https://www.deviantart.com/ekizius/art/Map-Small-Town-795100291) & [Map - Village](https://www.deviantart.com/ekizius/art/Map-Village-795100444) by Ekizius - [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/)
- [Fantasy portraits by TinySecretDoor](https://www.deviantart.com/tinysecretdoor/gallery/52921157/fantasy-portraits) - [CC BY-NC 3.0](https://creativecommons.org/licenses/by-nc/3.0/)
- [Hidden Gods of the Woods by Moira Games](https://moira-games.itch.io/hidden-gods-of-the-woods) ([They dug too deep](https://moira-games.itch.io/they-dug-too-deep) by the same author is also a great one-page dungeon)

Many more great CC BY-NC isometric maps can be found at [One Page Dungeon Contest](https://www.dungeoncontest.com/) or here: https://www.elventower.com/isometric-maps/

<!-- Other great illustrations:
* https://www.reddit.com/r/battlemaps/comments/lqf3yz/epic_isometric_crystal_dungeon/
* https://www.reddit.com/r/FantasyMaps/comments/l3g2os/secret_city_settlement_map/
* They Dug Too Deep : https://moira-games.itch.io/they-dug-too-deep
* Rudok's Tavern | Main Hall isometric map : https://i.redd.it/0wbqhfakpfl61.jpg
* https://i.redd.it/ugtywg3wejs61.jpg -> explo post-apo ?
-->
Expand Down
13 changes: 10 additions & 3 deletions sir.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ input[type='submit'] { font-size: 2rem; }
#table { background: black; }
svg {
width: 100%;
max-height: 98vh;
max-height: 100vh;
}
#timer {
position: absolute;
Expand All @@ -33,13 +33,12 @@ svg {
font-size: 3rem;
}
.clip {
stroke: white;
stroke-width: 1;
fill: url(#diagonalHatch);
}
.clip.enabled { fill: transparent; }
form.admin { color: white; }
input[type='checkbox'] { display: none; }
input[type='checkbox'].hidden { display: none; }
input[type='number'] { width: 3rem; }
.switch + label {
display: inline-block;
Expand All @@ -53,3 +52,11 @@ input[type='number'] { width: 3rem; }
background: white;
color: black;
}
#characters {
position: absolute;
top: 0; right: 0;
width: 14rem;
}
.character > input[type='checkbox'] { appearance: none; }
.character > input[type='checkbox']::before { content: "○" }
.character > input[type='checkbox']:checked::before { content: "⬤" }
71 changes: 50 additions & 21 deletions sir_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
# Note : Roll20 is free an can be a better an alternative :D

import copy, json, os, random, string, time
from collections import OrderedDict
Expand All @@ -16,7 +17,7 @@
SCENE_DEF_SCHEMA = json.load(schema_file)
SCENE_DEFS = (
{'name': 'Hallways of Thime', 'img': {
'url': 'https://chezsoi.org/lucas/jdr/shared-img-reveal/hallways_of_thime_by_djekspek_HD_no_comments.jpg',
'url': 'hallways_of_thime_by_djekspek_HD_no_comments.jpg',
'width': 1158, 'height': 818,
}, 'clips': [
{'type': 'rect', 'x': 190, 'y': 0, 'width': 780, 'height': 36}, # Title
Expand All @@ -43,11 +44,11 @@
{'type': 'ellipse', 'cx': 565, 'cy': 590, 'rx': 80, 'ry': 45}, # Water Basin
{'type': 'ellipse', 'cx': 300, 'cy': 460, 'rx': 110, 'ry': 80}, # Machine Room
{'type': 'ellipse', 'cx': 300, 'cy': 610, 'rx': 100, 'ry': 70}, # Treasure Cave
], 'add': [
{'type': 'image', 'xlink:href': 'https://chezsoi.org/lucas/jdr/shared-img-reveal/rocks.png', 'x': 360, 'y': 210, 'width': 80, 'height': 80},
], 'duration_in_min': 45},
], 'add': [
{'type': 'image', 'xlink:href': 'rocks.png', 'x': 360, 'y': 210, 'width': 80, 'height': 80},
], 'duration_in_min': 45},
{'name': 'Enquête sous pression à ValTordu', 'img': {
'url': 'https://chezsoi.org/lucas/jdr/shared-img-reveal/EnqueteAuVillage.jpg',
'url': 'EnqueteAuVillage.jpg',
'width': 1575, 'height': 881,
}, 'clips': [
# North-West
Expand All @@ -73,20 +74,35 @@
{'type': 'ellipse', 'cx': 120, 'cy': 740, 'rx': 110, 'ry': 140}, # Wizard
{'type': 'ellipse', 'cx': 378, 'cy': 740, 'rx': 110, 'ry': 140}, # Mayor
{'type': 'ellipse', 'cx': 635, 'cy': 740, 'rx': 110, 'ry': 140}, # Clockmaker
], 'add': [
{'type': 'text', 'content': 'Faraday', 'x': 32, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
{'type': 'text', 'content': 'Douglas', 'x': 290, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
{'type': 'text', 'content': 'Erneste', 'x': 550, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
{'type': 'text', 'content': 'Sirius', 'x': 870, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
{'type': 'text', 'content': 'Marko', 'x': 1110, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
{'type': 'text', 'content': 'Jacques', 'x': 1350, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'white'},
], 'duration_in_min': 45},
], 'add': [
{'type': 'text', 'content': 'Sirius', 'x': 870, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'red'},
{'type': 'text', 'content': 'Marko', 'x': 1110, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'green'},
{'type': 'text', 'content': 'Jacques', 'x': 1350, 'y': 220,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'purple'},
{'type': 'text', 'content': 'Faraday', 'x': 32, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'blue'},
{'type': 'text', 'content': 'Douglas', 'x': 290, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'orange'},
{'type': 'text', 'content': 'Erneste', 'x': 550, 'y': 825,
'font-family': 'Arial Black', 'font-size': 40, 'fill': 'grey'},
], 'characters': [
{'visible': True, 'name': 'Sirius', 'color': 'red'},
{'visible': True, 'name': 'Marko', 'color': 'green'},
{'visible': True, 'name': 'Jacques', 'color': 'purple'},
{'visible': True, 'name': 'Faraday', 'color': 'blue'},
{'visible': True, 'name': 'Douglas', 'color': 'orange'},
{'visible': True, 'name': 'Erneste', 'color': 'grey'},
], 'duration_in_min': 45, 'show_list_of_characters': False},
)
CHARACTERS = (
{'visible': False, 'name': '', 'color': 'red'},
{'visible': False, 'name': '', 'color': 'green'},
{'visible': False, 'name': '', 'color': 'purple'},
{'visible': False, 'name': '', 'color': 'blue'},
{'visible': False, 'name': '', 'color': 'orange'},
{'visible': False, 'name': '', 'color': 'grey'},
)
APP = Flask(__name__, static_folder='.', static_url_path='')
TABLES = OrderedDict() # in-memory data state
Expand All @@ -112,10 +128,10 @@ def index_as_json():
def admin_as_html(admin_id):
# Uncomment this while working on a scene, to help iterating on SVGs positions:
# TABLES[admin_id] = {'scene_def': SCENE_DEFS[0], 'public_id': 'ABCDEF', 'visible_clips': [], 'display_all': False}
if request.method == 'POST':
if request.method == 'POST' or True:
if admin_id not in TABLES: # => table creation
autocleanup()
scene_def_id = request.form.get('scene_def_id') and int(request.form.get('scene_def_id'))
scene_def_id = 1 # request.form.get('scene_def_id') and int(request.form.get('scene_def_id'))
image_url = request.form.get('image_url')
if scene_def_id:
scene_def = copy.deepcopy(SCENE_DEFS[scene_def_id - 1])
Expand All @@ -139,6 +155,10 @@ def admin_as_html(admin_id):
scene_def['clips'] = []
if 'add' not in scene_def:
scene_def['add'] = []
if 'color' not in scene_def:
scene_def['color'] = 'white'
if 'show_list_of_characters' not in scene_def:
scene_def['show_list_of_characters'] = True
table = {
'scene_def': scene_def,
'public_id': ''.join(random.choices(string.ascii_uppercase, k=6)), # nosec: Standard pseudo-random generators are not suitable for security/cryptographic purposes
Expand All @@ -147,6 +167,8 @@ def admin_as_html(admin_id):
'display_all': False,
'timer_end': None,
}
characters = scene_def.get('characters', CHARACTERS)
table['characters'] = [dict(**character) for character in characters]
TABLES[admin_id] = table
else: # => table update
table = TABLES[admin_id]
Expand All @@ -156,6 +178,13 @@ def admin_as_html(admin_id):
table['timer_end'] = (datetime.now() + timedelta(minutes=countdown_minutes)).timestamp()
table['visible_clips'] = [int(key.split('enable_clip_')[1]) for key, value in request.form.items()
if key.startswith('enable_clip_') and value == 'on']
for key, value in request.form.items():
if key.startswith('enable_character_'):
index = int(key.split('enable_character_')[1])
table['characters'][index]['visible'] = value == 'on'
if key.startswith('character_name_'):
index = int(key.split('character_name_')[1])
table['characters'][index]['name'] = value
table['added_elems'] = [int(key.split('enable_elem_')[1]) for key, value in request.form.items()
if key.startswith('enable_elem_') and value == 'on']
TABLES.move_to_end(admin_id) # move on top of OrderedDict (must be done manually on updates)
Expand Down
26 changes: 20 additions & 6 deletions templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>Shared image reveal app: {{ table.scene_def.name }}</title>
<link rel="stylesheet" href="../sir.css">
<base href="../">
<style>
.clip { stroke: {{ table.scene_def.color }}; }
</style>
</head>
<body id="table">
<!-- The viewBox ratio must match the image ratio, so that the image can fully fill the <svg>: -->
Expand All @@ -24,23 +28,33 @@
{% endfor %}
<pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="10" height="10">
<!-- Tweaked recipe from: https://stackoverflow.com/a/14500054/636849 -->
<path d="M0,10 l10,-10" style="stroke:white; stroke-width:1" />
<path d="M0,10 l10,-10" style="stroke: {{ table.scene_def.color }}; stroke-width:1" />
</pattern>
</svg>
{% if table.timer_end %}<div id="timer"></div>{% endif %}
<form class="admin" method="POST">
<div>Table public URL: <a href="../table/{{ table.public_id }}">{{ table.public_id }}</a></div>
{% for clip in table.scene_def.clips %}
<input type="checkbox" name="enable_clip_{{ loop.index0 }}" {{ 'checked' if loop.index0 in table.visible_clips else '' }}/>
{% if table.scene_def.show_list_of_characters %}
<fieldset id="characters">
{% for character in table.characters %}
<div class="character">
<input onclick="document.forms[0].submit()" type="checkbox" name="enable_character_{{ loop.index0 }}" {{ 'checked' if character.visible else '' }} style="color: {{ character.color }}"></span>
<input onfocusout="document.forms[0].submit()" name="character_name_{{ loop.index0 }}" value="{{ character.name }}" size="16"/>
</div>
{% endfor %}
</fieldset>
{% endif %}
{% for _ in table.scene_def.clips %}
<input type="checkbox" class="hidden" name="enable_clip_{{ loop.index0 }}" {{ 'checked' if loop.index0 in table.visible_clips else '' }}/>
{% endfor %}
{% for elem in table.scene_def.add %}
<input type="checkbox" name="enable_elem_{{ loop.index0 }}" {{ 'checked' if loop.index0 in table.added_elems else '' }}/>
<input type="checkbox" class="hidden" name="enable_elem_{{ loop.index0 }}" {{ 'checked' if loop.index0 in table.added_elems else '' }}/>
{% endfor %}
<input type="checkbox" id="display_all" name="display_all" class="switch" {{ 'checked' if table.display_all else '' }} onchange="this.form.submit()"/>
<input type="checkbox" class="switch hidden" id="display_all" name="display_all" {{ 'checked' if table.display_all else '' }} onchange="this.form.submit()"/>
<label for="display_all">Display all</label>
<label for="display_all">Countdown (min):</label>
<input type="number" name="countdown_minutes" value="{{ table.scene_def.duration_in_min or 20 }}"/>
<input type="checkbox" id="reset_timer" name="reset_timer" class="switch" onchange="this.form.submit()"/>
<input type="checkbox" class="switch hidden" id="reset_timer" name="reset_timer" onchange="this.form.submit()"/>
<label for="reset_timer">Reset timer</label>
</form>
<script>
Expand Down
12 changes: 12 additions & 0 deletions templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>Shared image reveal app: {{ table.scene_def.name }}</title>
<link rel="stylesheet" href="../sir.css">
<base href="../">
</head>
<body id="table">
{% if table.scene_def.show_list_of_characters %}
<div id="characters">
{% for character in table.characters %}
{% if character.visible %}
<div class="character">
<span style="color: {{ character.color }}">⬤ {{ character.name }}</span>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- The viewBox ratio must match the image ratio, so that the image can fully fill the <svg>: -->
<svg viewBox="0 0 {{ table.scene_def.img.width }} {{ table.scene_def.img.height }}">
{% if table.display_all %}
Expand Down

0 comments on commit 5755e95

Please sign in to comment.