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
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)
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
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 ,
"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
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
2022-07-21 04:55:07 +00:00
// modify: change existing part, by incrementing properties. do nothing if not exist
2022-07-07 04:15:41 +00:00
slider _max : Optional [ float ] // affected by slider_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
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 ) ;
}
}
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 . 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-07-11 13:15:53 +00:00
* Signature : AbilityTreeMergeNode ( player - class : WeaponType , atree : ATree , atree - state : RenderedATree ) => Map [ id , Ability ]
2022-06-28 18:43:35 +00:00
* /
const atree _merge = new ( class extends ComputeNode {
constructor ( ) { super ( 'builder-atree-merge' ) ; }
compute _func ( input _map ) {
2022-07-11 13:15:53 +00:00
const player _class = input _map . get ( 'player-class' ) ;
2022-06-28 18:43:35 +00:00
const atree _state = input _map . get ( 'atree-state' ) ;
const atree _order = input _map . get ( 'atree' ) ;
let abils _merged = new Map ( ) ;
2022-07-11 13:15:53 +00:00
for ( const abil of default _abils [ player _class ] ) {
2022-06-28 19:03:49 +00:00
let tmp _abil = deepcopy ( abil ) ;
2022-07-09 03:47:34 +00:00
if ( ! ( 'desc' in tmp _abil ) ) {
tmp _abil . desc = [ ] ;
}
else if ( ! Array . isArray ( tmp _abil . desc ) ) {
2022-06-28 19:03:49 +00:00
tmp _abil . desc = [ tmp _abil . desc ] ;
}
tmp _abil . subparts = [ abil . id ] ;
abils _merged . set ( abil . id , tmp _abil ) ;
2022-06-28 18:43:35 +00:00
}
for ( const node of atree _order ) {
const abil _id = node . ability . id ;
if ( ! atree _state . get ( abil _id ) . active ) {
continue ;
}
const abil = node . ability ;
2022-07-06 19:16:44 +00:00
if ( 'base_abil' in abil ) {
if ( abils _merged . has ( abil . base _abil ) ) {
// Merge abilities.
// TODO: What if there is more than one base abil?
let base _abil = abils _merged . get ( abil . base _abil ) ;
if ( Array . isArray ( abil . desc ) ) { base _abil . desc = base _abil . desc . concat ( abil . desc ) ; }
else { base _abil . desc . push ( abil . desc ) ; }
base _abil . subparts . push ( abil . id ) ;
base _abil . effects = base _abil . effects . concat ( abil . effects ) ;
for ( let propname in abil . properties ) {
base _abil [ propname ] = abil [ propname ] ;
}
2022-06-28 19:03:49 +00:00
}
2022-07-06 19:16:44 +00:00
// do nothing otherwise.
2022-06-28 18:43:35 +00:00
}
else {
2022-06-28 19:03:49 +00:00
let tmp _abil = deepcopy ( abil ) ;
if ( ! Array . isArray ( tmp _abil . desc ) ) {
tmp _abil . desc = [ tmp _abil . desc ] ;
}
tmp _abil . subparts = [ abil . id ] ;
abils _merged . set ( abil _id , tmp _abil ) ;
2022-06-28 18:43:35 +00:00
}
}
return abils _merged ;
}
2022-07-06 06:43:44 +00:00
} ) ( ) . link _to ( atree _node , 'atree' ) . link _to ( atree _state _node , 'atree-state' ) ;
2022-06-28 18:43:35 +00:00
2022-07-11 09:03:42 +00:00
/ * *
* Check if an atree node can be activated .
*
* Return : [ yes / no , hard error , reason ]
* /
function abil _can _activate ( atree _node , atree _state , reachable , archetype _count , points _remain ) {
const { parents , ability } = atree _node ;
if ( parents . length === 0 ) {
return [ true , false , "" ] ;
}
let failed _deps = [ ] ;
for ( const dep _id of ability . dependencies ) {
if ( ! atree _state . get ( dep _id ) . active ) { failed _deps . push ( dep _id ) }
}
if ( failed _deps . length > 0 ) {
const dep _strings = failed _deps . map ( i => '"' + atree _state . get ( i ) . ability . display _name + '"' ) ;
return [ false , true , 'missing dep: ' + dep _strings . join ( ", " ) ] ;
}
let blocking _ids = [ ] ;
for ( const blocker _id of ability . blockers ) {
if ( atree _state . get ( blocker _id ) . active ) { blocking _ids . push ( blocker _id ) ; }
}
if ( blocking _ids . length > 0 ) {
const blockers _strings = blocking _ids . map ( i => '"' + atree _state . get ( i ) . ability . display _name + '"' ) ;
return [ false , true , 'blocked by: ' + blockers _strings . join ( ", " ) ] ;
}
let node _reachable = false ;
for ( const parent of parents ) {
if ( reachable . has ( parent . ability . id ) ) {
node _reachable = true ;
break ;
}
}
if ( ! node _reachable ) {
return [ false , false , 'not reachable' ] ;
}
if ( 'archetype' in ability && ability . archetype !== "" ) {
if ( 'archetype_req' in ability && ability . archetype _req !== 0 ) {
const others = ( archetype _count . get ( ability . archetype ) || 0 ) ;
if ( others < ability . archetype _req ) {
return [ false , false , ability . archetype + ': ' + others + ' < ' + ability . archetype _req ] ;
}
}
}
if ( ability . cost > points _remain ) {
return [ false , false , "not enough ability points left" ] ;
}
return [ true , false , "" ] ;
}
2022-06-30 12:27:35 +00:00
/ * *
* Validate ability tree .
* Return list of errors for rendering .
*
* Signature : AbilityTreeMergeNode ( atree : ATree , atree - state : RenderedATree ) => List [ str ]
* /
const atree _validate = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-validator' ) ; }
compute _func ( input _map ) {
const atree _state = input _map . get ( 'atree-state' ) ;
const atree _order = input _map . get ( 'atree' ) ;
2022-07-11 09:03:42 +00:00
const level = parseInt ( input _map . get ( 'level' ) ) ;
2022-06-30 12:27:35 +00:00
2022-07-08 22:45:24 +00:00
if ( atree _order . length == 0 ) { return [ 0 , false , [ 'no atree data' ] ] ; }
2022-07-05 07:51:02 +00:00
2022-07-08 21:52:17 +00:00
let atree _to _add = [ ] ;
2022-07-11 09:03:42 +00:00
let atree _not _present = [ ] ;
// mark all selected nodes as bright, and mark all other nodes as dark.
// also initialize the "to check" list, and the "not present" list.
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
const abil = node . ability ;
2022-07-10 02:30:29 +00:00
if ( atree _state . get ( abil . id ) . active ) {
atree _to _add . push ( [ node , 'not reachable' , false ] ) ;
2022-07-12 22:06:40 +00:00
atree _state . get ( abil . id ) . img . src = '../media/atree/' + abil . display . icon + '_selected.png' ;
2022-06-30 12:27:35 +00:00
}
2022-07-10 02:30:29 +00:00
else {
2022-07-11 09:03:42 +00:00
atree _not _present . push ( abil . id ) ;
2022-07-12 22:06:40 +00:00
atree _state . get ( abil . id ) . img . src = '../media/atree/' + abil . display . icon + '_blocked.png' ;
2022-06-30 15:03:41 +00:00
}
2022-07-08 21:52:17 +00:00
}
2022-06-30 15:03:41 +00:00
2022-07-08 21:52:17 +00:00
let reachable = new Set ( ) ;
let abil _points _total = 0 ;
let archetype _count = new Map ( ) ;
while ( true ) {
let _add = [ ] ;
for ( const [ node , fail _reason , fail _hardness ] of atree _to _add ) {
2022-07-11 09:03:42 +00:00
const { ability } = node ;
const [ success , hard _error , reason ] = abil _can _activate ( node , atree _state , reachable , archetype _count , 9999 ) ;
if ( ! success ) {
_add . push ( [ node , reason , hard _error ] ) ;
2022-07-12 07:00:51 +00:00
continue ;
2022-06-30 15:03:41 +00:00
}
2022-07-08 21:52:17 +00:00
if ( 'archetype' in ability && ability . archetype !== "" ) {
let val = 1 ;
if ( archetype _count . has ( ability . archetype ) ) {
val = archetype _count . get ( ability . archetype ) + 1 ;
}
archetype _count . set ( ability . archetype , val ) ;
}
abil _points _total += ability . cost ;
reachable . add ( ability . id ) ;
2022-06-30 12:27:35 +00:00
}
2022-07-08 21:52:17 +00:00
if ( atree _to _add . length == _add . length ) {
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 ) {
node . img . src = '../media/atree/' + node . ability . display . icon + '.png' ;
2022-06-30 12:27:35 +00:00
}
}
2022-07-08 21:52:17 +00:00
let hard _error = false ;
let errors = [ ] ;
2022-07-11 09:03:42 +00:00
if ( abil _points _total > AP _cap ) {
errors . push ( 'too many ability points assigned! (' + abil _points _total + ' > ' + AP _cap + ')' ) ;
}
2022-07-08 21:52:17 +00:00
for ( const [ node , fail _reason , fail _hardness ] of atree _to _add ) {
if ( fail _hardness ) { hard _error = true ; }
errors . push ( node . ability . display _name + ": " + fail _reason ) ;
2022-06-30 15:03:41 +00:00
}
2022-07-11 09:03:42 +00:00
return [ hard _error , errors ] ;
2022-06-30 12:27:35 +00:00
}
2022-06-30 15:03:41 +00:00
} ) ( ) . link _to ( atree _node , 'atree' ) . link _to ( atree _state _node , 'atree-state' ) ;
2022-06-30 12:27:35 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Render ability tree .
* Return map of id - > corresponding html element .
*
* Signature : AbilityTreeRenderActiveNode ( atree - merged : MergedATree , atree - order : ATree , atree - errors : List [ str ] ) => Map [ int , ATreeNode ]
* /
2022-06-30 12:27:35 +00:00
const atree _render _active = new ( class extends ComputeNode {
constructor ( ) {
super ( 'atree-render-active' ) ;
this . list _elem = document . getElementById ( "atree-active" ) ;
}
compute _func ( input _map ) {
const merged _abils = input _map . get ( 'atree-merged' ) ;
const atree _order = input _map . get ( 'atree-order' ) ;
2022-07-11 09:03:42 +00:00
const [ hard _error , _errors ] = input _map . get ( 'atree-errors' ) ;
const errors = deepcopy ( _errors ) ;
2022-06-30 12:27:35 +00:00
this . list _elem . innerHTML = "" ; //reset all atree actives - should be done in a more general way later
2022-06-30 15:03:41 +00:00
// TODO: move to display?
2022-06-30 12:27:35 +00:00
if ( errors . length > 0 ) {
let errorbox = document . createElement ( 'div' ) ;
errorbox . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "mx-2" , "my-4" , "dark-shadow" ) ;
this . list _elem . appendChild ( errorbox ) ;
let error _title = document . createElement ( 'b' ) ;
error _title . classList . add ( "warning" , "scaled-font" ) ;
error _title . innerHTML = "ATree Error!" ;
errorbox . appendChild ( error _title ) ;
2022-07-08 21:52:17 +00:00
for ( let i = 0 ; i < 5 && i < errors . length ; ++ i ) {
const error = errors [ i ] ;
const atree _warning = make _elem ( "p" , [ "warning" , "small-text" ] , { textContent : error } ) ;
errorbox . appendChild ( atree _warning ) ;
}
if ( errors . length > 5 ) {
2022-07-09 10:09:50 +00:00
const error = '... ' + ( errors . length - 5 ) + ' errors not shown' ;
2022-07-08 21:52:17 +00:00
const atree _warning = make _elem ( "p" , [ "warning" , "small-text" ] , { textContent : error } ) ;
2022-06-30 12:27:35 +00:00
errorbox . appendChild ( atree _warning ) ;
}
}
2022-07-06 19:16:44 +00:00
const ret _map = new Map ( ) ;
2022-07-09 03:47:34 +00:00
const to _render _id = [ 999 , 998 ] ;
2022-06-30 12:27:35 +00:00
for ( const node of atree _order ) {
if ( ! merged _abils . has ( node . ability . id ) ) {
continue ;
}
2022-07-09 03:47:34 +00:00
to _render _id . push ( node . ability . id ) ;
}
for ( const id of to _render _id ) {
const abil = merged _abils . get ( id ) ;
2022-06-30 12:27:35 +00:00
let active _tooltip = document . createElement ( 'div' ) ;
active _tooltip . classList . add ( "rounded-bottom" , "dark-4" , "border" , "p-0" , "mx-2" , "my-4" , "dark-shadow" ) ;
let active _tooltip _title = document . createElement ( 'b' ) ;
active _tooltip _title . classList . add ( "scaled-font" ) ;
active _tooltip _title . innerHTML = abil . display _name ;
active _tooltip . appendChild ( active _tooltip _title ) ;
for ( const desc of abil . desc ) {
let active _tooltip _desc = document . createElement ( 'p' ) ;
active _tooltip _desc . classList . add ( "scaled-font-sm" , "my-0" , "mx-1" , "text-wrap" ) ;
active _tooltip _desc . textContent = desc ;
active _tooltip . appendChild ( active _tooltip _desc ) ;
}
2022-07-06 19:16:44 +00:00
ret _map . set ( abil . id , active _tooltip ) ;
2022-06-30 12:27:35 +00:00
this . list _elem . appendChild ( active _tooltip ) ;
}
2022-07-06 19:16:44 +00:00
return ret _map ;
2022-06-30 12:27:35 +00:00
}
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _validate , 'atree-errors' ) ;
2022-06-29 06:23:27 +00:00
/ * *
* Collect spells from abilities .
*
* Signature : AbilityCollectSpellsNode ( atree - merged : Map [ id , Ability ] ) => List [ Spell ]
* /
const atree _collect _spells = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-spell-collector' ) ; }
compute _func ( input _map ) {
2022-07-08 07:21:21 +00:00
const atree _merged = input _map . get ( 'atree-merged' ) ;
2022-07-11 09:03:42 +00:00
const [ hard _error , errors ] = input _map . get ( 'atree-errors' ) ;
2022-07-08 07:21:21 +00:00
if ( hard _error ) { return [ ] ; }
2022-06-29 06:23:27 +00:00
let ret _spells = new Map ( ) ;
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
// TODO: Possibly, make a better way for detecting "spell abilities"?
2022-06-30 15:58:26 +00:00
for ( const effect of abil . effects ) {
if ( effect . type === 'replace_spell' ) {
2022-07-01 08:23:06 +00:00
// replace_spell just replaces all (defined) aspects.
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( effect . base _spell ) ;
if ( ret _spell ) {
// NOTE: do not mutate results of previous steps!
for ( const key in effect ) {
ret _spell [ key ] = deepcopy ( effect [ key ] ) ;
}
}
else {
ret _spells . set ( effect . base _spell , deepcopy ( effect ) ) ;
2022-07-01 08:23:06 +00:00
}
2022-06-30 15:58:26 +00:00
}
}
2022-07-08 05:51:10 +00:00
}
2022-06-29 06:23:27 +00:00
2022-07-08 05:51:10 +00:00
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
2022-06-29 06:23:27 +00:00
for ( const effect of abil . effects ) {
switch ( effect . type ) {
case 'replace_spell' :
2022-07-01 08:23:06 +00:00
// Already handled above.
2022-06-29 06:23:27 +00:00
continue ;
case 'add_spell_prop' : {
2022-07-05 04:39:35 +00:00
const { base _spell , target _part = null , cost = 0 , behavior = 'merge' } = effect ;
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( base _spell ) ;
2022-07-11 08:25:25 +00:00
// TODO: unjankify this...
if ( 'cost' in ret _spell ) { ret _spell . cost += cost ; }
2022-06-29 06:23:27 +00:00
if ( target _part === null ) {
continue ;
}
2022-06-28 18:43:35 +00:00
2022-06-29 06:23:27 +00:00
let found _part = false ;
for ( let part of ret _spell . parts ) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if ( part . name === target _part ) {
if ( 'multipliers' in effect ) {
for ( const [ idx , v ] of effect . multipliers . entries ( ) ) { // python: enumerate()
part . multipliers [ idx ] += v ;
}
}
else if ( 'power' in effect ) {
part . power += effect . power ;
}
else if ( 'hits' in effect ) {
for ( const [ idx , v ] of Object . entries ( effect . hits ) ) { // looks kinda similar to multipliers case... hmm... can we unify all of these three? (make healpower a list)
2022-07-01 03:35:34 +00:00
if ( idx in part . hits ) { part . hits [ idx ] += v ; }
else { part . hits [ idx ] = v ; }
2022-07-08 05:51:10 +00:00
}
2022-06-29 06:23:27 +00:00
}
else {
throw "uhh invalid spell add effect" ;
}
found _part = true ;
break ;
}
}
2022-07-05 04:39:35 +00:00
if ( ! found _part && behavior === 'merge' ) { // add part. if behavior is merge
2022-06-29 06:23:27 +00:00
let spell _part = deepcopy ( effect ) ;
spell _part . name = target _part ; // has some extra fields but whatever
ret _spell . parts . push ( spell _part ) ;
}
2022-07-01 04:21:25 +00:00
if ( 'display' in effect ) {
ret _spell . display = effect . display ;
}
2022-06-29 06:23:27 +00:00
continue ;
}
case 'convert_spell_conv' :
const { base _spell , target _part , conversion } = effect ;
2022-07-08 05:51:10 +00:00
const ret _spell = ret _spells . get ( base _spell ) ;
2022-06-29 06:23:27 +00:00
const elem _idx = damageClasses . indexOf ( conversion ) ;
let filter = target _part === 'all' ;
for ( let part of ret _spell . parts ) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if ( filter || part . name === target _part ) {
if ( 'multipliers' in part ) {
let total _conv = 0 ;
for ( let i = 1 ; i < 6 ; ++ i ) { // skip neutral
total _conv += part . multipliers [ i ] ;
}
let new _conv = [ part . multipliers [ 0 ] , 0 , 0 , 0 , 0 , 0 ] ;
new _conv [ elem _idx ] = total _conv ;
part . multipliers = new _conv ;
}
}
}
continue ;
}
}
}
return ret _spells ;
}
2022-07-08 07:21:21 +00:00
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _validate , 'atree-errors' ) ;
2022-06-29 06:23:27 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Make interactive elements ( sliders , buttons )
*
2022-07-08 05:28:46 +00:00
* Signature : AbilityActiveUINode ( atree - merged : MergedATree ) => Map < str , slider _info >
2022-07-07 03:13:28 +00:00
*
* ElemState : {
* value : int // value for sliders; 0-1 for toggles
* }
2022-07-06 19:16:44 +00:00
* /
2022-07-07 04:15:41 +00:00
const atree _make _interactives = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-make-interactives' ) ; }
2022-07-06 19:16:44 +00:00
compute _func ( input _map ) {
const merged _abils = input _map . get ( 'atree-merged' ) ;
const atree _order = input _map . get ( 'atree-order' ) ;
const atree _html = input _map . get ( 'atree-elements' ) ;
2022-07-07 22:33:00 +00:00
2022-07-23 03:11:28 +00:00
document . getElementById ( "boost-sliders" ) . innerHTML = "" ;
document . getElementById ( "boost-toggles" ) . innerHTML = "" ;
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 ( ) ;
2022-07-18 09:42:43 +00:00
const button _map = new Map ( ) ;
2022-07-07 03:13:28 +00:00
2022-07-23 03:11:28 +00:00
// first, pull out all the sliders and toggles.
2022-07-07 22:33:00 +00:00
for ( const [ abil _id , ability ] of merged _abils . entries ( ) ) {
for ( const effect of ability . effects ) {
if ( effect [ 'type' ] === "stat_scaling" && effect [ 'slider' ] === true ) {
const { slider _name , slider _behavior = 'merge' , slider _max , slider _step } = effect ;
if ( slider _map . has ( slider _name ) ) {
2022-07-11 14:33:03 +00:00
if ( slider _max !== undefined ) {
const slider _info = slider _map . get ( slider _name ) ;
slider _info . max += slider _max ;
}
2022-07-07 22:33:00 +00:00
}
else if ( slider _behavior === 'merge' ) {
slider _map . set ( slider _name , {
2022-07-23 03:26:04 +00:00
label _name : slider _name + ' (' + ability . display _name + ')' ,
2022-07-07 22:33:00 +00:00
max : slider _max ,
step : slider _step ,
id : "ability-slider" + ability . id ,
//color: effect['slider_color'] TODO: add colors to json
abil : ability
} ) ;
}
}
2022-07-18 09:42:43 +00:00
if ( effect [ 'type' ] === "raw_stat" && effect [ 'toggle' ] ) {
const { toggle : toggle _name } = effect ;
button _map . set ( toggle _name , {
abil : ability
} ) ;
}
2022-07-07 22:33:00 +00:00
}
}
2022-07-23 03:11:28 +00:00
// next, render the sliders and toggles onto the abilities.
2022-07-07 22:33:00 +00:00
for ( const [ slider _name , slider _info ] of slider _map . entries ( ) ) {
let slider _container = gen _slider _labeled ( slider _info ) ;
2022-07-23 03:11:28 +00:00
document . getElementById ( "boost-sliders" ) . 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-18 09:42:43 +00:00
for ( const [ button _name , button _info ] of button _map . entries ( ) ) {
2022-07-23 03:11:28 +00:00
let button = make _elem ( 'button' , [ "button-boost" , "border-0" , "text-white" , "dark-8u" , "dark-shadow-sm" , "m-1" ] , {
2022-07-18 09:42:43 +00:00
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 _stats . mark _dirty ( ) . update ( )
} ) ;
button _info . button = button ;
2022-07-23 03:11:28 +00:00
document . getElementById ( "boost-toggles" ) . appendChild ( button ) ;
2022-07-18 09:42:43 +00:00
}
return [ slider _map , button _map ] ;
2022-07-06 19:16:44 +00:00
}
} ) ( ) . link _to ( atree _node , 'atree-order' ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _render _active , 'atree-elements' ) ;
2022-06-29 06:23:27 +00:00
2022-07-06 19:16:44 +00:00
/ * *
* Collect stats from ability tree .
* Return StatMap of added stats ( incl . cost modifications as raw cost )
*
2022-07-08 05:28:46 +00:00
* Signature : AbilityTreeStatsNode ( atree - merged : MergedATree , build : Build , atree - interactive : Map < str , slider _info > ) => StatMap
2022-07-06 19:16:44 +00:00
* /
2022-07-06 06:43:44 +00:00
const atree _stats = new ( class extends ComputeNode {
constructor ( ) { super ( 'atree-stats-collector' ) ; }
compute _func ( input _map ) {
2022-07-07 23:13:26 +00:00
const atree _merged = input _map . get ( 'atree-merged' ) ;
const item _stats = input _map . get ( 'build' ) . statMap ;
2022-07-18 09:42:43 +00:00
const [ slider _map , button _map ] = input _map . get ( 'atree-interactive' ) ;
2022-07-06 06:43:44 +00:00
let ret _effects = new Map ( ) ;
for ( const [ abil _id , abil ] of atree _merged . entries ( ) ) {
if ( abil . effects . length == 0 ) { continue ; }
for ( const effect of abil . effects ) {
switch ( effect . type ) {
case 'stat_scaling' :
2022-07-07 23:13:26 +00:00
if ( effect . slider ) {
2022-07-09 10:09:50 +00:00
if ( 'output' in effect ) { // sometimes nodes will modify slider without having effect.
2022-07-18 09:42:43 +00:00
const slider _val = slider _map . get ( effect . slider _name ) . slider . value ;
2022-07-19 05:23:16 +00:00
const { round = true } = effect ;
let total = parseInt ( slider _val ) * effect . scaling [ 0 ] ;
if ( round ) { total = Math . floor ( round _near ( total ) ) ; }
2022-07-09 10:09:50 +00:00
if ( 'max' in effect && total > effect . max ) { total = effect . max ; }
if ( Array . isArray ( effect . output ) ) {
for ( const output of effect . output ) {
if ( output . type === 'stat' ) { // TODO: prop
merge _stat ( ret _effects , output . name , total ) ;
}
2022-07-08 05:28:46 +00:00
}
}
2022-07-09 10:09:50 +00:00
else {
if ( effect . output . type === 'stat' ) {
merge _stat ( ret _effects , effect . output . name , total ) ;
}
2022-07-08 05:28:46 +00:00
}
}
2022-07-07 23:13:26 +00:00
}
else {
// TODO: type: prop?
let total = 0 ;
2022-07-19 05:23:16 +00:00
const { round = true } = effect ;
2022-07-07 23:13:26 +00:00
for ( const [ scaling , input ] of zip2 ( effect . scaling , effect . inputs ) ) {
total += scaling * item _stats . get ( input . name ) ;
}
2022-07-19 05:23:16 +00:00
if ( round ) { total = Math . floor ( round _near ( total ) ) ; }
2022-07-17 02:09:19 +00:00
if ( total < 0 ) { total = 0 ; } // Normal stat scaling will not go negative.
2022-07-08 21:52:17 +00:00
if ( 'max' in effect && total > effect . max ) { total = effect . max ; }
2022-07-07 23:13:26 +00:00
// TODO: output (list...)
2022-07-08 02:44:57 +00:00
if ( Array . isArray ( effect . output ) ) {
for ( const output of effect . output ) {
if ( output . type === 'stat' ) {
merge _stat ( ret _effects , output . name , total ) ;
}
}
}
else {
if ( effect . output . type === 'stat' ) {
merge _stat ( ret _effects , effect . output . name , total ) ;
}
}
2022-07-07 23:13:26 +00:00
}
2022-07-06 06:43:44 +00:00
continue ;
case 'raw_stat' :
// TODO: toggles...
2022-07-18 09:42:43 +00:00
if ( effect . toggle ) {
const button = button _map . get ( effect . toggle ) . button ;
if ( ! button . classList . contains ( "toggleOn" ) ) { 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 ;
case 'add_spell_prop' :
2022-07-11 08:25:25 +00:00
continue ;
2022-07-06 08:06:24 +00:00
// TODO unjankify....
// costs are converted to raw cost ID
2022-07-11 08:25:25 +00:00
// const { base_spell, cost = 0} = effect;
// if (cost) {
// const key = "spRaw"+base_spell;
// if (ret_effects.has(key)) { ret_effects.set(key, ret_effects.get(key) + cost); }
// else { ret_effects.set(key, cost); }
// }
// continue;
2022-07-06 06:43:44 +00:00
}
}
}
2022-07-08 02:44:57 +00:00
if ( ret _effects . has ( 'baseResist' ) ) {
2022-07-08 05:28:46 +00:00
merge _stat ( ret _effects , "defMult" , 1 - ( ret _effects . get ( 'baseResist' ) / 100 ) ) ;
2022-07-08 02:44:57 +00:00
}
2022-07-06 06:43:44 +00:00
return ret _effects ;
}
2022-07-08 05:28:46 +00:00
} ) ( ) . link _to ( atree _merge , 'atree-merged' ) . link _to ( atree _make _interactives , 'atree-interactive' ) ;
2022-07-06 06:43:44 +00:00
2022-06-29 06:23:27 +00:00
/ * *
* Construct compute nodes to link builder items and edit IDs to the appropriate display outputs .
* To make things a bit cleaner , the compute graph structure goes like
* [ builder , build stats ] - > [ one agg node that is just a passthrough ] - > all the spell calc nodes
* This way , when things have to be deleted i can just delete one node from the dependencies of builder / build stats ...
* thats the idea anyway .
*
* Whenever this is updated , it forces an update of all the newly created spell nodes ( if the build is clean ) .
*
* Signature : AbilityEnsureSpellsNodes ( spells : Map [ id , Spell ] ) => null
* /
class AbilityTreeEnsureNodesNode extends ComputeNode {
/ * *
* Kinda "hyper-node" : Constructor takes nodes that should be linked to ( build node and stat agg node )
* /
constructor ( build _node , stat _agg _node ) {
super ( 'atree-make-nodes' ) ;
this . build _node = build _node ;
this . stat _agg _node = stat _agg _node ;
// Slight amount of wasted compute to keep internal state non-changing.
this . passthrough = new PassThroughNode ( 'atree-make-nodes_internal' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
this . spelldmg _nodes = [ ] ; // debugging use
this . spell _display _elem = document . getElementById ( "all-spells-display" ) ;
}
compute _func ( input _map ) {
console . log ( 'atree make nodes' ) ;
this . passthrough . remove _link ( this . build _node ) ;
this . passthrough . remove _link ( this . stat _agg _node ) ;
this . passthrough = new PassThroughNode ( 'atree-make-nodes_internal' ) . link _to ( this . build _node , 'build' ) . link _to ( this . stat _agg _node , 'stats' ) ;
this . spell _display _elem . textContent = "" ;
const build _node = this . passthrough . get _node ( 'build' ) ; // aaaaaaaaa performance... savings... help....
const stat _agg _node = this . passthrough . get _node ( 'stats' ) ;
const spell _map = input _map . get ( 'spells' ) ; // TODO: is this gonna need more? idk...
// TODO shortcut update path for sliders
2022-06-30 12:27:35 +00:00
for ( const [ spell _id , spell ] of new Map ( [ ... spell _map ] . sort ( ( a , b ) => a [ 0 ] - b [ 0 ] ) ) . entries ( ) ) {
2022-06-29 06:23:27 +00:00
let spell _node = new SpellSelectNode ( spell ) ;
spell _node . link _to ( build _node , 'build' ) ;
let calc _node = new SpellDamageCalcNode ( spell . base _spell ) ;
calc _node . link _to ( build _node , 'build' ) . link _to ( stat _agg _node , 'stats' )
. link _to ( spell _node , 'spell-info' ) ;
this . spelldmg _nodes . push ( calc _node ) ;
let display _elem = document . createElement ( 'div' ) ;
display _elem . classList . add ( "col" , "pe-0" ) ;
// TODO: just pass these elements into the display node instead of juggling the raw IDs...
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" ] ,
{ id : "spell" + spell . base _spell + "-info" } ) ;
2022-06-29 06:23:27 +00:00
spell _detail . style . display = "none" ;
display _elem . appendChild ( spell _summary ) ; display _elem . appendChild ( spell _detail ) ;
let display _node = new SpellDisplayNode ( spell . base _spell ) ;
display _node . link _to ( stat _agg _node , 'stats' ) ;
display _node . link _to ( spell _node , 'spell-info' ) ;
display _node . link _to ( calc _node , 'spell-damage' ) ;
this . spell _display _elem . appendChild ( display _elem ) ;
}
this . passthrough . mark _dirty ( ) . update ( ) ; // Force update once.
}
}
2022-06-27 23:49:21 +00:00
/ * * T h e m a i n f u n c t i o n f o r r e n d e r i n g a n a b i l i t y t r e e .
*
* @ param { Element } UI _elem - the DOM element to draw the atree within .
* @ param { Element } list _elem - the DOM element to list selected abilities within .
* @ param { * } tree - the ability tree to work with .
* /
function render _AT ( UI _elem , list _elem , tree ) {
2022-06-23 11:24:12 +00:00
console . log ( "constructing ability tree UI" ) ;
2022-07-12 21:09:09 +00:00
// increase padding, since images are larger than the space provided
UI _elem . style . paddingRight = "calc(var(--bs-gutter-x) * 1)" ;
UI _elem . style . paddingLeft = "calc(var(--bs-gutter-x) * 1)" ;
UI _elem . style . paddingTop = "calc(var(--bs-gutter-x) * .5)" ;
2022-06-23 11:24:12 +00:00
// add in the "Active" title to atree
2022-07-07 03:04:53 +00:00
let active _row = make _elem ( "div" , [ "row" , "item-title" , "mx-auto" , "justify-content-center" ] ) ;
let active _word = make _elem ( "div" , [ "col-auto" ] , { textContent : "Active Abilities:" } ) ;
let active _AP _container = make _elem ( "div" , [ "col-auto" ] ) ;
let active _AP _subcontainer = make _elem ( "div" , [ "row" ] ) ;
let active _AP _cost = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cost" , textContent : "0" } ) ;
let active _AP _slash = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : "/" } ) ;
2022-07-11 03:28:08 +00:00
let active _AP _cap = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { id : "active_AP_cap" } ) ;
2022-07-07 03:04:53 +00:00
let active _AP _end = make _elem ( "div" , [ "col-auto" , "mx-0" , "px-0" ] , { textContent : " AP" } ) ;
2022-06-26 08:15:26 +00:00
active _AP _container . appendChild ( active _AP _subcontainer ) ;
2022-06-28 04:33:38 +00:00
active _AP _subcontainer . append ( active _AP _cost , active _AP _slash , active _AP _cap , active _AP _end ) ;
2022-06-26 08:15:26 +00:00
2022-06-28 04:33:38 +00:00
active _row . append ( active _word , active _AP _container ) ;
2022-06-27 23:49:21 +00:00
list _elem . appendChild ( active _row ) ;
2022-06-23 11:24:12 +00:00
2022-06-26 23:49:35 +00:00
let atree _map = new Map ( ) ;
let atree _connectors _map = new Map ( )
let max _row = 0 ;
for ( const i of tree ) {
2022-06-28 18:43:35 +00:00
atree _map . set ( i . ability . id , { ability : i . ability , connectors : new Map ( ) , active : false } ) ;
if ( i . ability . display . row > max _row ) {
max _row = i . ability . display . row ;
2022-06-26 23:49:35 +00:00
}
2022-06-24 02:36:50 +00:00
}
2022-06-26 23:49:35 +00:00
// Copy graph structure.
for ( const i of tree ) {
2022-06-28 18:43:35 +00:00
let node _wrapper = atree _map . get ( i . ability . id ) ;
2022-06-26 23:49:35 +00:00
node _wrapper . parents = [ ] ;
node _wrapper . children = [ ] ;
for ( const parent of i . parents ) {
2022-06-28 18:43:35 +00:00
node _wrapper . parents . push ( atree _map . get ( parent . ability . id ) ) ;
2022-06-26 12:18:23 +00:00
}
2022-06-26 23:49:35 +00:00
for ( const child of i . children ) {
2022-06-28 18:43:35 +00:00
node _wrapper . children . push ( atree _map . get ( child . ability . id ) ) ;
2022-06-23 15:00:15 +00:00
}
2022-06-26 23:49:35 +00:00
}
// Setup grid.
for ( let j = 0 ; j <= max _row ; j ++ ) {
let row = document . createElement ( 'div' ) ;
row . classList . add ( "row" ) ;
row . id = "atree-row-" + j ;
for ( let k = 0 ; k < 9 ; k ++ ) {
col = document . createElement ( 'div' ) ;
col . classList . add ( 'col' , 'px-0' ) ;
2022-07-12 03:17:00 +00:00
col . style = "position: relative; aspect-ratio: 1/1;"
2022-06-26 23:49:35 +00:00
row . appendChild ( col ) ;
2022-06-23 15:00:15 +00:00
}
2022-06-27 23:49:21 +00:00
UI _elem . appendChild ( row ) ;
2022-06-26 23:49:35 +00:00
}
2022-06-23 15:00:15 +00:00
2022-06-26 23:49:35 +00:00
for ( const _node of tree ) {
2022-06-28 18:43:35 +00:00
let node _wrap = atree _map . get ( _node . ability . id ) ;
let ability = _node . ability ;
2022-06-23 11:24:12 +00:00
// create connectors based on parent location
2022-06-26 23:49:35 +00:00
for ( let parent of node _wrap . parents ) {
node _wrap . connectors . set ( parent , [ ] ) ;
2022-06-26 08:59:02 +00:00
2022-06-28 18:43:35 +00:00
let parent _abil = parent . ability ;
const parent _id = parent _abil . id ;
2022-06-23 11:24:12 +00:00
let connect _elem = document . createElement ( "div" ) ;
2022-07-12 03:17:00 +00:00
connect _elem . style = "background-size: cover; width: 200%; height: 200%; position: absolute; top: -50%; left: -50%; image-rendering: pixelated;" ;
2022-06-23 11:24:12 +00:00
// connect up
2022-06-28 18:43:35 +00:00
for ( let i = ability . display . row - 1 ; i > parent _abil . display . row ; i -- ) {
const coord = i + "," + ability . display . col ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : [ 0 , 0 , 1 , 1 ] } ) ;
2022-06-23 11:24:12 +00:00
}
2022-06-23 14:29:25 +00:00
// connect horizontally
2022-06-28 18:43:35 +00:00
let min = Math . min ( parent _abil . display . col , ability . display . col ) ;
let max = Math . max ( parent _abil . display . col , ability . display . col ) ;
2022-06-23 14:29:25 +00:00
for ( let i = min + 1 ; i < max ; i ++ ) {
2022-06-28 18:43:35 +00:00
const coord = parent _abil . display . row + "," + i ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : [ 1 , 1 , 0 , 0 ] } ) ;
2022-06-23 11:24:12 +00:00
}
// connect corners
2022-06-28 18:43:35 +00:00
if ( parent _abil . display . row != ability . display . row && parent _abil . display . col != ability . display . col ) {
const coord = parent _abil . display . row + "," + ability . display . col ;
2022-06-24 02:36:50 +00:00
let connector = connect _elem . cloneNode ( ) ;
2022-06-28 18:43:35 +00:00
node _wrap . connectors . get ( parent ) . push ( coord ) ;
2022-06-26 23:49:35 +00:00
let connections = [ 0 , 0 , 0 , 1 ] ;
2022-06-28 18:43:35 +00:00
if ( parent _abil . display . col > ability . display . col ) {
2022-06-26 23:49:35 +00:00
connections [ 1 ] = 1 ;
2022-06-23 14:53:55 +00:00
}
else { // if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) {
2022-06-26 23:49:35 +00:00
connections [ 0 ] = 1 ;
2022-06-23 14:53:55 +00:00
}
2022-06-28 18:43:35 +00:00
resolve _connector ( atree _connectors _map , coord , { connector : connector , connections : connections } ) ;
2022-06-23 11:24:12 +00:00
}
}
// create node
2022-06-26 08:59:02 +00:00
let node _elem = document . createElement ( 'div' ) ;
2022-06-28 18:43:35 +00:00
let icon = ability . display . icon ;
2022-06-24 11:56:56 +00:00
if ( icon === undefined ) {
icon = "node" ;
}
2022-06-28 07:10:25 +00:00
let node _img = document . createElement ( 'img' ) ;
node _img . src = '../media/atree/' + icon + '.png' ;
2022-07-12 03:17:00 +00:00
node _img . style = "width: 200%; height: 200%; position: absolute; top: -50%; left: -50%; image-rendering: pixelated; z-index: 1;" ;
2022-06-28 07:10:25 +00:00
node _elem . appendChild ( node _img ) ;
2022-06-23 11:24:12 +00:00
2022-07-12 22:06:40 +00:00
node _wrap . img = node _img ;
2022-07-12 03:17:00 +00:00
// create hitbox
// this is necessary since images exceed the size of their square, but should only be interactible within that square
let hitbox = document . createElement ( "div" ) ;
hitbox . style = "position: absolute; cursor: pointer; left: 0; top: 0; width: 100%; height: 100%; z-index: 2;"
node _elem . appendChild ( hitbox ) ;
2022-06-23 11:24:12 +00:00
2022-06-30 15:03:41 +00:00
node _wrap . elem = node _elem ;
node _wrap . all _connectors _ref = atree _connectors _map ;
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'click' , function ( e ) {
2022-06-26 07:48:42 +00:00
if ( e . target !== this && e . target !== this . children [ 0 ] ) { return ; }
2022-06-30 15:03:41 +00:00
atree _set _state ( node _wrap , ! node _wrap . active ) ;
atree _state _node . mark _dirty ( ) . update ( ) ;
2022-06-23 11:24:12 +00:00
} ) ;
2022-06-26 07:48:42 +00:00
// add tooltip
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'mouseover' , function ( e ) {
2022-07-14 18:17:38 +00:00
if ( e . target !== this ) {
return ;
}
2022-07-14 23:10:05 +00:00
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
2022-07-14 18:17:38 +00:00
}
2022-07-19 18:40:37 +00:00
node _wrap . tooltip _elem = generateTooltip ( UI _elem , node _elem , ability , atree _map ) ;
2022-06-26 07:48:42 +00:00
} ) ;
2022-07-12 03:17:00 +00:00
hitbox . addEventListener ( 'mouseout' , function ( e ) {
2022-07-14 18:17:38 +00:00
if ( e . target !== this ) {
return ;
}
2022-07-14 23:10:05 +00:00
if ( node _wrap . tooltip _elem ) {
node _wrap . tooltip _elem . remove ( ) ;
delete node _wrap . tooltip _elem ;
2022-07-14 18:17:38 +00:00
}
2022-06-26 07:48:42 +00:00
} ) ;
2022-06-28 18:43:35 +00:00
document . getElementById ( "atree-row-" + ability . display . row ) . children [ ability . display . col ] . appendChild ( node _elem ) ;
2022-06-23 11:24:12 +00:00
} ;
2022-06-26 23:49:35 +00:00
atree _render _connection ( atree _connectors _map ) ;
2022-06-28 18:43:35 +00:00
return atree _map ;
2022-06-23 11:24:12 +00:00
} ;
2022-07-19 18:40:37 +00:00
function generateTooltip ( UI _elem , node _elem , ability , atree _map ) {
2022-07-18 22:45:12 +00:00
let container = make _elem ( "div" , [ "rounded-bottom" , "dark-4" , "border" , "mx-2" , "my-4" , "dark-shadow" , "text-start" ] , { "style" : "position: absolute; z-index: 100;" } ) ;
container . style . top = ( node _elem . getBoundingClientRect ( ) . top + window . pageYOffset + 50 ) + "px" ;
container . style . left = UI _elem . getBoundingClientRect ( ) . left + "px" ;
container . style . width = UI _elem . getBoundingClientRect ( ) . width * 0.95 + "px" ;
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-07-19 20:06:06 +00:00
let description = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" , "text-wrap" , "mc-gray" ] , { } ) ;
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-07-18 22:45:12 +00:00
container . appendChild ( document . createElement ( "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 !== "" ) {
let archetype = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" ] , { } ) ;
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 ) ;
container . appendChild ( document . createElement ( "br" ) ) ;
}
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 ) ;
let archChosen = 0 ;
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 ;
if ( node _wrap . ability . archetype == ability . archetype ) {
archChosen ++ ;
}
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
let cost = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" ] , { } ) ;
if ( apUsed + ability . cost > maxAP ) {
cost . innerHTML = reqNo ;
} else {
cost . innerHTML = reqYes ;
}
cost . innerHTML += " <span class = 'mc-gray'>Ability Points:</span> " + ( maxAP - apUsed ) + "<span class = 'mc-gray'>/" + ability . cost ;
container . appendChild ( cost ) ;
// archetype req
if ( ability . archetype _req > 0 && ability . archetype != null ) {
let archReq = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" ] , { } ) ;
if ( archChosen >= ability . archetype _req ) {
archReq . innerHTML = reqYes ;
} else {
archReq . innerHTML = reqNo ;
}
archReq . innerHTML += " <span class = 'mc-gray'>Min " + ability . archetype + " Archetype:</span> " + archChosen + "<span class = 'mc-gray'>/" + ability . archetype _req ;
container . appendChild ( archReq ) ;
}
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 ++ ) {
let dependency = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" ] , { } ) ;
if ( satisfiedDependencies . includes ( ability . dependencies [ i ] ) ) {
dependency . innerHTML = reqYes ;
} else {
dependency . innerHTML = reqNo ;
}
dependency . innerHTML += " <span class = 'mc-gray'>Required Ability:</span> " + atree _map . get ( ability . dependencies [ i ] ) . ability . display _name ;
container . appendChild ( dependency ) ;
}
2022-07-19 18:40:37 +00:00
// blockers
for ( let i = 0 ; i < blockedBy . length ; i ++ ) {
let blocker = make _elem ( "p" , [ "scaled-font-sm" , "my-0" , "mx-1" ] , { } ) ;
blocker . innerHTML = reqNo + " <span class = 'mc-gray'>Blocked By:</span> " + blockedBy [ i ] ;
container . appendChild ( blocker ) ;
}
2022-07-14 18:17:38 +00:00
2022-07-18 22:45:12 +00:00
UI _elem . appendChild ( container ) ;
return container ;
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
}
// draw the connector onto the screen
2022-06-26 23:49:35 +00:00
function atree _render _connection ( atree _connectors _map ) {
2022-06-24 02:36:50 +00:00
for ( let i of atree _connectors _map . keys ( ) ) {
2022-06-26 23:49:35 +00:00
let connector _info = atree _connectors _map . get ( i ) ;
let connector _elem = connector _info . connector ;
2022-06-28 07:10:25 +00:00
let connector _img = document . createElement ( 'img' ) ;
2022-06-26 23:49:35 +00:00
set _connector _type ( connector _info ) ;
2022-06-28 07:10:25 +00:00
connector _img . src = '../media/atree/connect_' + connector _info . type + '.png' ;
connector _img . style = "width: 100%; height: 100%;"
connector _elem . replaceChildren ( connector _img ) ;
2022-06-26 23:49:35 +00:00
connector _info . highlight = [ 0 , 0 , 0 , 0 ] ;
let target _elem = document . getElementById ( "atree-row-" + i . split ( "," ) [ 0 ] ) . children [ i . split ( "," ) [ 1 ] ] ;
if ( target _elem . children . length != 0 ) {
// janky special case...
connector _elem . style . display = 'none' ;
}
target _elem . appendChild ( connector _elem ) ;
2022-06-26 11:29:55 +00:00
} ;
} ;
2022-06-26 08:59:02 +00:00
2022-06-26 10:55:06 +00:00
// toggle the state of a node.
2022-06-30 15:03:41 +00:00
function atree _set _state ( node _wrapper , new _state ) {
2022-07-12 23:56:36 +00:00
let icon = node _wrapper . ability . display . icon ;
if ( icon === undefined ) {
icon = "node" ;
}
2022-06-30 15:03:41 +00:00
if ( new _state ) {
node _wrapper . active = true ;
2022-07-12 23:56:36 +00:00
node _wrapper . elem . children [ 0 ] . src = "../media/atree/" + icon + "_selected.png" ;
2022-06-30 15:03:41 +00:00
}
else {
node _wrapper . active = false ;
2022-07-12 23:56:36 +00:00
node _wrapper . elem . children [ 0 ] . src = "../media/atree/" + icon + ".png" ;
2022-06-30 15:03:41 +00:00
}
let atree _connectors _map = node _wrapper . all _connectors _ref ;
2022-06-26 23:49:35 +00:00
for ( const parent of node _wrapper . parents ) {
if ( parent . active ) {
atree _set _edge ( atree _connectors _map , parent , node _wrapper , new _state ) ; // self->parent state only changes if parent is on
}
}
for ( const child of node _wrapper . children ) {
if ( child . active ) {
atree _set _edge ( atree _connectors _map , node _wrapper , child , new _state ) ; // Same logic as above.
}
}
2022-06-26 11:29:55 +00:00
} ;
2022-06-26 08:59:02 +00:00
2022-06-26 23:49:35 +00:00
function atree _set _edge ( atree _connectors _map , parent , child , state ) {
const connectors = child . connectors . get ( parent ) ;
2022-06-28 18:43:35 +00:00
const parent _row = parent . ability . display . row ;
const parent _col = parent . ability . display . col ;
const child _row = child . ability . display . row ;
const child _col = child . ability . display . col ;
2022-06-26 23:49:35 +00:00
let state _delta = ( state ? 1 : - 1 ) ;
let child _side _idx = ( parent _col > child _col ? 0 : 1 ) ;
let parent _side _idx = 1 - child _side _idx ;
for ( const connector _label of connectors ) {
let connector _info = atree _connectors _map . get ( connector _label ) ;
let connector _elem = connector _info . connector ;
let highlight _state = connector _info . highlight ; // left right up down
2022-06-28 07:10:25 +00:00
let connector _img _elem = document . createElement ( "img" ) ;
connector _img _elem . style = "width: 100%; height: 100%;" ;
2022-06-26 23:49:35 +00:00
const ctype = connector _info . type ;
2022-07-12 20:48:55 +00:00
let num _1s = 0 ;
for ( let i = 0 ; i < 4 ; i ++ ) {
if ( ctype . charAt ( i ) == "1" ) {
num _1s ++ ;
}
}
if ( num _1s > 2 ) { // t branch or 4-way
2022-06-26 23:49:35 +00:00
const [ connector _row , connector _col ] = connector _label . split ( ',' ) . map ( x => parseInt ( x ) ) ;
if ( connector _row === parent _row ) {
highlight _state [ parent _side _idx ] += state _delta ;
}
else {
highlight _state [ 2 ] += state _delta ; // up connection guaranteed.
}
if ( connector _col === child _col ) {
highlight _state [ 3 ] += state _delta ;
}
else {
highlight _state [ child _side _idx ] += state _delta ;
}
2022-06-26 08:59:02 +00:00
2022-07-12 20:48:55 +00:00
let render = "" ;
for ( let i = 0 ; i < 4 ; i ++ ) {
render += highlight _state [ i ] === 0 ? "0" : "1" ;
}
2022-07-12 22:06:40 +00:00
if ( render == "0000" ) {
connector _img _elem . src = "../media/atree/connect_" + ctype + ".png" ;
} else {
connector _img _elem . src = "../media/atree/connect_" + ctype + "_" + render + ".png" ;
}
2022-06-28 07:10:25 +00:00
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 12:46:04 +00:00
continue ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
// lol bad overloading, [0] is just the whole state
highlight _state [ 0 ] += state _delta ;
if ( highlight _state [ 0 ] > 0 ) {
2022-07-12 20:48:55 +00:00
connector _img _elem . src = '../media/atree/connect_' + ctype + '_1.png' ;
2022-06-28 07:10:25 +00:00
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
else {
2022-06-28 07:10:25 +00:00
connector _img _elem . src = '../media/atree/connect_' + ctype + '.png' ;
connector _elem . replaceChildren ( connector _img _elem ) ;
2022-06-26 08:59:02 +00:00
}
2022-06-26 23:49:35 +00:00
}
2022-06-26 08:59:02 +00:00
}