2022-07-29 18:22:39 +00:00
/ * *
* This file defines computation graph nodes and display code relevant to the ability tree .
* TODO : possibly split it up into compute and render ... but its a bit complicated : /
* /
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
2022-12-16 10:29:01 +00:00
req _archetype : Optional [ str ] // what the req is for if no archetype defined... maybe clean up this data format later...?
2022-06-28 18:43:35 +00:00
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
2022-07-20 16:37:39 +00:00
// modify: increment existing part. do nothing if not exist
cost : Optional [ int ] // change to spellcost. If the spell is not spell 1-4, this must be left empty.
2022-06-28 18:43:35 +00:00
multipliers : Optional [ array [ float , 6 ] ] // Additive changes to spellmult (for damage spell)
power : Optional [ float ] // Additive change to healing power (for heal spell)
2022-08-11 10:13:53 +00:00
hits : Optional [ Map [ str , Union [ str , float ] ] ] // Additive changes to hits (for total entry)
// Can either be a raw value number, or a reference
// of the format <ability_id>.propname
2022-06-28 18:43:35 +00:00
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
2022-07-20 16:37:39 +00:00
// modify: increment 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 ,
2022-07-30 01:33:34 +00:00
positive : bool // True to keep stat above 0. False to ignore floor. Default: True for normal, False for scaling
2022-06-28 18:43:35 +00:00
"slider_name" : Optional [ str ] ,
"slider_step" : Optional [ float ] ,
2022-07-19 05:23:16 +00:00
round : Optional [ bool ] // Control floor behavior. True for stats and false for slider by default
2023-02-17 11:40:23 +00:00
behavior : Optional [ str ] // One of: "merge", "modify". default: merge
2022-07-07 04:15:41 +00:00
// merge: add if exist, make new part if not exist
2022-07-21 04:55:07 +00:00
// modify: change existing part, by incrementing properties. do nothing if not exist
2023-02-17 11:40:23 +00:00
slider _max : Optional [ float ] // affected by behavior
2022-07-21 04:55:07 +00:00
inputs : Optional [ list [ scaling _target ] ] // List of things to scale. Omit this if using slider
output : Optional [ scaling _target | List [ scaling _target ] ] // One of the following:
// 1. Single output scaling target
// 2. List of scaling targets (all scaled the same)
// 3. Omitted. no output (useful for modifying slider only without input or output)
scaling : Optional [ list [ float ] ] // One float for each input. Sums into output.
max : float
2022-06-28 18:43:35 +00:00
}
scaling _target : {
"type" : "stat" | "prop" ,
"abil" : Optional [ int ] ,
"name" : str
}
* /
2022-07-09 03:31:44 +00:00
2022-12-16 10:29:01 +00:00
// Space for big json data
let atrees ;
let atree _load _complete = false ;
/ *
* Load atree info remote DB ( aka a big json file ) .
* /
async function load _atree _data ( version _str ) {
let getUrl = window . location ;
let baseUrl = ` ${ getUrl . protocol } // ${ getUrl . host } / ` ;
// No random string -- we want to use caching
let url = ` ${ baseUrl } /data/ ${ version _str } /atree.json ` ;
atrees = await ( await fetch ( url ) ) . json ( ) ;
atree _load _complete = true ;
}
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-20 16:21:13 +00:00
let sccs = make _SCC _graph ( atree _head , atree _map . values ( ) ) ;
2022-06-26 23:49:35 +00:00
let atree _topo _sort = [ ] ;
2022-07-20 16:21:13 +00:00
for ( const scc of sccs ) {
for ( const node of scc . nodes ) {
delete node . visited ;
delete node . assigned ;
delete node . scc ;
atree _topo _sort . push ( node ) ;
}
}
2022-08-11 10:13:53 +00:00
//console.log("Approximate topological order ability tree:");
//console.log(atree_topo_sort);
2022-06-26 23:49:35 +00:00
return atree _topo _sort ;
}
} ) ( ) ;
/ * *
* 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 . 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-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 ) {
2022-07-24 03:20:16 +00:00
if ( ! reachable . has ( dep _id ) ) { failed _deps . push ( dep _id ) }
2022-07-11 09:03:42 +00:00
}
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 ) {
2022-07-24 03:20:16 +00:00
if ( reachable . has ( blocker _id ) ) { blocking _ids . push ( blocker _id ) ; }
2022-07-11 09:03:42 +00:00
}
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' ] ;
}
2022-12-16 10:29:01 +00:00
if ( 'archetype_req' in ability && ability . archetype _req !== 0 ) {
let req _archetype ;
if ( 'req_archetype' in ability && ability . req _archetype !== "" ) {
req _archetype = ability . req _archetype ;
}
else {
req _archetype = ability . archetype ;
}
const others = ( archetype _count . get ( req _archetype ) || 0 ) ;
if ( others < ability . archetype _req ) {
return [ false , false , req _archetype + ': ' + others + ' < ' + ability . archetype _req ] ;
2022-07-11 09:03:42 +00:00
}
}
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-08-11 21:45:23 +00:00
draw _atlas _image ( atree _state . get ( abil . id ) . img , atree _node _atlas _img , [ atree _node _atlas _positions [ abil . display . icon ] , 2 ] , atree _node _tile _size ) ;
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-08-11 21:45:23 +00:00
draw _atlas _image ( atree _state . get ( abil . id ) . img , atree _node _atlas _img , [ atree _node _atlas _positions [ abil . display . icon ] , 0 ] , atree _node _tile _size ) ;
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 ) {
2022-07-20 09:17:54 +00:00
atree _to _add = _add ;
2022-07-08 21:52:17 +00:00
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 ) {
2022-08-11 21:45:23 +00:00
draw _atlas _image ( node . img , atree _node _atlas _img , [ atree _node _atlas _positions [ node . ability . display . icon ] , 1 ] , atree _node _tile _size ) ;
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-08-15 17:30:02 +00:00
/ * *
* Collect abilities and condense them into a list of "final abils" .
* 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
*
* Signature : AbilityTreeMergeNode ( player - class : WeaponType , atree : ATree , atree - state : RenderedATree ) => Map [ id , Ability ]
* /
const atree _merge = new ( class extends ComputeNode {
constructor ( ) { super ( 'builder-atree-merge' ) ; }
compute _func ( input _map ) {
const [ hard _error , errors ] = input _map . get ( 'atree-errors' ) ;
if ( hard _error ) { return null ; }
const player _class = input _map . get ( 'player-class' ) ;
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 [ player _class ] ) {
let tmp _abil = deepcopy ( abil ) ;
if ( ! ( 'desc' in tmp _abil ) ) {
tmp _abil . desc = [ ] ;
}
else if ( ! Array . isArray ( tmp _abil . desc ) ) {
tmp _abil . desc = [ tmp _abil . desc ] ;
}
tmp _abil . subparts = [ abil . id ] ;
abils _merged . set ( abil . id , tmp _abil ) ;
}
for ( const node of atree _order ) {
const abil _id = node . ability . id ;
if ( ! atree _state . get ( abil _id ) . active ) {
continue ;
}
const abil = node . ability ;
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 ) {
if ( propname in base _abil . properties ) {
base _abil . properties [ propname ] += abil . properties [ propname ] ;
}
else { base _abil . properties [ propname ] = abil . properties [ propname ] ; }
}
}
// do nothing otherwise.
}
else {
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 ) ;
}
}
return abils _merged ;
}
} ) ( ) . link _to ( atree _node , 'atree' ) . link _to ( atree _state _node , 'atree-state' ) . link _to ( atree _validate , 'atree-errors' ) ;
2022-08-11 10:13:53 +00:00
/ * *
* Make interactive elements ( sliders , buttons )
*
* Signature : AbilityActiveUINode ( atree - merged : MergedATree ) => Map < str , slider _info >
*
* ElemState : {
* value : int // value for sliders; 0-1 for toggles
* }
* /
const atree _make _interactives = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-make-interactives' ) ; }
compute _func ( input _map ) {
const merged _abils = input _map . get ( 'atree-merged' ) ;
const atree _order = input _map . get ( 'atree-order' ) ;
const boost _slider _parent = document . getElementById ( "boost-sliders" ) ;
const boost _toggle _parent = document . getElementById ( "boost-toggles" ) ;
boost _slider _parent . innerHTML = "" ;
boost _toggle _parent . innerHTML = "" ;
/ * *
* slider _info
* label _name : str ,
* max : int ,
* step : int ,
* id : str ,
* abil : atree _node
* slider : html element
* }
* /
// Map<str, slider_info>
const slider _map = new Map ( ) ;
const button _map = new Map ( ) ;
2023-02-17 11:40:23 +00:00
let to _process = [ ] ;
for ( const [ abil _id , ability ] of merged _abils ) {
2022-08-11 10:13:53 +00:00
for ( const effect of ability . effects ) {
if ( effect [ 'type' ] === "stat_scaling" && effect [ 'slider' ] === true ) {
2023-02-17 11:40:23 +00:00
to _process . push ( [ effect , abil _id , ability ] ) ;
}
if ( effect [ 'type' ] === "raw_stat" && effect [ 'toggle' ] ) {
to _process . push ( [ effect , abil _id , ability ] ) ;
}
}
}
let unprocessed = [ ] ;
// first, pull out all the sliders and toggles.
let k = to _process . length ;
for ( let i = 0 ; i < k ; ++ i ) {
for ( const [ effect , abil _id , ability ] of to _process ) {
if ( effect [ 'type' ] === "stat_scaling" && effect [ 'slider' ] === true ) {
const { slider _name , behavior = 'merge' , slider _max , slider _step } = effect ;
2022-08-11 10:13:53 +00:00
if ( slider _map . has ( slider _name ) ) {
if ( slider _max !== undefined ) {
const slider _info = slider _map . get ( slider _name ) ;
slider _info . max += slider _max ;
}
2023-02-17 11:40:23 +00:00
else {
unprocessed . push ( [ effect , abil _id , ability ] ) ;
}
2022-08-11 10:13:53 +00:00
}
2023-02-17 11:40:23 +00:00
else if ( behavior === 'merge' ) {
console . log ( effect ) ;
2022-08-11 10:13:53 +00:00
slider _map . set ( slider _name , {
label _name : slider _name + ' (' + ability . display _name + ')' ,
max : slider _max ,
step : slider _step ,
id : "ability-slider" + ability . id ,
//color: effect['slider_color'] TODO: add colors to json
abil : ability
} ) ;
}
2023-02-17 11:40:23 +00:00
else {
unprocessed . push ( [ effect , abil _id , ability ] ) ;
}
2022-08-11 10:13:53 +00:00
}
if ( effect [ 'type' ] === "raw_stat" && effect [ 'toggle' ] ) {
const { toggle : toggle _name } = effect ;
button _map . set ( toggle _name , {
abil : ability
} ) ;
}
}
2023-02-17 11:40:23 +00:00
if ( unprocessed . length == to _process . length ) { break ; }
to _process = unprocessed ;
unprocessed = [ ] ;
2022-08-11 10:13:53 +00:00
}
// next, render the sliders and toggles onto the abilities.
for ( const [ slider _name , slider _info ] of slider _map . entries ( ) ) {
let slider _container = gen _slider _labeled ( slider _info ) ;
boost _slider _parent . appendChild ( slider _container ) ;
slider _info . slider = document . getElementById ( slider _info . id ) ;
slider _info . slider . addEventListener ( "change" , ( e ) => atree _scaling . mark _dirty ( ) . update ( ) ) ;
}
for ( const [ button _name , button _info ] of button _map . entries ( ) ) {
let button = make _elem ( 'button' , [ "button-boost" , "border-0" , "text-white" , "dark-8u" , "dark-shadow-sm" , "m-1" ] , {
id : button _info . abil . id ,
textContent : button _name
} ) ;
button . addEventListener ( "click" , ( e ) => {
if ( button . classList . contains ( "toggleOn" ) ) {
button . classList . remove ( "toggleOn" ) ;
} else {
button . classList . add ( "toggleOn" ) ;
}
atree _scaling . mark _dirty ( ) . update ( )
} ) ;
button _info . button = button ;
boost _toggle _parent . appendChild ( button ) ;
}
return [ slider _map , button _map ] ;
}
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _merge , 'atree-merged' ) ;
/ * *
* Scaling stats from ability tree .
* Return StatMap of added stats ,
*
* Signature : AbilityTreeScalingNode ( atree - merged : MergedATree , scale - scats : StatMap ,
* atree - interactive : [ Map < str , slider _info > , Map < str , button _info > ] ) => ( ATree , StatMap )
* /
const atree _scaling = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-scaling-collector' ) ; }
compute _func ( input _map ) {
const atree _merged = input _map . get ( 'atree-merged' ) ;
const pre _scale _stats = input _map . get ( 'scale-stats' ) ;
const [ slider _map , button _map ] = input _map . get ( 'atree-interactive' ) ;
const atree _edit = new Map ( ) ;
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
atree _edit . set ( abil _id , deepcopy ( abil ) ) ;
}
let ret _effects = new Map ( ) ;
// Apply a stat bonus.
function apply _bonus ( bonus _info , value ) {
const { type , name , abil = null } = bonus _info ;
if ( type === 'stat' ) {
merge _stat ( ret _effects , name , value ) ;
} else if ( type === 'prop' ) {
const merge _abil = atree _edit . get ( abil ) ;
2022-08-16 16:24:16 +00:00
if ( merge _abil ) {
merge _abil . properties [ name ] += value ;
}
2022-08-11 10:13:53 +00:00
}
}
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 'raw_stat' :
if ( effect . toggle ) {
const button = button _map . get ( effect . toggle ) . button ;
if ( ! button . classList . contains ( "toggleOn" ) ) { continue ; }
for ( const bonus of effect . bonuses ) {
apply _bonus ( bonus , bonus . value ) ;
}
} else {
for ( const bonus of effect . bonuses ) {
// Stat was applied earlier...
if ( bonus . type === 'stat' ) { continue ; }
apply _bonus ( bonus , bonus . value ) ;
}
}
continue ;
case 'stat_scaling' :
let total = 0 ;
2023-02-17 11:40:23 +00:00
const { slider = false , scaling = [ 0 ] , behavior = "merge" } = effect ;
2022-08-11 10:13:53 +00:00
let { positive = true , round = true } = effect ;
if ( slider ) {
2023-02-17 11:40:23 +00:00
if ( behavior == "modify" && ! slider _map . has ( effect . slider _name ) ) {
// Dangerous control flow.. early continue
continue ;
}
2022-08-11 10:13:53 +00:00
const slider _val = slider _map . get ( effect . slider _name ) . slider . value ;
total = parseInt ( slider _val ) * scaling [ 0 ] ;
round = false ;
positive = false ;
}
else {
// TODO: type: prop?
for ( const [ _scaling , input ] of zip2 ( scaling , effect . inputs ) ) {
total += _scaling * pre _scale _stats . get ( input . name ) ;
}
}
if ( 'output' in effect ) { // sometimes nodes will modify slider without having effect.
if ( round ) { total = Math . floor ( round _near ( total ) ) ; }
if ( positive && total < 0 ) { total = 0 ; } // Normal stat scaling will not go negative.
2023-02-17 09:46:28 +00:00
if ( 'max' in effect ) {
if ( effect . max > 0 && total > effect . max ) { total = effect . max ; }
if ( effect . max < 0 && total < effect . max ) { total = effect . max ; }
}
2022-08-11 10:13:53 +00:00
if ( Array . isArray ( effect . output ) ) {
for ( const output of effect . output ) {
apply _bonus ( output , total ) ;
}
}
else {
apply _bonus ( effect . output , total ) ;
}
}
continue ;
}
}
}
return [ atree _edit , ret _effects ] ;
}
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _make _interactives , 'atree-interactive' ) ;
/ * *
* These following two nodes are just boilerplate that breaks down the scaling node .
* /
const atree _scaling _tree = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-scaling-tree' ) ; }
compute _func ( input _map ) {
const [ [ tree , stats ] ] = input _map . values ( ) ;
return tree ;
}
} ) ( ) . link _to ( atree _scaling , 'atree-scaling' ) ;
const atree _scaling _stats = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-scaling-stats' ) ; }
compute _func ( input _map ) {
const [ [ tree , stats ] ] = input _map . values ( ) ;
return stats ;
}
} ) ( ) . link _to ( atree _scaling , 'atree-scaling' ) ;
2022-08-15 17:30:02 +00:00
const atree _render _errors = new ( class extends ComputeNode {
2022-06-30 12:27:35 +00:00
constructor ( ) {
2022-08-15 17:30:02 +00:00
super ( 'atree-render-errors' ) ;
this . list _elem = document . getElementById ( "atree-warning" ) ;
2022-06-30 12:27:35 +00:00
}
compute _func ( input _map ) {
2022-08-15 17:30:02 +00:00
const [ hard _error , 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?
2022-06-30 12:27:35 +00:00
if ( errors . length > 0 ) {
2022-08-15 17:30:02 +00:00
const errorbox = make _elem ( 'div' , [ 'rounded-bottom' , 'dark-4' , 'border' , 'p-0' , 'mx-2' , 'mb-0' , 'mt-4' , 'dark-shadow' ] ) ;
2022-08-05 06:24:53 +00:00
this . list _elem . append ( errorbox ) ;
2022-06-30 12:27:35 +00:00
2022-08-05 06:41:41 +00:00
const error _title = make _elem ( 'b' , [ 'warning' , 'scaled-font' ] , { innerHTML : "ATree Error!" } ) ;
2022-08-05 06:24:53 +00:00
errorbox . append ( error _title ) ;
2022-06-30 12:27:35 +00:00
2022-07-08 21:52:17 +00:00
for ( let i = 0 ; i < 5 && i < errors . length ; ++ i ) {
2022-08-05 06:24:53 +00:00
errorbox . append ( make _elem ( "p" , [ "warning" , "small-text" ] , { textContent : errors [ i ] } ) ) ;
2022-07-08 21:52:17 +00:00
}
if ( errors . length > 5 ) {
2022-07-09 10:09:50 +00:00
const error = '... ' + ( errors . length - 5 ) + ' errors not shown' ;
2022-08-05 06:24:53 +00:00
errorbox . append ( make _elem ( "p" , [ "warning" , "small-text" ] , { textContent : error } ) ) ;
2022-06-30 12:27:35 +00:00
}
}
2022-08-15 17:30:02 +00:00
}
} ) ( ) . link _to ( atree _validate , 'atree-errors' ) ;
/ * *
* 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 ]
* /
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' ) ;
this . list _elem . innerHTML = "" ; //reset all atree actives - should be done in a more general way later
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
2022-08-05 06:24:53 +00:00
const active _tooltip = make _elem ( 'div' , [ 'rounded-bottom' , 'dark-4' , 'border' , 'p-0' , 'mx-2' , 'my-4' , 'dark-shadow' ] ) ;
active _tooltip . append ( make _elem ( 'b' , [ 'scaled-font' ] , { innerHTML : abil . display _name } ) ) ;
2022-06-30 12:27:35 +00:00
for ( const desc of abil . desc ) {
2022-08-05 06:24:53 +00:00
active _tooltip . append ( make _elem ( 'p' , [ 'scaled-font-sm' , 'my-0' , 'mx-1' , 'text-wrap' ] , { textContent : desc } ) ) ;
2022-06-30 12:27:35 +00:00
}
2022-07-06 19:16:44 +00:00
ret _map . set ( abil . id , active _tooltip ) ;
2022-06-30 12:27:35 +00:00
2022-08-05 06:24:53 +00:00
this . list _elem . append ( active _tooltip ) ;
2022-06-30 12:27:35 +00:00
}
2022-07-06 19:16:44 +00:00
return ret _map ;
2022-06-30 12:27:35 +00:00
}
2022-08-15 17:30:02 +00:00
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _scaling _tree , 'atree-merged' ) ;
2022-08-11 10:13:53 +00:00
2022-06-30 12:27:35 +00:00
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-06-29 06:23:27 +00:00
2022-08-11 10:13:53 +00:00
/ * *
* Parse out "parametrized entries" .
* Straight replace .
*
* Format : ability _id . propname
* /
function translate ( v ) {
if ( typeof v === 'string' ) {
const [ id _str , propname ] = v . split ( '.' ) ;
const id = parseInt ( id _str ) ;
const ret = atree _merged . get ( id ) . properties [ propname ] ;
return ret ;
}
return v ;
}
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-08-11 10:13:53 +00:00
let ret _spell = ret _spells . get ( effect . base _spell ) ;
2022-07-08 05:51:10 +00:00
if ( ret _spell ) {
// NOTE: do not mutate results of previous steps!
for ( const key in effect ) {
ret _spell [ key ] = deepcopy ( effect [ key ] ) ;
}
}
else {
2022-08-11 10:13:53 +00:00
ret _spell = deepcopy ( effect ) ;
ret _spells . set ( effect . base _spell , ret _spell ) ;
}
for ( const part of ret _spell . parts ) {
if ( 'hits' in part ) {
for ( const idx in part . hits ) {
part . hits [ idx ] = translate ( part . hits [ idx ] ) ;
}
}
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 ;
2023-03-05 07:15:46 +00:00
if ( ! ret _spells . has ( base _spell ) ) {
2022-06-29 06:23:27 +00:00
continue ;
}
2023-03-05 09:44:21 +00:00
2023-03-05 07:15:46 +00:00
const ret _spell = ret _spells . get ( base _spell ) ;
2022-06-28 18:43:35 +00:00
2023-03-05 09:44:21 +00:00
// :enraged:
// NOTE to hpp: this is out here because:
// target_part doesn't exist for spell cost modification abilities
// except when it does... in which case it should apply exactly once.
if ( 'cost' in ret _spell ) { ret _spell . cost += cost ; }
// NOTE: see above comment for the weird placement of this code block.
if ( target _part === null ) { continue ; }
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
2022-08-05 06:24:53 +00:00
if ( part . name !== target _part ) {
continue ;
}
2023-03-05 07:15:46 +00:00
// we found the part. merge or modify it!
2022-08-05 06:24:53 +00:00
if ( 'multipliers' in effect ) {
for ( const [ idx , v ] of effect . multipliers . entries ( ) ) { // python: enumerate()
part . multipliers [ idx ] += v ;
2022-06-29 06:23:27 +00:00
}
2022-08-05 06:24:53 +00:00
}
else if ( 'power' in effect ) {
part . power += effect . power ;
}
else if ( 'hits' in effect ) {
2022-08-11 10:13:53 +00:00
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)
let v = translate ( _v ) ;
2022-08-05 06:24:53 +00:00
if ( idx in part . hits ) { part . hits [ idx ] += v ; }
else { part . hits [ idx ] = v ; }
2022-06-29 06:23:27 +00:00
}
}
2022-08-05 06:24:53 +00:00
else {
throw "uhh invalid spell add effect" ;
}
found _part = true ;
break ;
2022-06-29 06:23:27 +00:00
}
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
2022-08-11 10:13:53 +00:00
if ( 'hits' in spell _part ) {
for ( const idx in spell _part . hits ) {
spell _part . hits [ idx ] = translate ( spell _part . hits [ idx ] ) ;
}
}
2022-06-29 06:23:27 +00:00
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-08-15 17:30:02 +00:00
} ) ( ) . link _to ( atree _scaling _tree , 'atree-merged' ) ;
2022-07-23 19:30:59 +00:00
/ * *
2022-08-11 10:13:53 +00:00
* Collect raw stats from ability tree .
2022-07-23 19:30:59 +00:00
* Return StatMap of added stats .
*
* Signature : AbilityTreeStatsNode ( atree - merged : MergedATree ) => StatMap
* /
2022-08-11 10:13:53 +00:00
const atree _raw _stats = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-raw-stats-collector' ) ; }
2022-07-23 19:30:59 +00:00
compute _func ( input _map ) {
const atree _merged = input _map . get ( 'atree-merged' ) ;
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 ) {
2022-07-06 06:43:44 +00:00
case 'raw_stat' :
2022-07-24 16:05:57 +00:00
// toggles are handled in atree_scaling.
if ( effect . toggle ) { continue ; }
2022-07-06 06:43:44 +00:00
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 ;
}
}
}
return ret _effects ;
}
2022-07-24 16:05:57 +00:00
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) ;
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.
2022-07-23 19:30:59 +00:00
this . passthrough = new PassThroughNode ( 'spell-calc-buffer' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
2022-06-29 06:23:27 +00:00
this . spelldmg _nodes = [ ] ; // debugging use
this . spell _display _elem = document . getElementById ( "all-spells-display" ) ;
}
compute _func ( input _map ) {
this . passthrough . remove _link ( this . build _node ) ;
this . passthrough . remove _link ( this . stat _agg _node ) ;
2022-07-23 19:30:59 +00:00
this . passthrough = new PassThroughNode ( 'spell-calc-buffer' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
2022-06-29 06:23:27 +00:00
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-08-05 06:24:53 +00:00
let spell _node = new SpellSelectNode ( spell )
. link _to ( build _node , 'build' ) ;
2022-06-29 06:23:27 +00:00
2022-08-05 06:24:53 +00:00
let calc _node = new SpellDamageCalcNode ( spell . base _spell )
. link _to ( build _node , 'build' )
. link _to ( stat _agg _node , 'stats' )
2022-06-29 06:23:27 +00:00
. link _to ( spell _node , 'spell-info' ) ;
this . spelldmg _nodes . push ( calc _node ) ;
2022-08-05 06:24:53 +00:00
let display _elem = make _elem ( 'div' , [ "col" , "pe-0" ] ) ;
2022-06-29 06:23:27 +00:00
// TODO: just pass these elements into the display node instead of juggling the raw IDs...
2022-07-20 17:31:38 +00:00
let spell _summary = make _elem ( 'div' , [ "col" , "spell-display" , "fake-button" , "dark-5" , "rounded" , "dark-shadow" , "pt-2" , "border" , "border-dark" ] ,
2022-07-19 08:09:21 +00:00
{ id : "spell" + spell . base _spell + "-infoAvg" } ) ;
let spell _detail = make _elem ( 'div' , [ "col" , "spell-display" , "dark-5" , "rounded" , "dark-shadow" , "py-2" ] ,
2022-08-05 06:24:53 +00:00
{ id : "spell" + spell . base _spell + "-info" , style : { display : 'none' } } ) ;
2022-06-29 06:23:27 +00:00
2022-08-05 06:24:53 +00:00
display _elem . append ( spell _summary , spell _detail ) ;
2022-06-29 06:23:27 +00:00
2022-08-05 06:24:53 +00:00
let display _node = new SpellDisplayNode ( spell . base _spell )
. link _to ( stat _agg _node , 'stats' )
. link _to ( spell _node , 'spell-info' )
. link _to ( calc _node , 'spell-damage' ) ;
2022-06-29 06:23:27 +00:00
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-08-05 06:24:53 +00:00
const active _row = make _elem ( "div" , [ "row" , "item-title" , "mx-auto" , "justify-content-center" ] ) ;
const active _word = make _elem ( "div" , [ "col-auto" ] , { textContent : "Active Abilities:" } ) ;
2022-07-07 03:04:53 +00:00
2022-08-05 06:24:53 +00:00
const active _AP _container = make _elem ( "div" , [ "col-auto" ] ) ;
const active _AP _subcontainer = make _elem ( "div" , [ "row" ] ) ;
const active _AP _cost = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cost" , textContent : "0" } ) ;
2022-07-07 03:04:53 +00:00
2022-08-05 06:24:53 +00:00
const active _AP _slash = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : "/" } ) ;
const active _AP _cap = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cap" } ) ;
const active _AP _end = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : " AP" } ) ;
2022-06-26 08:15:26 +00:00
2022-08-05 06:24:53 +00:00
active _AP _container . append ( 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-08-05 06:24:53 +00:00
list _elem . append ( active _row ) ;
2022-06-23 11:24:12 +00:00
2022-08-05 06:24:53 +00:00
const atree _map = new Map ( ) ;
const atree _connectors _map = new Map ( )
2022-06-26 23:49:35 +00:00
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 ++ ) {
2022-08-05 06:41:00 +00:00
const row = make _elem ( 'div' , [ 'row' ] , { id : "atree-row-" + j } ) ;
2022-06-26 23:49:35 +00:00
for ( let k = 0 ; k < 9 ; k ++ ) {
2022-08-05 06:41:00 +00:00
row . append ( make _elem ( 'div' , [ 'col' , 'px-0' ] , { style : "position: relative; aspect-ratio: 1/1;" } ) ) ;
2022-06-23 15:00:15 +00:00
}
2022-08-05 06:41:00 +00:00
UI _elem . append ( 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
2022-07-28 15:23:01 +00:00
let connect _elem = make _elem ( "canvas" , [ ] , { style : "width: 112.5%; height: 112.5%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); image-rendering: pixelated;" , width : "18" , height : "18" } ) ;
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-07-23 22:14:03 +00:00
let node _elem = document . getElementById ( "atree-row-" + ability . display . row ) . children [ ability . display . col ] ;
2022-07-28 15:23:01 +00:00
node _wrap . img = make _elem ( "canvas" , [ ] , { style : "width: 200%; height: 200%; position: absolute; top: -50%; left: -50%; image-rendering: pixelated; z-index: 1;" , width : "32" , height : "32" } )
2022-07-23 22:10:44 +00:00
node _elem . appendChild ( node _wrap . img ) ;
2022-07-12 22:06:40 +00:00
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
2022-08-05 06:24:53 +00:00
let hitbox = make _elem ( 'div' , [ ] , {
style : 'position: absolute; cursor: pointer; left: 0; top: 0; width: 100%; height: 100%; z-index: 2;'
} ) ;
2022-07-12 03:17:00 +00:00
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-08-02 18:58:06 +00:00
// add listeners
// listeners differ between mobile and desktop since hovering is a bit fucky on mobile
if ( ! isMobile ) { // desktop
hitbox . addEventListener ( 'click' , function ( e ) {
atree _set _state ( node _wrap , ! node _wrap . active ) ;
atree _state _node . mark _dirty ( ) . update ( ) ;
} ) ;
2022-06-26 07:48:42 +00:00
2022-08-02 18:58:06 +00:00
// add tooltip
hitbox . addEventListener ( 'mouseover' , function ( e ) {
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
}
2022-08-03 18:17:16 +00:00
node _wrap . tooltip _elem = make _elem ( "div" , [ "rounded-bottom" , "dark-4" , "border" , "mx-2" , "my-4" , "dark-shadow" , "text-start" ] , {
style : {
position : "absolute" ,
zIndex : "100" ,
top : ( node _elem . getBoundingClientRect ( ) . top + window . pageYOffset + 50 ) + "px" ,
left : UI _elem . getBoundingClientRect ( ) . left + "px" ,
width : ( UI _elem . getBoundingClientRect ( ) . width * 0.95 ) + "px"
}
} ) ;
2022-08-02 18:58:06 +00:00
generateTooltip ( node _wrap . tooltip _elem , node _elem , ability , atree _map ) ;
UI _elem . appendChild ( node _wrap . tooltip _elem ) ;
} ) ;
hitbox . addEventListener ( 'mouseout' , function ( e ) {
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
}
} ) ;
} else { // mobile
// tap to toggle
// long press to make a popup with the tooltip and a button to turn off/on
2022-08-03 17:45:29 +00:00
let touchTimer = null ;
let didLongPress = false ;
2022-08-02 18:58:06 +00:00
hitbox . addEventListener ( "touchstart" , function ( e ) {
clearTimeout ( touchTimer ) ;
touchTimer = setTimeout ( function ( ) {
let popup = make _elem ( "div" , [ ] , { style : "position: fixed; z-index: 10000; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.6); padding-top: 10vh; padding-left: 2.5vw; user-select: none;" } ) ;
popup . addEventListener ( "click" , function ( e ) {
// close popup if the background is clicked
2022-08-03 17:45:29 +00:00
if ( e . target !== this ) { return ; } // e.target is the lowest element that was the target of the event
2022-08-02 18:58:06 +00:00
popup . remove ( ) ;
} ) ;
let tooltip = make _elem ( "div" , [ "rounded-bottom" , "dark-4" , "border" , "dark-shadow" , "text-start" ] , { "style" : "width: 95vw; max-height: 80vh; overflow-y: scroll;" } ) ;
generateTooltip ( tooltip , node _elem , ability , atree _map ) ;
popup . appendChild ( tooltip ) ;
let toggleButton = make _elem ( "button" , [ "scaled-font" , "disable-select" ] , { innerHTML : ( node _wrap . active ? "Unselect Ability" : "Select Ability" ) , style : "width: 95vw; height: 8vh; margin-top: 2vh; text-align: center;" } ) ;
toggleButton . addEventListener ( "click" , function ( e ) {
atree _set _state ( node _wrap , ! node _wrap . active ) ;
atree _state _node . mark _dirty ( ) . update ( ) ;
popup . remove ( ) ;
} ) ;
popup . appendChild ( toggleButton ) ;
document . body . appendChild ( popup ) ;
didLongPress = true ;
touchTimer = null ;
} , 500 ) ;
} ) ;
hitbox . addEventListener ( "touchend" , function ( e ) {
if ( ! didLongPress ) {
clearTimeout ( touchTimer ) ;
touchTimer = null ;
atree _set _state ( node _wrap , ! node _wrap . active ) ;
atree _state _node . mark _dirty ( ) . update ( ) ;
} else {
didLongPress = false ;
}
} ) ;
}
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-08-02 18:58:06 +00:00
function generateTooltip ( container , node _elem , ability , atree _map ) {
2022-07-19 18:40:37 +00:00
// title
2022-07-18 22:45:12 +00:00
let title = make _elem ( "b" , [ "scaled-font" , "mx-1" ] , { } ) ;
title . innerHTML = ability . display _name ;
switch ( ability . display . icon ) {
2022-07-20 02:50:14 +00:00
case "node_0" :
// already white
break ;
2022-07-18 22:45:12 +00:00
case "node_1" :
title . classList . add ( "mc-gold" ) ;
break ;
case "node_2" :
title . classList . add ( "mc-light-purple" ) ;
break ;
case "node_3" :
title . classList . add ( "mc-red" ) ;
break ;
case "node_warrior" :
case "node_archer" :
case "node_mage" :
case "node_assassin" :
case "node_shaman" :
title . classList . add ( "mc-green" ) ;
break ;
}
container . appendChild ( title ) ;
2022-07-14 18:17:38 +00:00
2022-07-18 22:45:12 +00:00
container . innerHTML += "<br/><br/>" ;
2022-07-14 18:17:38 +00:00
2022-07-19 18:40:37 +00:00
// description
2022-08-02 18:58:06 +00:00
let description = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" , "text-wrap" , "mc-gray" ] , { } ) ;
2022-07-19 20:06:06 +00:00
let numberRegex = /[+-]?\d+(\.\d+)?[%+s]?/g ; // +/- (optional), 1 or more digits, period followed by 1 or more digits (optional), %/+/s (optional)
description . innerHTML = ability . desc . replaceAll ( numberRegex , ( m ) => { return "<span class = 'mc-white'>" + m + "</span>" } ) ;
2022-07-18 22:45:12 +00:00
container . appendChild ( description ) ;
2022-07-14 18:17:38 +00:00
2022-08-05 06:24:53 +00:00
container . appendChild ( make _elem ( 'br' ) ) ;
2022-07-14 18:17:38 +00:00
2022-07-19 18:40:37 +00:00
// archetype
2022-07-18 22:45:12 +00:00
if ( "archetype" in ability && ability . archetype !== "" ) {
2022-08-02 18:58:06 +00:00
let archetype = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" ] , { } ) ;
2022-07-18 22:45:12 +00:00
archetype . innerHTML = ability . archetype + " Archetype" ;
switch ( ability . archetype ) {
case "Riftwalker" :
case "Paladin" :
archetype . classList . add ( "mc-aqua" ) ;
break ;
case "Fallen" :
archetype . classList . add ( "mc-red" ) ;
break ;
case "Boltslinger" :
case "Battle Monk" :
archetype . classList . add ( "mc-yellow" ) ;
break ;
case "Trapper" :
archetype . classList . add ( "mc-dark-green" ) ;
break ;
2022-07-19 23:03:13 +00:00
case "Trickster" :
2022-07-18 22:45:12 +00:00
case "Sharpshooter" :
archetype . classList . add ( "mc-light-purple" ) ;
break ;
case "Arcanist" :
archetype . classList . add ( "mc-dark-purple" ) ;
break ;
2022-07-19 23:03:13 +00:00
case "Acrobat" :
2022-07-18 22:45:12 +00:00
case "Light Bender" :
// already white
break ;
2022-07-19 23:03:13 +00:00
case "Shadestepper" :
archetype . classList . add ( "mc-dark-red" ) ;
break ;
2022-07-18 22:45:12 +00:00
}
container . appendChild ( archetype ) ;
2022-08-05 06:24:53 +00:00
container . appendChild ( make _elem ( 'br' ) ) ;
2022-07-18 22:45:12 +00:00
}
2022-07-14 18:17:38 +00:00
2022-07-19 18:40:37 +00:00
// calculate if requirements are satisfied
let apUsed = 0 ;
let maxAP = parseInt ( document . getElementById ( "active_AP_cap" ) . innerHTML ) ;
2022-12-16 10:29:01 +00:00
let arch _chosen = 0 ;
const node _arch = ability . req _archetype || ability . archetype ;
2022-07-21 18:26:46 +00:00
let satisfiedDependencies = [ ] ;
2022-07-19 18:40:37 +00:00
let blockedBy = [ ] ;
for ( let [ id , node _wrap ] of atree _map . entries ( ) ) {
if ( ! node _wrap . active || id == ability . id ) {
continue ; // we don't want to count abilities that are not selected, and an ability should not count towards itself
}
apUsed += node _wrap . ability . cost ;
2022-12-16 10:29:01 +00:00
if ( node _wrap . ability . archetype == node _arch ) {
arch _chosen ++ ;
2022-07-19 18:40:37 +00:00
}
2022-07-21 18:26:46 +00:00
if ( ability . dependencies . includes ( id ) ) {
satisfiedDependencies . push ( id ) ;
}
2022-07-19 18:40:37 +00:00
if ( ability . blockers . includes ( id ) ) {
blockedBy . push ( node _wrap . ability . display _name ) ;
}
2022-07-14 18:17:38 +00:00
}
2022-07-19 18:40:37 +00:00
let reqYes = "<span class = 'mc-green'>✔</span>" // green check mark
let reqNo = "<span class = 'mc-red'>✖</span>" // red x
2022-07-14 18:17:38 +00:00
2022-07-19 18:40:37 +00:00
// cost
2022-08-02 18:58:06 +00:00
let cost = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" ] , { } ) ;
2022-07-19 18:40:37 +00:00
if ( apUsed + ability . cost > maxAP ) {
cost . innerHTML = reqNo ;
} else {
cost . innerHTML = reqYes ;
}
2022-08-11 00:15:47 +00:00
cost . innerHTML += "<span class = 'mc-gray'>Ability Points:</span> " + ( maxAP - apUsed ) + "<span class = 'mc-gray'>/" + ability . cost ;
2022-07-19 18:40:37 +00:00
container . appendChild ( cost ) ;
// archetype req
2022-12-16 10:29:01 +00:00
if ( ability . archetype _req > 0 ) {
let arch _req = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" ] , { } ) ;
if ( 'req_archetype' in ability && ability . req _archetype !== "" ) {
req _archetype = ability . req _archetype ;
}
else {
req _archetype = ability . archetype ;
}
if ( arch _chosen >= ability . archetype _req ) {
arch _req . innerHTML = reqYes ;
2022-07-19 18:40:37 +00:00
} else {
2022-12-16 10:29:01 +00:00
arch _req . innerHTML = reqNo ;
2022-07-19 18:40:37 +00:00
}
2022-12-16 10:29:01 +00:00
arch _req . innerHTML += "<span class = 'mc-gray'>Min " + req _archetype + " Archetype:</span> " + arch _chosen + "<span class = 'mc-gray'>/" + ability . archetype _req ;
container . appendChild ( arch _req ) ;
2022-07-19 18:40:37 +00:00
}
2022-07-14 18:17:38 +00:00
2022-07-21 18:26:46 +00:00
// dependencies
for ( let i = 0 ; i < ability . dependencies . length ; i ++ ) {
2022-08-02 18:58:06 +00:00
let dependency = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" ] , { } ) ;
2022-07-21 18:26:46 +00:00
if ( satisfiedDependencies . includes ( ability . dependencies [ i ] ) ) {
dependency . innerHTML = reqYes ;
} else {
dependency . innerHTML = reqNo ;
}
2022-08-11 00:15:47 +00:00
dependency . innerHTML += "<span class = 'mc-gray'>Required Ability:</span> " + atree _map . get ( ability . dependencies [ i ] ) . ability . display _name ;
2022-07-21 18:26:46 +00:00
container . appendChild ( dependency ) ;
}
2022-07-19 18:40:37 +00:00
// blockers
for ( let i = 0 ; i < blockedBy . length ; i ++ ) {
2022-08-02 18:58:06 +00:00
let blocker = make _elem ( "p" , [ "scaled-font" , "my-0" , "mx-1" ] , { } ) ;
2022-08-11 00:15:47 +00:00
blocker . innerHTML = reqNo + "<span class = 'mc-gray'>Blocked By:</span> " + blockedBy [ i ] ;
2022-07-19 18:40:37 +00:00
container . appendChild ( blocker ) ;
}
2022-07-14 18:17:38 +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
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
}
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-08-11 21:45:23 +00:00
draw _atlas _image ( node _wrapper . img , atree _node _atlas _img , [ atree _node _atlas _positions [ icon ] , 2 ] , atree _node _tile _size ) ;
2022-06-30 15:03:41 +00:00
}
else {
node _wrapper . active = false ;
2022-08-11 21:45:23 +00:00
draw _atlas _image ( node _wrapper . img , atree _node _atlas _img , [ atree _node _atlas _positions [ icon ] , 1 ] , atree _node _tile _size ) ;
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-07-28 15:23:01 +00:00
// atlas vars
2022-07-23 17:18:35 +00:00
// first key is connector type, second key is highlight, then [x, y] pair of 0-index positions in the tile atlas
2022-08-11 21:45:23 +00:00
const atree _connector _atlas _positions = {
2022-07-23 17:18:35 +00:00
"1100" : { "0000" : [ 0 , 0 ] , "1100" : [ 1 , 0 ] } ,
"1010" : { "0000" : [ 2 , 0 ] , "1010" : [ 3 , 0 ] } ,
"0110" : { "0000" : [ 4 , 0 ] , "0110" : [ 5 , 0 ] } ,
"1001" : { "0000" : [ 6 , 0 ] , "1001" : [ 7 , 0 ] } ,
"0101" : { "0000" : [ 8 , 0 ] , "0101" : [ 9 , 0 ] } ,
"0011" : { "0000" : [ 10 , 0 ] , "0011" : [ 11 , 0 ] } ,
"1101" : { "0000" : [ 0 , 1 ] , "1101" : [ 1 , 1 ] , "1100" : [ 2 , 1 ] , "1001" : [ 3 , 1 ] , "0101" : [ 4 , 1 ] } ,
"0111" : { "0000" : [ 5 , 1 ] , "0111" : [ 6 , 1 ] , "0110" : [ 7 , 1 ] , "0101" : [ 8 , 1 ] , "0011" : [ 9 , 1 ] } ,
"1110" : { "0000" : [ 0 , 2 ] , "1110" : [ 1 , 2 ] , "1100" : [ 2 , 2 ] , "1010" : [ 3 , 2 ] , "0110" : [ 4 , 2 ] } ,
"1011" : { "0000" : [ 5 , 2 ] , "1011" : [ 6 , 2 ] , "1010" : [ 7 , 2 ] , "1001" : [ 8 , 2 ] , "0011" : [ 9 , 2 ] } ,
"1111" : { "0000" : [ 0 , 3 ] , "1111" : [ 1 , 3 ] , "1110" : [ 2 , 3 ] , "1101" : [ 3 , 3 ] , "1100" : [ 4 , 3 ] , "1011" : [ 5 , 3 ] , "1010" : [ 6 , 3 ] , "1001" : [ 7 , 3 ] , "0111" : [ 8 , 3 ] , "0110" : [ 9 , 3 ] , "0101" : [ 10 , 3 ] , "0011" : [ 11 , 3 ] }
}
2022-08-11 21:45:23 +00:00
const atree _connector _tile _size = 18 ;
const atree _connector _atlas _img = make _elem ( "img" , [ ] , { src : "../media/atree/connectors.png" , loaded : false } ) ;
atree _connector _atlas _img . addEventListener ( "load" , ( ) => {
atree _connector _atlas _img . loaded = true ;
for ( const to _draw of atlas _to _draw . get ( atree _connector _atlas _img ) ) {
draw _atlas _image ( to _draw [ 0 ] , atree _connector _atlas _img , to _draw [ 1 ] , to _draw [ 2 ] ) ;
}
atlas _to _draw . set ( atree _connector _atlas _img , [ ] ) ;
} ) ;
2022-07-28 15:23:01 +00:00
2022-07-23 22:10:44 +00:00
// just has the x position, y is based on state
2022-08-11 21:45:23 +00:00
const atree _node _atlas _positions = {
2022-07-23 22:10:44 +00:00
"node_0" : 0 ,
"node_1" : 1 ,
"node_2" : 2 ,
"node_3" : 3 ,
"node_archer" : 4 ,
"node_warrior" : 5 ,
"node_mage" : 6 ,
"node_assassin" : 7 ,
"node_shaman" : 8
}
2022-08-11 21:45:23 +00:00
const atree _node _tile _size = 32 ;
const atree _node _atlas _img = make _elem ( "img" , [ ] , { src : "../media/atree/icons.png" , loaded : false } ) ;
atree _node _atlas _img . addEventListener ( "load" , ( ) => {
atree _node _atlas _img . loaded = true ;
for ( const to _draw of atlas _to _draw . get ( atree _node _atlas _img ) ) {
draw _atlas _image ( to _draw [ 0 ] , atree _node _atlas _img , to _draw [ 1 ] , to _draw [ 2 ] ) ;
}
atlas _to _draw . set ( atree _node _atlas _img , [ ] ) ;
} ) ;
const atlas _to _draw = new Map ( ) ;
atlas _to _draw . set ( atree _connector _atlas _img , [ ] ) ;
atlas _to _draw . set ( atree _node _atlas _img , [ ] ) ;
function draw _atlas _image ( canvas , img , pos , tile _size ) {
if ( ! img . loaded ) {
atlas _to _draw . get ( img ) . push ( [ canvas , pos , tile _size ] ) ;
return ;
}
2022-07-28 15:23:01 +00:00
let ctx = canvas . getContext ( "2d" ) ;
2022-08-11 21:45:23 +00:00
ctx . clearRect ( 0 , 0 , tile _size , tile _size ) ;
ctx . drawImage ( img , tile _size * pos [ 0 ] , tile _size * pos [ 1 ] , tile _size , tile _size , 0 , 0 , tile _size , tile _size ) ;
2022-07-23 17:18:35 +00:00
}
// draw the connector onto the screen
function atree _render _connection ( atree _connectors _map ) {
for ( let i of atree _connectors _map . keys ( ) ) {
let connector _info = atree _connectors _map . get ( i ) ;
let connector _elem = connector _info . connector ;
set _connector _type ( connector _info ) ;
connector _info . highlight = [ 0 , 0 , 0 , 0 ] ;
2022-08-11 21:45:23 +00:00
draw _atlas _image ( connector _elem , atree _connector _atlas _img , atree _connector _atlas _positions [ connector _info . type ] [ "0000" ] , atree _connector _tile _size ) ;
2022-07-23 17:18:35 +00:00
let target _elem = document . getElementById ( "atree-row-" + i . split ( "," ) [ 0 ] ) . children [ i . split ( "," ) [ 1 ] ] ;
if ( target _elem . children . length != 0 ) {
2022-08-05 06:24:53 +00:00
// janky special case... sometimes the ability tree tries to draw a link on top of a node...
2022-07-23 17:18:35 +00:00
connector _elem . style . display = 'none' ;
}
target _elem . appendChild ( connector _elem ) ;
} ;
} ;
// update the connector (after being drawn the first time by atree_render_connection)
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
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-08-11 21:45:23 +00:00
draw _atlas _image ( connector _elem , atree _connector _atlas _img , atree _connector _atlas _positions [ ctype ] [ render ] , atree _connector _tile _size ) ;
2022-07-23 17:18:35 +00:00
continue ;
} else {
// lol bad overloading, [0] is just the whole state
highlight _state [ 0 ] += state _delta ;
if ( highlight _state [ 0 ] > 0 ) {
2022-08-11 21:45:23 +00:00
draw _atlas _image ( connector _elem , atree _connector _atlas _img , atree _connector _atlas _positions [ ctype ] [ ctype ] , atree _connector _tile _size ) ;
2022-07-12 22:06:40 +00:00
} else {
2022-08-11 21:45:23 +00:00
draw _atlas _image ( connector _elem , atree _connector _atlas _img , atree _connector _atlas _positions [ ctype ] [ "0000" ] , atree _connector _tile _size ) ;
2022-07-12 22:06:40 +00:00
}
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
}
2022-06-26 08:59:02 +00:00
}