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
}
* /
// TODO: Range numbers
const default _abils = {
wand : [ {
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-06-28 18:43:35 +00:00
} ] ,
spear : [ {
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-06-28 18:43:35 +00:00
} ] ,
bow : [ {
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-06-28 18:43:35 +00:00
} ] ,
dagger : [ {
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-06-28 18:43:35 +00:00
} ] ,
relik : [ {
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-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 instead but annoy
*
2022-06-30 12:27:35 +00:00
* Signature : AbilityTreeMergeNode ( build : Build , 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 ) {
const build = input _map . get ( 'build' ) ;
const atree _state = input _map . get ( 'atree-state' ) ;
const atree _order = input _map . get ( 'atree' ) ;
let abils _merged = new Map ( ) ;
for ( const abil of default _abils [ build . weapon . statMap . get ( 'type' ) ] ) {
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
}
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-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-05 07:51:02 +00:00
if ( atree _order . length == 0 ) { return [ 0 , [ 'no atree data' ] ] ; }
2022-06-30 12:27:35 +00:00
let errors = [ ] ;
let reachable = new Map ( ) ;
atree _dfs _mark ( atree _order [ 0 ] , atree _state , reachable ) ;
2022-06-30 15:03:41 +00:00
let abil _points _total = 0 ;
let archetype _count = new Map ( ) ;
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
const abil = node . ability ;
if ( ! atree _state . get ( abil . id ) . active ) { continue ; }
2022-06-30 15:03:41 +00:00
abil _points _total += abil . cost ;
2022-06-30 12:27:35 +00:00
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 ) {
2022-06-30 15:03:41 +00:00
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 ( ", " ) ) ;
2022-06-30 12:27:35 +00:00
}
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 ) {
2022-06-30 15:03:41 +00:00
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 ( ", " ) ) ;
}
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 ) ;
}
}
// 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 )
}
2022-06-30 12:27:35 +00:00
}
}
2022-06-30 15:03:41 +00:00
if ( abil _points _total > 45 ) {
errors . push ( 'too many ability points assigned! (' + abil _points _total + ' > 45)' ) ;
}
return [ abil _points _total , 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
function atree _dfs _mark ( start , atree _state , mark ) {
2022-06-30 15:03:41 +00:00
if ( mark . get ( start . ability . id ) ) { return ; }
2022-06-30 12:27:35 +00:00
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 ) ;
}
}
}
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-06-30 15:03:41 +00:00
const [ abil _points _total , errors ] = input _map . get ( 'atree-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?
document . getElementById ( "active_AP_cost" ) . textContent = abil _points _total ;
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 ) ;
for ( const error of errors ) {
let atree _warning = document . createElement ( "p" ) ;
atree _warning . classList . add ( "warning" , "small-text" ) ;
atree _warning . textContent = error ;
errorbox . appendChild ( atree _warning ) ;
}
}
2022-07-06 19:16:44 +00:00
const ret _map = new Map ( ) ;
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
if ( ! merged _abils . has ( node . ability . id ) ) {
continue ;
}
const abil = merged _abils . get ( node . ability . id ) ;
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 ) {
if ( input _map . size !== 1 ) { throw "AbilityTreeCollectSpellsNode accepts exactly one input (atree-merged)" ; }
const [ atree _merged ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
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-06 06:43:44 +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 ;
}
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) ;
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 ) ) {
const slider _info = slider _map . get ( slider _name ) ;
slider _info . max += slider _max ;
}
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' ) ;
/ * *
* 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 ) {
// TODO: handle
2022-07-08 05:28:46 +00:00
const slider _val = interactive _map . get ( effect . slider _name ) . slider . value ;
const total = parseInt ( slider _val ) * effect . scaling [ 0 ] ;
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
}
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 ; }
// 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-06 08:06:24 +00:00
// TODO unjankify....
// costs are converted to raw cost ID
2022-07-06 06:43:44 +00:00
const { base _spell , cost = 0 } = effect ;
if ( cost ) {
const key = "spRaw" + base _spell ;
2022-07-06 06:51:16 +00:00
if ( ret _effects . has ( key ) ) { ret _effects . set ( key , ret _effects . get ( key ) + cost ) ; }
2022-07-06 06:43:44 +00:00
else { ret _effects . set ( key , cost ) ; }
}
continue ;
}
}
}
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" ) ;
// 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 : "/" } ) ;
let active _AP _cap = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cap" , textContent : "45" } ) ;
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' ) ;
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" ) ;
connect _elem . style = "background-size: cover; width: 100%; height: 100%;" ;
// 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 ) {
2022-06-29 23:28:41 +00:00
icon = "node_0" ;
2022-06-24 11:56:56 +00:00
}
2022-06-28 07:10:25 +00:00
let node _img = document . createElement ( 'img' ) ;
node _img . src = '../media/atree/' + icon + '.png' ;
node _img . style = "width: 100%; height: 100%;" ;
node _elem . appendChild ( node _img ) ;
2022-06-26 08:59:02 +00:00
node _elem . classList . add ( "atree-circle" ) ;
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
// add node tooltip
2022-06-23 11:24:12 +00:00
node _elem . addEventListener ( 'mouseover' , function ( e ) {
if ( e . target !== this ) { return ; }
let tooltip = this . children [ 0 ] ;
tooltip . style . top = this . getBoundingClientRect ( ) . bottom + window . scrollY * 1.02 + "px" ;
tooltip . style . left = this . parentElement . parentElement . getBoundingClientRect ( ) . left + ( elem . getBoundingClientRect ( ) . width * . 2 / 2 ) + "px" ;
tooltip . style . display = "block" ;
} ) ;
node _elem . addEventListener ( 'mouseout' , function ( e ) {
if ( e . target !== this ) { return ; }
let tooltip = this . children [ 0 ] ;
tooltip . style . display = "none" ;
} ) ;
2022-06-29 23:28:41 +00:00
//node tooltip and active tooltip have common parts - let's make them first
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
let tooltip _title = document . createElement ( 'div' ) ;
tooltip _title . classList . add ( "row" , "justify-content-center" , "fw-bold" ) ;
2022-07-01 22:52:13 +00:00
tooltip _title . textContent = ability . display _name ;
2022-06-26 07:48:42 +00:00
2022-07-02 00:19:01 +00:00
let tooltip _archetype ;
2022-06-30 15:03:41 +00:00
if ( 'archetype' in ability && ability . archetype !== "" ) {
2022-07-02 00:19:01 +00:00
tooltip _archetype = document . createElement ( 'div' ) ;
tooltip _archetype . classList . add ( "row" , "mx-1" , "text-start" ) ;
tooltip _archetype . textContent = "(Archetype: " + ability . archetype + ")" ;
2022-06-30 15:03:41 +00:00
}
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
let tooltip _desc = document . createElement ( 'div' ) ;
tooltip _desc . classList . add ( "row" , "mx-1" , "text-wrap" ) ;
2022-07-01 22:52:13 +00:00
tooltip _desc . textContent = ability . desc ;
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
let tooltip _cost = document . createElement ( 'div' ) ;
tooltip _cost . classList . add ( "row" , "mx-1" , "text-start" ) ;
2022-07-01 22:52:13 +00:00
tooltip _cost . textContent = "Cost: " + ability . cost + " AP" ;
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
//create node tooltip
let node _tooltip = document . createElement ( 'div' ) ;
node _tooltip . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "my-1" , "dark-shadow" , "scaled-font" , "container" ) ;
node _tooltip . style . display = "none" ;
2022-07-02 00:19:01 +00:00
node _tooltip . append ( tooltip _title , tooltip _archetype ? tooltip _archetype : "" , tooltip _desc ) ;
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
//active = copy of node
let active _tooltip = node _tooltip . cloneNode ( true ) ;
//node tooltip specific stuff that we don't want to be copied
2022-06-23 11:24:12 +00:00
node _tooltip . style . position = "absolute" ;
node _tooltip . style . zIndex = "100" ;
2022-06-29 23:28:41 +00:00
node _tooltip . appendChild ( tooltip _cost ) ;
2022-06-30 02:20:51 +00:00
2022-06-29 23:28:41 +00:00
//add in anything new for active tooltips
2022-07-01 22:52:13 +00:00
active _tooltip . id = "atree-ab-" + ability . id ;
2022-06-29 23:28:41 +00:00
2022-07-01 22:52:13 +00:00
if ( ability . blockers . length > 0 ) {
2022-06-29 23:28:41 +00:00
let active _tooltip _blockers = document . createElement ( "div" ) ;
active _tooltip _blockers . classList . add ( "row" , "mx-1" , "text-start" ) ;
2022-07-01 22:52:13 +00:00
active _tooltip _blockers . textContent = "Blockers: " + ability . blockers . join ( ", " ) ;
2022-06-29 23:28:41 +00:00
active _tooltip . append ( active _tooltip _blockers ) ;
}
2022-06-23 11:24:12 +00:00
2022-06-29 23:28:41 +00:00
//append node and active tooltips to corresponding parent elems
2022-06-23 11:24:12 +00:00
node _elem . appendChild ( node _tooltip ) ;
2022-06-30 12:27:35 +00:00
//list_elem.appendChild(active_tooltip); NOTE: moved to `atree_render_active`
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-06-23 11:24:12 +00:00
node _elem . 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
node _elem . addEventListener ( 'mouseover' , function ( e ) {
if ( e . target !== this && e . target !== this . children [ 0 ] ) { return ; }
let tooltip = this . children [ this . children . length - 1 ] ;
tooltip . style . top = this . getBoundingClientRect ( ) . bottom + window . scrollY * 1.02 + "px" ;
tooltip . style . left = this . parentElement . parentElement . getBoundingClientRect ( ) . left + ( elem . getBoundingClientRect ( ) . width * . 2 / 2 ) + "px" ;
2022-06-28 07:28:45 +00:00
tooltip . style . maxWidth = UI _elem . getBoundingClientRect ( ) . width * . 95 + "px" ;
2022-06-26 07:48:42 +00:00
tooltip . style . display = "block" ;
} ) ;
node _elem . addEventListener ( 'mouseout' , function ( e ) {
if ( e . target !== this && e . target !== this . children [ 0 ] ) { return ; }
let tooltip = this . children [ this . children . length - 1 ] ;
tooltip . style . display = "none" ;
} ) ;
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-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
const connections = connector _info . connections ;
const connector _elem = connector _info . connector ;
if ( connections [ 2 ] ) {
if ( connections [ 0 ] ) {
connector _info . type = 'c' ; // cross
return ;
}
connector _info . type = 'line' ; // vert line
return ;
2022-06-23 11:24:12 +00:00
}
2022-06-26 23:49:35 +00:00
if ( connections [ 3 ] ) { // if down:
if ( connections [ 0 ] && connections [ 1 ] ) {
connector _info . type = 't' ; // all 3 t
return ;
}
connector _info . type = 'angle' ; // elbow
if ( connections [ 1 ] ) {
connector _elem . classList . add ( "rotate-180" ) ;
}
else {
connector _elem . classList . add ( "rotate-270" ) ;
}
return ;
2022-06-24 02:36:50 +00:00
}
2022-06-26 23:49:35 +00:00
connector _info . type = 'line' ; // horiz line
connector _elem . classList . add ( "rotate-90" ) ;
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 ) {
if ( new _state ) {
node _wrapper . active = true ;
node _wrapper . elem . classList . add ( "atree-selected" ) ;
}
else {
node _wrapper . active = false ;
node _wrapper . elem . classList . remove ( "atree-selected" ) ;
}
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 10:55:06 +00:00
// refresh all connector to default state, then try to calculate the connector for all node
2022-06-26 08:59:02 +00:00
function atree _update _connector ( ) {
2022-06-26 10:48:00 +00:00
atree _connectors _map . forEach ( ( v ) => {
if ( v . length != 0 ) {
2022-06-28 07:10:25 +00:00
let connector _elem = document . createElement ( "img" ) ;
connector _elem . style = "width: 100%; height: 100%;" ;
connector _elem . src = '../media/atree/connect_' + v [ 0 ] . type + '.png'
v [ 0 ] . replaceChildren ( connector _elem ) ;
2022-06-26 10:48:00 +00:00
}
} ) ;
2022-06-26 08:59:02 +00:00
atree _map . forEach ( ( v ) => {
2022-06-26 09:04:02 +00:00
atree _compute _highlight ( v ) ;
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 ;
if ( ctype === 't' || ctype === 'c' ) {
// c, t
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-06-26 23:49:35 +00:00
let render _state = highlight _state . map ( x => ( x > 0 ? 1 : 0 ) ) ;
2022-06-26 10:48:00 +00:00
2022-06-26 23:49:35 +00:00
let connector _img = atree _parse _connector ( render _state , ctype ) ;
2022-06-28 07:10:25 +00:00
connector _img _elem . src = connector _img . img
2022-06-26 23:49:35 +00:00
connector _elem . className = "" ;
connector _elem . classList . add ( "rotate-" + connector _img . rotate ) ;
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-06-28 07:10:25 +00:00
connector _img _elem . src = '../media/atree/highlight_' + ctype + '.png' ;
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
}
2022-06-26 10:55:06 +00:00
// parse a sequence of left, right, up, down to appropriate connector image
2022-06-26 08:59:02 +00:00
function atree _parse _connector ( orient , type ) {
// left, right, up, down
2022-06-26 23:49:35 +00:00
2022-06-26 09:25:54 +00:00
let c _connector _dict = {
2022-06-26 08:59:02 +00:00
"1100" : { attrib : "_2_l" , rotate : 0 } ,
"1010" : { attrib : "_2_a" , rotate : 0 } ,
"1001" : { attrib : "_2_a" , rotate : 270 } ,
"0110" : { attrib : "_2_a" , rotate : 90 } ,
"0101" : { attrib : "_2_a" , rotate : 180 } ,
"0011" : { attrib : "_2_l" , rotate : 90 } ,
"1110" : { attrib : "_3" , rotate : 0 } ,
"1101" : { attrib : "_3" , rotate : 180 } ,
"1011" : { attrib : "_3" , rotate : 270 } ,
"0111" : { attrib : "_3" , rotate : 90 } ,
"1111" : { attrib : "" , rotate : 0 }
2022-06-26 11:29:55 +00:00
} ;
2022-06-26 08:59:02 +00:00
2022-06-26 09:25:54 +00:00
let t _connector _dict = {
"1100" : { attrib : "_2_l" , rotate : 0 } ,
"1001" : { attrib : "_2_a" , rotate : "flip" } ,
"0101" : { attrib : "_2_a" , rotate : 0 } ,
"1101" : { attrib : "_3" , rotate : 0 }
2022-06-26 11:29:55 +00:00
} ;
2022-06-26 09:25:54 +00:00
2022-06-26 11:29:55 +00:00
let res = "" ;
for ( let i of orient ) {
res += i ;
2022-06-26 23:49:35 +00:00
}
if ( res === "0000" ) {
2022-06-28 07:10:25 +00:00
return { img : "../media/atree/connect_" + type + ".png" , rotate : 0 } ;
2022-06-26 23:49:35 +00:00
}
2022-06-26 08:59:02 +00:00
2022-06-26 23:49:35 +00:00
let ret ;
2022-06-26 09:25:54 +00:00
if ( type == "c" ) {
2022-06-26 23:49:35 +00:00
ret = c _connector _dict [ res ] ;
2022-06-26 09:25:54 +00:00
} else {
2022-06-26 23:49:35 +00:00
ret = t _connector _dict [ res ] ;
2022-06-26 11:29:55 +00:00
} ;
2022-06-28 07:10:25 +00:00
ret . img = "../media/atree/highlight_" + type + ret . attrib + ".png" ;
2022-06-26 23:49:35 +00:00
return ret ;
2022-06-26 11:29:55 +00:00
} ;