Structure map: DeckCraft → TcgEngine (Unity)
LIVING DOCUMENT — update on every model change. Goal: if DeckCraft games are ever exported into the purchased TcgEngine template as a digital playtest client before printing, this map says what each DeckCraft structure becomes on their side, what adapts with a mapping table, and what would need engine surgery. Companion to the TcgEngine study.
Purpose & the honest verdict
TcgEngine is a Hearthstone-shaped engine: fixed turn structure (draw → main → end), fixed resources (hp + ramping mana), fixed piles, combat between board creatures. DeckCraft is genre-generic. Consequence: games that fit the Hearthstone shape export cleanly; deck-builders and market games need C# work in the template first (their sanctioned extension path is subclassing EffectData/ConditionData and editing GameLogic.cs — the docs say so explicitly). The map below is the itemized version of that sentence. Second consequence: the template would be a client, not a second referee — its engine is not ours, so balance data still comes from our simulator; the export is for humans to FEEL the game.
The map — structure by structure
| DeckCraft structure | TcgEngine counterpart | Fit | Notes |
|---|---|---|---|
card (name, description, type, cost, attributes) |
CardData asset (title, desc, type, mana, attack/hp) |
adapt | Their type is a closed enum (Hero/Character/Spell/Artifact/Secret/Equipment) — needs a per-game mapping table (unit→Character, event→Spell, structure→Artifact). attack/hp only exist as two fixed stats; other numeric attributes become TraitStats. |
tags[] / families (+ all_tags view) |
TraitData assets + TeamData (one axis) |
clean | One TraitData asset per distinct tag; a family axis can ride TeamData (gets UI color treatment for free). |
Free-form attributes + counters |
TraitStat (trait + int) / EffectSetStatCustom |
clean | Identical concept. ƒx-computed attributes must be baked to numbers at export (their side has no expression evaluator) — or dropped with a warning. |
Card cost (multi-currency ƒx) |
mana (single int) |
adapt | Single-currency games map the currency to mana. Multi-currency (Ascension-style) has no home — export the primary, warn on the rest. |
Effects: trigger presetson_play / at_start_of_turn / at_end_of_turn / on_destroy / activated |
AbilityTrigger:OnPlay / StartOfTurn / EndOfTurn / OnDeath / Activate |
clean | 1:1. Our activation.cost→mana_cost (single currency), exhaust→exhaust. |
| Pattern triggers (event bus: card_moved, tracker_adjusted, action_performed…) | OnPlayOther / OnDeathOther + Secrets | adapt | Only play/death of other cards exist as triggers on their side; tracker-adjusted or zone-movement subscriptions need new AbilityTrigger enum values (surgery) or approximation. |
Effect subject (self/opponent/each_opponent/all_players) + amounts |
AbilityTarget (PlayerSelf/PlayerOpponent/AllPlayers) + value |
clean | each_opponent ≡ PlayerOpponent in 1v1. Amounts must be literal ints (bake ƒx). |
| Effect mechanics (draw/damage/heal/gain/discard/adjust-attribute/apply-status/remove-status/create-card) | Effect* primitives (EffectDraw, EffectDamage, EffectHeal, EffectAddStat, EffectAddTrait, status[] on abilities, EffectSummon/Create) | clean | Their 30-primitive catalog covers ours. gain_resource maps to EffectMana only if the tracker mapped to mana. |
| Selection specs (chooser/count/filter) + selectors (random/highest/lowest) | SelectTarget / CardSelector + conditions + FilterRandom/HighestStat/LowestStat | clean | Their filters are the model we copied (U6). Player-chosen = SelectTarget; automatic = filters. |
Named conditions (tracker_compare, count_cards_compare, card_has_tag, zone_empty) |
ConditionData assets (ConditionPlayerStat, ConditionCount, ConditionCardType, ConditionCardPile) | clean | Preset-for-preset — their catalog seeded ours (U1). One asset per named condition, referenced by every ability that uses it: same single-source pattern. |
Card sets / has_set |
— | surgery | No same-field grouping concept; would be a custom ConditionData subclass (~50 lines of C#, their sanctioned path). |
Bindings requires: |
conditions_trigger on abilities |
adapt | Their conditions gate ABILITIES, not verbs; per-verb legality gates need GameLogic edits. |
Effect condition: |
conditions_trigger on the ability |
clean | Same semantics: ability silently doesn't fire. |
| Statuses (dynamic behaviours, value + duration) | StatusData + CardStatus (value + duration) |
adapt | Same runtime model (we copied it) — but their StatusType is a CLOSED enum with hardcoded semantics. Behaviours matching their enum (Taunt↔Outpost-ish, Stealth, Armor…) map; custom behaviours need enum + GameLogic additions. |
| Modifiers (buy_cost / draw_amount / damage_dealt / attribute:* / forbid) | Ongoing abilities (EffectAddStat ongoing, AddManaCost status) | adapt | Stat auras map cleanly. buy_cost→AddManaCost. action_legality forbid has no counterpart (surgery). |
| Trackers (arbitrary, per-player/shared, regen rules) | hp + mana only | surgery | THE structural gap. Two trackers map (life→hp, main resource→mana with their ramp OR a custom refill edit). Every further tracker (Dominion's actions/buys, VP) needs new Player fields + UI. |
| Zones (authored, per-player/shared, refills, markets) | Fixed piles: deck/hand/board/discard/secret/equip/temp | surgery | Standard piles map by role bindings. Shared market rows, token piles, refills: no counterpart — new List<Card> + UI per zone. |
| Custom actions (step compositions) + action bindings | — (fixed verb set: play / attack / activate / move) | surgery | The biggest gap. Jaipur-style verbs (take/trade/sell) don't exist; each would be a new GameAction + server handler + UI affordance. Simple ones could be faked as activated abilities on a hero. |
| Phases / turn structure (authored, automatic phases, step queue) | Fixed StartTurn → Main → EndTurn | adapt | Games with one player-driven phase fit. Multi-phase turns (action/buy/cleanup) collapse into Main or need surgery. |
| Endings + scoring (JSON Logic, sum_cards metrics, turn-limit borrow) | hp ≤ 0 (or deck+hand+board empty) | adapt | Combat games map. Scoring/emptiness endings need a CheckForWinner edit (small, contained). |
| Decks, copies (ƒx), starters | DeckData assets (+ GameplayData deck lists) |
clean | Bake ƒx copies at export per player count. |
| Setup spec (fill_zone, opening deals) | Hardcoded StartGame() (draw 5, coin) | adapt | Hand size/start values are GameplayData fields; anything richer is surgery. |
| Bots (greedy/random, deterministic replays) | Minimax + heuristic (AILogic) | clean | Their AI plays whatever the data says — free opponent for playtests. Determinism/replay parity is NOT preserved (different engine); balance stays our simulator's job. |
| Card art (cards/ renders) | art_full / art_board sprites |
clean | Our generated images drop in as sprites — the visual payoff of the whole idea. |
Export pipeline sketch (when the day comes)
TcgEngine loads everything from Resources/ ScriptableObject
assets (DataLoader + per-type Load() calls). Two
viable routes: (a) generate Unity .asset
YAML files directly from a game-def (they are plain text; GUID wiring is
the fiddly part), or (b) a small Unity editor script that
reads our deckcraft-game/1 JSON and instantiates the
ScriptableObjects — cleaner, keeps GUIDs Unity's problem. Either way the
exporter's input is exportGameDefinition() output, and ƒx
values are baked at a chosen player count. Start with a
Hearthstone-shaped probe game to keep the surgery list empty.
Maintenance rule
Every model change that ships (new structure, new mechanic, new card field) adds or updates its row here — treat this page as a fifth column of the four-level working rule. Rows added so far track the model through: ƒx, subjects, decisions, triggers/bus, bindings, modifiers, scoring, custom actions, card-state, tags, sets+conditions (U1), statuses (U2), activated abilities (U3), and the U4–U9 batch (combat events, choose-one, selectors, event-card targeting, runtime tokens, dice).