Skill tree: subtrees, loadout slots, label fix

Skill trees now render the way the source does: each class has three named
subtrees (e.g. Swordmaster: The Blade / The Will / The Way), each with its
own 3-col or 5-col grid, sized in 72px cells. Extractor parses subtrees
separately so the per-tree row/col coordinates are correct (previously all
22 nodes were stacked on one combined grid and overlapped). Connector
edges are mapped per-subtree too.

Loadout: new global 3-ability + 3-technique slot row at the bottom of the
Skill Trees panel. The cap is global across all 5 classes (matches the
source HTML which has `id=active-Ability-N` / `id=active-Technique-N`
without per-tree scope). Click a slot to pick from any allocated Ability
or Spice (for Ability slots) or any allocated Perk (for Technique slots);
right-click clears. Slot backgrounds use the local ability.png /
technique.png artwork copied into /icons.

Label overlap fix: constrained the name label under each node to the node
width (72px) and bumped the vertical gap from 44 to 60px so 2-3 line names
have room without bleeding into the row below.

Existing saved builds migrate cleanly — loadout normalizes to length-3
slot arrays if absent or malformed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-05-23 08:21:09 -04:00
parent 5b3ccf630d
commit f142725dd8
15 changed files with 2802 additions and 1835 deletions

View file

@ -16,36 +16,61 @@
"id": "benegesserit", "id": "benegesserit",
"name": "Bene Gesserit", "name": "Bene Gesserit",
"file": "skills-benegesserit.json", "file": "skills-benegesserit.json",
"subtrees": [
"Weirding Way",
"The Voice",
"Body Control"
],
"nodes": 22, "nodes": 22,
"edges": 23 "edges": 29
}, },
{ {
"id": "mentat", "id": "mentat",
"name": "Mentat", "name": "Mentat",
"file": "skills-mentat.json", "file": "skills-mentat.json",
"subtrees": [
"Mental Calculus",
"Assassination",
"Tactician"
],
"nodes": 22, "nodes": 22,
"edges": 22 "edges": 28
}, },
{ {
"id": "planetologist", "id": "planetologist",
"name": "Planetologist", "name": "Planetologist",
"file": "skills-planetologist.json", "file": "skills-planetologist.json",
"subtrees": [
"Scientist",
"Explorer",
"Mechanic"
],
"nodes": 20, "nodes": 20,
"edges": 10 "edges": 26
}, },
{ {
"id": "swordmaster", "id": "swordmaster",
"name": "Swordmaster", "name": "Swordmaster",
"file": "skills-swordmaster.json", "file": "skills-swordmaster.json",
"subtrees": [
"The Blade",
"The Will",
"The Way"
],
"nodes": 22, "nodes": 22,
"edges": 22 "edges": 28
}, },
{ {
"id": "trooper", "id": "trooper",
"name": "Trooper", "name": "Trooper",
"file": "skills-trooper.json", "file": "skills-trooper.json",
"subtrees": [
"Gunnery",
"Suspensor Training",
"Tactical Tech"
],
"nodes": 22, "nodes": 22,
"edges": 22 "edges": 28
} }
], ],
"icons": { "icons": {

View file

@ -1,342 +1,388 @@
{ {
"id": "benegesserit", "id": "benegesserit",
"name": "Bene Gesserit", "name": "Bene Gesserit",
"nodes": [ "subtrees": [
{ {
"tag": "Skills.Spice.BinduDodge", "name": "Weirding Way",
"id": "BinduDodge", "cols": 3,
"name": "Bindu Dodge", "nodes": [
"kind": "Spice", {
"row": 1, "tag": "Skills.Spice.BinduDodge",
"col": 2, "id": "BinduDodge",
"maxPoints": 1, "name": "Bindu Dodge",
"icon": "t_ui_iconskilltreebindudodge_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-spice-bindudodge" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreebindudodge_d.webp",
"url": "https://dune.gaming.tools/skills/skills-spice-bindudodge"
},
{
"tag": "Skills.Ability.BinduNerveStrike",
"id": "BinduNerveStrike",
"name": "Prana-Bindu Strikes",
"kind": "Ability",
"row": 2,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconabilitybindunervestrike_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-bindunervestrike"
},
{
"tag": "Skills.Ability.WeirdingStep",
"id": "WeirdingStep",
"name": "Weirding Step",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilityweirdingstep_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-weirdingstep"
},
{
"tag": "Skills.Attribute.WeirdingWay2",
"id": "WeirdingWay2",
"name": "Short Blade Damage",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway2"
},
{
"tag": "Skills.Perk.Backstabber",
"id": "Backstabber",
"name": "Manipulate Instability",
"kind": "Perk",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkbackstabber_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-backstabber"
},
{
"tag": "Skills.Attribute.WeirdingWay1",
"id": "WeirdingWay1",
"name": "Blade Damage",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway1"
},
{
"tag": "Skills.Ability.Hypersprint",
"id": "Hypersprint",
"name": "Bindu Sprint",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconabilitydash_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-hypersprint"
}
],
"edges": [
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Spice.BinduDodge"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Spice.BinduDodge"
},
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Attribute.WeirdingWay1"
},
{
"from": "Skills.Attribute.WeirdingWay2",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Attribute.WeirdingWay1",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.Hypersprint",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Attribute.WeirdingWay1",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Ability.Hypersprint",
"to": "Skills.Attribute.WeirdingWay1"
}
]
}, },
{ {
"tag": "Skills.Ability.BinduNerveStrike", "name": "The Voice",
"id": "BinduNerveStrike", "cols": 3,
"name": "Prana-Bindu Strikes", "nodes": [
"kind": "Ability", {
"row": 2, "tag": "Skills.Spice.VoiceSplash",
"col": 1, "id": "VoiceSplash",
"maxPoints": 1, "name": "Screech",
"icon": "t_ui_iconabilitybindunervestrike_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-ability-bindunervestrike" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreescreech_d.webp",
"url": "https://dune.gaming.tools/skills/skills-spice-voicesplash"
},
{
"tag": "Skills.Perk.VoiceAnalysis",
"id": "VoiceAnalysis",
"name": "Rapid Register",
"kind": "Perk",
"row": 2,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconskilltreevoiceanalysis_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-voiceanalysis"
},
{
"tag": "Skills.Ability.VoiceStop",
"id": "VoiceStop",
"name": "Stop",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilityvoicestop_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-voicestop"
},
{
"tag": "Skills.Ability.Blindspot",
"id": "Blindspot",
"name": "Ignore",
"kind": "Ability",
"row": 4,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconabilityblindspot_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-blindspot"
},
{
"tag": "Skills.Attribute.Manipulation1",
"id": "Manipulation1",
"name": "Voice Training",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreebenegesseritcooldown_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-manipulation1"
},
{
"tag": "Skills.Ability.VoiceCompel",
"id": "VoiceCompel",
"name": "Compel",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconabilitythevoicecompel_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-voicecompel"
}
],
"edges": [
{
"from": "Skills.Perk.VoiceAnalysis",
"to": "Skills.Spice.VoiceSplash"
},
{
"from": "Skills.Ability.VoiceStop",
"to": "Skills.Spice.VoiceSplash"
},
{
"from": "Skills.Ability.Blindspot",
"to": "Skills.Perk.VoiceAnalysis"
},
{
"from": "Skills.Ability.VoiceStop",
"to": "Skills.Attribute.Manipulation1"
},
{
"from": "Skills.Ability.Blindspot",
"to": "Skills.Ability.VoiceCompel"
},
{
"from": "Skills.Ability.VoiceCompel",
"to": "Skills.Attribute.Manipulation1"
}
]
}, },
{ {
"tag": "Skills.Ability.WeirdingStep", "name": "Body Control",
"id": "WeirdingStep", "cols": 5,
"name": "Weirding Step", "nodes": [
"kind": "Ability", {
"row": 2, "tag": "Skills.Ability.LitanyAgainstFear",
"col": 3, "id": "LitanyAgainstFear",
"maxPoints": 1, "name": "Litany Against Fear",
"icon": "t_ui_iconabilityweirdingstep_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-ability-weirdingstep" "row": 1,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Attribute.WeirdingWay2", "icon": "t_ui_iconabilitylitanyagainstfear_d.webp",
"id": "WeirdingWay2", "url": "https://dune.gaming.tools/skills/skills-ability-litanyagainstfear"
"name": "Short Blade Damage", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Perk.BinduStability",
"col": 2, "id": "BinduStability",
"maxPoints": 3, "name": "Prana-Bindu Stability",
"icon": "t_ui_iconskilltreeskillbrawler_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway2" "row": 2,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Perk.Backstabber", "icon": "t_ui_iconskilltreebindustability_d.webp",
"id": "Backstabber", "url": "https://dune.gaming.tools/skills/skills-perk-bindustability"
"name": "Manipulate Instability", },
"kind": "Perk", {
"row": 4, "tag": "Skills.Perk.MetabolizePoison",
"col": 1, "id": "MetabolizePoison",
"maxPoints": 3, "name": "Metabolize Poison",
"icon": "t_ui_iconskilltreeperkbackstabber_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-perk-backstabber" "row": 2,
}, "col": 4,
{ "maxPoints": 1,
"tag": "Skills.Attribute.WeirdingWay1", "icon": "t_ui_iconskilltreemetabolizeposion_d.webp",
"id": "WeirdingWay1", "url": "https://dune.gaming.tools/skills/skills-perk-metabolizepoison"
"name": "Blade Damage", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Attribute.SelfControl3",
"col": 3, "id": "SelfControl3",
"maxPoints": 3, "name": "Vitality",
"icon": "t_ui_iconskilltreeskillbrawler_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway1" "row": 3,
}, "col": 1,
{ "maxPoints": 3,
"tag": "Skills.Ability.Hypersprint", "icon": "t_ui_iconskilltreeattributemaxhpbonus_d.webp",
"id": "Hypersprint", "url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol3"
"name": "Bindu Sprint", },
"kind": "Ability", {
"row": 5, "tag": "Skills.Attribute.SelfControl4",
"col": 2, "id": "SelfControl4",
"maxPoints": 3, "name": "Self-Healing",
"icon": "t_ui_iconabilitydash_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-ability-hypersprint" "row": 3,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Spice.VoiceSplash", "icon": "t_ui_iconskilltreeskillmaxhealth_d.webp",
"id": "VoiceSplash", "url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol4"
"name": "Screech", },
"kind": "Spice", {
"row": 1, "tag": "Skills.Attribute.SelfControl5",
"col": 2, "id": "SelfControl5",
"maxPoints": 1, "name": "Poison Tolerance",
"icon": "t_ui_iconskilltreescreech_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-spice-voicesplash" "row": 3,
}, "col": 5,
{ "maxPoints": 3,
"tag": "Skills.Perk.VoiceAnalysis", "icon": "t_ui_iconskilltreeattributepoisondefense_d.webp",
"id": "VoiceAnalysis", "url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol5"
"name": "Rapid Register", },
"kind": "Perk", {
"row": 2, "tag": "Skills.Perk.RegenCap",
"col": 1, "id": "RegenCap",
"maxPoints": 1, "name": "Trauma Recovery",
"icon": "t_ui_iconskilltreevoiceanalysis_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-perk-voiceanalysis" "row": 4,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Ability.VoiceStop", "icon": "t_ui_iconskilltreeperkhealingfactor_d.webp",
"id": "VoiceStop", "url": "https://dune.gaming.tools/skills/skills-perk-regencap"
"name": "Stop", },
"kind": "Ability", {
"row": 2, "tag": "Skills.Attribute.SelfControl2",
"col": 3, "id": "SelfControl2",
"maxPoints": 1, "name": "Sun Tolerance",
"icon": "t_ui_iconabilityvoicestop_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-ability-voicestop" "row": 4,
}, "col": 4,
{ "maxPoints": 3,
"tag": "Skills.Ability.Blindspot", "icon": "t_ui_iconskilltreeattributesundefense_d.webp",
"id": "Blindspot", "url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol2"
"name": "Ignore", },
"kind": "Ability", {
"row": 4, "tag": "Skills.Attribute.SelfControl1",
"col": 1, "id": "SelfControl1",
"maxPoints": 1, "name": "Recovery",
"icon": "t_ui_iconabilityblindspot_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-ability-blindspot" "row": 5,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Manipulation1", "icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
"id": "Manipulation1", "url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol1"
"name": "Voice Training", }
"kind": "Attribute", ],
"row": 4, "edges": [
"col": 3, {
"maxPoints": 3, "from": "Skills.Ability.LitanyAgainstFear",
"icon": "t_ui_iconskilltreebenegesseritcooldown_d.webp", "to": "Skills.Perk.BinduStability"
"url": "https://dune.gaming.tools/skills/skills-attribute-manipulation1" },
}, {
{ "from": "Skills.Ability.LitanyAgainstFear",
"tag": "Skills.Ability.VoiceCompel", "to": "Skills.Perk.MetabolizePoison"
"id": "VoiceCompel", },
"name": "Compel", {
"kind": "Ability", "from": "Skills.Attribute.SelfControl3",
"row": 5, "to": "Skills.Perk.BinduStability"
"col": 2, },
"maxPoints": 1, {
"icon": "t_ui_iconabilitythevoicecompel_d.webp", "from": "Skills.Attribute.SelfControl4",
"url": "https://dune.gaming.tools/skills/skills-ability-voicecompel" "to": "Skills.Perk.BinduStability"
}, },
{ {
"tag": "Skills.Ability.LitanyAgainstFear", "from": "Skills.Attribute.SelfControl4",
"id": "LitanyAgainstFear", "to": "Skills.Perk.MetabolizePoison"
"name": "Litany Against Fear", },
"kind": "Ability", {
"row": 1, "from": "Skills.Attribute.SelfControl5",
"col": 3, "to": "Skills.Perk.MetabolizePoison"
"maxPoints": 3, },
"icon": "t_ui_iconabilitylitanyagainstfear_d.webp", {
"url": "https://dune.gaming.tools/skills/skills-ability-litanyagainstfear" "from": "Skills.Attribute.SelfControl3",
}, "to": "Skills.Perk.RegenCap"
{ },
"tag": "Skills.Perk.BinduStability", {
"id": "BinduStability", "from": "Skills.Attribute.SelfControl4",
"name": "Prana-Bindu Stability", "to": "Skills.Perk.RegenCap"
"kind": "Perk", },
"row": 2, {
"col": 2, "from": "Skills.Attribute.SelfControl2",
"maxPoints": 3, "to": "Skills.Attribute.SelfControl4"
"icon": "t_ui_iconskilltreebindustability_d.webp", },
"url": "https://dune.gaming.tools/skills/skills-perk-bindustability" {
}, "from": "Skills.Attribute.SelfControl2",
{ "to": "Skills.Attribute.SelfControl5"
"tag": "Skills.Perk.MetabolizePoison", },
"id": "MetabolizePoison", {
"name": "Metabolize Poison", "from": "Skills.Attribute.SelfControl1",
"kind": "Perk", "to": "Skills.Perk.RegenCap"
"row": 2, },
"col": 4, {
"maxPoints": 1, "from": "Skills.Attribute.SelfControl1",
"icon": "t_ui_iconskilltreemetabolizeposion_d.webp", "to": "Skills.Attribute.SelfControl2"
"url": "https://dune.gaming.tools/skills/skills-perk-metabolizepoison" }
}, ]
{
"tag": "Skills.Attribute.SelfControl3",
"id": "SelfControl3",
"name": "Vitality",
"kind": "Attribute",
"row": 3,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributemaxhpbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol3"
},
{
"tag": "Skills.Attribute.SelfControl4",
"id": "SelfControl4",
"name": "Self-Healing",
"kind": "Attribute",
"row": 3,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillmaxhealth_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol4"
},
{
"tag": "Skills.Attribute.SelfControl5",
"id": "SelfControl5",
"name": "Poison Tolerance",
"kind": "Attribute",
"row": 3,
"col": 5,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributepoisondefense_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol5"
},
{
"tag": "Skills.Perk.RegenCap",
"id": "RegenCap",
"name": "Trauma Recovery",
"kind": "Perk",
"row": 4,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkhealingfactor_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-regencap"
},
{
"tag": "Skills.Attribute.SelfControl2",
"id": "SelfControl2",
"name": "Sun Tolerance",
"kind": "Attribute",
"row": 4,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributesundefense_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol2"
},
{
"tag": "Skills.Attribute.SelfControl1",
"id": "SelfControl1",
"name": "Recovery",
"kind": "Attribute",
"row": 5,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol1"
}
],
"edges": [
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Spice.BinduDodge"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Spice.BinduDodge"
},
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.BinduNerveStrike",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.WeirdingStep",
"to": "Skills.Attribute.WeirdingWay1"
},
{
"from": "Skills.Attribute.WeirdingWay2",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Attribute.WeirdingWay1",
"to": "Skills.Attribute.WeirdingWay2"
},
{
"from": "Skills.Ability.Hypersprint",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Attribute.WeirdingWay1",
"to": "Skills.Perk.Backstabber"
},
{
"from": "Skills.Ability.Hypersprint",
"to": "Skills.Attribute.WeirdingWay1"
},
{
"from": "Skills.Ability.LitanyAgainstFear",
"to": "Skills.Perk.BinduStability"
},
{
"from": "Skills.Ability.LitanyAgainstFear",
"to": "Skills.Perk.MetabolizePoison"
},
{
"from": "Skills.Attribute.SelfControl3",
"to": "Skills.Perk.BinduStability"
},
{
"from": "Skills.Attribute.SelfControl4",
"to": "Skills.Perk.BinduStability"
},
{
"from": "Skills.Attribute.SelfControl4",
"to": "Skills.Perk.MetabolizePoison"
},
{
"from": "Skills.Attribute.SelfControl5",
"to": "Skills.Perk.MetabolizePoison"
},
{
"from": "Skills.Attribute.SelfControl3",
"to": "Skills.Perk.RegenCap"
},
{
"from": "Skills.Attribute.SelfControl4",
"to": "Skills.Perk.RegenCap"
},
{
"from": "Skills.Attribute.SelfControl2",
"to": "Skills.Attribute.SelfControl4"
},
{
"from": "Skills.Attribute.SelfControl2",
"to": "Skills.Attribute.SelfControl5"
},
{
"from": "Skills.Attribute.SelfControl1",
"to": "Skills.Perk.RegenCap"
},
{
"from": "Skills.Attribute.SelfControl1",
"to": "Skills.Attribute.SelfControl2"
} }
] ]
} }

View file

@ -1,338 +1,384 @@
{ {
"id": "mentat", "id": "mentat",
"name": "Mentat", "name": "Mentat",
"nodes": [ "subtrees": [
{ {
"tag": "Skills.Perk.ShieldWeakpoint", "name": "Mental Calculus",
"id": "ShieldWeakpoint", "cols": 5,
"name": "Shield Overcharge", "nodes": [
"kind": "Perk", {
"row": 1, "tag": "Skills.Perk.ShieldWeakpoint",
"col": 3, "id": "ShieldWeakpoint",
"maxPoints": 1, "name": "Shield Overcharge",
"icon": "t_ui_iconskilltreeshieldovercharge_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-perk-shieldweakpoint" "row": 1,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconskilltreeshieldovercharge_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-shieldweakpoint"
},
{
"tag": "Skills.Perk.ExploitWeakness",
"id": "ExploitWeakness",
"name": "Exploit Weakness",
"kind": "Perk",
"row": 2,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreespiceeffectexploitweakness_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-exploitweakness"
},
{
"tag": "Skills.Attribute.MentalCalculus5",
"id": "MentalCalculus5",
"name": "Rifle Damage",
"kind": "Attribute",
"row": 2,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagebonusscattergun_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus5"
},
{
"tag": "Skills.Attribute.MentalCalculus3",
"id": "MentalCalculus3",
"name": "Tailoring",
"kind": "Attribute",
"row": 3,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus3"
},
{
"tag": "Skills.Perk.HeadShots",
"id": "HeadShots",
"name": "Marksman",
"kind": "Perk",
"row": 3,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkmarksman_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-headshots"
},
{
"tag": "Skills.Attribute.MentalCalculus4",
"id": "MentalCalculus4",
"name": "Pistol Damage",
"kind": "Attribute",
"row": 3,
"col": 5,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagebonusgun_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus4"
},
{
"tag": "Skills.Attribute.MentalCalculus1",
"id": "MentalCalculus1",
"name": "Garment Keeper",
"kind": "Attribute",
"row": 4,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributerepair_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus1"
},
{
"tag": "Skills.Attribute.MentalCalculus2",
"id": "MentalCalculus2",
"name": "Ranged Damage",
"kind": "Attribute",
"row": 4,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus2"
},
{
"tag": "Skills.Ability.TurretSeeker",
"id": "TurretSeeker",
"name": "The Sentinel",
"kind": "Ability",
"row": 5,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconabilityturretseeker_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-turretseeker"
}
],
"edges": [
{
"from": "Skills.Perk.ExploitWeakness",
"to": "Skills.Perk.ShieldWeakpoint"
},
{
"from": "Skills.Attribute.MentalCalculus5",
"to": "Skills.Perk.ShieldWeakpoint"
},
{
"from": "Skills.Attribute.MentalCalculus3",
"to": "Skills.Perk.ExploitWeakness"
},
{
"from": "Skills.Perk.ExploitWeakness",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus5",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus4",
"to": "Skills.Attribute.MentalCalculus5"
},
{
"from": "Skills.Attribute.MentalCalculus1",
"to": "Skills.Attribute.MentalCalculus3"
},
{
"from": "Skills.Attribute.MentalCalculus1",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus2",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus2",
"to": "Skills.Attribute.MentalCalculus4"
},
{
"from": "Skills.Ability.TurretSeeker",
"to": "Skills.Attribute.MentalCalculus1"
},
{
"from": "Skills.Ability.TurretSeeker",
"to": "Skills.Attribute.MentalCalculus2"
}
]
}, },
{ {
"tag": "Skills.Perk.ExploitWeakness", "name": "Assassination",
"id": "ExploitWeakness", "cols": 3,
"name": "Exploit Weakness", "nodes": [
"kind": "Perk", {
"row": 2, "tag": "Skills.Ability.HunterSeeker",
"col": 2, "id": "HunterSeeker",
"maxPoints": 1, "name": "Hunter-Seeker",
"icon": "t_ui_iconskilltreespiceeffectexploitweakness_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-perk-exploitweakness" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_icongadgethunterseeker_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-hunterseeker"
},
{
"tag": "Skills.Perk.PoisonTooth",
"id": "PoisonTooth",
"name": "Poison Tooth",
"kind": "Perk",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreepoisontooth_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-poisontooth"
},
{
"tag": "Skills.Ability.StunDart",
"id": "StunDart",
"name": "Stunner",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitystunnerdart_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-stundart"
},
{
"tag": "Skills.Attribute.Assassination2",
"id": "Assassination2",
"name": "Assassin&#39;s Shot",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination2"
},
{
"tag": "Skills.Ability.PoisonMine",
"id": "PoisonMine",
"name": "Poison Mine",
"kind": "Ability",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconabilitypoisonmine_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-poisonmine"
},
{
"tag": "Skills.Attribute.Assassination1",
"id": "Assassination1",
"name": "Headshot Damage",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributeheadshotbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination1"
},
{
"tag": "Skills.Ability.PoisonCapsuleLauncher",
"id": "PoisonCapsuleLauncher",
"name": "Poison Capsule",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_icongadgetpoisoncapsulelauncher_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-poisoncapsulelauncher"
}
],
"edges": [
{
"from": "Skills.Ability.HunterSeeker",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.HunterSeeker",
"to": "Skills.Ability.StunDart"
},
{
"from": "Skills.Attribute.Assassination2",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.PoisonMine",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.StunDart",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Ability.StunDart",
"to": "Skills.Attribute.Assassination1"
},
{
"from": "Skills.Ability.PoisonMine",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Attribute.Assassination1",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Ability.PoisonCapsuleLauncher",
"to": "Skills.Ability.PoisonMine"
},
{
"from": "Skills.Ability.PoisonCapsuleLauncher",
"to": "Skills.Attribute.Assassination1"
}
]
}, },
{ {
"tag": "Skills.Attribute.MentalCalculus5", "name": "Tactician",
"id": "MentalCalculus5", "cols": 3,
"name": "Rifle Damage", "nodes": [
"kind": "Attribute", {
"row": 2, "tag": "Skills.Ability.PortableGenerator",
"col": 4, "id": "PortableGenerator",
"maxPoints": 3, "name": "Source of Power",
"icon": "t_ui_iconskilltreeattributedamagebonusscattergun_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus5" "row": 1,
}, "col": 2,
{ "maxPoints": 1,
"tag": "Skills.Attribute.MentalCalculus3", "icon": "t_ui_iconabilityportablegenerator_d.webp",
"id": "MentalCalculus3", "url": "https://dune.gaming.tools/skills/skills-ability-portablegenerator"
"name": "Tailoring", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Ability.SuspensorMine_Reduction",
"col": 1, "id": "SuspensorMine_Reduction",
"maxPoints": 3, "name": "Anti-gravity Mine",
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus3" "row": 2,
}, "col": 1,
{ "maxPoints": 1,
"tag": "Skills.Perk.HeadShots", "icon": "t_ui_icongadgetreductionremotemine_d.webp",
"id": "HeadShots", "url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_reduction"
"name": "Marksman", },
"kind": "Perk", {
"row": 3, "tag": "Skills.Perk.IronWill",
"col": 3, "id": "IronWill",
"maxPoints": 3, "name": "Iron Will",
"icon": "t_ui_iconskilltreeperkmarksman_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-perk-headshots" "row": 2,
}, "col": 3,
{ "maxPoints": 1,
"tag": "Skills.Attribute.MentalCalculus4", "icon": "t_ui_iconskilltreeironwill_d.webp",
"id": "MentalCalculus4", "url": "https://dune.gaming.tools/skills/skills-perk-ironwill"
"name": "Pistol Damage", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Ability.SuspensorMine_Amplification",
"col": 5, "id": "SuspensorMine_Amplification",
"maxPoints": 3, "name": "Gravity Mine",
"icon": "t_ui_iconskilltreeattributedamagebonusgun_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus4" "row": 4,
}, "col": 1,
{ "maxPoints": 1,
"tag": "Skills.Attribute.MentalCalculus1", "icon": "t_ui_icongadgetamplificationremotemine_d.webp",
"id": "MentalCalculus1", "url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_amplification"
"name": "Garment Keeper", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Ability.SolidoDecoy",
"col": 2, "id": "SolidoDecoy",
"maxPoints": 3, "name": "Solido Decoy",
"icon": "t_ui_iconskilltreeattributerepair_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus1" "row": 4,
}, "col": 3,
{ "maxPoints": 1,
"tag": "Skills.Attribute.MentalCalculus2", "icon": "t_ui_iconabilitysolidodecoy_d.webp",
"id": "MentalCalculus2", "url": "https://dune.gaming.tools/skills/skills-ability-solidodecoy"
"name": "Ranged Damage", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Ability.SuspensorWall",
"col": 4, "id": "SuspensorWall",
"maxPoints": 3, "name": "Shield Wall",
"icon": "t_ui_iconskilltreeattributedamage_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus2" "row": 5,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Ability.TurretSeeker", "icon": "t_ui_iconabilitysuspensorwall_d.webp",
"id": "TurretSeeker", "url": "https://dune.gaming.tools/skills/skills-ability-suspensorwall"
"name": "The Sentinel", }
"kind": "Ability", ],
"row": 5, "edges": [
"col": 3, {
"maxPoints": 3, "from": "Skills.Ability.PortableGenerator",
"icon": "t_ui_iconabilityturretseeker_d.webp", "to": "Skills.Ability.SuspensorMine_Reduction"
"url": "https://dune.gaming.tools/skills/skills-ability-turretseeker" },
}, {
{ "from": "Skills.Ability.PortableGenerator",
"tag": "Skills.Ability.HunterSeeker", "to": "Skills.Perk.IronWill"
"id": "HunterSeeker", },
"name": "Hunter-Seeker", {
"kind": "Ability", "from": "Skills.Ability.SuspensorMine_Amplification",
"row": 1, "to": "Skills.Ability.SuspensorMine_Reduction"
"col": 2, },
"maxPoints": 1, {
"icon": "t_ui_icongadgethunterseeker_d.webp", "from": "Skills.Ability.SolidoDecoy",
"url": "https://dune.gaming.tools/skills/skills-ability-hunterseeker" "to": "Skills.Perk.IronWill"
}, },
{ {
"tag": "Skills.Perk.PoisonTooth", "from": "Skills.Ability.SuspensorMine_Amplification",
"id": "PoisonTooth", "to": "Skills.Ability.SuspensorWall"
"name": "Poison Tooth", },
"kind": "Perk", {
"row": 2, "from": "Skills.Ability.SolidoDecoy",
"col": 1, "to": "Skills.Ability.SuspensorWall"
"maxPoints": 3, }
"icon": "t_ui_iconskilltreepoisontooth_d.webp", ]
"url": "https://dune.gaming.tools/skills/skills-perk-poisontooth"
},
{
"tag": "Skills.Ability.StunDart",
"id": "StunDart",
"name": "Stunner",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitystunnerdart_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-stundart"
},
{
"tag": "Skills.Attribute.Assassination2",
"id": "Assassination2",
"name": "Assassin&#39;s Shot",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination2"
},
{
"tag": "Skills.Ability.PoisonMine",
"id": "PoisonMine",
"name": "Poison Mine",
"kind": "Ability",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconabilitypoisonmine_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-poisonmine"
},
{
"tag": "Skills.Attribute.Assassination1",
"id": "Assassination1",
"name": "Headshot Damage",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributeheadshotbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination1"
},
{
"tag": "Skills.Ability.PoisonCapsuleLauncher",
"id": "PoisonCapsuleLauncher",
"name": "Poison Capsule",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_icongadgetpoisoncapsulelauncher_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-poisoncapsulelauncher"
},
{
"tag": "Skills.Ability.PortableGenerator",
"id": "PortableGenerator",
"name": "Source of Power",
"kind": "Ability",
"row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconabilityportablegenerator_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-portablegenerator"
},
{
"tag": "Skills.Ability.SuspensorMine_Reduction",
"id": "SuspensorMine_Reduction",
"name": "Anti-gravity Mine",
"kind": "Ability",
"row": 2,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_icongadgetreductionremotemine_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_reduction"
},
{
"tag": "Skills.Perk.IronWill",
"id": "IronWill",
"name": "Iron Will",
"kind": "Perk",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconskilltreeironwill_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-ironwill"
},
{
"tag": "Skills.Ability.SuspensorMine_Amplification",
"id": "SuspensorMine_Amplification",
"name": "Gravity Mine",
"kind": "Ability",
"row": 4,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_icongadgetamplificationremotemine_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_amplification"
},
{
"tag": "Skills.Ability.SolidoDecoy",
"id": "SolidoDecoy",
"name": "Solido Decoy",
"kind": "Ability",
"row": 4,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitysolidodecoy_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-solidodecoy"
},
{
"tag": "Skills.Ability.SuspensorWall",
"id": "SuspensorWall",
"name": "Shield Wall",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconabilitysuspensorwall_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorwall"
}
],
"edges": [
{
"from": "Skills.Perk.ExploitWeakness",
"to": "Skills.Perk.ShieldWeakpoint"
},
{
"from": "Skills.Attribute.MentalCalculus5",
"to": "Skills.Perk.ShieldWeakpoint"
},
{
"from": "Skills.Attribute.MentalCalculus3",
"to": "Skills.Perk.ExploitWeakness"
},
{
"from": "Skills.Perk.ExploitWeakness",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus5",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus4",
"to": "Skills.Attribute.MentalCalculus5"
},
{
"from": "Skills.Attribute.MentalCalculus1",
"to": "Skills.Attribute.MentalCalculus3"
},
{
"from": "Skills.Attribute.MentalCalculus1",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus2",
"to": "Skills.Perk.HeadShots"
},
{
"from": "Skills.Attribute.MentalCalculus2",
"to": "Skills.Attribute.MentalCalculus4"
},
{
"from": "Skills.Ability.TurretSeeker",
"to": "Skills.Attribute.MentalCalculus1"
},
{
"from": "Skills.Ability.TurretSeeker",
"to": "Skills.Attribute.MentalCalculus2"
},
{
"from": "Skills.Ability.HunterSeeker",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.HunterSeeker",
"to": "Skills.Ability.StunDart"
},
{
"from": "Skills.Attribute.Assassination2",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.PoisonMine",
"to": "Skills.Perk.PoisonTooth"
},
{
"from": "Skills.Ability.StunDart",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Ability.StunDart",
"to": "Skills.Attribute.Assassination1"
},
{
"from": "Skills.Ability.PoisonMine",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Attribute.Assassination1",
"to": "Skills.Attribute.Assassination2"
},
{
"from": "Skills.Ability.PoisonCapsuleLauncher",
"to": "Skills.Ability.PoisonMine"
},
{
"from": "Skills.Ability.PoisonCapsuleLauncher",
"to": "Skills.Attribute.Assassination1"
} }
] ]
} }

View file

@ -1,268 +1,354 @@
{ {
"id": "planetologist", "id": "planetologist",
"name": "Planetologist", "name": "Planetologist",
"nodes": [ "subtrees": [
{ {
"tag": "Skills.Perk.BatteryExpert", "name": "Scientist",
"id": "BatteryExpert", "cols": 3,
"name": "Conservation of Energy", "nodes": [
"kind": "Perk", {
"row": 1, "tag": "Skills.Perk.BatteryExpert",
"col": 2, "id": "BatteryExpert",
"maxPoints": 3, "name": "Conservation of Energy",
"icon": "t_ui_iconskilltreeperkbatteryexpert_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-perk-batteryexpert" "row": 1,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkbatteryexpert_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-batteryexpert"
},
{
"tag": "Skills.Attribute.Scientist5",
"id": "Scientist5",
"name": "Compaction",
"kind": "Attribute",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributespiceyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist5"
},
{
"tag": "Skills.Science.m_PowerMax",
"id": "m_PowerMax",
"name": "Overcharge",
"kind": "Science",
"row": 2,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-science-m_powermax"
},
{
"tag": "Skills.Attribute.Scientist4",
"id": "Scientist4",
"name": "Deep Analysis",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist4"
},
{
"tag": "Skills.Attribute.Scientist2",
"id": "Scientist2",
"name": "Dew Gathering",
"kind": "Attribute",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributewatheryield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist2"
},
{
"tag": "Skills.Attribute.Scientist3",
"id": "Scientist3",
"name": "Rerouting",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist3"
},
{
"tag": "Skills.Attribute.Scientist1",
"id": "Scientist1",
"name": "Cutteray Mining",
"kind": "Attribute",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist1"
}
],
"edges": [
{
"from": "Skills.Attribute.Scientist5",
"to": "Skills.Perk.BatteryExpert"
},
{
"from": "Skills.Perk.BatteryExpert",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist4",
"to": "Skills.Attribute.Scientist5"
},
{
"from": "Skills.Attribute.Scientist2",
"to": "Skills.Attribute.Scientist5"
},
{
"from": "Skills.Attribute.Scientist4",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist3",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist2",
"to": "Skills.Attribute.Scientist4"
},
{
"from": "Skills.Attribute.Scientist3",
"to": "Skills.Attribute.Scientist4"
},
{
"from": "Skills.Attribute.Scientist1",
"to": "Skills.Attribute.Scientist2"
},
{
"from": "Skills.Attribute.Scientist1",
"to": "Skills.Attribute.Scientist3"
}
]
}, },
{ {
"tag": "Skills.Attribute.Scientist5", "name": "Explorer",
"id": "Scientist5", "cols": 3,
"name": "Compaction", "nodes": [
"kind": "Attribute", {
"row": 2, "tag": "Skills.Attribute.Explorer5",
"col": 1, "id": "Explorer5",
"maxPoints": 3, "name": "Spice Surveyor",
"icon": "t_ui_iconskilltreeattributespiceyield_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist5" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreeattributespice_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer5"
},
{
"tag": "Skills.Attribute.Explorer3",
"id": "Explorer3",
"name": "Scanner Mastery",
"kind": "Attribute",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributescanningbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer3"
},
{
"tag": "Skills.Attribute.Explorer4",
"id": "Explorer4",
"name": "Stillsuit Seals",
"kind": "Attribute",
"row": 2,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillhydration_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer4"
},
{
"tag": "Skills.Attribute.Explorer1",
"id": "Explorer1",
"name": "Cartographer",
"kind": "Attribute",
"row": 4,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconskilltreeskillobservation_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer1"
},
{
"tag": "Skills.Attribute.Explorer2",
"id": "Explorer2",
"name": "Mountaineer",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillclimber_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer2"
},
{
"tag": "Skills.Ability.SuspensorPad",
"id": "SuspensorPad",
"name": "Suspensor Pad",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconabilitysuspensorpad_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorpad"
}
],
"edges": [
{
"from": "Skills.Attribute.Explorer3",
"to": "Skills.Attribute.Explorer5"
},
{
"from": "Skills.Attribute.Explorer4",
"to": "Skills.Attribute.Explorer5"
},
{
"from": "Skills.Attribute.Explorer1",
"to": "Skills.Attribute.Explorer3"
},
{
"from": "Skills.Attribute.Explorer2",
"to": "Skills.Attribute.Explorer4"
},
{
"from": "Skills.Ability.SuspensorPad",
"to": "Skills.Attribute.Explorer1"
},
{
"from": "Skills.Ability.SuspensorPad",
"to": "Skills.Attribute.Explorer2"
}
]
}, },
{ {
"tag": "Skills.Science.m_PowerMax", "name": "Mechanic",
"id": "m_PowerMax", "cols": 3,
"name": "Overcharge", "nodes": [
"kind": "Science", {
"row": 2, "tag": "Skills.Spice.VehicleHeat",
"col": 3, "id": "VehicleHeat",
"maxPoints": 3, "name": "Heat Management",
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-science-m_powermax" "row": 1,
}, "col": 2,
{ "maxPoints": 1,
"tag": "Skills.Attribute.Scientist4", "icon": "t_ui_iconskilltreeheatmanagement_d.webp",
"id": "Scientist4", "url": "https://dune.gaming.tools/skills/skills-spice-vehicleheat"
"name": "Deep Analysis", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Attribute.Driver5",
"col": 2, "id": "Driver5",
"maxPoints": 3, "name": "Fuel Efficient Pilot",
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist4" "row": 2,
}, "col": 1,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Scientist2", "icon": "t_ui_iconskilltreeattributevehicle_d.webp",
"id": "Scientist2", "url": "https://dune.gaming.tools/skills/skills-attribute-driver5"
"name": "Dew Gathering", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Attribute.Driver6",
"col": 1, "id": "Driver6",
"maxPoints": 3, "name": "Sandcrawler Yield",
"icon": "t_ui_iconskilltreeattributewatheryield_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist2" "row": 2,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Scientist3", "icon": "t_ui_iconskilltreeattributespiceyield_d.webp",
"id": "Scientist3", "url": "https://dune.gaming.tools/skills/skills-attribute-driver6"
"name": "Rerouting", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Attribute.Driver4",
"col": 3, "id": "Driver4",
"maxPoints": 3, "name": "Vehicle Scanning",
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist3" "row": 3,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Scientist1", "icon": "t_ui_iconskilltreeattributescanningbonus_d.webp",
"id": "Scientist1", "url": "https://dune.gaming.tools/skills/skills-attribute-driver4"
"name": "Cutteray Mining", },
"kind": "Attribute", {
"row": 5, "tag": "Skills.Attribute.Driver2",
"col": 2, "id": "Driver2",
"maxPoints": 3, "name": "Fuel Efficient Driver",
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist1" "row": 4,
}, "col": 1,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Explorer5", "icon": "t_ui_iconskilltreeattributevehicle_d.webp",
"id": "Explorer5", "url": "https://dune.gaming.tools/skills/skills-attribute-driver2"
"name": "Spice Surveyor", },
"kind": "Attribute", {
"row": 1, "tag": "Skills.Attribute.Driver3",
"col": 2, "id": "Driver3",
"maxPoints": 1, "name": "Vehicle Mining",
"icon": "t_ui_iconskilltreeattributespice_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer5" "row": 4,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Explorer3", "icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
"id": "Explorer3", "url": "https://dune.gaming.tools/skills/skills-attribute-driver3"
"name": "Scanner Mastery", },
"kind": "Attribute", {
"row": 2, "tag": "Skills.Attribute.Driver1",
"col": 1, "id": "Driver1",
"maxPoints": 3, "name": "Vehicle Repair",
"icon": "t_ui_iconskilltreeattributescanningbonus_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer3" "row": 5,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Explorer4", "icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
"id": "Explorer4", "url": "https://dune.gaming.tools/skills/skills-attribute-driver1"
"name": "Stillsuit Seals", }
"kind": "Attribute", ],
"row": 2, "edges": [
"col": 3, {
"maxPoints": 3, "from": "Skills.Attribute.Driver5",
"icon": "t_ui_iconskilltreeskillhydration_d.webp", "to": "Skills.Spice.VehicleHeat"
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer4" },
}, {
{ "from": "Skills.Attribute.Driver6",
"tag": "Skills.Attribute.Explorer1", "to": "Skills.Spice.VehicleHeat"
"id": "Explorer1", },
"name": "Cartographer", {
"kind": "Attribute", "from": "Skills.Attribute.Driver4",
"row": 4, "to": "Skills.Attribute.Driver5"
"col": 1, },
"maxPoints": 1, {
"icon": "t_ui_iconskilltreeskillobservation_d.webp", "from": "Skills.Attribute.Driver2",
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer1" "to": "Skills.Attribute.Driver5"
}, },
{ {
"tag": "Skills.Attribute.Explorer2", "from": "Skills.Attribute.Driver4",
"id": "Explorer2", "to": "Skills.Attribute.Driver6"
"name": "Mountaineer", },
"kind": "Attribute", {
"row": 4, "from": "Skills.Attribute.Driver3",
"col": 3, "to": "Skills.Attribute.Driver6"
"maxPoints": 3, },
"icon": "t_ui_iconskilltreeskillclimber_d.webp", {
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer2" "from": "Skills.Attribute.Driver2",
}, "to": "Skills.Attribute.Driver4"
{ },
"tag": "Skills.Ability.SuspensorPad", {
"id": "SuspensorPad", "from": "Skills.Attribute.Driver3",
"name": "Suspensor Pad", "to": "Skills.Attribute.Driver4"
"kind": "Ability", },
"row": 5, {
"col": 2, "from": "Skills.Attribute.Driver1",
"maxPoints": 1, "to": "Skills.Attribute.Driver2"
"icon": "t_ui_iconabilitysuspensorpad_d.webp", },
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorpad" {
}, "from": "Skills.Attribute.Driver1",
{ "to": "Skills.Attribute.Driver3"
"tag": "Skills.Spice.VehicleHeat", }
"id": "VehicleHeat", ]
"name": "Heat Management",
"kind": "Spice",
"row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreeheatmanagement_d.webp",
"url": "https://dune.gaming.tools/skills/skills-spice-vehicleheat"
},
{
"tag": "Skills.Attribute.Driver5",
"id": "Driver5",
"name": "Fuel Efficient Pilot",
"kind": "Attribute",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributevehicle_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver5"
},
{
"tag": "Skills.Attribute.Driver6",
"id": "Driver6",
"name": "Sandcrawler Yield",
"kind": "Attribute",
"row": 2,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributespiceyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver6"
},
{
"tag": "Skills.Attribute.Driver4",
"id": "Driver4",
"name": "Vehicle Scanning",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributescanningbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver4"
},
{
"tag": "Skills.Attribute.Driver2",
"id": "Driver2",
"name": "Fuel Efficient Driver",
"kind": "Attribute",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributevehicle_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver2"
},
{
"tag": "Skills.Attribute.Driver3",
"id": "Driver3",
"name": "Vehicle Mining",
"kind": "Attribute",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver3"
},
{
"tag": "Skills.Attribute.Driver1",
"id": "Driver1",
"name": "Vehicle Repair",
"kind": "Attribute",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-driver1"
}
],
"edges": [
{
"from": "Skills.Attribute.Scientist5",
"to": "Skills.Perk.BatteryExpert"
},
{
"from": "Skills.Perk.BatteryExpert",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist4",
"to": "Skills.Attribute.Scientist5"
},
{
"from": "Skills.Attribute.Scientist2",
"to": "Skills.Attribute.Scientist5"
},
{
"from": "Skills.Attribute.Scientist4",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist3",
"to": "Skills.Science.m_PowerMax"
},
{
"from": "Skills.Attribute.Scientist2",
"to": "Skills.Attribute.Scientist4"
},
{
"from": "Skills.Attribute.Scientist3",
"to": "Skills.Attribute.Scientist4"
},
{
"from": "Skills.Attribute.Scientist1",
"to": "Skills.Attribute.Scientist2"
},
{
"from": "Skills.Attribute.Scientist1",
"to": "Skills.Attribute.Scientist3"
} }
] ]
} }

View file

@ -1,338 +1,384 @@
{ {
"id": "swordmaster", "id": "swordmaster",
"name": "Swordmaster", "name": "Swordmaster",
"nodes": [ "subtrees": [
{ {
"tag": "Skills.Spice.ParryBoost", "name": "The Blade",
"id": "ParryBoost", "cols": 3,
"name": "Precise Parry", "nodes": [
"kind": "Spice", {
"row": 1, "tag": "Skills.Spice.ParryBoost",
"col": 2, "id": "ParryBoost",
"maxPoints": 3, "name": "Precise Parry",
"icon": "t_ui_iconskilltreepreciseparry_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-spice-parryboost" "row": 1,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreepreciseparry_d.webp",
"url": "https://dune.gaming.tools/skills/skills-spice-parryboost"
},
{
"tag": "Skills.Ability.Whirlwind",
"id": "Whirlwind",
"name": "Eye of the Storm",
"kind": "Ability",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconabilitywhirlwind_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-whirlwind"
},
{
"tag": "Skills.Ability.RiposteBreak",
"id": "RiposteBreak",
"name": "Foil",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitybreakingreposte_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-ripostebreak"
},
{
"tag": "Skills.Attribute.Blade2",
"id": "Blade2",
"name": "Long Blade Damage",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-blade2"
},
{
"tag": "Skills.Perk.MeleeChain",
"id": "MeleeChain",
"name": "Dance of Blades",
"kind": "Perk",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkbladechaining_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-meleechain"
},
{
"tag": "Skills.Ability.RiposteInjure",
"id": "RiposteInjure",
"name": "Retaliate",
"kind": "Ability",
"row": 4,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilityinjuringreposte_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-riposteinjure"
},
{
"tag": "Skills.Attribute.Blade1",
"id": "Blade1",
"name": "Blade Damage",
"kind": "Attribute",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-blade1"
}
],
"edges": [
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Spice.ParryBoost"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Spice.ParryBoost"
},
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Ability.RiposteInjure"
},
{
"from": "Skills.Attribute.Blade2",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteInjure",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Attribute.Blade1",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteInjure",
"to": "Skills.Attribute.Blade1"
}
]
}, },
{ {
"tag": "Skills.Ability.Whirlwind", "name": "The Will",
"id": "Whirlwind", "cols": 3,
"name": "Eye of the Storm", "nodes": [
"kind": "Ability", {
"row": 2, "tag": "Skills.Perk.ThriveOnDanger",
"col": 1, "id": "ThriveOnDanger",
"maxPoints": 3, "name": "Thrive on Danger",
"icon": "t_ui_iconabilitywhirlwind_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-ability-whirlwind" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreethriveondanger_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-thriveondanger"
},
{
"tag": "Skills.Attribute.Resolve2",
"id": "Resolve2",
"name": "Solid Stance",
"kind": "Attribute",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributepoisedefense_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve2"
},
{
"tag": "Skills.Attribute.UnstoppableAttacks",
"id": "UnstoppableAttacks",
"name": "Confidence",
"kind": "Attribute",
"row": 2,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagemitigation_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-unstoppableattacks"
},
{
"tag": "Skills.Attribute.Resolve1",
"id": "Resolve1",
"name": "Bleed Tolerance",
"kind": "Attribute",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillmaxhealth_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve1"
},
{
"tag": "Skills.Perk.ToughLunge",
"id": "ToughLunge",
"name": "Reckless Lunge",
"kind": "Perk",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreetoughlunge_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-toughlunge"
},
{
"tag": "Skills.Ability.DeflectionSlow",
"id": "DeflectionSlow",
"name": "Deflection",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconabilitydeflection_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-deflectionslow"
}
],
"edges": [
{
"from": "Skills.Attribute.Resolve2",
"to": "Skills.Perk.ThriveOnDanger"
},
{
"from": "Skills.Attribute.UnstoppableAttacks",
"to": "Skills.Perk.ThriveOnDanger"
},
{
"from": "Skills.Attribute.Resolve1",
"to": "Skills.Attribute.Resolve2"
},
{
"from": "Skills.Attribute.UnstoppableAttacks",
"to": "Skills.Perk.ToughLunge"
},
{
"from": "Skills.Ability.DeflectionSlow",
"to": "Skills.Attribute.Resolve1"
},
{
"from": "Skills.Ability.DeflectionSlow",
"to": "Skills.Perk.ToughLunge"
}
]
}, },
{ {
"tag": "Skills.Ability.RiposteBreak", "name": "The Way",
"id": "RiposteBreak", "cols": 5,
"name": "Foil", "nodes": [
"kind": "Ability", {
"row": 2, "tag": "Skills.Spice.ShadowStrike",
"col": 3, "id": "ShadowStrike",
"maxPoints": 1, "name": "Prescient Strike",
"icon": "t_ui_iconabilitybreakingreposte_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-ability-ripostebreak" "row": 1,
}, "col": 3,
{ "maxPoints": 1,
"tag": "Skills.Attribute.Blade2", "icon": "t_ui_iconskilltreeprescientstrike_d.webp",
"id": "Blade2", "url": "https://dune.gaming.tools/skills/skills-spice-shadowstrike"
"name": "Long Blade Damage", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Attribute.Aggression3",
"col": 2, "id": "Aggression3",
"maxPoints": 3, "name": "General Conditioning",
"icon": "t_ui_iconskilltreeskillbrawler_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-blade2" "row": 2,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Perk.MeleeChain", "icon": "t_ui_iconskilltreeattributestamina_d.webp",
"id": "MeleeChain", "url": "https://dune.gaming.tools/skills/skills-attribute-aggression3"
"name": "Dance of Blades", },
"kind": "Perk", {
"row": 4, "tag": "Skills.Attribute.Aggression4",
"col": 1, "id": "Aggression4",
"maxPoints": 3, "name": "Desert Conditioning",
"icon": "t_ui_iconskilltreeperkbladechaining_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-perk-meleechain" "row": 2,
}, "col": 4,
{ "maxPoints": 3,
"tag": "Skills.Ability.RiposteInjure", "icon": "t_ui_iconskilltreeattributewatherdefense_d.webp",
"id": "RiposteInjure", "url": "https://dune.gaming.tools/skills/skills-attribute-aggression4"
"name": "Retaliate", },
"kind": "Ability", {
"row": 4, "tag": "Skills.Ability.CripplingStrike",
"col": 3, "id": "CripplingStrike",
"maxPoints": 1, "name": "Crippling Strike",
"icon": "t_ui_iconabilityinjuringreposte_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-ability-riposteinjure" "row": 3,
}, "col": 1,
{ "maxPoints": 1,
"tag": "Skills.Attribute.Blade1", "icon": "t_ui_iconabilitycripplingstrike_d.webp",
"id": "Blade1", "url": "https://dune.gaming.tools/skills/skills-ability-cripplingstrike"
"name": "Blade Damage", },
"kind": "Attribute", {
"row": 5, "tag": "Skills.Perk.SprintStamina",
"col": 2, "id": "SprintStamina",
"maxPoints": 3, "name": "Disciplined Breathing",
"icon": "t_ui_iconskilltreeskillbrawler_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-attribute-blade1" "row": 3,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Perk.ThriveOnDanger", "icon": "t_ui_iconskilltreeperkrunner_d.webp",
"id": "ThriveOnDanger", "url": "https://dune.gaming.tools/skills/skills-perk-sprintstamina"
"name": "Thrive on Danger", },
"kind": "Perk", {
"row": 1, "tag": "Skills.Ability.BattleCry",
"col": 2, "id": "BattleCry",
"maxPoints": 1, "name": "Inspiration",
"icon": "t_ui_iconskilltreethriveondanger_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-perk-thriveondanger" "row": 3,
}, "col": 5,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Resolve2", "icon": "t_ui_iconabilitybattlecry_d.webp",
"id": "Resolve2", "url": "https://dune.gaming.tools/skills/skills-ability-battlecry"
"name": "Solid Stance", },
"kind": "Attribute", {
"row": 2, "tag": "Skills.Attribute.Aggression1",
"col": 1, "id": "Aggression1",
"maxPoints": 3, "name": "Field Medicine",
"icon": "t_ui_iconskilltreeattributepoisedefense_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve2" "row": 4,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Attribute.UnstoppableAttacks", "icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
"id": "UnstoppableAttacks", "url": "https://dune.gaming.tools/skills/skills-attribute-aggression1"
"name": "Confidence", },
"kind": "Attribute", {
"row": 2, "tag": "Skills.Attribute.Aggression2",
"col": 3, "id": "Aggression2",
"maxPoints": 3, "name": "Optimized Hydration",
"icon": "t_ui_iconskilltreeattributedamagemitigation_d.webp", "kind": "Attribute",
"url": "https://dune.gaming.tools/skills/skills-attribute-unstoppableattacks" "row": 4,
}, "col": 4,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Resolve1", "icon": "t_ui_iconskilltreeattributewatherbonus_d.webp",
"id": "Resolve1", "url": "https://dune.gaming.tools/skills/skills-attribute-aggression2"
"name": "Bleed Tolerance", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Ability.KneeCharge",
"col": 1, "id": "KneeCharge",
"maxPoints": 3, "name": "Knee Charge",
"icon": "t_ui_iconskilltreeskillmaxhealth_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve1" "row": 5,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Perk.ToughLunge", "icon": "t_ui_iconabilitykneecharge_d.webp",
"id": "ToughLunge", "url": "https://dune.gaming.tools/skills/skills-ability-kneecharge"
"name": "Reckless Lunge", }
"kind": "Perk", ],
"row": 4, "edges": [
"col": 3, {
"maxPoints": 3, "from": "Skills.Attribute.Aggression3",
"icon": "t_ui_iconskilltreetoughlunge_d.webp", "to": "Skills.Spice.ShadowStrike"
"url": "https://dune.gaming.tools/skills/skills-perk-toughlunge" },
}, {
{ "from": "Skills.Attribute.Aggression4",
"tag": "Skills.Ability.DeflectionSlow", "to": "Skills.Spice.ShadowStrike"
"id": "DeflectionSlow", },
"name": "Deflection", {
"kind": "Ability", "from": "Skills.Ability.CripplingStrike",
"row": 5, "to": "Skills.Attribute.Aggression3"
"col": 2, },
"maxPoints": 1, {
"icon": "t_ui_iconabilitydeflection_d.webp", "from": "Skills.Attribute.Aggression3",
"url": "https://dune.gaming.tools/skills/skills-ability-deflectionslow" "to": "Skills.Perk.SprintStamina"
}, },
{ {
"tag": "Skills.Spice.ShadowStrike", "from": "Skills.Attribute.Aggression4",
"id": "ShadowStrike", "to": "Skills.Perk.SprintStamina"
"name": "Prescient Strike", },
"kind": "Spice", {
"row": 1, "from": "Skills.Ability.BattleCry",
"col": 3, "to": "Skills.Attribute.Aggression4"
"maxPoints": 1, },
"icon": "t_ui_iconskilltreeprescientstrike_d.webp", {
"url": "https://dune.gaming.tools/skills/skills-spice-shadowstrike" "from": "Skills.Ability.CripplingStrike",
}, "to": "Skills.Attribute.Aggression1"
{ },
"tag": "Skills.Attribute.Aggression3", {
"id": "Aggression3", "from": "Skills.Attribute.Aggression1",
"name": "General Conditioning", "to": "Skills.Perk.SprintStamina"
"kind": "Attribute", },
"row": 2, {
"col": 2, "from": "Skills.Attribute.Aggression2",
"maxPoints": 3, "to": "Skills.Perk.SprintStamina"
"icon": "t_ui_iconskilltreeattributestamina_d.webp", },
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression3" {
}, "from": "Skills.Ability.BattleCry",
{ "to": "Skills.Attribute.Aggression2"
"tag": "Skills.Attribute.Aggression4", },
"id": "Aggression4", {
"name": "Desert Conditioning", "from": "Skills.Ability.KneeCharge",
"kind": "Attribute", "to": "Skills.Attribute.Aggression1"
"row": 2, },
"col": 4, {
"maxPoints": 3, "from": "Skills.Ability.KneeCharge",
"icon": "t_ui_iconskilltreeattributewatherdefense_d.webp", "to": "Skills.Attribute.Aggression2"
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression4" }
}, ]
{
"tag": "Skills.Ability.CripplingStrike",
"id": "CripplingStrike",
"name": "Crippling Strike",
"kind": "Ability",
"row": 3,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconabilitycripplingstrike_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-cripplingstrike"
},
{
"tag": "Skills.Perk.SprintStamina",
"id": "SprintStamina",
"name": "Disciplined Breathing",
"kind": "Perk",
"row": 3,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkrunner_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-sprintstamina"
},
{
"tag": "Skills.Ability.BattleCry",
"id": "BattleCry",
"name": "Inspiration",
"kind": "Ability",
"row": 3,
"col": 5,
"maxPoints": 3,
"icon": "t_ui_iconabilitybattlecry_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-battlecry"
},
{
"tag": "Skills.Attribute.Aggression1",
"id": "Aggression1",
"name": "Field Medicine",
"kind": "Attribute",
"row": 4,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression1"
},
{
"tag": "Skills.Attribute.Aggression2",
"id": "Aggression2",
"name": "Optimized Hydration",
"kind": "Attribute",
"row": 4,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributewatherbonus_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression2"
},
{
"tag": "Skills.Ability.KneeCharge",
"id": "KneeCharge",
"name": "Knee Charge",
"kind": "Ability",
"row": 5,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconabilitykneecharge_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-kneecharge"
}
],
"edges": [
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Spice.ParryBoost"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Spice.ParryBoost"
},
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Ability.Whirlwind",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Ability.RiposteBreak",
"to": "Skills.Ability.RiposteInjure"
},
{
"from": "Skills.Attribute.Blade2",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteInjure",
"to": "Skills.Attribute.Blade2"
},
{
"from": "Skills.Attribute.Blade1",
"to": "Skills.Perk.MeleeChain"
},
{
"from": "Skills.Ability.RiposteInjure",
"to": "Skills.Attribute.Blade1"
},
{
"from": "Skills.Attribute.Aggression3",
"to": "Skills.Spice.ShadowStrike"
},
{
"from": "Skills.Attribute.Aggression4",
"to": "Skills.Spice.ShadowStrike"
},
{
"from": "Skills.Ability.CripplingStrike",
"to": "Skills.Attribute.Aggression3"
},
{
"from": "Skills.Attribute.Aggression3",
"to": "Skills.Perk.SprintStamina"
},
{
"from": "Skills.Attribute.Aggression4",
"to": "Skills.Perk.SprintStamina"
},
{
"from": "Skills.Ability.BattleCry",
"to": "Skills.Attribute.Aggression4"
},
{
"from": "Skills.Ability.CripplingStrike",
"to": "Skills.Attribute.Aggression1"
},
{
"from": "Skills.Attribute.Aggression1",
"to": "Skills.Perk.SprintStamina"
},
{
"from": "Skills.Attribute.Aggression2",
"to": "Skills.Perk.SprintStamina"
},
{
"from": "Skills.Ability.BattleCry",
"to": "Skills.Attribute.Aggression2"
},
{
"from": "Skills.Ability.KneeCharge",
"to": "Skills.Attribute.Aggression1"
},
{
"from": "Skills.Ability.KneeCharge",
"to": "Skills.Attribute.Aggression2"
} }
] ]
} }

View file

@ -1,338 +1,384 @@
{ {
"id": "trooper", "id": "trooper",
"name": "Trooper", "name": "Trooper",
"nodes": [ "subtrees": [
{ {
"tag": "Skills.Ability.EnergyCapsule", "name": "Gunnery",
"id": "EnergyCapsule", "cols": 5,
"name": "Energy Capsule", "nodes": [
"kind": "Ability", {
"row": 1, "tag": "Skills.Ability.EnergyCapsule",
"col": 3, "id": "EnergyCapsule",
"maxPoints": 1, "name": "Energy Capsule",
"icon": "t_ui_iconabilityenergycapsule_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-ability-energycapsule" "row": 1,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilityenergycapsule_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-energycapsule"
},
{
"tag": "Skills.Attribute.Weaponry5",
"id": "Weaponry5",
"name": "Heavy Weapon Damage",
"kind": "Attribute",
"row": 2,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagebonusheavyweapon_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry5"
},
{
"tag": "Skills.Attribute.Weaponry6",
"id": "Weaponry6",
"name": "Gunsmith",
"kind": "Attribute",
"row": 2,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry6"
},
{
"tag": "Skills.Perk.HeavyWeaponNaib",
"id": "HeavyWeaponNaib",
"name": "Heavy Weapon Agility",
"kind": "Perk",
"row": 3,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkheavyweaponnaib_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-heavyweaponnaib"
},
{
"tag": "Skills.Attribute.Weaponry3",
"id": "Weaponry3",
"name": "Scattergun Damage",
"kind": "Attribute",
"row": 3,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagebonusrifle_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry3"
},
{
"tag": "Skills.Attribute.Weaponry4",
"id": "Weaponry4",
"name": "Field Maintenance",
"kind": "Attribute",
"row": 3,
"col": 5,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributerepair_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry4"
},
{
"tag": "Skills.Attribute.Weaponry2",
"id": "Weaponry2",
"name": "Disruptor Damage",
"kind": "Attribute",
"row": 4,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamagebonussmg_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry2"
},
{
"tag": "Skills.Perk.BodyShots",
"id": "BodyShots",
"name": "Center of Mass",
"kind": "Perk",
"row": 4,
"col": 4,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkcentralaim_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-bodyshots"
},
{
"tag": "Skills.Attribute.Weaponry1",
"id": "Weaponry1",
"name": "Ranged Damage",
"kind": "Attribute",
"row": 5,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry1"
}
],
"edges": [
{
"from": "Skills.Ability.EnergyCapsule",
"to": "Skills.Attribute.Weaponry5"
},
{
"from": "Skills.Ability.EnergyCapsule",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry5",
"to": "Skills.Perk.HeavyWeaponNaib"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Attribute.Weaponry5"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry4",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry2",
"to": "Skills.Perk.HeavyWeaponNaib"
},
{
"from": "Skills.Attribute.Weaponry2",
"to": "Skills.Attribute.Weaponry3"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Perk.BodyShots"
},
{
"from": "Skills.Attribute.Weaponry4",
"to": "Skills.Perk.BodyShots"
},
{
"from": "Skills.Attribute.Weaponry1",
"to": "Skills.Attribute.Weaponry2"
},
{
"from": "Skills.Attribute.Weaponry1",
"to": "Skills.Perk.BodyShots"
}
]
}, },
{ {
"tag": "Skills.Attribute.Weaponry5", "name": "Suspensor Training",
"id": "Weaponry5", "cols": 3,
"name": "Heavy Weapon Damage", "nodes": [
"kind": "Attribute", {
"row": 2, "tag": "Skills.Ability.SuspensorBlast",
"col": 2, "id": "SuspensorBlast",
"maxPoints": 3, "name": "Suspensor Blast",
"icon": "t_ui_iconskilltreeattributedamagebonusheavyweapon_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry5" "row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconabilitysuspensorblast_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorblast"
},
{
"tag": "Skills.Perk.DeathFromAbove",
"id": "DeathFromAbove",
"name": "Death from Above",
"kind": "Perk",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeperkdeathfromabove_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-deathfromabove"
},
{
"tag": "Skills.Ability.CollapseGrenade",
"id": "CollapseGrenade",
"name": "Collapse Grenade",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitycollapsegrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-collapsegrenade"
},
{
"tag": "Skills.Attribute.SuspensorTech1",
"id": "SuspensorTech1",
"name": "Suspensor Efficiency",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-suspensortech1"
},
{
"tag": "Skills.Perk.SuspensorDash",
"id": "SuspensorDash",
"name": "Suspensor Dash",
"kind": "Perk",
"row": 4,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconskilltreesuspensordash_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-suspensordash"
},
{
"tag": "Skills.Ability.SuspensorGrenade_Amplification",
"id": "SuspensorGrenade_Amplification",
"name": "Gravity Field",
"kind": "Ability",
"row": 4,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_icongadgetamplificationgrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_amplification"
},
{
"tag": "Skills.Ability.SuspensorGrenade_Reduction",
"id": "SuspensorGrenade_Reduction",
"name": "Anti-gravity Field",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_icongadgetreductionsuspensorgrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_reduction"
}
],
"edges": [
{
"from": "Skills.Ability.SuspensorBlast",
"to": "Skills.Perk.DeathFromAbove"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Ability.SuspensorBlast"
},
{
"from": "Skills.Attribute.SuspensorTech1",
"to": "Skills.Perk.DeathFromAbove"
},
{
"from": "Skills.Perk.DeathFromAbove",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Attribute.SuspensorTech1"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Ability.SuspensorGrenade_Amplification"
},
{
"from": "Skills.Attribute.SuspensorTech1",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.SuspensorGrenade_Amplification",
"to": "Skills.Attribute.SuspensorTech1"
},
{
"from": "Skills.Ability.SuspensorGrenade_Reduction",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.SuspensorGrenade_Amplification",
"to": "Skills.Ability.SuspensorGrenade_Reduction"
}
]
}, },
{ {
"tag": "Skills.Attribute.Weaponry6", "name": "Tactical Tech",
"id": "Weaponry6", "cols": 3,
"name": "Gunsmith", "nodes": [
"kind": "Attribute", {
"row": 2, "tag": "Skills.Spice.GadgetReload",
"col": 4, "id": "GadgetReload",
"maxPoints": 3, "name": "Reflexive Reload",
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp", "kind": "Spice",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry6" "row": 1,
}, "col": 2,
{ "maxPoints": 1,
"tag": "Skills.Perk.HeavyWeaponNaib", "icon": "t_ui_iconskilltreereflexivereload_d.webp",
"id": "HeavyWeaponNaib", "url": "https://dune.gaming.tools/skills/skills-spice-gadgetreload"
"name": "Heavy Weapon Agility", },
"kind": "Perk", {
"row": 3, "tag": "Skills.Ability.AssaultSeeker",
"col": 1, "id": "AssaultSeeker",
"maxPoints": 3, "name": "Assault Seeker",
"icon": "t_ui_iconskilltreeperkheavyweaponnaib_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-perk-heavyweaponnaib" "row": 2,
}, "col": 1,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Weaponry3", "icon": "t_ui_iconabilityassaultseeker_d.webp",
"id": "Weaponry3", "url": "https://dune.gaming.tools/skills/skills-ability-assaultseeker"
"name": "Scattergun Damage", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Ability.MagneticAttractor",
"col": 3, "id": "MagneticAttractor",
"maxPoints": 3, "name": "Attractor Field",
"icon": "t_ui_iconskilltreeattributedamagebonusrifle_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry3" "row": 2,
}, "col": 3,
{ "maxPoints": 1,
"tag": "Skills.Attribute.Weaponry4", "icon": "t_ui_icongadgetshigmultitoolmagneticattractor_d.webp",
"id": "Weaponry4", "url": "https://dune.gaming.tools/skills/skills-ability-magneticattractor"
"name": "Field Maintenance", },
"kind": "Attribute", {
"row": 3, "tag": "Skills.Ability.FragGrenade",
"col": 5, "id": "FragGrenade",
"maxPoints": 3, "name": "Explosive Grenade",
"icon": "t_ui_iconskilltreeattributerepair_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry4" "row": 4,
}, "col": 1,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Weaponry2", "icon": "t_ui_icongadgetfraggrenades_d.webp",
"id": "Weaponry2", "url": "https://dune.gaming.tools/skills/skills-ability-fraggrenade"
"name": "Disruptor Damage", },
"kind": "Attribute", {
"row": 4, "tag": "Skills.Perk.TrooperCooldowns",
"col": 2, "id": "TrooperCooldowns",
"maxPoints": 3, "name": "Battle Hardened",
"icon": "t_ui_iconskilltreeattributedamagebonussmg_d.webp", "kind": "Perk",
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry2" "row": 4,
}, "col": 3,
{ "maxPoints": 3,
"tag": "Skills.Perk.BodyShots", "icon": "t_ui_iconskilltreetroopercooldown_d.webp",
"id": "BodyShots", "url": "https://dune.gaming.tools/skills/skills-perk-troopercooldowns"
"name": "Center of Mass", },
"kind": "Perk", {
"row": 4, "tag": "Skills.Ability.CablePull",
"col": 4, "id": "CablePull",
"maxPoints": 3, "name": "Shigawire Claw",
"icon": "t_ui_iconskilltreeperkcentralaim_d.webp", "kind": "Ability",
"url": "https://dune.gaming.tools/skills/skills-perk-bodyshots" "row": 5,
}, "col": 2,
{ "maxPoints": 3,
"tag": "Skills.Attribute.Weaponry1", "icon": "t_ui_icongadgetshigmultitoolsardaukarpull_d.webp",
"id": "Weaponry1", "url": "https://dune.gaming.tools/skills/skills-ability-cablepull"
"name": "Ranged Damage", }
"kind": "Attribute", ],
"row": 5, "edges": [
"col": 3, {
"maxPoints": 3, "from": "Skills.Ability.AssaultSeeker",
"icon": "t_ui_iconskilltreeattributedamage_d.webp", "to": "Skills.Spice.GadgetReload"
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry1" },
}, {
{ "from": "Skills.Ability.MagneticAttractor",
"tag": "Skills.Ability.SuspensorBlast", "to": "Skills.Spice.GadgetReload"
"id": "SuspensorBlast", },
"name": "Suspensor Blast", {
"kind": "Ability", "from": "Skills.Ability.AssaultSeeker",
"row": 1, "to": "Skills.Ability.FragGrenade"
"col": 2, },
"maxPoints": 1, {
"icon": "t_ui_iconabilitysuspensorblast_d.webp", "from": "Skills.Ability.MagneticAttractor",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorblast" "to": "Skills.Perk.TrooperCooldowns"
}, },
{ {
"tag": "Skills.Perk.DeathFromAbove", "from": "Skills.Ability.CablePull",
"id": "DeathFromAbove", "to": "Skills.Ability.FragGrenade"
"name": "Death from Above", },
"kind": "Perk", {
"row": 2, "from": "Skills.Ability.CablePull",
"col": 1, "to": "Skills.Perk.TrooperCooldowns"
"maxPoints": 3, }
"icon": "t_ui_iconskilltreeperkdeathfromabove_d.webp", ]
"url": "https://dune.gaming.tools/skills/skills-perk-deathfromabove"
},
{
"tag": "Skills.Ability.CollapseGrenade",
"id": "CollapseGrenade",
"name": "Collapse Grenade",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_iconabilitycollapsegrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-collapsegrenade"
},
{
"tag": "Skills.Attribute.SuspensorTech1",
"id": "SuspensorTech1",
"name": "Suspensor Efficiency",
"kind": "Attribute",
"row": 3,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp",
"url": "https://dune.gaming.tools/skills/skills-attribute-suspensortech1"
},
{
"tag": "Skills.Perk.SuspensorDash",
"id": "SuspensorDash",
"name": "Suspensor Dash",
"kind": "Perk",
"row": 4,
"col": 1,
"maxPoints": 1,
"icon": "t_ui_iconskilltreesuspensordash_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-suspensordash"
},
{
"tag": "Skills.Ability.SuspensorGrenade_Amplification",
"id": "SuspensorGrenade_Amplification",
"name": "Gravity Field",
"kind": "Ability",
"row": 4,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_icongadgetamplificationgrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_amplification"
},
{
"tag": "Skills.Ability.SuspensorGrenade_Reduction",
"id": "SuspensorGrenade_Reduction",
"name": "Anti-gravity Field",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_icongadgetreductionsuspensorgrenade_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_reduction"
},
{
"tag": "Skills.Spice.GadgetReload",
"id": "GadgetReload",
"name": "Reflexive Reload",
"kind": "Spice",
"row": 1,
"col": 2,
"maxPoints": 1,
"icon": "t_ui_iconskilltreereflexivereload_d.webp",
"url": "https://dune.gaming.tools/skills/skills-spice-gadgetreload"
},
{
"tag": "Skills.Ability.AssaultSeeker",
"id": "AssaultSeeker",
"name": "Assault Seeker",
"kind": "Ability",
"row": 2,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_iconabilityassaultseeker_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-assaultseeker"
},
{
"tag": "Skills.Ability.MagneticAttractor",
"id": "MagneticAttractor",
"name": "Attractor Field",
"kind": "Ability",
"row": 2,
"col": 3,
"maxPoints": 1,
"icon": "t_ui_icongadgetshigmultitoolmagneticattractor_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-magneticattractor"
},
{
"tag": "Skills.Ability.FragGrenade",
"id": "FragGrenade",
"name": "Explosive Grenade",
"kind": "Ability",
"row": 4,
"col": 1,
"maxPoints": 3,
"icon": "t_ui_icongadgetfraggrenades_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-fraggrenade"
},
{
"tag": "Skills.Perk.TrooperCooldowns",
"id": "TrooperCooldowns",
"name": "Battle Hardened",
"kind": "Perk",
"row": 4,
"col": 3,
"maxPoints": 3,
"icon": "t_ui_iconskilltreetroopercooldown_d.webp",
"url": "https://dune.gaming.tools/skills/skills-perk-troopercooldowns"
},
{
"tag": "Skills.Ability.CablePull",
"id": "CablePull",
"name": "Shigawire Claw",
"kind": "Ability",
"row": 5,
"col": 2,
"maxPoints": 3,
"icon": "t_ui_icongadgetshigmultitoolsardaukarpull_d.webp",
"url": "https://dune.gaming.tools/skills/skills-ability-cablepull"
}
],
"edges": [
{
"from": "Skills.Ability.EnergyCapsule",
"to": "Skills.Attribute.Weaponry5"
},
{
"from": "Skills.Ability.EnergyCapsule",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry5",
"to": "Skills.Perk.HeavyWeaponNaib"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Attribute.Weaponry5"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry4",
"to": "Skills.Attribute.Weaponry6"
},
{
"from": "Skills.Attribute.Weaponry2",
"to": "Skills.Perk.HeavyWeaponNaib"
},
{
"from": "Skills.Attribute.Weaponry2",
"to": "Skills.Attribute.Weaponry3"
},
{
"from": "Skills.Attribute.Weaponry3",
"to": "Skills.Perk.BodyShots"
},
{
"from": "Skills.Attribute.Weaponry4",
"to": "Skills.Perk.BodyShots"
},
{
"from": "Skills.Attribute.Weaponry1",
"to": "Skills.Attribute.Weaponry2"
},
{
"from": "Skills.Attribute.Weaponry1",
"to": "Skills.Perk.BodyShots"
},
{
"from": "Skills.Ability.SuspensorBlast",
"to": "Skills.Perk.DeathFromAbove"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Ability.SuspensorBlast"
},
{
"from": "Skills.Attribute.SuspensorTech1",
"to": "Skills.Perk.DeathFromAbove"
},
{
"from": "Skills.Perk.DeathFromAbove",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Attribute.SuspensorTech1"
},
{
"from": "Skills.Ability.CollapseGrenade",
"to": "Skills.Ability.SuspensorGrenade_Amplification"
},
{
"from": "Skills.Attribute.SuspensorTech1",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.SuspensorGrenade_Amplification",
"to": "Skills.Attribute.SuspensorTech1"
},
{
"from": "Skills.Ability.SuspensorGrenade_Reduction",
"to": "Skills.Perk.SuspensorDash"
},
{
"from": "Skills.Ability.SuspensorGrenade_Amplification",
"to": "Skills.Ability.SuspensorGrenade_Reduction"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -4,6 +4,7 @@ import XpProgressCard from './components/XpProgressCard.vue';
import FactionTrack from './components/FactionTrack.vue'; import FactionTrack from './components/FactionTrack.vue';
import SkillTree from './components/SkillTree.vue'; import SkillTree from './components/SkillTree.vue';
import CharacterSummary from './components/CharacterSummary.vue'; import CharacterSummary from './components/CharacterSummary.vue';
import LoadoutSlots from './components/LoadoutSlots.vue';
import { import {
applyBuild, applyBuild,
build, build,
@ -143,8 +144,10 @@ const spentByClass = computed<Record<ClassId, number>>(() => {
for (const c of CLASSES) { for (const c of CLASSES) {
const tree = skillTrees.value[c.id]; const tree = skillTrees.value[c.id];
if (!tree) continue; if (!tree) continue;
for (const node of tree.nodes) { for (const st of tree.subtrees) {
out[c.id] += build.skills[node.tag] || 0; for (const node of st.nodes) {
out[c.id] += build.skills[node.tag] || 0;
}
} }
} }
return out; return out;
@ -433,6 +436,14 @@ const specMeta: Record<SpecId, { name: string; sym: string }> = {
@update:allocations="(a) => (build.skills = a)" @update:allocations="(a) => (build.skills = a)"
@reset="resetAllSkills" @reset="resetAllSkills"
/> />
<LoadoutSlots
:loadout="build.loadout"
:skill-trees="skillTrees"
:allocations="build.skills"
:classes="CLASSES"
@update:loadout="(l) => (build.loadout = l)"
/>
</section> </section>
</div> </div>

View file

@ -104,17 +104,19 @@ const skillsByClass = computed<ClassSkillSummary[]>(() => {
const allocations: ClassSkillSummary['allocations'] = []; const allocations: ClassSkillSummary['allocations'] = [];
let spent = 0; let spent = 0;
if (tree) { if (tree) {
for (const node of tree.nodes) { for (const st of tree.subtrees) {
const pts = props.build.skills[node.tag] || 0; for (const node of st.nodes) {
if (pts > 0) { const pts = props.build.skills[node.tag] || 0;
spent += pts; if (pts > 0) {
allocations.push({ spent += pts;
tag: node.tag, allocations.push({
name: node.name, tag: node.tag,
kind: node.kind, name: node.name,
points: pts, kind: node.kind,
max: node.maxPoints, points: pts,
}); max: node.maxPoints,
});
}
} }
} }
} }

View file

@ -0,0 +1,424 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { ClassId, Loadout, SkillNode, SkillTree } from '../types';
const props = defineProps<{
loadout: Loadout;
skillTrees: Record<ClassId, SkillTree | null>;
allocations: Record<string, number>;
classes: { id: ClassId; name: string }[];
}>();
const emit = defineEmits<{
'update:loadout': [next: Loadout];
}>();
type SlotKind = 'ability' | 'technique';
interface SkillOption {
tag: string;
name: string;
icon: string | null;
kind: string;
classId: ClassId;
className: string;
}
// Index every allocated node across all 5 trees so we can both render the
// chosen slot icon AND show a picker of eligible options.
const allAllocatedByTag = computed(() => {
const out = new Map<string, SkillOption>();
for (const c of props.classes) {
const tree = props.skillTrees[c.id];
if (!tree) continue;
for (const st of tree.subtrees) {
for (const node of st.nodes) {
if ((props.allocations[node.tag] || 0) > 0) {
out.set(node.tag, {
tag: node.tag,
name: node.name,
icon: node.icon,
kind: node.kind,
classId: c.id,
className: c.name,
});
}
}
}
}
return out;
});
function eligibleFor(kind: SlotKind): SkillOption[] {
const result: SkillOption[] = [];
for (const opt of allAllocatedByTag.value.values()) {
if (kind === 'ability') {
if (opt.kind === 'Ability' || opt.kind === 'Spice') result.push(opt);
} else {
if (opt.kind === 'Perk') result.push(opt);
}
}
result.sort(
(a, b) =>
a.className.localeCompare(b.className) || a.name.localeCompare(b.name),
);
return result;
}
const pickerOpenFor = ref<{ kind: SlotKind; index: number } | null>(null);
function openPicker(kind: SlotKind, index: number) {
pickerOpenFor.value = { kind, index };
}
function closePicker() {
pickerOpenFor.value = null;
}
function assign(tag: string | null) {
const sel = pickerOpenFor.value;
if (!sel) return;
const next: Loadout = {
abilities: [...props.loadout.abilities],
techniques: [...props.loadout.techniques],
};
const arr = sel.kind === 'ability' ? next.abilities : next.techniques;
// If the same tag is already in another slot of the same kind, clear it.
if (tag) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === tag) arr[i] = null;
}
}
arr[sel.index] = tag;
emit('update:loadout', next);
closePicker();
}
function clearSlot(kind: SlotKind, index: number) {
const next: Loadout = {
abilities: [...props.loadout.abilities],
techniques: [...props.loadout.techniques],
};
(kind === 'ability' ? next.abilities : next.techniques)[index] = null;
emit('update:loadout', next);
}
function lookup(tag: string | null): SkillOption | null {
if (!tag) return null;
return allAllocatedByTag.value.get(tag) || null;
}
</script>
<template>
<div class="loadout">
<div class="loadout-head">
<h3>Loadout</h3>
<div class="hint">
3 Abilities + 3 Techniques · drawn from your allocated skills across
all classes
</div>
</div>
<div class="loadout-row">
<div class="row-label">Abilities</div>
<div class="slots">
<div
v-for="(tag, i) in loadout.abilities"
:key="`ab-${i}`"
class="slot slot-ability"
:class="{ filled: !!tag }"
@click="openPicker('ability', i)"
@contextmenu.prevent="clearSlot('ability', i)"
:title="lookup(tag)?.name || `Empty ability slot ${i + 1}`"
>
<img
v-if="lookup(tag)?.icon"
:src="`/icons/${lookup(tag)!.icon}`"
:alt="lookup(tag)?.name"
class="slot-icon"
draggable="false"
/>
<span v-else class="slot-placeholder">+</span>
<span v-if="lookup(tag)" class="slot-label">
{{ lookup(tag)!.name }}
</span>
</div>
</div>
</div>
<div class="loadout-row">
<div class="row-label">Techniques</div>
<div class="slots">
<div
v-for="(tag, i) in loadout.techniques"
:key="`te-${i}`"
class="slot slot-technique"
:class="{ filled: !!tag }"
@click="openPicker('technique', i)"
@contextmenu.prevent="clearSlot('technique', i)"
:title="lookup(tag)?.name || `Empty technique slot ${i + 1}`"
>
<img
v-if="lookup(tag)?.icon"
:src="`/icons/${lookup(tag)!.icon}`"
:alt="lookup(tag)?.name"
class="slot-icon"
draggable="false"
/>
<span v-else class="slot-placeholder">+</span>
<span v-if="lookup(tag)" class="slot-label">
{{ lookup(tag)!.name }}
</span>
</div>
</div>
</div>
<!-- Picker modal -->
<div v-if="pickerOpenFor" class="picker-overlay" @click="closePicker">
<div class="picker" @click.stop>
<div class="picker-head">
<h4>
Pick {{ pickerOpenFor.kind === 'ability' ? 'Ability' : 'Technique' }}
for Slot {{ pickerOpenFor.index + 1 }}
</h4>
<button @click="closePicker">Cancel</button>
</div>
<div class="picker-options" v-if="eligibleFor(pickerOpenFor.kind).length > 0">
<button class="picker-clear" @click="assign(null)">
Clear slot
</button>
<button
v-for="opt in eligibleFor(pickerOpenFor.kind)"
:key="opt.tag"
class="picker-option"
:class="{
selected:
(pickerOpenFor.kind === 'ability'
? loadout.abilities
: loadout.techniques
).includes(opt.tag),
}"
@click="assign(opt.tag)"
>
<img
v-if="opt.icon"
:src="`/icons/${opt.icon}`"
:alt="opt.name"
class="picker-icon"
draggable="false"
/>
<span class="picker-cls">{{ opt.className }}</span>
<span class="picker-name">{{ opt.name }}</span>
</button>
</div>
<div v-else class="picker-empty">
No allocated
{{ pickerOpenFor.kind === 'ability' ? 'Abilities or Spice skills' : 'Perks' }}
yet. Spend points in a skill tree first.
</div>
</div>
</div>
</div>
</template>
<style scoped>
.loadout {
margin-top: 22px;
padding-top: 18px;
border-top: 1px dashed var(--line-soft);
}
.loadout-head {
display: flex;
align-items: baseline;
gap: 14px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.loadout-head h3 {
font-family: 'Cormorant Garamond', serif;
font-size: 22px;
margin: 0;
font-weight: 500;
color: var(--sand);
}
.loadout-head .hint {
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
color: var(--ink-muted);
letter-spacing: 0.14em;
}
.loadout-row {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 14px;
}
.row-label {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--ink-muted);
width: 100px;
flex-shrink: 0;
}
.slots {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.slot {
width: 96px;
height: 96px;
border: 2px dashed var(--line);
border-radius: 6px;
background-color: var(--bg-2);
background-size: cover;
background-position: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.15s, transform 0.1s;
position: relative;
}
.slot:hover { border-color: var(--sand-2); transform: translateY(-1px); }
.slot.filled {
border-style: solid;
border-color: var(--spice);
background-color: rgba(224, 138, 60, 0.08);
}
.slot-ability { background-image: url('/icons/slot-ability.png'); }
.slot-technique { background-image: url('/icons/slot-technique.png'); }
.slot.filled.slot-ability,
.slot.filled.slot-technique {
/* dim the background tile so the icon stands out */
background-blend-mode: multiply;
background-color: rgba(0, 0, 0, 0.55);
}
.slot-icon {
width: 72px;
height: 72px;
object-fit: contain;
filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.6));
pointer-events: none;
user-select: none;
}
.slot-placeholder {
font-family: 'Cormorant Garamond', serif;
font-size: 32px;
color: var(--ink-muted);
}
.slot-label {
position: absolute;
bottom: -22px;
left: 50%;
transform: translateX(-50%);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--ink-dim);
white-space: nowrap;
text-shadow: 0 0 4px var(--bg);
pointer-events: none;
}
/* Picker */
.picker-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
}
.picker {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 6px;
padding: 22px;
width: 100%;
max-width: 640px;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.picker-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--line-soft);
}
.picker-head h4 {
font-family: 'Cormorant Garamond', serif;
font-size: 20px;
margin: 0;
font-weight: 500;
color: var(--sand);
}
.picker-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
overflow-y: auto;
padding-right: 4px;
}
.picker-option,
.picker-clear {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 6px;
border: 1px solid var(--line-soft);
border-radius: 4px;
background: var(--bg-2);
color: var(--ink-dim);
font-family: 'Inter', sans-serif;
font-size: 12px;
cursor: pointer;
text-transform: none;
letter-spacing: 0;
}
.picker-option:hover,
.picker-clear:hover {
border-color: var(--sand-2);
color: var(--sand);
}
.picker-option.selected {
border-color: var(--spice);
background: rgba(224, 138, 60, 0.08);
}
.picker-icon {
width: 48px;
height: 48px;
object-fit: contain;
}
.picker-cls {
font-family: 'JetBrains Mono', monospace;
font-size: 9.5px;
letter-spacing: 0.16em;
color: var(--ink-muted);
text-transform: uppercase;
}
.picker-name {
text-align: center;
line-height: 1.2;
}
.picker-clear {
font-style: italic;
color: var(--ember);
}
.picker-empty {
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--ink-muted);
text-align: center;
}
</style>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { SkillTree } from '../types'; import type { SkillSubtree, SkillTree } from '../types';
const props = defineProps<{ const props = defineProps<{
tree: SkillTree | null; tree: SkillTree | null;
@ -13,87 +13,36 @@ const emit = defineEmits<{
reset: []; reset: [];
}>(); }>();
// Cell size + gap in pixels. Match CSS .tree-cell. const CELL = 72; // cell size in px matches the source HTML (72px tracks)
const CELL = 96; const GAP_X = 32; // horizontal gap between cells in a subtree
const GAP = 36; // visual spacing including label area const GAP_Y = 60; // vertical gap (leaves space for a multi-line name label below each node)
const gridSize = computed(() => {
if (!props.tree) return { rows: 0, cols: 0 };
let maxRow = 0;
let maxCol = 0;
for (const n of props.tree.nodes) {
if (n.row > maxRow) maxRow = n.row;
if (n.col > maxCol) maxCol = n.col;
}
return { rows: maxRow, cols: maxCol };
});
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${gridSize.value.cols}, ${CELL}px)`,
gridTemplateRows: `repeat(${gridSize.value.rows}, ${CELL}px)`,
width: `${gridSize.value.cols * CELL + (gridSize.value.cols - 1) * GAP}px`,
columnGap: `${GAP}px`,
rowGap: `${GAP}px`,
}));
// Total points spent in the entire build (across all 5 trees). Used for the
// global budget check.
const totalAllocated = computed(() => const totalAllocated = computed(() =>
Object.values(props.allocations).reduce((a, b) => a + (b || 0), 0), Object.values(props.allocations).reduce((a, b) => a + (b || 0), 0),
); );
// Points spent inside THIS tree only.
const thisTreeAllocated = computed(() => { const thisTreeAllocated = computed(() => {
if (!props.tree) return 0; if (!props.tree) return 0;
let n = 0; let n = 0;
for (const node of props.tree.nodes) { for (const st of props.tree.subtrees) {
n += props.allocations[node.tag] || 0; for (const node of st.nodes) {
n += props.allocations[node.tag] || 0;
}
} }
return n; return n;
}); });
function centerForNode(row: number, col: number) {
const x = (col - 1) * (CELL + GAP) + CELL / 2;
const y = (row - 1) * (CELL + GAP) + CELL / 2;
return { x, y };
}
const edgeLines = computed(() => {
if (!props.tree) return [];
const byTag = new Map(props.tree.nodes.map((n) => [n.tag, n] as const));
const lines: { x1: number; y1: number; x2: number; y2: number }[] = [];
for (const e of props.tree.edges) {
const a = byTag.get(e.from);
const b = byTag.get(e.to);
if (!a || !b) continue;
const pa = centerForNode(a.row, a.col);
const pb = centerForNode(b.row, b.col);
lines.push({ x1: pa.x, y1: pa.y, x2: pb.x, y2: pb.y });
}
return lines;
});
const svgSize = computed(() => {
const w = gridSize.value.cols * CELL + (gridSize.value.cols - 1) * GAP;
const h = gridSize.value.rows * CELL + (gridSize.value.rows - 1) * GAP;
return { w, h };
});
function pointsFor(tag: string): number { function pointsFor(tag: string): number {
return props.allocations[tag] || 0; return props.allocations[tag] || 0;
} }
function add(tag: string, delta: number) { function add(tag: string, delta: number, maxPoints: number) {
if (!props.tree) return;
const node = props.tree.nodes.find((n) => n.tag === tag);
if (!node) return;
const cur = pointsFor(tag); const cur = pointsFor(tag);
const max = node.maxPoints;
let next = cur + delta; let next = cur + delta;
if (next < 0) next = 0; if (next < 0) next = 0;
if (next > max) next = max; if (next > maxPoints) next = maxPoints;
if (delta > 0 && totalAllocated.value + (next - cur) > props.availablePoints) { if (delta > 0 && totalAllocated.value + (next - cur) > props.availablePoints) {
return; // not enough points return;
} }
const out = { ...props.allocations }; const out = { ...props.allocations };
if (next === 0) delete out[tag]; if (next === 0) delete out[tag];
@ -101,110 +50,177 @@ function add(tag: string, delta: number) {
emit('update:allocations', out); emit('update:allocations', out);
} }
function nodeClick(tag: string, e: MouseEvent) { function nodeClick(tag: string, maxPoints: number, e: MouseEvent) {
if (e.shiftKey || e.button === 2) add(tag, -1); if (e.shiftKey || e.button === 2) add(tag, -1, maxPoints);
else add(tag, 1); else add(tag, 1, maxPoints);
} }
function onContext(tag: string, e: MouseEvent) { function onContext(tag: string, maxPoints: number, e: MouseEvent) {
e.preventDefault(); e.preventDefault();
add(tag, -1); add(tag, -1, maxPoints);
}
// Build per-subtree layout metadata.
interface LaidOutSubtree {
name: string;
cols: number;
rows: number;
width: number;
height: number;
nodes: SubtreeNode[];
edges: EdgeLine[];
}
interface SubtreeNode {
tag: string;
id: string;
name: string;
kind: string;
row: number;
col: number;
maxPoints: number;
icon: string | null;
cx: number;
cy: number;
}
interface EdgeLine {
x1: number;
y1: number;
x2: number;
y2: number;
}
function layoutSubtree(st: SkillSubtree): LaidOutSubtree {
const cols = st.cols;
let rows = 0;
for (const n of st.nodes) {
if (n.row > rows) rows = n.row;
}
// pixel center for grid (row,col) where (1,1) is the top-left cell center
const centerFor = (row: number, col: number) => ({
cx: (col - 1) * (CELL + GAP_X) + CELL / 2,
cy: (row - 1) * (CELL + GAP_Y) + CELL / 2,
});
const nodes: SubtreeNode[] = st.nodes.map((n) => {
const c = centerFor(n.row, n.col);
return { ...n, cx: c.cx, cy: c.cy };
});
const byTag = new Map(nodes.map((n) => [n.tag, n] as const));
const edges: EdgeLine[] = [];
for (const e of st.edges) {
const a = byTag.get(e.from);
const b = byTag.get(e.to);
if (!a || !b) continue;
edges.push({ x1: a.cx, y1: a.cy, x2: b.cx, y2: b.cy });
}
const width = cols * CELL + (cols - 1) * GAP_X;
const height = rows * CELL + (rows - 1) * GAP_Y;
return {
name: st.name,
cols,
rows,
width,
height,
nodes,
edges,
};
}
const subtrees = computed<LaidOutSubtree[]>(() => {
if (!props.tree) return [];
return props.tree.subtrees.map(layoutSubtree);
});
function subtreeSpent(st: LaidOutSubtree): number {
let n = 0;
for (const node of st.nodes) {
n += props.allocations[node.tag] || 0;
}
return n;
} }
</script> </script>
<template> <template>
<div> <div>
<div <div class="tree-toolbar">
style=" <div class="tree-stats">
display: flex; <span class="sand">{{ thisTreeAllocated }}</span> in this tree
align-items: center; <span class="dot">·</span>
justify-content: space-between; <span :class="{ over: totalAllocated > availablePoints }">
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
"
>
<div
style="
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-dim);
"
>
<span style="color: var(--sand)">{{ thisTreeAllocated }}</span> in this
tree
<span style="color: var(--ink-muted); margin: 0 8px">·</span>
<span :style="{ color: totalAllocated > availablePoints ? 'var(--ember)' : 'var(--ink-dim)' }">
{{ totalAllocated }} {{ totalAllocated }}
</span> </span>
/ {{ availablePoints }} total / {{ availablePoints }} total
</div> </div>
<div <div class="tree-hint">
style="
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--ink-muted);
letter-spacing: 0.16em;
"
>
Click +1 · Shift-click / right-click 1 Click +1 · Shift-click / right-click 1
</div> </div>
<button @click="emit('reset')">Reset All Trees</button> <button @click="emit('reset')">Reset All Trees</button>
</div> </div>
<div class="tree-wrap" v-if="tree"> <div class="subtree-row" v-if="tree">
<div class="tree-grid" :style="gridStyle"> <div v-for="st in subtrees" :key="st.name" class="subtree">
<svg <div class="subtree-head">
class="tree-edges" <h3>{{ st.name }}</h3>
:viewBox="`0 0 ${svgSize.w} ${svgSize.h}`" <span class="subtree-pts" v-if="subtreeSpent(st) > 0">
:width="svgSize.w" {{ subtreeSpent(st) }} pts
:height="svgSize.h" </span>
preserveAspectRatio="none" </div>
>
<line
v-for="(ln, i) in edgeLines"
:key="i"
:x1="ln.x1"
:y1="ln.y1"
:x2="ln.x2"
:y2="ln.y2"
stroke="var(--line)"
stroke-width="2"
/>
</svg>
<div <div
v-for="n in tree.nodes" class="subtree-canvas"
:key="n.tag"
class="tree-node-wrap"
:style="{ :style="{
gridColumn: n.col, width: st.width + 'px',
gridRow: n.row, height: st.height + 'px',
}" }"
> >
<div <svg
:class="[ class="tree-edges"
'tree-node', :viewBox="`0 0 ${st.width} ${st.height}`"
`kind-${n.kind.toLowerCase()}`, :width="st.width"
pointsFor(n.tag) > 0 ? 'allocated' : '', :height="st.height"
pointsFor(n.tag) >= n.maxPoints ? 'maxed' : '', preserveAspectRatio="none"
]"
@click="(e) => nodeClick(n.tag, e)"
@contextmenu="(e) => onContext(n.tag, e)"
:title="`${n.name} (${n.kind})`"
> >
<img <line
v-if="n.icon" v-for="(ln, i) in st.edges"
:src="`/icons/${n.icon}`" :key="i"
:alt="n.name" :x1="ln.x1"
class="node-icon" :y1="ln.y1"
loading="lazy" :x2="ln.x2"
draggable="false" :y2="ln.y2"
stroke="var(--line)"
stroke-width="2"
/> />
<div v-else class="name">{{ n.name }}</div> </svg>
<div class="pts">{{ pointsFor(n.tag) }}/{{ n.maxPoints }}</div> <div
v-for="n in st.nodes"
:key="n.tag"
class="abs-node-wrap"
:style="{
left: n.cx - 36 + 'px',
top: n.cy - 36 + 'px',
}"
>
<div
:class="[
'tree-node',
`kind-${n.kind.toLowerCase()}`,
pointsFor(n.tag) > 0 ? 'allocated' : '',
pointsFor(n.tag) >= n.maxPoints ? 'maxed' : '',
]"
@click="(e) => nodeClick(n.tag, n.maxPoints, e)"
@contextmenu="(e) => onContext(n.tag, n.maxPoints, e)"
:title="`${n.name} (${n.kind})`"
>
<img
v-if="n.icon"
:src="`/icons/${n.icon}`"
:alt="n.name"
class="node-icon"
loading="lazy"
draggable="false"
/>
<div v-else class="name">{{ n.name }}</div>
<div class="pts">{{ pointsFor(n.tag) }}/{{ n.maxPoints }}</div>
</div>
<div class="label">{{ n.name }}</div>
</div> </div>
<div class="label">{{ n.name }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -213,3 +229,162 @@ function onContext(tag: string, e: MouseEvent) {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.tree-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.tree-stats {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-dim);
}
.tree-stats .sand { color: var(--sand); }
.tree-stats .over { color: var(--ember); }
.tree-stats .dot { color: var(--ink-muted); margin: 0 8px; }
.tree-hint {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--ink-muted);
letter-spacing: 0.16em;
}
.subtree-row {
display: flex;
flex-wrap: wrap;
gap: 48px 56px;
justify-content: center;
background: var(--bg);
border: 1px solid var(--line-soft);
border-radius: 4px;
padding: 36px 32px;
overflow-x: auto;
}
.subtree {
display: flex;
flex-direction: column;
align-items: center;
}
.subtree-head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 18px;
}
.subtree-head h3 {
font-family: 'Cormorant Garamond', serif;
font-size: 22px;
margin: 0;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--ink-dim);
}
.subtree-pts {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--sand);
letter-spacing: 0.14em;
}
.subtree-canvas {
position: relative;
}
.tree-edges {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.abs-node-wrap {
position: absolute;
width: 72px;
height: 72px;
z-index: 1;
}
.abs-node-wrap .label {
position: absolute;
top: 76px;
left: 50%;
transform: translateX(-50%);
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
letter-spacing: 0.04em;
color: var(--ink-dim);
text-align: center;
/* Constrain to node width so adjacent labels can't overlap horizontally.
Long names wrap to multiple lines, but the row gap (44px) gives them
room. */
width: 72px;
pointer-events: none;
text-shadow: 0 0 4px var(--bg);
line-height: 1.15;
word-wrap: break-word;
overflow-wrap: anywhere;
}
.tree-node {
width: 72px;
height: 72px;
border: 2px solid var(--line);
border-radius: 8px;
background: var(--bg-2);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.tree-node:hover { border-color: var(--sand-2); }
.tree-node.allocated {
border-color: var(--spice);
background: rgba(224, 138, 60, 0.1);
}
.tree-node.maxed {
border-color: var(--sand);
background: rgba(230, 201, 138, 0.14);
}
.tree-node.kind-ability { border-style: solid; }
.tree-node.kind-attribute { border-style: dashed; }
.tree-node.kind-perk { border-radius: 50%; }
.tree-node.kind-spice { border-style: double; }
.tree-node .node-icon {
width: 56px;
height: 56px;
object-fit: contain;
filter: brightness(0.7) saturate(0.85);
transition: filter 0.15s;
pointer-events: none;
user-select: none;
}
.tree-node:hover .node-icon { filter: brightness(0.95); }
.tree-node.allocated .node-icon { filter: brightness(1) saturate(1.1); }
.tree-node.maxed .node-icon {
filter: brightness(1.1) saturate(1.2) drop-shadow(0 0 6px var(--sand-2));
}
.tree-node .name {
font-family: 'Cormorant Garamond', serif;
font-size: 12px;
text-align: center;
line-height: 1.1;
padding: 2px;
color: var(--ink-dim);
}
.tree-node .pts {
position: absolute;
bottom: 2px;
right: 2px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 1px 4px;
border-radius: 2px;
color: var(--sand);
}
</style>

View file

@ -19,9 +19,20 @@ export function defaultBuild(): BuildState {
specs, specs,
faction: { tier: 0, standingInto: 0 }, faction: { tier: 0, standingInto: 0 },
skills: {}, skills: {},
loadout: { abilities: [null, null, null], techniques: [null, null, null] },
}; };
} }
function normalizeSlots(raw: unknown, len: number): (string | null)[] {
const arr = Array.isArray(raw) ? raw : [];
const out: (string | null)[] = [];
for (let i = 0; i < len; i++) {
const v = arr[i];
out.push(typeof v === 'string' && v.length > 0 ? v : null);
}
return out;
}
function migrate(raw: any): BuildState { function migrate(raw: any): BuildState {
const def = defaultBuild(); const def = defaultBuild();
if (!raw || typeof raw !== 'object') return def; if (!raw || typeof raw !== 'object') return def;
@ -33,6 +44,10 @@ function migrate(raw: any): BuildState {
specs: { ...def.specs }, specs: { ...def.specs },
faction: { ...def.faction, ...(raw.faction || {}) }, faction: { ...def.faction, ...(raw.faction || {}) },
skills: { ...(raw.skills || {}) }, skills: { ...(raw.skills || {}) },
loadout: {
abilities: normalizeSlots(raw.loadout?.abilities, 3),
techniques: normalizeSlots(raw.loadout?.techniques, 3),
},
}; };
if (raw.specs && typeof raw.specs === 'object') { if (raw.specs && typeof raw.specs === 'object') {
for (const k of Object.keys(out.specs) as SpecId[]) { for (const k of Object.keys(out.specs) as SpecId[]) {

View file

@ -66,11 +66,17 @@ export interface SkillEdge {
to: string; to: string;
} }
export interface SkillSubtree {
name: string;
cols: number;
nodes: SkillNode[];
edges: SkillEdge[];
}
export interface SkillTree { export interface SkillTree {
id: ClassId; id: ClassId;
name: string; name: string;
nodes: SkillNode[]; subtrees: SkillSubtree[];
edges: SkillEdge[];
} }
export interface SpecProgress { export interface SpecProgress {
@ -78,6 +84,14 @@ export interface SpecProgress {
xpInto: number; xpInto: number;
} }
// Loadout: 3 active Ability slots + 3 active Technique slots, both pulled
// from the union of all allocated skills across all 5 trees (the cap is
// global, not per-tree).
export interface Loadout {
abilities: (string | null)[]; // length 3 — element is a skill tag, or null
techniques: (string | null)[]; // length 3
}
export interface BuildState { export interface BuildState {
v: 1; // schema version v: 1; // schema version
house: House; house: House;
@ -95,4 +109,5 @@ export interface BuildState {
// Allocated skill points keyed by full tag (e.g. Skills.Ability.BattleCry). // Allocated skill points keyed by full tag (e.g. Skills.Ability.BattleCry).
// Tags are globally unique across classes, so one flat map works for all 5. // Tags are globally unique across classes, so one flat map works for all 5.
skills: Record<string, number>; skills: Record<string, number>;
loadout: Loadout;
} }

View file

@ -216,109 +216,123 @@ LINE_RE = re.compile(
) )
SUBTREE_H3_RE = re.compile(
r'<h3[^>]*class="[^"]*text-xl[^"]*"[^>]*>([^<]+)</h3>'
)
GRID_COLS_RE = re.compile(r"grid-template-columns:\s*repeat\((\d+),\s*72px\)")
def _extract_node(chunk: str, tag: str) -> dict | None:
gm = GRID_RE.search(chunk)
if not gm:
return None
row, col = int(gm.group(1)), int(gm.group(2))
alt = ALT_RE.search(chunk)
href = HREF_RE.search(chunk)
icon = ICON_RE.search(chunk)
max_pts = MAX_PTS_RE.search(chunk)
kind = tag.split(".")[1] if "." in tag else "Unknown"
return {
"tag": tag,
"id": tag.split(".")[-1],
"name": alt.group(1) if alt else tag.split(".")[-1],
"kind": kind, # Ability | Attribute | Perk | Spice
"row": row,
"col": col,
"maxPoints": int(max_pts.group(1)) if max_pts else 1,
"icon": icon.group(1) if icon else None,
"url": href.group(1) if href else None,
}
def _map_edges(html_slice: str, nodes: list[dict]) -> list[dict]:
"""Pixel-match connector <line> endpoints to the nearest nodes in this
subtree. Returns deduped edge list."""
lines = list(LINE_RE.finditer(html_slice))
if not lines or not nodes:
return []
all_x = [int(x) for ln in lines for x in (ln.group("x1"), ln.group("x2"))]
all_y = [int(y) for ln in lines for y in (ln.group("y1"), ln.group("y2"))]
min_x, max_x = min(all_x), max(all_x)
min_y, max_y = min(all_y), max(all_y)
cols = [n["col"] for n in nodes]
rows_ = [n["row"] for n in nodes]
min_c, max_c = min(cols), max(cols)
min_r, max_r = min(rows_), max(rows_)
sx = (max_x - min_x) / max(1, (max_c - min_c))
sy = (max_y - min_y) / max(1, (max_r - min_r))
centers = {
n["tag"]: (min_x + (n["col"] - min_c) * sx, min_y + (n["row"] - min_r) * sy)
for n in nodes
}
def nearest(x: int, y: int) -> str | None:
best, best_d = None, float("inf")
for tag, (cx, cy) in centers.items():
d = (cx - x) ** 2 + (cy - y) ** 2
if d < best_d:
best_d, best = d, tag
return best
seen, edges = set(), []
for ln in lines:
x1, y1, x2, y2 = (int(ln.group(k)) for k in ("x1", "y1", "x2", "y2"))
a, b = nearest(x1, y1), nearest(x2, y2)
if a and b and a != b:
key = tuple(sorted((a, b)))
if key not in seen:
seen.add(key)
edges.append({"from": key[0], "to": key[1]})
return edges
def extract_skill_tree(path: Path, class_id: str, class_name: str) -> dict: def extract_skill_tree(path: Path, class_id: str, class_name: str) -> dict:
"""Parse a class skill tree into its named subtrees.
Each class is composed of 3 subtrees (e.g. Swordmaster has "The Blade",
"The Will", "The Way"). Each subtree is its own CSS grid with its own
column count, node positions, and connectors. Treating them as one big
grid (the prior behavior) collapsed all 22 nodes on top of each other.
"""
html = path.read_text() html = path.read_text()
nodes = [] # Split on subtree H3 headers; first chunk is preamble.
# We need to also find the alt text + href + icon WITHIN each node's HTML. chunks = SUBTREE_H3_RE.split(html)
# Strategy: walk through all data-tag="Skills..." occurrences, slice from preamble, pairs = chunks[0], chunks[1:]
# opening of the node <div> to a balanced close. Simple slice: take 2000 chars
# after the tag and parse first alt/href/icon/max within it. subtrees: list[dict] = []
for m in re.finditer(r'data-tag="(Skills\.[^"]+)"', html): for i in range(0, len(pairs), 2):
tag = m.group(1) name = pairs[i].strip()
start = m.start() body = pairs[i + 1] if i + 1 < len(pairs) else ""
chunk = html[start : start + 2500] # Slice off anything that belongs to the next subtree (already handled
gm = GRID_RE.search(chunk) # by split) or to trailing page chrome — search for the closing of the
if not gm: # graph div by counting from the start of the graph element.
graph_start = body.find('<div class="graph svelte-1dvag2h"')
if graph_start < 0:
continue continue
row, col = int(gm.group(1)), int(gm.group(2)) body = body[graph_start:]
alt = ALT_RE.search(chunk) cols_m = GRID_COLS_RE.search(body)
href = HREF_RE.search(chunk) cols = int(cols_m.group(1)) if cols_m else 3
icon = ICON_RE.search(chunk)
max_pts = MAX_PTS_RE.search(chunk) # Parse nodes inside this subtree.
kind = tag.split(".")[1] if "." in tag else "Unknown" nodes: list[dict] = []
nodes.append( seen_tags: set[str] = set()
{ for m in re.finditer(r'data-tag="(Skills\.[^"]+)"', body):
"tag": tag, tag = m.group(1)
"id": tag.split(".")[-1], if tag in seen_tags:
"name": alt.group(1) if alt else tag.split(".")[-1], continue
"kind": kind, # Ability | Attribute | Perk | Spice chunk = body[m.start() : m.start() + 2500]
"row": row, node = _extract_node(chunk, tag)
"col": col, if node:
"maxPoints": int(max_pts.group(1)) if max_pts else 1, nodes.append(node)
"icon": icon.group(1) if icon else None, seen_tags.add(tag)
"url": href.group(1) if href else None,
} edges = _map_edges(body, nodes)
subtrees.append(
{"name": name, "cols": cols, "nodes": nodes, "edges": edges}
) )
# de-duplicate nodes by tag (the regex can match twice if the same tag appears in return {"id": class_id, "name": class_name, "subtrees": subtrees}
# a connector tooltip etc.)
seen = {}
for n in nodes:
if n["tag"] not in seen:
seen[n["tag"]] = n
nodes = list(seen.values())
# Build a position->node lookup. Grid is roughly square with ~73px cells based
# on observed example (grid 3/5 -> center 364,220 means col*~73, row*~73 with offset).
# We'll learn cell size from the data: if there are connectors, we map each (x,y) to
# the nearest node by Euclidean distance.
# First compute approximate node centers via grid math, calibrated from any node
# we can pin: actually a more reliable approach is to use the connector geometry.
edges = []
lines = list(LINE_RE.finditer(html))
if lines and nodes:
# Calibrate: find scale by looking at min/max grid coords vs min/max line coords.
all_x = [int(x) for ln in lines for x in (ln.group("x1"), ln.group("x2"))]
all_y = [int(y) for ln in lines for y in (ln.group("y1"), ln.group("y2"))]
min_x, max_x = min(all_x), max(all_x)
min_y, max_y = min(all_y), max(all_y)
cols = [n["col"] for n in nodes]
rows = [n["row"] for n in nodes]
min_c, max_c = min(cols), max(cols)
min_r, max_r = min(rows), max(rows)
# avoid div by zero
sx = (max_x - min_x) / max(1, (max_c - min_c))
sy = (max_y - min_y) / max(1, (max_r - min_r))
def center(n):
return (
min_x + (n["col"] - min_c) * sx,
min_y + (n["row"] - min_r) * sy,
)
centers = {n["tag"]: center(n) for n in nodes}
def nearest(x, y):
best_tag, best_d = None, float("inf")
for t, (cx, cy) in centers.items():
d = (cx - x) ** 2 + (cy - y) ** 2
if d < best_d:
best_d = d
best_tag = t
return best_tag
seen_edges = set()
for ln in lines:
x1, y1, x2, y2 = (
int(ln.group("x1")),
int(ln.group("y1")),
int(ln.group("x2")),
int(ln.group("y2")),
)
a = nearest(x1, y1)
b = nearest(x2, y2)
if a and b and a != b:
key = tuple(sorted((a, b)))
if key not in seen_edges:
seen_edges.add(key)
edges.append({"from": key[0], "to": key[1]})
return {
"id": class_id,
"name": class_name,
"nodes": nodes,
"edges": edges,
}
# ---------- main ---------- # ---------- main ----------
@ -413,13 +427,16 @@ def main():
continue continue
tree = extract_skill_tree(path, cls_id, cls_name) tree = extract_skill_tree(path, cls_id, cls_name)
(OUT / f"skills-{cls_id}.json").write_text(json.dumps(tree, indent=2)) (OUT / f"skills-{cls_id}.json").write_text(json.dumps(tree, indent=2))
total_nodes = sum(len(st["nodes"]) for st in tree["subtrees"])
total_edges = sum(len(st["edges"]) for st in tree["subtrees"])
manifest["skills"].append( manifest["skills"].append(
{ {
"id": cls_id, "id": cls_id,
"name": cls_name, "name": cls_name,
"file": f"skills-{cls_id}.json", "file": f"skills-{cls_id}.json",
"nodes": len(tree["nodes"]), "subtrees": [st["name"] for st in tree["subtrees"]],
"edges": len(tree["edges"]), "nodes": total_nodes,
"edges": total_edges,
} }
) )
@ -429,9 +446,10 @@ def main():
path = OUT / f"skills-{cls_id}.json" path = OUT / f"skills-{cls_id}.json"
if path.exists(): if path.exists():
tree = json.loads(path.read_text()) tree = json.loads(path.read_text())
for n in tree["nodes"]: for st in tree["subtrees"]:
if n.get("icon"): for n in st["nodes"]:
icon_names.add(n["icon"]) if n.get("icon"):
icon_names.add(n["icon"])
for spec in specs: for spec in specs:
slug = spec.lower() slug = spec.lower()
spec_data = json.loads((OUT / f"spec-{slug}.json").read_text()) spec_data = json.loads((OUT / f"spec-{slug}.json").read_text())
@ -440,6 +458,18 @@ def main():
if p.get("icon"): if p.get("icon"):
icon_names.add(p["icon"]) icon_names.add(p["icon"])
copied, missing = copy_icons(icon_names) copied, missing = copy_icons(icon_names)
# Slot background images for the global Abilities + Techniques loadout —
# the source HTML references them from a CDN, but local copies live in
# the per-class _files directories.
for src_name, dst_name in [
("ability.png", "slot-ability.png"),
("technique.png", "slot-technique.png"),
]:
src = find_icon_source(src_name)
if src:
shutil.copy2(src, ICONS_OUT / dst_name)
manifest["icons"] = { manifest["icons"] = {
"directory": "frontend/public/icons", "directory": "frontend/public/icons",
"served_at": "/icons/", "served_at": "/icons/",