Icons live in initrack_data/icons. Curated set from Game-Icons.net (CC‑BY 3.0). Authors: Lorc, Delapouite, Skoll, sbed. Replace any icon by dropping an SVG with the same name (e.g., poisoned.svg).
-
-
-
-
-"""
-
-# ------------------------------
-# Favicon (inline SVG)
-# ------------------------------
-FAVICON_SVG = """"""
-
-@app.route('/favicon.svg')
-def favicon_svg():
- return FAVICON_SVG, 200, {'Content-Type': 'image/svg+xml'}
-
-# ------------------------------
-# Routes
-# ------------------------------
-@app.route('/')
-def root():
- t = app.config.get('COMBAT_TOKEN', 'changeme')
- return redirect(url_for('admin') + f'?token={t}')
-
-@app.route('/avatars/')
-def avatars(fn: str):
- return send_from_directory(AVATAR_DIR, fn)
-
-@app.route('/icons/')
-def icons(fn: str):
- return send_from_directory(ICON_DIR, fn)
-
-@app.route('/board')
-def board():
- return render_template_string(BOARD_HTML, css=BOARD_CSS, name=PRODUCT_NAME, subtitle=PRODUCT_SUBTITLE)
-
-@app.route('/admin')
-@require_token
-def admin():
- tok = request.args.get('token', '')
- return render_template_string(ADMIN_HTML, token=tok, name=PRODUCT_NAME, subtitle=PRODUCT_SUBTITLE)
-
-@app.post('/admin/seed_icons')
-@require_token
-def admin_seed_icons():
- seed_curated_icons(overwrite=True)
- tok = request.args.get('token', '')
- return redirect(url_for('admin', token=tok))
-
-# --- Long-poll state ---
-@app.get('/api/state')
-@require_token
-def api_state():
- try:
- since = int(request.args.get('since', '0'))
- except ValueError:
- since = 0
- data = STATE.wait_for(since, timeout=25.0)
- return jsonify(data)
-
-# --- Uploads ---
-@app.post('/api/upload_avatar')
-@require_token
-def api_upload_avatar():
- if 'file' not in request.files:
- abort(400, 'missing file field')
- f = request.files['file']
- if not f or f.filename == '':
- abort(400, 'empty file')
- fn = secure_filename(f.filename)
- ext = os.path.splitext(fn)[1].lower()
- if ext not in ALLOWED_AVATAR_EXTS:
- abort(415, 'unsupported file type')
- newname = f"{uuid.uuid4().hex}{ext}"
- dest = os.path.join(AVATAR_DIR, newname)
- f.save(dest)
- return jsonify({'ok': True, 'filename': newname, 'url': f'/avatars/{newname}'})
-
-# ------------------------------
-# Mutations
-# ------------------------------
-@app.post('/api/add')
-@require_token
-def api_add():
- data = request.get_json(force=True, silent=True) or {}
- t = str(data.get('type', 'pc'))
- rev = data.get('reveal_ac', None)
- reveal = bool(rev) if rev is not None else (t == 'pc')
- a = Actor(
- id=str(uuid.uuid4()),
- name=str(data.get('name', 'Unknown'))[:64],
- init=float(data.get('init', 0)),
- hp=int(data.get('hp', 0)),
- ac=int(data.get('ac', 0)),
- type=t,
- note=str(data.get('note', ''))[:120],
- avatar=str(data.get('avatar',''))[:200],
- reveal_ac=reveal,
- )
- STATE.actors.append(a)
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'id': a.id})
-
-@app.post('/api/update')
-@require_token
-def api_update():
- data = request.get_json(force=True, silent=True) or {}
- aid = str(data.get('id', ''))
- target = next((x for x in STATE.actors if x.id == aid), None)
- if not target:
- abort(404)
- for k, v in data.items():
- if k == 'id':
- continue
- if v == 'toggle':
- if hasattr(target, k) and isinstance(getattr(target, k), bool):
- setattr(target, k, not getattr(target, k))
- else:
- if k in ('name','type','note','avatar'):
- setattr(target, k, str(v))
- elif k in ('init','hp','ac'):
- setattr(target, k, int(v) if k in ('hp','ac') else float(v))
- elif k in ('active','visible','dead','conc','reveal_ac'):
- setattr(target, k, bool(v))
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True})
-
-@app.post('/api/remove')
-@require_token
-def api_remove():
- data = request.get_json(force=True, silent=True) or {}
- aid = str(data.get('id', ''))
- before = len(STATE.actors)
- STATE.actors = [x for x in STATE.actors if x.id != aid]
- if len(STATE.actors) != before:
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'removed': len(STATE.actors) != before})
-
-@app.post('/api/next')
-@require_token
-def api_next():
- if STATE.actors:
- STATE.turn_idx = (STATE.turn_idx + 1) % len(STATE.actors)
- if STATE.turn_idx == 0:
- STATE.round += 1
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'round': STATE.round, 'turn_idx': STATE.turn_idx})
-
-@app.post('/api/prev')
-@require_token
-def api_prev():
- if STATE.actors:
- STATE.turn_idx = (STATE.turn_idx - 1) % len(STATE.actors)
- if STATE.turn_idx == len(STATE.actors) - 1:
- STATE.round = max(1, STATE.round - 1)
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'round': STATE.round, 'turn_idx': STATE.turn_idx})
-
-@app.post('/api/clear')
-@require_token
-def api_clear():
- STATE.actors.clear()
- STATE.turn_idx = 0
- STATE.round = 1
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True})
-
-@app.post('/api/toggle_visible')
-@require_token
-def api_toggle_visible():
- STATE.visible = not STATE.visible
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'visible': STATE.visible})
-
-@app.post('/api/toggle_effect')
-@require_token
-def api_toggle_effect():
- data = request.get_json(force=True, silent=True) or {}
- aid = str(data.get('id',''))
- effect = str(data.get('effect','')).strip().lower()
- if not effect:
- abort(400)
- target = next((x for x in STATE.actors if x.id == aid), None)
- if not target:
- abort(404)
- if effect in target.effects:
- target.effects = [e for e in target.effects if e != effect]
- else:
- target.effects.append(effect)
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'effects': target.effects})
-
-@app.post('/api/clear_effects')
-@require_token
-def api_clear_effects():
- data = request.get_json(force=True, silent=True) or {}
- aid = str(data.get('id',''))
- target = next((x for x in STATE.actors if x.id == aid), None)
- if not target:
- abort(404)
- target.effects = []
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True})
-
-# --- Presets ---
-@app.get('/api/presets')
-@require_token
-def api_presets():
- return jsonify({ 'items': [asdict(p) for p in PRESETS] })
-
-@app.post('/api/preset/add')
-@require_token
-def api_preset_add():
- data = request.get_json(force=True, silent=True) or {}
- p = Preset(
- id=str(uuid.uuid4()),
- name=str(data.get('name','Unnamed'))[:64],
- hp=int(data.get('hp',0)),
- ac=int(data.get('ac',0)),
- type=str(data.get('type','pc')),
- note=str(data.get('note',''))[:120],
- avatar=str(data.get('avatar',''))[:200],
- )
- PRESETS.append(p)
- save_presets()
- return jsonify({'ok': True, 'id': p.id})
-
-@app.post('/api/preset/remove')
-@require_token
-def api_preset_remove():
- data = request.get_json(force=True, silent=True) or {}
- pid = str(data.get('id',''))
- before = len(PRESETS)
- PRESETS[:] = [x for x in PRESETS if x.id != pid]
- changed = len(PRESETS) != before
- if changed:
- save_presets()
- return jsonify({'ok': True, 'removed': changed})
-
-@app.post('/api/preset/apply')
-@require_token
-def api_preset_apply():
- data = request.get_json(force=True, silent=True) or {}
- pid = str(data.get('id',''))
- initv = float(data.get('init', 0))
- src = next((x for x in PRESETS if x.id == pid), None)
- if not src:
- abort(404)
- reveal = (src.type == 'pc')
- a = Actor(
- id=str(uuid.uuid4()), name=src.name, init=initv, hp=src.hp, ac=src.ac,
- type=src.type, note=src.note, avatar=src.avatar, reveal_ac=reveal,
- )
- STATE.actors.append(a)
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'id': a.id})
-
-# --- Preset Groups ---
-@app.get('/api/preset_groups')
-@require_token
-def api_preset_groups():
- return jsonify({ 'items': [asdict(g) for g in PRESET_GROUPS] })
-
-@app.post('/api/preset/group/add')
-@require_token
-def api_group_add():
- data = request.get_json(force=True, silent=True) or {}
- name = str(data.get('name','')).strip()
- mids = list(data.get('member_ids') or [])
- if not name or not mids:
- abort(400)
- g = PresetGroup(id=str(uuid.uuid4()), name=name, member_ids=mids)
- PRESET_GROUPS.append(g)
- save_preset_groups()
- return jsonify({'ok': True, 'id': g.id})
-
-@app.post('/api/preset/group/remove')
-@require_token
-def api_group_remove():
- data = request.get_json(force=True, silent=True) or {}
- gid = str(data.get('id',''))
- before = len(PRESET_GROUPS)
- PRESET_GROUPS[:] = [x for x in PRESET_GROUPS if x.id != gid]
- changed = len(PRESET_GROUPS) != before
- if changed:
- save_preset_groups()
- return jsonify({'ok': True, 'removed': changed})
-
-@app.post('/api/preset/group/apply')
-@require_token
-def api_group_apply():
- data = request.get_json(force=True, silent=True) or {}
- gid = str(data.get('id',''))
- inits = data.get('inits')
- init_single = data.get('init', None)
- g = next((x for x in PRESET_GROUPS if x.id == gid), None)
- if not g:
- abort(404)
- added = []
- for idx, pid in enumerate(g.member_ids):
- src = next((x for x in PRESETS if x.id == pid), None)
- if not src: continue
- # initiative logic
- if isinstance(inits, list) and idx < len(inits):
- initv = float(inits[idx] or 0)
- elif init_single is not None:
- initv = float(init_single)
- else:
- initv = 10.0
- reveal = (src.type == 'pc')
- a = Actor(
- id=str(uuid.uuid4()), name=src.name, init=initv, hp=src.hp, ac=src.ac,
- type=src.type, note=src.note, avatar=src.avatar, reveal_ac=reveal,
- )
- STATE.actors.append(a)
- added.append(a.id)
- if added:
- STATE.normalize()
- save_state()
- STATE.broadcast()
- return jsonify({'ok': True, 'added': added, 'count': len(added)})
-
-# ------------------------------
-# Main
-# ------------------------------
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('--host', default='127.0.0.1')
- parser.add_argument('--port', type=int, default=5050)
- parser.add_argument('--token', default=os.environ.get('COMBAT_TOKEN','changeme'))
- args = parser.parse_args()
- app.config['COMBAT_TOKEN'] = args.token
- print(f"""
-Name: Battleflow — by Aetryos Workshop
-Admin: http://{args.host}:{args.port}/admin?token={args.token}
-Board: http://{args.host}:{args.port}/board?token={args.token}
-Data dir: {DATA_DIR}
-Avatars: put files in {AVATAR_DIR} (or upload via Admin) and refer by filename (e.g. goblin.png)
-Icons: place SVGs in {ICON_DIR} named after conditions (e.g. poisoned.svg). Default placeholders are auto-created; use the Admin button to seed curated Game-Icons (CC‑BY 3.0).
-Allowed avatar types: {', '.join(sorted(ALLOWED_AVATAR_EXTS))}
-""")
- app.run(host=args.host, port=args.port, debug=False)
diff --git a/initrack_data/icons/blinded.svg b/initrack_data/icons/blinded.svg
deleted file mode 100644
index 5c50e05..0000000
--- a/initrack_data/icons/blinded.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/charmed.svg b/initrack_data/icons/charmed.svg
deleted file mode 100644
index 3990a0b..0000000
--- a/initrack_data/icons/charmed.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/deafened.svg b/initrack_data/icons/deafened.svg
deleted file mode 100644
index 84b10bc..0000000
--- a/initrack_data/icons/deafened.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/frightened.svg b/initrack_data/icons/frightened.svg
deleted file mode 100644
index f931513..0000000
--- a/initrack_data/icons/frightened.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/grappled.svg b/initrack_data/icons/grappled.svg
deleted file mode 100644
index 2935fe8..0000000
--- a/initrack_data/icons/grappled.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/paralyzed.svg b/initrack_data/icons/paralyzed.svg
deleted file mode 100644
index d2ac179..0000000
--- a/initrack_data/icons/paralyzed.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/petrified.svg b/initrack_data/icons/petrified.svg
deleted file mode 100644
index abb7d50..0000000
--- a/initrack_data/icons/petrified.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/poisoned.svg b/initrack_data/icons/poisoned.svg
deleted file mode 100644
index b9e4fe9..0000000
--- a/initrack_data/icons/poisoned.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/prone.svg b/initrack_data/icons/prone.svg
deleted file mode 100644
index 5cf7e88..0000000
--- a/initrack_data/icons/prone.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/restrained.svg b/initrack_data/icons/restrained.svg
deleted file mode 100644
index 925e7b9..0000000
--- a/initrack_data/icons/restrained.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/stunned.svg b/initrack_data/icons/stunned.svg
deleted file mode 100644
index aac5447..0000000
--- a/initrack_data/icons/stunned.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/initrack_data/icons/unconscious.svg b/initrack_data/icons/unconscious.svg
deleted file mode 100644
index d2af3e7..0000000
--- a/initrack_data/icons/unconscious.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file