| """D&D Initiative Tracker — turn order only, no AI, no external dependencies.""" |
|
|
| import gradio as gr |
|
|
| from src import content, tool |
| from src.space_config import CONFIG |
| from src.shared.ctas import cta_html |
| from src.shared.page import standard_launch |
| from src.shared.seo import hero_html, section_html, faq_html, related_html |
|
|
| COPY_JS = """ |
| () => { |
| const el = document.querySelector('#lf-copy-src textarea'); |
| if (!el || !el.value) return; |
| const flash = () => { |
| const btn = document.querySelector('#lf-copy-btn button') || |
| document.querySelector('#lf-copy-btn'); |
| if (btn) { const t = btn.textContent; btn.textContent = 'Copied!'; |
| setTimeout(() => (btn.textContent = t), 1500); } |
| }; |
| const fallback = () => { |
| el.focus({preventScroll: true}); el.select(); |
| try { document.execCommand('copy'); } catch (e) {} |
| flash(); |
| }; |
| try { |
| navigator.clipboard.writeText(el.value).then(flash).catch(fallback); |
| } catch (e) { fallback(); } |
| } |
| """ |
|
|
| QUICK_PLACEHOLDER = ( |
| "Kira 18 (player total)\n" |
| "Torvin +2 (roll d20+2 for them)\n" |
| "Goblin x3 +2 (three goblins, rolled separately)\n" |
| "Shadow (bare name — rolls a d20)" |
| ) |
|
|
|
|
| def _dropdown_choices(state): |
| return [ |
| (f"{e['name']} ({tool._fmt_init(e['initiative'])})", e["id"]) |
| for e in state["entries"] |
| ] |
|
|
|
|
| def _views(state, selected=None): |
| """Rendered outputs shared by every event: list, banner, dropdown, copy.""" |
| valid = {e["id"] for e in state["entries"]} |
| value = selected if selected in valid else None |
| return ( |
| tool.render_list(state), |
| tool.render_banner(state), |
| gr.update(choices=_dropdown_choices(state), value=value), |
| tool.export_text(state), |
| ) |
|
|
|
|
| def on_load(saved): |
| state = tool.coerce_state(saved) |
| return (state, *_views(state)) |
|
|
|
|
| def on_add(state, text, roll_blank): |
| state, messages = tool.quick_add(state, text, roll_blank) |
| return ( |
| state, state, |
| gr.update(value=""), |
| tool.render_roll_log(messages), |
| *_views(state), |
| ) |
|
|
|
|
| def on_next(state): |
| state = tool.next_turn(state) |
| return (state, state, *_views(state)) |
|
|
|
|
| def on_prev(state): |
| state = tool.prev_turn(state) |
| return (state, state, *_views(state)) |
|
|
|
|
| def on_reset(state): |
| state = tool.reset_rounds(state) |
| return (state, state, *_views(state)) |
|
|
|
|
| def on_clear(state): |
| state = tool.clear_all() |
| return (state, state, "", *_views(state)) |
|
|
|
|
| def on_move_up(state, eid): |
| if eid is not None: |
| state = tool.move_up(state, eid) |
| return (state, state, *_views(state, selected=eid)) |
|
|
|
|
| def on_move_down(state, eid): |
| if eid is not None: |
| state = tool.move_down(state, eid) |
| return (state, state, *_views(state, selected=eid)) |
|
|
|
|
| def on_remove(state, eid): |
| if eid is not None: |
| state = tool.remove_entry(state, eid) |
| return (state, state, *_views(state)) |
|
|
|
|
| _initial = tool.new_state() |
|
|
| with gr.Blocks(title=CONFIG.title) as demo: |
| gr.HTML(hero_html(CONFIG)) |
| gr.HTML(cta_html(CONFIG, CONFIG.hero_cta)) |
|
|
| state = gr.State(_initial) |
| browser_state = gr.BrowserState( |
| _initial, storage_key="lf_initiative_tracker_v1" |
| ) |
|
|
| with gr.Row(equal_height=False): |
| with gr.Column(scale=5): |
| quick_box = gr.Textbox( |
| label="Quick add — one combatant per line", |
| lines=5, |
| placeholder=QUICK_PLACEHOLDER, |
| ) |
| roll_blank = gr.Checkbox( |
| value=True, label="Roll d20 for names without a number" |
| ) |
| add_btn = gr.Button( |
| "Add to Order", variant="primary", elem_classes=["lf-generate"] |
| ) |
| roll_log = gr.HTML("") |
| entry_dd = gr.Dropdown( |
| choices=[], value=None, label="Manage a combatant", |
| info="Pick an entry, then move it for tie-breaks or remove it.", |
| ) |
| with gr.Row(): |
| up_btn = gr.Button("Move Up") |
| down_btn = gr.Button("Move Down") |
| remove_btn = gr.Button("Remove") |
|
|
| with gr.Column(scale=6): |
| banner = gr.HTML(tool.render_banner(_initial)) |
| order_html = gr.HTML(tool.render_list(_initial)) |
| with gr.Row(): |
| next_btn = gr.Button("Next ▶", variant="primary", scale=2, |
| elem_classes=["lf-generate"]) |
| prev_btn = gr.Button("◀ Back", scale=1) |
| with gr.Row(): |
| reset_btn = gr.Button("Reset Rounds") |
| clear_btn = gr.Button("Clear All") |
| copy_btn = gr.Button("Copy Order", elem_id="lf-copy-btn") |
| |
| |
| copy_src = gr.Textbox( |
| elem_id="lf-copy-src", elem_classes=["lf-offscreen"], |
| container=False, show_label=False, |
| ) |
|
|
| gr.HTML(cta_html(CONFIG, CONFIG.post_tool_cta)) |
|
|
| gr.HTML(section_html("Get Your Turn Order in Seconds", content.HOW_TO_HTML)) |
| gr.HTML(section_html("Why This DnD Initiative Tracker Is Turn Order Only", |
| content.WHY_HTML)) |
| gr.HTML(section_html("Worked Example", content.EXAMPLE_HTML)) |
| gr.HTML(section_html("Initiative Rules Refresher", content.RULES_HTML)) |
| gr.HTML(faq_html(content.FAQS)) |
| gr.HTML(related_html(CONFIG)) |
| gr.HTML(cta_html(CONFIG, CONFIG.footer_cta)) |
| gr.HTML(content.DISCLAIMER_HTML) |
|
|
| views = [order_html, banner, entry_dd, copy_src] |
|
|
| demo.load(on_load, [browser_state], [state] + views, |
| api_name=False, queue=False) |
|
|
| add_btn.click( |
| on_add, [state, quick_box, roll_blank], |
| [state, browser_state, quick_box, roll_log] + views, |
| api_name=False, queue=False, |
| ) |
| next_btn.click(on_next, [state], [state, browser_state] + views, |
| api_name=False, queue=False) |
| prev_btn.click(on_prev, [state], [state, browser_state] + views, |
| api_name=False, queue=False) |
| reset_btn.click(on_reset, [state], [state, browser_state] + views, |
| api_name=False, queue=False) |
| clear_btn.click(on_clear, [state], |
| [state, browser_state, roll_log] + views, |
| api_name=False, queue=False) |
| up_btn.click(on_move_up, [state, entry_dd], |
| [state, browser_state] + views, |
| api_name=False, queue=False) |
| down_btn.click(on_move_down, [state, entry_dd], |
| [state, browser_state] + views, |
| api_name=False, queue=False) |
| remove_btn.click(on_remove, [state, entry_dd], |
| [state, browser_state] + views, |
| api_name=False, queue=False) |
| copy_btn.click(None, None, None, js=COPY_JS, api_name=False, queue=False) |
|
|
|
|
| if __name__ == "__main__": |
| standard_launch(demo, CONFIG) |
|
|