diff --git a/js/atree.js b/js/atree.js index e5a6b2b..9fc62fa 100644 --- a/js/atree.js +++ b/js/atree.js @@ -318,77 +318,88 @@ const atree_validate = new (class extends ComputeNode { if (atree_order.length == 0) { return [0, ['no atree data']]; } - let errors = []; - let reachable = new Map(); - atree_dfs_mark(atree_order[0], atree_state, reachable); + console.log(atree_state); + + let atree_to_add = []; + for (const node of atree_order) { + const abil = node.ability; + if (atree_state.get(abil.id).active) { atree_to_add.push([node, 'not reachable', false]); } + } + + let reachable = new Set(); let abil_points_total = 0; let archetype_count = new Map(); + while (true) { + let _add = []; + for (const [node, fail_reason, fail_hardness] of atree_to_add) { + const {parents, ability} = node; + if (parents.length === 0) { + reachable.add(ability.id); + // root abil has no archetype. + abil_points_total += ability.cost; + continue; + } + let failed_deps = []; + for (const dep_id of ability.dependencies) { + if (!atree_state.get(dep_id).active) { failed_deps.push(dep_id) } + } + if (failed_deps.length > 0) { + const dep_strings = failed_deps.map(i => '"' + atree_state.get(i).ability.display_name + '"'); + _add.push([node, 'missing dep: ' + dep_strings.join(", "), true]); + continue; + } + let blocking_ids = []; + for (const blocker_id of ability.blockers) { + if (atree_state.get(blocker_id).active) { blocking_ids.push(blocker_id); } + } + if (blocking_ids.length > 0) { + const blockers_strings = blocking_ids.map(i => '"' + atree_state.get(i).ability.display_name + '"'); + _add.push([node, 'blocked by: '+blockers_strings.join(", "), true]); + continue; + } + let node_reachable = false; + for (const parent of parents) { + if (reachable.has(parent.ability.id)) { + node_reachable = true; + break; + } + } + if (!node_reachable) { + _add.push([node, 'not reachable', false]) + continue; + } + if ('archetype' in ability && ability.archetype !== "") { + if ('archetype_req' in ability && ability.archetype_req !== 0) { + const others = archetype_count.get(ability.archetype); + if (others < ability.archetype_req) { + _add.push([node, abil.archetype+': '+others+' < '+abil.archetype_req, false]) + continue; + } + } + let val = 1; + if (archetype_count.has(ability.archetype)) { + val = archetype_count.get(ability.archetype) + 1; + } + archetype_count.set(ability.archetype, val); + } + abil_points_total += ability.cost; + reachable.add(ability.id); + } + if (atree_to_add.length == _add.length) { + break; + } + atree_to_add = _add; + } let hard_error = false; - for (const node of atree_order) { - const abil = node.ability; - if (!atree_state.get(abil.id).active) { continue; } - abil_points_total += abil.cost; - if (!reachable.get(abil.id)) { errors.push(abil.display_name + ' is not reachable!'); } - - let failed_deps = []; - for (const dep_id of abil.dependencies) { - if (!atree_state.get(dep_id).active) { failed_deps.push(dep_id) } - } - if (failed_deps.length > 0) { - const dep_string = failed_deps.map(i => '"' + atree_state.get(i).ability.display_name + '"'); - errors.push(abil.display_name + ' dependencies not satisfied: ' + dep_string.join(", ")); - hard_error = true; - } - - let blocking_ids = []; - for (const blocker_id of abil.blockers) { - if (atree_state.get(blocker_id).active) { blocking_ids.push(blocker_id); } - } - if (blocking_ids.length > 0) { - const blockers_string = blocking_ids.map(i => '"' + atree_state.get(i).ability.display_name + '"'); - errors.push(abil.display_name+' is blocked by: '+blockers_string.join(", ")); - hard_error = true; - } - - if ('archetype' in abil && abil.archetype !== "") { - let val = 1; - if (archetype_count.has(abil.archetype)) { - val = archetype_count.get(abil.archetype) + 1; - } - archetype_count.set(abil.archetype, val); - } + let errors = []; + for (const [node, fail_reason, fail_hardness] of atree_to_add) { + if (fail_hardness) { hard_error = true; } + errors.push(node.ability.display_name + ": " + fail_reason); } - // TODO: FIX THIS! ARCHETYPE REQ IS A PAIN IN THE ASS - // it doesn't follow topological order and theres some cases where "equip order" matters. - for (const node of atree_order) { - const abil = node.ability; - if (!atree_state.get(abil.id).active) { continue; } - if ('archetype_req' in abil && abil.archetype_req !== 0) { - const others = archetype_count.get(abil.archetype) - 1; - if (others < abil.archetype_req) { - errors.push(abil.display_name+' fails archetype: '+abil.archetype+': '+others+' < '+abil.archetype_req) - } - } - } - - if (abil_points_total > 45) { - errors.push('too many ability points assigned! ('+abil_points_total+' > 45)'); - } - return [abil_points_total, hard_error, errors]; } })().link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state'); -function atree_dfs_mark(start, atree_state, mark) { - if (mark.get(start.ability.id)) { return; } - mark.set(start.ability.id, true); - for (const child of start.children) { - if (atree_state.get(child.ability.id).active) { - atree_dfs_mark(child, atree_state, mark); - } - } -} - /** * Render ability tree. * Return map of id -> corresponding html element. @@ -420,10 +431,19 @@ const atree_render_active = new (class extends ComputeNode { error_title.innerHTML = "ATree Error!"; errorbox.appendChild(error_title); - for (const error of errors) { - let atree_warning = document.createElement("p"); - atree_warning.classList.add("warning", "small-text"); - atree_warning.textContent = error; + for (let i = 0; i < 5 && i < errors.length; ++i) { + const error = errors[i]; + const atree_warning = make_elem("p", ["warning", "small-text"], {textContent: error}); + errorbox.appendChild(atree_warning); + } + if (errors.length > 5) { + const error = '... ' + errors.length-5 + ' errors not shown'; + const atree_warning = make_elem("p", ["warning", "small-text"], {textContent: error}); + errorbox.appendChild(atree_warning); + } + if (abil_points_total > 45) { + const error = 'too many ability points assigned! ('+abil_points_total+' > 45)'; + const atree_warning = make_elem("p", ["warning", "small-text"], {textContent: error}); errorbox.appendChild(atree_warning); } } @@ -652,7 +672,8 @@ const atree_stats = new (class extends ComputeNode { if (effect.slider) { // TODO: handle const slider_val = interactive_map.get(effect.slider_name).slider.value; - const total = parseInt(slider_val) * effect.scaling[0]; + let total = parseInt(slider_val) * effect.scaling[0]; + if ('max' in effect && total > effect.max) { total = effect.max; } if (Array.isArray(effect.output)) { for (const output of effect.output) { if (output.type === 'stat') { @@ -667,13 +688,12 @@ const atree_stats = new (class extends ComputeNode { } } else { - const cap = effect.max; // TODO: type: prop? let total = 0; for (const [scaling, input] of zip2(effect.scaling, effect.inputs)) { total += scaling * item_stats.get(input.name); } - if (total > cap) { total = cap; } + if ('max' in effect && total > effect.max) { total = effect.max; } // TODO: output (list...) if (Array.isArray(effect.output)) { for (const output of effect.output) { diff --git a/js/atree_constants.js b/js/atree_constants.js index f7500cc..c619362 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -2888,14 +2888,12 @@ const atrees = { { "display_name": "Provoke", - "desc": "Mobs damaged by War Scream will target only you for at least 5s \n\nReduce the Mana cost of War Scream", - "archetype": "Paladin", - "archetype_req": 0, + "desc": "Mobs damaged by War Scream will target only you for at least 5s. Reduce the Mana cost of War Scream", "base_abil": "War Scream", "parents": ["Aerodynamics", "Mantle of the Bovemists"], "dependencies": [], "blockers": [], - "cost": 1, + "cost": 2, "display": { "row": 17, "col": 7,