2022-06-28 18:43:35 +00:00
/ * *
ATreeNode spec :
ATreeNode : {
children : List [ ATreeNode ] // nodes that this node can link to downstream (or sideways)
parents : List [ ATreeNode ] // nodes that can link to this one from upstream (or sideways)
ability : atree _node // raw data from atree json
}
atree _node : {
display _name : str
id : int
desc : str
archetype : Optional [ str ] // not present or empty string = no arch
archetype _req : Optional [ int ] // default: 0
base _abil : Optional [ int ] // Modify another abil? poorly defined...
parents : List [ int ]
dependencies : List [ int ] // Hard reqs
blockers : List [ int ] // If any in here are taken, i am invalid
cost : int // cost in AP
display : { // stuff for rendering ATree
row : int
col : int
icon : str
}
properties : Map [ str , float ] // Dynamic (modifiable) misc. properties; ex. AOE
effects : List [ effect ]
}
effect : replace _spell | add _spell _prop | convert _spell _conv | raw _stat | stat _scaling
replace _spell : {
type : "replace_spell"
... rest of fields are same as ` spell ` type ( see : damage _calc . js )
}
add _spell _prop : {
type : "add_spell_prop"
base _spell : int // spell identifier
target _part : Optional [ str ] // Part of the spell to modify. Can be not present/empty for ex. cost modifier.
// If target part does not exist, a new part is created.
2022-07-05 01:02:21 +00:00
behavior : Optional [ str ] // One of: "merge", "modify". default: merge
// merge: add if exist, make new part if not exist
// modify: change existing part. do nothing if not exist
2022-06-28 18:43:35 +00:00
cost : Optional [ int ] // change to spellcost
multipliers : Optional [ array [ float , 6 ] ] // Additive changes to spellmult (for damage spell)
power : Optional [ float ] // Additive change to healing power (for heal spell)
hits : Optional [ Map [ str , float ] ] // Additive changes to hits (for total entry)
display : Optional [ str ] // Optional change to the displayed entry. Replaces old
}
convert _spell _conv : {
2022-06-30 12:27:35 +00:00
type : "convert_spell_conv"
base _spell : int // spell identifier
target _part : "all" | str // Part of the spell to modify. Can be not present/empty for ex. cost modifier.
// "all" means modify all parts.
2022-06-30 15:58:26 +00:00
conversion : element _str
2022-06-28 18:43:35 +00:00
}
raw _stat : {
2022-06-30 15:58:26 +00:00
type : "raw_stat"
2022-07-01 08:23:06 +00:00
toggle : Optional [ bool | str ] // default: false; true means create anon. toggle,
// string value means bind to (or create) named button
2022-07-05 04:39:35 +00:00
behavior : Optional [ str ] // One of: "merge", "modify". default: merge
// merge: add if exist, make new part if not exist
// modify: change existing part. do nothing if not exist
2022-06-30 15:58:26 +00:00
bonuses : List [ stat _bonus ]
2022-06-28 18:43:35 +00:00
}
stat _bonus : {
"type" : "stat" | "prop" ,
"abil" : Optional [ int ] ,
"name" : str ,
"value" : float
}
stat _scaling : {
"type" : "stat_scaling" ,
"slider" : bool ,
"slider_name" : Optional [ str ] ,
"slider_step" : Optional [ float ] ,
2022-07-07 04:15:41 +00:00
slider _behavior : Optional [ str ] // One of: "merge", "modify". default: merge
// merge: add if exist, make new part if not exist
// modify: change existing part. do nothing if not exist
slider _max : Optional [ float ] // affected by slider_behavior
2022-06-28 18:43:35 +00:00
"inputs" : Optional [ list [ scaling _target ] ] ,
2022-06-29 07:14:40 +00:00
"output" : scaling _target | List [ scaling _target ] ,
2022-06-28 18:43:35 +00:00
"scaling" : list [ float ] ,
"max" : float
}
scaling _target : {
"type" : "stat" | "prop" ,
"abil" : Optional [ int ] ,
"name" : str
}
* /
2022-07-09 03:31:44 +00:00
const elem _mastery _abil = { display _name : "Elemental Mastery" , id : 998 , properties : { } , effects : [ ] } ;
2022-06-28 18:43:35 +00:00
// TODO: Range numbers
const default _abils = {
2022-07-11 13:15:53 +00:00
Mage : [ {
2022-06-28 18:43:35 +00:00
display _name : "Mage Melee" ,
id : 999 ,
desc : "Mage basic attack." ,
properties : { range : 5000 } ,
2022-06-29 06:23:27 +00:00
effects : [ default _spells . wand [ 0 ] ]
2022-07-09 03:31:44 +00:00
} , elem _mastery _abil ] ,
2022-07-11 13:15:53 +00:00
Warrior : [ {
2022-06-28 18:43:35 +00:00
display _name : "Warrior Melee" ,
id : 999 ,
desc : "Warrior basic attack." ,
properties : { range : 2 } ,
2022-06-29 06:23:27 +00:00
effects : [ default _spells . spear [ 0 ] ]
2022-07-09 03:31:44 +00:00
} , elem _mastery _abil ] ,
2022-07-11 13:15:53 +00:00
Archer : [ {
2022-06-28 18:43:35 +00:00
display _name : "Archer Melee" ,
id : 999 ,
desc : "Archer basic attack." ,
properties : { range : 20 } ,
2022-06-29 06:23:27 +00:00
effects : [ default _spells . bow [ 0 ] ]
2022-07-09 03:31:44 +00:00
} , elem _mastery _abil ] ,
2022-07-11 13:15:53 +00:00
Assassin : [ {
2022-06-28 18:43:35 +00:00
display _name : "Assassin Melee" ,
id : 999 ,
desc : "Assassin basic attack." ,
properties : { range : 2 } ,
2022-06-29 06:23:27 +00:00
effects : [ default _spells . dagger [ 0 ] ]
2022-07-09 03:31:44 +00:00
} , elem _mastery _abil ] ,
2022-07-11 13:15:53 +00:00
Shaman : [ {
2022-06-28 18:43:35 +00:00
display _name : "Shaman Melee" ,
id : 999 ,
desc : "Shaman basic attack." ,
properties : { range : 15 , speed : 0 } ,
2022-06-29 06:23:27 +00:00
effects : [ default _spells . relik [ 0 ] ]
2022-07-09 03:31:44 +00:00
} , elem _mastery _abil ] ,
2022-06-28 18:43:35 +00:00
} ;
2022-06-26 23:49:35 +00:00
/ * *
* Update ability tree internal representation . ( topologically sorted node list )
*
2022-07-01 05:22:15 +00:00
* Signature : AbilityTreeUpdateNode ( player - class : str ) => ATree ( List of atree nodes in topological order )
2022-06-26 23:49:35 +00:00
* /
const atree _node = new ( class extends ComputeNode {
constructor ( ) { super ( 'builder-atree-update' ) ; }
compute _func ( input _map ) {
2022-07-01 05:22:15 +00:00
if ( input _map . size !== 1 ) { throw "AbilityTreeUpdateNode accepts exactly one input (player-class)" ; }
const [ player _class ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
2022-06-26 23:49:35 +00:00
2022-07-01 05:22:15 +00:00
const atree _raw = atrees [ player _class ] ;
2022-07-05 07:51:02 +00:00
if ( ! atree _raw ) return [ ] ;
2022-06-26 23:49:35 +00:00
let atree _map = new Map ( ) ;
let atree _head ;
for ( const i of atree _raw ) {
2022-06-28 18:43:35 +00:00
atree _map . set ( i . id , { children : [ ] , ability : i } ) ;
2022-06-26 23:49:35 +00:00
if ( i . parents . length == 0 ) {
// Assuming there is only one head.
atree _head = atree _map . get ( i . id ) ;
}
}
for ( const i of atree _raw ) {
let node = atree _map . get ( i . id ) ;
let parents = [ ] ;
2022-06-28 18:43:35 +00:00
for ( const parent _id of node . ability . parents ) {
2022-06-26 23:49:35 +00:00
let parent _node = atree _map . get ( parent _id ) ;
parent _node . children . push ( node ) ;
parents . push ( parent _node ) ;
}
node . parents = parents ;
}
2022-07-08 08:20:05 +00:00
console . log ( atree _map ) ;
2022-06-26 23:49:35 +00:00
let atree _topo _sort = [ ] ;
topological _sort _tree ( atree _head , atree _topo _sort , new Map ( ) ) ;
atree _topo _sort . reverse ( ) ;
return atree _topo _sort ;
}
} ) ( ) ;
2022-07-07 22:33:00 +00:00
/ * *
* Create a reverse topological sort of the tree in the result list .
2022-07-07 23:13:26 +00:00
* NOTE : our structure isn 't a tree... it isn' t even acyclic ... but do it anyway i guess ...
2022-07-07 22:33:00 +00:00
*
* https : //en.wikipedia.org/wiki/Topological_sorting
* @ param tree : Root of tree to sort
* @ param res : Result list ( reverse topological order )
* @ param mark _state : Bookkeeping . Call with empty Map ( )
* /
function topological _sort _tree ( tree , res , mark _state ) {
const state = mark _state . get ( tree ) ;
if ( state === undefined ) {
// unmarked.
mark _state . set ( tree , false ) ; // temporary mark
for ( const child of tree . children ) {
topological _sort _tree ( child , res , mark _state ) ;
}
mark _state . set ( tree , true ) ; // permanent mark
res . push ( tree ) ;
}
// these cases are not needed. Case 1 does nothing, case 2 should never happen.
// else if (state === true) { return; } // permanent mark.
// else if (state === false) { throw "not a DAG"; } // temporary mark.
}
2022-06-26 23:49:35 +00:00
/ * *
* Display ability tree from topologically sorted list .
*
2022-06-28 18:43:35 +00:00
* Signature : AbilityTreeRenderNode ( atree : ATree ) => RenderedATree ( Map [ id , RenderedATNode ] )
2022-06-26 23:49:35 +00:00
* /
const atree _render = new ( class extends ComputeNode {
2022-06-30 15:03:41 +00:00
constructor ( ) {
super ( 'builder-atree-render' ) ;
this . fail _cb = true ;
this . UI _elem = document . getElementById ( "atree-ui" ) ;
this . list _elem = document . getElementById ( "atree-header" ) ;
}
2022-06-26 23:49:35 +00:00
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "AbilityTreeRenderNode accepts exactly one input (atree)" ; }
const [ atree ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
//for some reason we have to cast to string
2022-06-30 15:03:41 +00:00
this . list _elem . innerHTML = "" ; //reset all atree actives - should be done in a more general way later
this . UI _elem . innerHTML = "" ; //reset the atree in the DOM
2022-06-28 18:43:35 +00:00
let ret = null ;
2022-06-30 15:03:41 +00:00
if ( atree ) { ret = render _AT ( this . UI _elem , this . list _elem , atree ) ; }
2022-06-26 23:49:35 +00:00
2022-06-27 08:32:26 +00:00
//Toggle on, previously was toggled off
toggle _tab ( 'atree-dropdown' ) ; toggleButton ( 'toggle-atree' ) ;
2022-06-26 23:49:35 +00:00
2022-06-28 18:43:35 +00:00
return ret ;
}
} ) ( ) . link _to ( atree _node ) ;
2022-06-26 23:49:35 +00:00
2022-06-30 15:03:41 +00:00
// This exists so i don't have to re-render the UI to push atree updates.
const atree _state _node = new ( class extends ComputeNode {
constructor ( ) { super ( 'builder-atree-state' ) ; }
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "AbilityTreeStateNode accepts exactly one input (atree-rendered)" ; }
const [ rendered _atree ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
return rendered _atree ;
}
} ) ( ) . link _to ( atree _render , 'atree-render' ) ;
2022-06-28 18:43:35 +00:00
/ * *
* Collect abilities and condense them into a list of "final abils" .
2022-06-29 06:23:27 +00:00
* This is just for rendering purposes , and for collecting things that modify spells into one chunk .
* I stg if wynn makes abils that modify multiple spells
* ... well we can extend this by making ` base_abil ` a list i nstead but annoy
*
2022-07-11 13:15:53 +00:00
* Signature : AbilityTreeMergeNode ( player - class : WeaponType , atree : ATree , atree - state : RenderedATree ) => Map [ id , Ability ]
2022-06-28 18:43:35 +00:00
* /
const atree _merge = new ( class extends ComputeNode {
constructor ( ) { super ( 'builder-atree-merge' ) ; }
compute _func ( input _map ) {
2022-07-11 13:15:53 +00:00
const player _class = input _map . get ( 'player-class' ) ;
2022-06-28 18:43:35 +00:00
const atree _state = input _map . get ( 'atree-state' ) ;
const atree _order = input _map . get ( 'atree' ) ;
let abils _merged = new Map ( ) ;
2022-07-11 13:15:53 +00:00
for ( const abil of default _abils [ player _class ] ) {
2022-06-28 19:03:49 +00:00
let tmp _abil = deepcopy ( abil ) ;
2022-07-09 03:47:34 +00:00
if ( ! ( 'desc' in tmp _abil ) ) {
tmp _abil . desc = [ ] ;
}
else if ( ! Array . isArray ( tmp _abil . desc ) ) {
2022-06-28 19:03:49 +00:00
tmp _abil . desc = [ tmp _abil . desc ] ;
}
tmp _abil . subparts = [ abil . id ] ;
abils _merged . set ( abil . id , tmp _abil ) ;
2022-06-28 18:43:35 +00:00
}
for ( const node of atree _order ) {
const abil _id = node . ability . id ;
if ( ! atree _state . get ( abil _id ) . active ) {
continue ;
}
const abil = node . ability ;
2022-07-06 19:16:44 +00:00
if ( 'base_abil' in abil ) {
if ( abils _merged . has ( abil . base _abil ) ) {
// Merge abilities.
// TODO: What if there is more than one base abil?
let base _abil = abils _merged . get ( abil . base _abil ) ;
if ( Array . isArray ( abil . desc ) ) { base _abil . desc = base _abil . desc . concat ( abil . desc ) ; }
else { base _abil . desc . push ( abil . desc ) ; }
base _abil . subparts . push ( abil . id ) ;
base _abil . effects = base _abil . effects . concat ( abil . effects ) ;
for ( let propname in abil . properties ) {
base _abil [ propname ] = abil [ propname ] ;
}
2022-06-28 19:03:49 +00:00
}
2022-07-06 19:16:44 +00:00
// do nothing otherwise.
2022-06-28 18:43:35 +00:00
}
else {
2022-06-28 19:03:49 +00:00
let tmp _abil = deepcopy ( abil ) ;
if ( ! Array . isArray ( tmp _abil . desc ) ) {
tmp _abil . desc = [ tmp _abil . desc ] ;
}
tmp _abil . subparts = [ abil . id ] ;
abils _merged . set ( abil _id , tmp _abil ) ;
2022-06-28 18:43:35 +00:00
}
}
return abils _merged ;
}
2022-07-06 06:43:44 +00:00
} ) ( ) . link _to ( atree _node , 'atree' ) . link _to ( atree _state _node , 'atree-state' ) ;
2022-06-28 18:43:35 +00:00
2022-07-11 09:03:42 +00:00
/ * *
* Check if an atree node can be activated .
*
* Return : [ yes / no , hard error , reason ]
* /
function abil _can _activate ( atree _node , atree _state , reachable , archetype _count , points _remain ) {
const { parents , ability } = atree _node ;
if ( parents . length === 0 ) {
return [ true , false , "" ] ;
}
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 + '"' ) ;
return [ false , true , 'missing dep: ' + dep _strings . join ( ", " ) ] ;
}
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 + '"' ) ;
return [ false , true , 'blocked by: ' + blockers _strings . join ( ", " ) ] ;
}
let node _reachable = false ;
for ( const parent of parents ) {
if ( reachable . has ( parent . ability . id ) ) {
node _reachable = true ;
break ;
}
}
if ( ! node _reachable ) {
return [ false , false , 'not reachable' ] ;
}
if ( 'archetype' in ability && ability . archetype !== "" ) {
if ( 'archetype_req' in ability && ability . archetype _req !== 0 ) {
const others = ( archetype _count . get ( ability . archetype ) || 0 ) ;
if ( others < ability . archetype _req ) {
return [ false , false , ability . archetype + ': ' + others + ' < ' + ability . archetype _req ] ;
}
}
}
if ( ability . cost > points _remain ) {
return [ false , false , "not enough ability points left" ] ;
}
return [ true , false , "" ] ;
}
2022-06-30 12:27:35 +00:00
/ * *
* Validate ability tree .
* Return list of errors for rendering .
*
* Signature : AbilityTreeMergeNode ( atree : ATree , atree - state : RenderedATree ) => List [ str ]
* /
const atree _validate = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-validator' ) ; }
compute _func ( input _map ) {
const atree _state = input _map . get ( 'atree-state' ) ;
const atree _order = input _map . get ( 'atree' ) ;
2022-07-11 09:03:42 +00:00
const level = parseInt ( input _map . get ( 'level' ) ) ;
2022-06-30 12:27:35 +00:00
2022-07-08 22:45:24 +00:00
if ( atree _order . length == 0 ) { return [ 0 , false , [ 'no atree data' ] ] ; }
2022-07-05 07:51:02 +00:00
2022-07-08 21:52:17 +00:00
let atree _to _add = [ ] ;
2022-07-11 09:03:42 +00:00
let atree _not _present = [ ] ;
// mark all selected nodes as bright, and mark all other nodes as dark.
// also initialize the "to check" list, and the "not present" list.
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
const abil = node . ability ;
2022-07-10 02:30:29 +00:00
if ( atree _state . get ( abil . id ) . active ) {
atree _to _add . push ( [ node , 'not reachable' , false ] ) ;
2022-07-12 22:06:40 +00:00
atree _state . get ( abil . id ) . img . src = '../media/atree/' + abil . display . icon + '_selected.png' ;
2022-06-30 12:27:35 +00:00
}
2022-07-10 02:30:29 +00:00
else {
2022-07-11 09:03:42 +00:00
atree _not _present . push ( abil . id ) ;
2022-07-12 22:06:40 +00:00
atree _state . get ( abil . id ) . img . src = '../media/atree/' + abil . display . icon + '_blocked.png' ;
2022-06-30 15:03:41 +00:00
}
2022-07-08 21:52:17 +00:00
}
2022-06-30 15:03:41 +00:00
2022-07-08 21:52:17 +00:00
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 ) {
2022-07-11 09:03:42 +00:00
const { ability } = node ;
const [ success , hard _error , reason ] = abil _can _activate ( node , atree _state , reachable , archetype _count , 9999 ) ;
if ( ! success ) {
_add . push ( [ node , reason , hard _error ] ) ;
2022-07-12 07:00:51 +00:00
continue ;
2022-06-30 15:03:41 +00:00
}
2022-07-08 21:52:17 +00:00
if ( 'archetype' in ability && ability . archetype !== "" ) {
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 ) ;
2022-06-30 12:27:35 +00:00
}
2022-07-08 21:52:17 +00:00
if ( atree _to _add . length == _add . length ) {
break ;
2022-06-30 15:03:41 +00:00
}
2022-07-08 21:52:17 +00:00
atree _to _add = _add ;
2022-06-30 15:03:41 +00:00
}
2022-07-11 09:03:42 +00:00
const atree _level _table = [ 'lvl0wtf' , 1 , 2 , 2 , 3 , 3 , 4 , 4 , 5 , 5 , 6 , 6 , 7 , 8 , 8 , 9 , 9 , 10 , 11 , 11 , 12 , 12 , 13 , 14 , 14 , 15 , 16 , 16 , 17 , 17 , 18 , 18 , 19 , 19 , 20 , 20 , 20 , 21 , 21 , 22 , 22 , 23 , 23 , 23 , 24 , 24 , 25 , 25 , 26 , 26 , 27 , 27 , 28 , 28 , 29 , 29 , 30 , 30 , 31 , 31 , 32 , 32 , 33 , 33 , 34 , 34 , 34 , 35 , 35 , 35 , 36 , 36 , 36 , 37 , 37 , 37 , 38 , 38 , 38 , 38 , 39 , 39 , 39 , 39 , 40 , 40 , 40 , 40 , 41 , 41 , 41 , 41 , 42 , 42 , 42 , 42 , 43 , 43 , 43 , 43 , 44 , 44 , 44 , 44 , 45 , 45 , 45 ] ;
let AP _cap ;
if ( isNaN ( level ) ) {
AP _cap = 45 ;
}
else {
AP _cap = atree _level _table [ level ] ;
}
document . getElementById ( 'active_AP_cap' ) . textContent = AP _cap ;
document . getElementById ( "active_AP_cost" ) . textContent = abil _points _total ;
const ap _left = AP _cap - abil _points _total ;
// using the "not present" list, highlight one-step reachable nodes.
for ( const node _id of atree _not _present ) {
const node = atree _state . get ( node _id ) ;
const [ success , hard _error , reason ] = abil _can _activate ( node , atree _state , reachable , archetype _count , ap _left ) ;
if ( success ) {
node . img . src = '../media/atree/' + node . ability . display . icon + '.png' ;
2022-06-30 12:27:35 +00:00
}
}
2022-07-08 21:52:17 +00:00
let hard _error = false ;
let errors = [ ] ;
2022-07-11 09:03:42 +00:00
if ( abil _points _total > AP _cap ) {
errors . push ( 'too many ability points assigned! (' + abil _points _total + ' > ' + AP _cap + ')' ) ;
}
2022-07-08 21:52:17 +00:00
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 ) ;
2022-06-30 15:03:41 +00:00
}
2022-07-11 09:03:42 +00:00
return [ hard _error , errors ] ;
2022-06-30 12:27:35 +00:00
}
2022-06-30 15:03:41 +00:00
} ) ( ) . link _to ( atree _node , 'atree' ) . link _to ( atree _state _node , 'atree-state' ) ;
2022-06-30 12:27:35 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Render ability tree .
* Return map of id - > corresponding html element .
*
* Signature : AbilityTreeRenderActiveNode ( atree - merged : MergedATree , atree - order : ATree , atree - errors : List [ str ] ) => Map [ int , ATreeNode ]
* /
2022-06-30 12:27:35 +00:00
const atree _render _active = new ( class extends ComputeNode {
constructor ( ) {
super ( 'atree-render-active' ) ;
this . list _elem = document . getElementById ( "atree-active" ) ;
}
compute _func ( input _map ) {
const merged _abils = input _map . get ( 'atree-merged' ) ;
const atree _order = input _map . get ( 'atree-order' ) ;
2022-07-11 09:03:42 +00:00
const [ hard _error , _errors ] = input _map . get ( 'atree-errors' ) ;
const errors = deepcopy ( _errors ) ;
2022-06-30 12:27:35 +00:00
this . list _elem . innerHTML = "" ; //reset all atree actives - should be done in a more general way later
2022-06-30 15:03:41 +00:00
// TODO: move to display?
2022-06-30 12:27:35 +00:00
if ( errors . length > 0 ) {
let errorbox = document . createElement ( 'div' ) ;
errorbox . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "mx-2" , "my-4" , "dark-shadow" ) ;
this . list _elem . appendChild ( errorbox ) ;
let error _title = document . createElement ( 'b' ) ;
error _title . classList . add ( "warning" , "scaled-font" ) ;
error _title . innerHTML = "ATree Error!" ;
errorbox . appendChild ( error _title ) ;
2022-07-08 21:52:17 +00:00
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 ) {
2022-07-09 10:09:50 +00:00
const error = '... ' + ( errors . length - 5 ) + ' errors not shown' ;
2022-07-08 21:52:17 +00:00
const atree _warning = make _elem ( "p" , [ "warning" , "small-text" ] , { textContent : error } ) ;
2022-06-30 12:27:35 +00:00
errorbox . appendChild ( atree _warning ) ;
}
}
2022-07-06 19:16:44 +00:00
const ret _map = new Map ( ) ;
2022-07-09 03:47:34 +00:00
const to _render _id = [ 999 , 998 ] ;
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
if ( ! merged _abils . has ( node . ability . id ) ) {
continue ;
}
2022-07-09 03:47:34 +00:00
to _render _id . push ( node . ability . id ) ;
}
for ( const id of to _render _id ) {
const abil = merged _abils . get ( id ) ;
2022-06-30 12:27:35 +00:00
let active _tooltip = document . createElement ( 'div' ) ;
active _tooltip . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "mx-2" , "my-4" , "dark-shadow" ) ;
let active _tooltip _title = document . createElement ( 'b' ) ;
active _tooltip _title . classList . add ( "scaled-font" ) ;
active _tooltip _title . innerHTML = abil . display _name ;
active _tooltip . appendChild ( active _tooltip _title ) ;
for ( const desc of abil . desc ) {
let active _tooltip _desc = document . createElement ( 'p' ) ;
active _tooltip _desc . classList . add ( "scaled-font-sm" , "my-0" , "mx-1" , "text-wrap" ) ;
active _tooltip _desc . textContent = desc ;
active _tooltip . appendChild ( active _tooltip _desc ) ;
}
2022-07-06 19:16:44 +00:00
ret _map . set ( abil . id , active _tooltip ) ;
2022-06-30 12:27:35 +00:00
this . list _elem . appendChild ( active _tooltip ) ;
}
2022-07-06 19:16:44 +00:00
return ret _map ;
2022-06-30 12:27:35 +00:00
}
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _validate , 'atree-errors' ) ;
2022-06-29 06:23:27 +00:00
/ * *
* Collect spells from abilities .
*
* Signature : AbilityCollectSpellsNode ( atree - merged : Map [ id , Ability ] ) => List [ Spell ]
* /
const atree _collect _spells = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-spell-collector' ) ; }
compute _func ( input _map ) {
2022-07-08 07:21:21 +00:00
const atree _merged = input _map . get ( 'atree-merged' ) ;
2022-07-11 09:03:42 +00:00
const [ hard _error , errors ] = input _map . get ( 'atree-errors' ) ;
2022-07-08 07:21:21 +00:00
if ( hard _error ) { return [ ] ; }
2022-06-29 06:23:27 +00:00
let ret _spells = new Map ( ) ;
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
// TODO: Possibly, make a better way for detecting "spell abilities"?
2022-06-30 15:58:26 +00:00
for ( const effect of abil . effects ) {
if ( effect . type === 'replace_spell' ) {
2022-07-01 08:23:06 +00:00
// replace_spell just replaces all (defined) aspects.
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( effect . base _spell ) ;
if ( ret _spell ) {
// NOTE: do not mutate results of previous steps!
for ( const key in effect ) {
ret _spell [ key ] = deepcopy ( effect [ key ] ) ;
}
}
else {
ret _spells . set ( effect . base _spell , deepcopy ( effect ) ) ;
2022-07-01 08:23:06 +00:00
}
2022-06-30 15:58:26 +00:00
}
}
2022-07-08 05:51:10 +00:00
}
2022-06-29 06:23:27 +00:00
2022-07-08 05:51:10 +00:00
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
2022-06-29 06:23:27 +00:00
for ( const effect of abil . effects ) {
switch ( effect . type ) {
case 'replace_spell' :
2022-07-01 08:23:06 +00:00
// Already handled above.
2022-06-29 06:23:27 +00:00
continue ;
case 'add_spell_prop' : {
2022-07-05 04:39:35 +00:00
const { base _spell , target _part = null , cost = 0 , behavior = 'merge' } = effect ;
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( base _spell ) ;
2022-07-11 08:25:25 +00:00
// TODO: unjankify this...
if ( 'cost' in ret _spell ) { ret _spell . cost += cost ; }
2022-06-29 06:23:27 +00:00
if ( target _part === null ) {
continue ;
}
2022-06-28 18:43:35 +00:00
2022-06-29 06:23:27 +00:00
let found _part = false ;
for ( let part of ret _spell . parts ) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if ( part . name === target _part ) {
if ( 'multipliers' in effect ) {
for ( const [ idx , v ] of effect . multipliers . entries ( ) ) { // python: enumerate()
part . multipliers [ idx ] += v ;
}
}
else if ( 'power' in effect ) {
part . power += effect . power ;
}
else if ( 'hits' in effect ) {
for ( const [ idx , v ] of Object . entries ( effect . hits ) ) { // looks kinda similar to multipliers case... hmm... can we unify all of these three? (make healpower a list)
2022-07-01 03:35:34 +00:00
if ( idx in part . hits ) { part . hits [ idx ] += v ; }
else { part . hits [ idx ] = v ; }
2022-07-08 05:51:10 +00:00
}
2022-06-29 06:23:27 +00:00
}
else {
throw "uhh invalid spell add effect" ;
}
found _part = true ;
break ;
}
}
2022-07-05 04:39:35 +00:00
if ( ! found _part && behavior === 'merge' ) { // add part. if behavior is merge
2022-06-29 06:23:27 +00:00
let spell _part = deepcopy ( effect ) ;
spell _part . name = target _part ; // has some extra fields but whatever
ret _spell . parts . push ( spell _part ) ;
}
2022-07-01 04:21:25 +00:00
if ( 'display' in effect ) {
ret _spell . display = effect . display ;
}
2022-06-29 06:23:27 +00:00
continue ;
}
case 'convert_spell_conv' :
const { base _spell , target _part , conversion } = effect ;
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( base _spell ) ;
2022-06-29 06:23:27 +00:00
const elem _idx = damageClasses . indexOf ( conversion ) ;
let filter = target _part === 'all' ;
for ( let part of ret _spell . parts ) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if ( filter || part . name === target _part ) {
if ( 'multipliers' in part ) {
let total _conv = 0 ;
for ( let i = 1 ; i < 6 ; ++ i ) { // skip neutral
total _conv += part . multipliers [ i ] ;
}
let new _conv = [ part . multipliers [ 0 ] , 0 , 0 , 0 , 0 , 0 ] ;
new _conv [ elem _idx ] = total _conv ;
part . multipliers = new _conv ;
}
}
}
continue ;
}
}
}
return ret _spells ;
}
2022-07-08 07:21:21 +00:00
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _validate , 'atree-errors' ) ;
2022-06-29 06:23:27 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Make interactive elements ( sliders , buttons )
*
2022-07-08 05:28:46 +00:00
* Signature : AbilityActiveUINode ( atree - merged : MergedATree ) => Map < str , slider _info >
2022-07-07 03:13:28 +00:00
*
* ElemState : {
* value : int // value for sliders; 0-1 for toggles
* }
2022-07-06 19:16:44 +00:00
* /
2022-07-07 04:15:41 +00:00
const atree _make _interactives = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-make-interactives' ) ; }
2022-07-06 19:16:44 +00:00
compute _func ( input _map ) {
const merged _abils = input _map . get ( 'atree-merged' ) ;
const atree _order = input _map . get ( 'atree-order' ) ;
const atree _html = input _map . get ( 'atree-elements' ) ;
2022-07-07 22:33:00 +00:00
/ * *
2022-07-08 05:28:46 +00:00
* slider _info
2022-07-07 22:33:00 +00:00
* label _name : str ,
* max : int ,
* step : int ,
* id : str ,
* abil : atree _node
2022-07-08 05:28:46 +00:00
* slider : html element
2022-07-07 22:33:00 +00:00
* }
* /
// Map<str, slider_info>
2022-07-07 03:13:28 +00:00
const slider _map = new Map ( ) ;
// first, pull out all the sliders.
2022-07-07 22:33:00 +00:00
for ( const [ abil _id , ability ] of merged _abils . entries ( ) ) {
for ( const effect of ability . effects ) {
if ( effect [ 'type' ] === "stat_scaling" && effect [ 'slider' ] === true ) {
const { slider _name , slider _behavior = 'merge' , slider _max , slider _step } = effect ;
if ( slider _map . has ( slider _name ) ) {
2022-07-11 14:33:03 +00:00
if ( slider _max !== undefined ) {
const slider _info = slider _map . get ( slider _name ) ;
slider _info . max += slider _max ;
}
2022-07-07 22:33:00 +00:00
}
else if ( slider _behavior === 'merge' ) {
slider _map . set ( slider _name , {
label _name : slider _name ,
max : slider _max ,
step : slider _step ,
id : "ability-slider" + ability . id ,
//color: effect['slider_color'] TODO: add colors to json
abil : ability
} ) ;
}
}
}
}
// next, render the sliders onto the abilities.
for ( const [ slider _name , slider _info ] of slider _map . entries ( ) ) {
let slider _container = gen _slider _labeled ( slider _info ) ;
atree _html . get ( slider _info . abil . id ) . appendChild ( slider _container ) ;
2022-07-08 05:28:46 +00:00
slider _info . slider = document . getElementById ( slider _info . id ) ;
slider _info . slider . addEventListener ( "change" , ( e ) => atree _stats . mark _dirty ( ) . update ( ) ) ;
2022-07-07 04:15:41 +00:00
}
2022-07-08 05:28:46 +00:00
return slider _map ;
2022-07-06 19:16:44 +00:00
}
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _render _active , 'atree-elements' ) ;
2022-06-29 06:23:27 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Collect stats from ability tree .
* Return StatMap of added stats ( incl . cost modifications as raw cost )
*
2022-07-08 05:28:46 +00:00
* Signature : AbilityTreeStatsNode ( atree - merged : MergedATree , build : Build , atree - interactive : Map < str , slider _info > ) => StatMap
2022-07-06 19:16:44 +00:00
* /
2022-07-06 06:43:44 +00:00
const atree _stats = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-stats-collector' ) ; }
compute _func ( input _map ) {
2022-07-07 23:13:26 +00:00
const atree _merged = input _map . get ( 'atree-merged' ) ;
const item _stats = input _map . get ( 'build' ) . statMap ;
2022-07-08 05:28:46 +00:00
const interactive _map = input _map . get ( 'atree-interactive' ) ;
2022-07-06 06:43:44 +00:00
let ret _effects = new Map ( ) ;
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
if ( abil . effects . length == 0 ) { continue ; }
for ( const effect of abil . effects ) {
switch ( effect . type ) {
case 'stat_scaling' :
2022-07-07 23:13:26 +00:00
if ( effect . slider ) {
2022-07-09 10:09:50 +00:00
if ( 'output' in effect ) { // sometimes nodes will modify slider without having effect.
const slider _val = interactive _map . get ( effect . slider _name ) . slider . value ;
2022-07-13 06:36:01 +00:00
let total = Math . floor ( round _near ( parseInt ( slider _val ) * effect . scaling [ 0 ] ) ) ;
2022-07-09 10:09:50 +00:00
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' ) { // TODO: prop
merge _stat ( ret _effects , output . name , total ) ;
}
2022-07-08 05:28:46 +00:00
}
}
2022-07-09 10:09:50 +00:00
else {
if ( effect . output . type === 'stat' ) {
merge _stat ( ret _effects , effect . output . name , total ) ;
}
2022-07-08 05:28:46 +00:00
}
}
2022-07-07 23:13:26 +00:00
}
else {
// TODO: type: prop?
let total = 0 ;
for ( const [ scaling , input ] of zip2 ( effect . scaling , effect . inputs ) ) {
total += scaling * item _stats . get ( input . name ) ;
}
2022-07-17 02:09:19 +00:00
if ( total < 0 ) { total = 0 ; } // Normal stat scaling will not go negative.
2022-07-08 21:52:17 +00:00
if ( 'max' in effect && total > effect . max ) { total = effect . max ; }
2022-07-07 23:13:26 +00:00
// TODO: output (list...)
2022-07-08 02:44:57 +00:00
if ( Array . isArray ( effect . output ) ) {
for ( const output of effect . output ) {
if ( output . type === 'stat' ) {
merge _stat ( ret _effects , output . name , total ) ;
}
}
}
else {
if ( effect . output . type === 'stat' ) {
merge _stat ( ret _effects , effect . output . name , total ) ;
}
}
2022-07-07 23:13:26 +00:00
}
2022-07-06 06:43:44 +00:00
continue ;
case 'raw_stat' :
// TODO: toggles...
for ( const bonus of effect . bonuses ) {
2022-07-06 06:55:47 +00:00
const { type , name , abil = "" , value } = bonus ;
2022-07-06 06:43:44 +00:00
// TODO: prop
if ( type === "stat" ) {
2022-07-08 02:44:57 +00:00
merge _stat ( ret _effects , name , value ) ;
2022-07-06 06:43:44 +00:00
}
}
continue ;
case 'add_spell_prop' :
2022-07-11 08:25:25 +00:00
continue ;
2022-07-06 08:06:24 +00:00
// TODO unjankify....
// costs are converted to raw cost ID
2022-07-11 08:25:25 +00:00
// const { base_spell, cost = 0} = effect;
// if (cost) {
// const key = "spRaw"+base_spell;
// if (ret_effects.has(key)) { ret_effects.set(key, ret_effects.get(key) + cost); }
// else { ret_effects.set(key, cost); }
// }
// continue;
2022-07-06 06:43:44 +00:00
}
}
}
2022-07-08 02:44:57 +00:00
if ( ret _effects . has ( 'baseResist' ) ) {
2022-07-08 05:28:46 +00:00
merge _stat ( ret _effects , "defMult" , 1 - ( ret _effects . get ( 'baseResist' ) / 100 ) ) ;
2022-07-08 02:44:57 +00:00
}
2022-07-06 06:43:44 +00:00
return ret _effects ;
}
2022-07-08 05:28:46 +00:00
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _make _interactives , 'atree-interactive' ) ;
2022-07-06 06:43:44 +00:00
2022-06-29 06:23:27 +00:00
/ * *
* Construct compute nodes to link builder items and edit IDs to the appropriate display outputs .
* To make things a bit cleaner , the compute graph structure goes like
* [ builder , build stats ] - > [ one agg node that is just a passthrough ] - > all the spell calc nodes
* This way , when things have to be deleted i can just delete one node from the dependencies of builder / build stats ...
* thats the idea anyway .
*
* Whenever this is updated , it forces an update of all the newly created spell nodes ( if the build is clean ) .
*
* Signature : AbilityEnsureSpellsNodes ( spells : Map [ id , Spell ] ) => null
* /
class AbilityTreeEnsureNodesNode extends ComputeNode {
/ * *
* Kinda "hyper-node" : Constructor takes nodes that should be linked to ( build node and stat agg node )
* /
constructor ( build _node , stat _agg _node ) {
super ( 'atree-make-nodes' ) ;
this . build _node = build _node ;
this . stat _agg _node = stat _agg _node ;
// Slight amount of wasted compute to keep internal state non-changing.
this . passthrough = new PassThroughNode ( 'atree-make-nodes_internal' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
this . spelldmg _nodes = [ ] ; // debugging use
this . spell _display _elem = document . getElementById ( "all-spells-display" ) ;
}
compute _func ( input _map ) {
console . log ( 'atree make nodes' ) ;
this . passthrough . remove _link ( this . build _node ) ;
this . passthrough . remove _link ( this . stat _agg _node ) ;
this . passthrough = new PassThroughNode ( 'atree-make-nodes_internal' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
this . spell _display _elem . textContent = "" ;
const build _node = this . passthrough . get _node ( 'build' ) ; // aaaaaaaaa performance... savings... help....
const stat _agg _node = this . passthrough . get _node ( 'stats' ) ;
const spell _map = input _map . get ( 'spells' ) ; // TODO: is this gonna need more? idk...
// TODO shortcut update path for sliders
2022-06-30 12:27:35 +00:00
for ( const [ spell _id , spell ] of new Map ( [ ... spell _map ] . sort ( ( a , b ) => a [ 0 ] - b [ 0 ] ) ) . entries ( ) ) {
2022-06-29 06:23:27 +00:00
let spell _node = new SpellSelectNode ( spell ) ;
spell _node . link _to ( build _node , 'build' ) ;
let calc _node = new SpellDamageCalcNode ( spell . base _spell ) ;
calc _node . link _to ( build _node , 'build' ) . link _to ( stat _agg _node , 'stats' )
. link _to ( spell _node , 'spell-info' ) ;
this . spelldmg _nodes . push ( calc _node ) ;
let display _elem = document . createElement ( 'div' ) ;
display _elem . classList . add ( "col" , "pe-0" ) ;
// TODO: just pass these elements into the display node instead of juggling the raw IDs...
let spell _summary = document . createElement ( 'div' ) ; spell _summary . setAttribute ( 'id' , "spell" + spell . base _spell + "-infoAvg" ) ;
spell _summary . classList . add ( "col" , "spell-display" , "spell-expand" , "dark-5" , "rounded" , "dark-shadow" , "pt-2" , "border" , "border-dark" ) ;
let spell _detail = document . createElement ( 'div' ) ; spell _detail . setAttribute ( 'id' , "spell" + spell . base _spell + "-info" ) ;
spell _detail . classList . add ( "col" , "spell-display" , "dark-5" , "rounded" , "dark-shadow" , "py-2" ) ;
spell _detail . style . display = "none" ;
display _elem . appendChild ( spell _summary ) ; display _elem . appendChild ( spell _detail ) ;
let display _node = new SpellDisplayNode ( spell . base _spell ) ;
display _node . link _to ( stat _agg _node , 'stats' ) ;
display _node . link _to ( spell _node , 'spell-info' ) ;
display _node . link _to ( calc _node , 'spell-damage' ) ;
this . spell _display _elem . appendChild ( display _elem ) ;
}
this . passthrough . mark _dirty ( ) . update ( ) ; // Force update once.
}
}
2022-06-27 23:49:21 +00:00
/ * * T h e m a i n f u n c t i o n f o r r e n d e r i n g a n a b i l i t y t r e e .
*
* @ param { Element } UI _elem - the DOM element to draw the atree within .
* @ param { Element } list _elem - the DOM element to list selected abilities within .
* @ param { * } tree - the ability tree to work with .
* /
function render _AT ( UI _elem , list _elem , tree ) {
2022-06-23 11:24:12 +00:00
console . log ( "constructing ability tree UI" ) ;
2022-07-12 21:09:09 +00:00
// increase padding, since images are larger than the space provided
UI _elem . style . paddingRight = "calc(var(--bs-gutter-x) * 1)" ;
UI _elem . style . paddingLeft = "calc(var(--bs-gutter-x) * 1)" ;
UI _elem . style . paddingTop = "calc(var(--bs-gutter-x) * .5)" ;
2022-06-23 11:24:12 +00:00
// add in the "Active" title to atree
2022-07-07 03:04:53 +00:00
let active _row = make _elem ( "div" , [ "row" , "item-title" , "mx-auto" , "justify-content-center" ] ) ;
let active _word = make _elem ( "div" , [ "col-auto" ] , { textContent : "Active Abilities:" } ) ;
let active _AP _container = make _elem ( "div" , [ "col-auto" ] ) ;
let active _AP _subcontainer = make _elem ( "div" , [ "row" ] ) ;
let active _AP _cost = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cost" , textContent : "0" } ) ;
let active _AP _slash = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : "/" } ) ;
2022-07-11 03:28:08 +00:00
let active _AP _cap = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cap" } ) ;
2022-07-07 03:04:53 +00:00
let active _AP _end = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : " AP" } ) ;
2022-06-26 08:15:26 +00:00
active _AP _container . appendChild ( active _AP _subcontainer ) ;
2022-06-28 04:33:38 +00:00
active _AP _subcontainer . append ( active _AP _cost , active _AP _slash , active _AP _cap , active _AP _end ) ;
2022-06-26 08:15:26 +00:00
2022-06-28 04:33:38 +00:00
active _row . append ( active _word , active _AP _container ) ;
2022-06-27 23:49:21 +00:00
list _elem . appendChild ( active _row ) ;
2022-06-23 11:24:12 +00:00
2022-06-26 23:49:35 +00:00
let atree _map = new Map ( ) ;
let atree _connectors _map = new Map ( )
let max _row = 0 ;
for ( const i of tree ) {
2022-06-28 18:43:35 +00:00
atree _map . set ( i . ability . id , { ability : i . ability , connectors : new Map ( ) , active : false } ) ;
if ( i . ability . display . row > max _row ) {
max _row = i . ability . display . row ;
2022-06-26 23:49:35 +00:00
}
2022-06-24 02:36:50 +00:00
}
2022-06-26 23:49:35 +00:00
// Copy graph structure.
for ( const i of tree ) {
2022-06-28 18:43:35 +00:00
let node _wrapper = atree _map . get ( i . ability . id ) ;
2022-06-26 23:49:35 +00:00
node _wrapper . parents = [ ] ;
node _wrapper . children = [ ] ;
for ( const parent of i . parents ) {
2022-06-28 18:43:35 +00:00
node _wrapper . parents . push ( atree _map . get ( parent . ability . id ) ) ;
2022-06-26 12:18:23 +00:00
}
2022-06-26 23:49:35 +00:00
for ( const child of i . children ) {
2022-06-28 18:43:35 +00:00
node _wrapper . children . push ( atree _map . get ( child . ability . id ) ) ;
2022-06-23 15:00:15 +00:00
}
2022-06-26 23:49:35 +00:00
}
// Setup grid.
for ( let j = 0 ; j <= max _row ; j ++ ) {
let row = document . createElement ( 'div' ) ;
row . classList . add ( "row" ) ;
row . id = "atree-row-" + j ;
for ( let k = 0 ; k < 9 ; k ++ ) {
col = document . createElement ( 'div' ) ;
col . classList . add ( 'col' , 'px-0' ) ;
2022-07-12 03:17:00 +00:00
col . style = "position: relative; aspect-ratio: 1/1;"
2022-06-26 23:49:35 +00:00
row . appendChild ( col ) ;
2022-06-23 15:00:15 +00:00
}
2022-06-27 23:49:21 +00:00
UI _elem . appendChild ( row ) ;
2022-06-26 23:49:35 +00:00
}
2022-06-23 15:00:15 +00:00
2022-06-26 23:49:35 +00:00
for ( const _node of tree ) {
2022-06-28 18:43:35 +00:00
let node _wrap = atree _map . get ( _node . ability . id ) ;
let ability = _node . ability ;
2022-06-23 11:24:12 +00:00
// create connectors based on parent location
2022-06-26 23:49:35 +00:00
for ( let parent of node _wrap . parents ) {
node _wrap . connectors . set ( parent , [ ] ) ;
2022-06-26 08:59:02 +00:00
2022-06-28 18:43:35 +00:00
let parent _abil = parent . ability ;
const parent _id = parent _abil . id ;
2022-06-23 11:24:12 +00:00
let connect _elem = document . createElement ( "div" ) ;
2022-07-12 03:17:00 +00:00
connect _elem . style = "background-size: cover; width: 200%; height: 200%; position: absolute; top: -50%; left: -50%; image-rendering: pixelated;" ;
2022-06-23 11:24:12 +00:00
// connect up
2022-06-28 18:43:35 +00:00
for ( let i = ability . display . row - 1 ; i > parent _abil . display . row ; i -- ) {
const coord = i + "," + ability . display . col ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : [ 0 , 0 , 1 , 1 ] } ) ;
2022-06-23 11:24:12 +00:00
}
2022-06-23 14:29:25 +00:00
// connect horizontally
2022-06-28 18:43:35 +00:00
let min = Math . min ( parent _abil . display . col , ability . display . col ) ;
let max = Math . max ( parent _abil . display . col , ability . display . col ) ;
2022-06-23 14:29:25 +00:00
for ( let i = min + 1 ; i < max ; i ++ ) {
2022-06-28 18:43:35 +00:00
const coord = parent _abil . display . row + "," + i ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : [ 1 , 1 , 0 , 0 ] } ) ;
2022-06-23 11:24:12 +00:00
}
// connect corners
2022-06-28 18:43:35 +00:00
if ( parent _abil . display . row != ability . display . row && parent _abil . display . col != ability . display . col ) {
const coord = parent _abil . display . row + "," + ability . display . col ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
2022-06-26 23:49:35 +00:00
let connections = [ 0 , 0 , 0 , 1 ] ;
2022-06-28 18:43:35 +00:00
if ( parent _abil . display . col > ability . display . col ) {
2022-06-26 23:49:35 +00:00
connections [ 1 ] = 1 ;
2022-06-23 14:53:55 +00:00
}
else { // if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) {
2022-06-26 23:49:35 +00:00
connections [ 0 ] = 1 ;
2022-06-23 14:53:55 +00:00
}
2022-06-28 18:43:35 +00:00
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : connections } ) ;
2022-06-23 11:24:12 +00:00
}
}
// create node
2022-06-26 08:59:02 +00:00
let node _elem = document . createElement ( 'div' ) ;
2022-06-28 18:43:35 +00:00
let icon = ability . display . icon ;
2022-06-24 11:56:56 +00:00
if ( icon === undefined ) {
icon = "node" ;
}
2022-06-28 07:10:25 +00:00
let node _img = document . createElement ( 'img' ) ;
node _img . src = '../media/atree/' + icon + '.png' ;
2022-07-12 03:17:00 +00:00
node _img . style = "width: 200%; height: 200%; position: absolute; top: -50%; left: -50%; image-rendering: pixelated; z-index: 1;" ;
2022-06-28 07:10:25 +00:00
node _elem . appendChild ( node _img ) ;
2022-06-23 11:24:12 +00:00
2022-07-12 22:06:40 +00:00
node _wrap . img = node _img ;
2022-07-12 03:17:00 +00:00
// create hitbox
// this is necessary since images exceed the size of their square, but should only be interactible within that square
let hitbox = document . createElement ( "div" ) ;
hitbox . style = "position: absolute; cursor: pointer; left: 0; top: 0; width: 100%; height: 100%; z-index: 2;"
node _elem . appendChild ( hitbox ) ;
2022-06-23 11:24:12 +00:00
2022-06-30 15:03:41 +00:00
node _wrap . elem = node _elem ;
node _wrap . all _connectors _ref = atree _connectors _map ;
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'click' , function ( e ) {
2022-06-26 07:48:42 +00:00
if ( e . target !== this && e . target !== this . children [ 0 ] ) { return ; }
2022-06-30 15:03:41 +00:00
atree _set _state ( node _wrap , ! node _wrap . active ) ;
atree _state _node . mark _dirty ( ) . update ( ) ;
2022-06-23 11:24:12 +00:00
} ) ;
2022-06-26 07:48:42 +00:00
// add tooltip
2022-07-14 18:17:38 +00:00
// tooltips are being changed to generate on mouseover for fin444's future style updates
// this is being implemented before those updates since it helps with a hotfix
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'mouseover' , function ( e ) {
2022-07-14 18:17:38 +00:00
if ( e . target !== this ) {
return ;
}
2022-07-14 23:10:05 +00:00
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
2022-07-14 18:17:38 +00:00
}
2022-07-14 23:10:05 +00:00
node _wrap . tooltip _elem = generateTooltip ( UI _elem , node _elem , ability ) ;
2022-06-26 07:48:42 +00:00
} ) ;
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'mouseout' , function ( e ) {
2022-07-14 18:17:38 +00:00
if ( e . target !== this ) {
return ;
}
2022-07-14 23:10:05 +00:00
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
2022-07-14 18:17:38 +00:00
}
2022-06-26 07:48:42 +00:00
} ) ;
2022-06-28 18:43:35 +00:00
document . getElementById ( "atree-row-" + ability . display . row ) . children [ ability . display . col ] . appendChild ( node _elem ) ;
2022-06-23 11:24:12 +00:00
} ;
2022-06-26 23:49:35 +00:00
atree _render _connection ( atree _connectors _map ) ;
2022-06-28 18:43:35 +00:00
return atree _map ;
2022-06-23 11:24:12 +00:00
} ;
2022-07-14 18:17:38 +00:00
function generateTooltip ( UI _elem , node _elem , ability ) {
let tooltip = document . createElement ( 'div' ) ;
tooltip . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "mx-2" , "my-4" , "dark-shadow" ) ;
// tooltip text formatting
let tooltip _title = document . createElement ( 'b' ) ;
tooltip _title . classList . add ( "scaled-font" ) ;
tooltip _title . innerHTML = ability . display _name ;
tooltip . appendChild ( tooltip _title ) ;
if ( 'archetype' in ability && ability . archetype !== "" ) {
let tooltip _archetype = document . createElement ( 'p' ) ;
tooltip _archetype . classList . add ( "scaled-font" ) ;
tooltip _archetype . innerHTML = "(Archetype: " + ability . archetype + ")" ;
tooltip . appendChild ( tooltip _archetype ) ;
}
let tooltip _desc = document . createElement ( 'p' ) ;
tooltip _desc . classList . add ( "scaled-font-sm" , "my-0" , "mx-1" , "text-wrap" ) ;
tooltip _desc . textContent = ability . desc ;
tooltip . appendChild ( tooltip _desc ) ;
let tooltip _cost = document . createElement ( 'p' ) ;
tooltip _cost . classList . add ( "scaled-font-sm" , "my-0" , "mx-1" , "text-start" ) ;
tooltip _cost . textContent = "Cost: " + ability . cost + " AP" ;
tooltip . appendChild ( tooltip _cost ) ;
tooltip . style . position = "absolute" ;
tooltip . style . zIndex = "100" ;
tooltip . style . top = ( node _elem . getBoundingClientRect ( ) . top + window . pageYOffset + 50 ) + "px" ;
tooltip . style . left = UI _elem . getBoundingClientRect ( ) . left + "px" ;
tooltip . style . width = UI _elem . getBoundingClientRect ( ) . width * 0.95 + "px" ;
UI _elem . appendChild ( tooltip ) ;
return tooltip ;
}
2022-06-26 10:55:06 +00:00
// resolve connector conflict, when they occupy the same cell.
2022-06-26 23:49:35 +00:00
function resolve _connector ( atree _connectors _map , pos , new _connector ) {
if ( ! atree _connectors _map . has ( pos ) ) {
atree _connectors _map . set ( pos , new _connector ) ;
return ;
2022-06-23 11:24:12 +00:00
}
2022-06-26 23:49:35 +00:00
let existing = atree _connectors _map . get ( pos ) . connections ;
for ( let i = 0 ; i < 4 ; ++ i ) {
existing [ i ] += new _connector . connections [ i ] ;
}
}
2022-06-23 11:24:12 +00:00
2022-06-26 23:49:35 +00:00
function set _connector _type ( connector _info ) { // left right up down
2022-07-12 20:48:55 +00:00
connector _info . type = "" ;
for ( let i = 0 ; i < 4 ; i ++ ) {
connector _info . type += connector _info . connections [ i ] == 0 ? "0" : "1" ;
2022-06-23 11:24:12 +00:00
}
2022-06-24 02:36:50 +00:00
}
// draw the connector onto the screen
2022-06-26 23:49:35 +00:00
function atree _render _connection ( atree _connectors _map ) {
2022-06-24 02:36:50 +00:00
for ( let i of atree _connectors _map . keys ( ) ) {
2022-06-26 23:49:35 +00:00
let connector _info = atree _connectors _map . get ( i ) ;
let connector _elem = connector _info . connector ;
2022-06-28 07:10:25 +00:00
let connector _img = document . createElement ( 'img' ) ;
2022-06-26 23:49:35 +00:00
set _connector _type ( connector _info ) ;
2022-06-28 07:10:25 +00:00
connector _img . src = '../media/atree/connect_' + connector _info . type + '.png' ;
connector _img . style = "width: 100%; height: 100%;"
connector _elem . replaceChildren ( connector _img ) ;
2022-06-26 23:49:35 +00:00
connector _info . highlight = [ 0 , 0 , 0 , 0 ] ;
let target _elem = document . getElementById ( "atree-row-" + i . split ( "," ) [ 0 ] ) . children [ i . split ( "," ) [ 1 ] ] ;
if ( target _elem . children . length != 0 ) {
// janky special case...
connector _elem . style . display = 'none' ;
}
target _elem . appendChild ( connector _elem ) ;
2022-06-26 11:29:55 +00:00
} ;
} ;
2022-06-26 08:59:02 +00:00
2022-06-26 10:55:06 +00:00
// toggle the state of a node.
2022-06-30 15:03:41 +00:00
function atree _set _state ( node _wrapper , new _state ) {
2022-07-12 23:56:36 +00:00
let icon = node _wrapper . ability . display . icon ;
if ( icon === undefined ) {
icon = "node" ;
}
2022-06-30 15:03:41 +00:00
if ( new _state ) {
node _wrapper . active = true ;
2022-07-12 23:56:36 +00:00
node _wrapper . elem . children [ 0 ] . src = "../media/atree/" + icon + "_selected.png" ;
2022-06-30 15:03:41 +00:00
}
else {
node _wrapper . active = false ;
2022-07-12 23:56:36 +00:00
node _wrapper . elem . children [ 0 ] . src = "../media/atree/" + icon + ".png" ;
2022-06-30 15:03:41 +00:00
}
let atree _connectors _map = node _wrapper . all _connectors _ref ;
2022-06-26 23:49:35 +00:00
for ( const parent of node _wrapper . parents ) {
if ( parent . active ) {
atree _set _edge ( atree _connectors _map , parent , node _wrapper , new _state ) ; // self->parent state only changes if parent is on
}
}
for ( const child of node _wrapper . children ) {
if ( child . active ) {
atree _set _edge ( atree _connectors _map , node _wrapper , child , new _state ) ; // Same logic as above.
}
}
2022-06-26 11:29:55 +00:00
} ;
2022-06-26 08:59:02 +00:00
2022-06-26 23:49:35 +00:00
function atree _set _edge ( atree _connectors _map , parent , child , state ) {
const connectors = child . connectors . get ( parent ) ;
2022-06-28 18:43:35 +00:00
const parent _row = parent . ability . display . row ;
const parent _col = parent . ability . display . col ;
const child _row = child . ability . display . row ;
const child _col = child . ability . display . col ;
2022-06-26 23:49:35 +00:00
let state _delta = ( state ? 1 : - 1 ) ;
let child _side _idx = ( parent _col > child _col ? 0 : 1 ) ;
let parent _side _idx = 1 - child _side _idx ;
for ( const connector _label of connectors ) {
let connector _info = atree _connectors _map . get ( connector _label ) ;
let connector _elem = connector _info . connector ;
let highlight _state = connector _info . highlight ; // left right up down
2022-06-28 07:10:25 +00:00
let connector _img _elem = document . createElement ( "img" ) ;
connector _img _elem . style = "width: 100%; height: 100%;" ;
2022-06-26 23:49:35 +00:00
const ctype = connector _info . type ;
2022-07-12 20:48:55 +00:00
let num _1s = 0 ;
for ( let i = 0 ; i < 4 ; i ++ ) {
if ( ctype . charAt ( i ) == "1" ) {
num _1s ++ ;
}
}
if ( num _1s > 2 ) { // t branch or 4-way
2022-06-26 23:49:35 +00:00
const [ connector _row , connector _col ] = connector _label . split ( ',' ) . map ( x => parseInt ( x ) ) ;
if ( connector _row === parent _row ) {
highlight _state [ parent _side _idx ] += state _delta ;
}
else {
highlight _state [ 2 ] += state _delta ; // up connection guaranteed.
}
if ( connector _col === child _col ) {
highlight _state [ 3 ] += state _delta ;
}
else {
highlight _state [ child _side _idx ] += state _delta ;
}
2022-06-26 08:59:02 +00:00
2022-07-12 20:48:55 +00:00
let render = "" ;
for ( let i = 0 ; i < 4 ; i ++ ) {
render += highlight _state [ i ] === 0 ? "0" : "1" ;
}
2022-07-12 22:06:40 +00:00
if ( render == "0000" ) {
connector _img _elem . src = "../media/atree/connect_" + ctype + ".png" ;
} else {
connector _img _elem . src = "../media/atree/connect_" + ctype + "_" + render + ".png" ;
}
2022-06-28 07:10:25 +00:00
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 12:46:04 +00:00
continue ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
// lol bad overloading, [0] is just the whole state
highlight _state [ 0 ] += state _delta ;
if ( highlight _state [ 0 ] > 0 ) {
2022-07-12 20:48:55 +00:00
connector _img _elem . src = '../media/atree/connect_' + ctype + '_1.png' ;
2022-06-28 07:10:25 +00:00
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
else {
2022-06-28 07:10:25 +00:00
connector _img _elem . src = '../media/atree/connect_' + ctype + '.png' ;
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
}
2022-06-26 08:59:02 +00:00
}