# Critical Tables DB Model ## What the PDFs look like The PDFs are not one uniform table shape. I found three families: 1. Standard tables - Columns are severity-like keys such as `A` through `E`. - Rows are roll bands such as `01-05`, `66`, `96-99`, or `100`. - Examples: `Slash.pdf`, `Puncture.pdf`, `Arcane Aether.pdf`. 2. Variant-column tables - Columns are not severity letters; they are variant keys such as `normal`, `magic`, `mithril`, `holy arms`, `slaying`. - Rows are still roll bands. - Example: `Large Creature - Weapon.pdf`. 3. Grouped variant tables - There is an extra grouping axis above the column axis. - Example: `Large Creature - Magic.pdf` has: - group: `large`, `super_large` - column: `normal`, `slaying` - row: roll band There are also extraction constraints: - Most PDFs are text extractable with `pdftotext -layout`. - `Void.pdf` appears image-based and will need OCR or manual transcription. - A single cell can contain: - base description text - symbolic affixes such as `+5H - 2S - 3B` - conditional branches such as `with helmet`, `w/o leg greaves`, `if foe has shield` Because of that, the safest model is hybrid: - relational tables for lookup axes and indexed effects - raw text storage for fidelity - structured JSON for irregular branches that are hard to normalize perfectly on first pass ## Recommended logical model ### 1. `critical_table` One record per PDF/table, which is the primary "critical type" for lookup. Examples: - `slash` - `puncture` - `arcane_aether` - `large_creature_weapon` - `large_creature_magic` ### 2. `critical_group` Optional extra axis for tables that need more than type + column + roll. Examples: - `large` - `super_large` Most tables will have no group rows. ### 3. `critical_column` Generalized "severity/column" axis. Examples: - `A`, `B`, `C`, `D`, `E` - `normal`, `magic`, `mithril`, `holy_arms`, `slaying` Do not hardcode this as a single severity enum. Treat it as a table-defined dimension. ### 4. `critical_roll_band` Stores row bands and supports exact row lookup by roll. Examples: - `01-05` - `66` - `96-99` - `251+` Recommended fields: - `min_roll` - `max_roll` nullable for open-ended rows like `251+` - display label - sort order ### 5. `critical_result` One record per lookup cell: - table - optional group - column - roll band This stores: - `raw_cell_text` - `description_text` - `raw_affix_text` - `parsed_json` - parse status / source metadata ### 6. `critical_branch` Optional conditional branches inside a result cell. Examples: - `with helmet` - `without helmet` - `with leg greaves` - `if foe has shield` Each branch can carry: - `condition_text` - optional structured `condition_json` - branch description text - branch raw affix text - parsed JSON ### 7. `critical_effect` Normalized machine-readable effects parsed from the symbol line and, over time, from prose. Recommended canonical `effect_code` values: - `direct_hits` - `must_parry_rounds` - `no_parry_rounds` - `stunned_rounds` - `bleed_per_round` - `foe_penalty` - `attacker_bonus_next_round` - `initiative_gain` - `initiative_loss` - `drop_item` - `item_breakage_check` - `limb_useless` - `knockdown` - `prone` - `coma` - `paralyzed` - `blind` - `deaf` - `mute` - `dies_in_rounds` - `instant_death` - `armor_destroyed` - `weapon_stuck` Each effect should point to either: - the base `critical_result`, or - a `critical_branch` This lets you keep the raw text but still filter/query on effects. ## Why this works for your lookup Your lookup target is mostly: - `critical type` - `severity(column)` - `roll` That maps cleanly to: - `critical_table.slug` - `critical_column.column_key` - numeric roll matched against `critical_roll_band` For the outlier tables, add an optional `group_key`. That means the API can still stay simple: ```json { "critical_type": "slash", "column": "C", "roll": 38, "group": null } ``` or: ```json { "critical_type": "large_creature_magic", "group": "super_large", "column": "slaying", "roll": 88 } ``` ## Example return object This is the shape I would return from a lookup: ```json { "critical_type": "slash", "table_name": "Slash Critical Strike Table", "group": null, "column": { "key": "B", "label": "B", "role": "severity" }, "roll": { "input": 38, "band": "36-45", "min": 36, "max": 45 }, "description": "Strike foe in shin.", "raw_affix_text": "+2H - must_parry", "affixes": [ { "effect_code": "direct_hits", "value": 2 } ], "conditions": [ { "when": "with leg greaves", "description": null, "raw_affix_text": "+2H - must_parry", "affixes": [ { "effect_code": "direct_hits", "value": 2 }, { "effect_code": "must_parry_rounds", "value": 1 } ] }, { "when": "without leg greaves", "description": "You slash open foe's shin.", "raw_affix_text": "+2H - bleed", "affixes": [ { "effect_code": "direct_hits", "value": 2 }, { "effect_code": "bleed_per_round", "value": 1 } ] } ], "raw_text": "Original full cell text as extracted from the PDF", "source": { "pdf": "Slash.pdf", "page": 1, "extraction_method": "text" } } ``` ## Ingestion notes Recommended import flow: 1. Create `critical_table`, `critical_group`, `critical_column`, and `critical_roll_band` from each PDF's visible axes. 2. Store each cell in `critical_result.raw_cell_text` exactly as extracted. 3. Parse the symbol line into `critical_effect`. 4. Split explicit conditional branches into `critical_branch`. 5. Gradually enrich prose-derived effects such as death, blindness, paralysis, limb loss, initiative changes, and item breakage. 6. Route image PDFs like `Void.pdf` through OCR before the same parser. The important design decision is: never throw away the original text. The prose is too irregular to rely on normalized fields alone.