2022-06-19 20:44:02 +00:00
/ * *
* Node for getting an item ' s stats from an item input field .
*
* Signature : ItemInputNode ( ) => Item | null
* /
class ItemInputNode extends InputNode {
/ * *
* Make an item stat pulling compute node .
*
* @ param name : Name of this node .
* @ param item _input _field : Input field ( html element ) to listen for item names from .
* @ param none _item : Item object to use as the "none" for this field .
* /
constructor ( name , item _input _field , none _item ) {
super ( name , item _input _field ) ;
this . none _item = new Item ( none _item ) ;
this . none _item . statMap . set ( 'NONE' , true ) ;
}
2022-05-22 10:21:34 +00:00
2022-06-19 20:44:02 +00:00
compute _func ( input _map ) {
// built on the assumption of no one will type in CI/CR letter by letter
2022-05-22 10:21:34 +00:00
2022-06-19 20:44:02 +00:00
let item _text = this . input _field . value ;
if ( ! item _text ) {
return this . none _item ;
}
let item ;
if ( item _text . slice ( 0 , 3 ) == "CI-" ) {
item = getCustomFromHash ( item _text ) ;
}
else if ( item _text . slice ( 0 , 3 ) == "CR-" ) {
item = getCraftFromHash ( item _text ) ;
}
else if ( itemMap . has ( item _text ) ) {
item = new Item ( itemMap . get ( item _text ) ) ;
}
else if ( tomeMap . has ( item _text ) ) {
item = new Item ( tomeMap . get ( item _text ) ) ;
}
if ( item ) {
let type _match ;
if ( this . none _item . statMap . get ( 'category' ) === 'weapon' ) {
type _match = item . statMap . get ( 'category' ) === 'weapon' ;
} else {
type _match = item . statMap . get ( 'type' ) === this . none _item . statMap . get ( 'type' ) ;
}
if ( type _match ) { return item ; }
}
return null ;
}
}
/ * *
* Node for updating item input fields from parsed items .
*
* Signature : ItemInputDisplayNode ( item : Item ) => null
* /
class ItemInputDisplayNode extends ComputeNode {
constructor ( name , eq , item _image ) {
super ( name ) ;
this . input _field = document . getElementById ( eq + "-choice" ) ;
this . health _field = document . getElementById ( eq + "-health" ) ;
this . level _field = document . getElementById ( eq + "-lv" ) ;
this . image = item _image ;
this . fail _cb = true ;
2022-06-19 07:42:49 +00:00
}
compute _func ( input _map ) {
2022-06-19 20:44:02 +00:00
if ( input _map . size !== 1 ) { throw "ItemInputDisplayNode accepts exactly one input (item)" ; }
const [ item ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
this . input _field . classList . remove ( "text-light" , "is-invalid" , 'Normal' , 'Unique' , 'Rare' , 'Legendary' , 'Fabled' , 'Mythic' , 'Set' , 'Crafted' , 'Custom' ) ;
this . input _field . classList . add ( "text-light" ) ;
this . image . classList . remove ( 'Normal-shadow' , 'Unique-shadow' , 'Rare-shadow' , 'Legendary-shadow' , 'Fabled-shadow' , 'Mythic-shadow' , 'Set-shadow' , 'Crafted-shadow' , 'Custom-shadow' ) ;
if ( ! item ) {
this . input _field . classList . add ( "is-invalid" ) ;
return null ;
}
if ( item . statMap . has ( 'NONE' ) ) {
return null ;
}
const tier = item . statMap . get ( 'tier' ) ;
this . input _field . classList . add ( tier ) ;
if ( this . health _field ) {
// Doesn't exist for weapons.
this . health _field . textContent = item . statMap . get ( 'hp' ) ;
}
this . level _field . textContent = item . statMap . get ( 'lvl' ) ;
this . image . classList . add ( tier + "-shadow" ) ;
return null ;
2022-06-19 07:42:49 +00:00
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Node for rendering an item .
*
* Signature : ItemDisplayNode ( item : Item ) => null
* /
class ItemDisplayNode extends ComputeNode {
constructor ( name , target _elem ) {
super ( name ) ;
this . target _elem = target _elem ;
}
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "ItemInputDisplayNode accepts exactly one input (item)" ; }
const [ item ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
displayExpandedItem ( item . statMap , this . target _elem ) ;
collapse _element ( "#" + this . target _elem ) ;
}
}
/ * *
* Change the weapon to match correct type .
*
* Signature : WeaponInputDisplayNode ( item : Item ) => null
* /
class WeaponInputDisplayNode extends ComputeNode {
constructor ( name , image _field ) {
super ( name ) ;
this . image = image _field ;
2022-06-19 07:42:49 +00:00
}
2022-06-19 20:44:02 +00:00
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "WeaponDisplayNode accepts exactly one input (item)" ; }
const [ item ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
const type = item . statMap . get ( 'type' ) ;
this . image . setAttribute ( 'src' , '../media/items/new/generic-' + type + '.png' ) ;
}
}
/ * *
* Encode the build into a url - able string .
*
* Signature : BuildEncodeNode ( build : Build ,
helmet - powder : List [ powder ] ,
chestplate - powder : List [ powder ] ,
leggings - powder : List [ powder ] ,
boots - powder : List [ powder ] ,
weapon - powder : List [ powder ] ) => str
* /
class BuildEncodeNode extends ComputeNode {
constructor ( ) { super ( "builder-encode" ) ; }
compute _func ( input _map ) {
const build = input _map . get ( 'build' ) ;
let powders = [
input _map . get ( 'helmet-powder' ) ,
input _map . get ( 'chestplate-powder' ) ,
input _map . get ( 'leggings-powder' ) ,
input _map . get ( 'boots-powder' ) ,
input _map . get ( 'weapon-powder' )
] ;
// TODO: grr global state for copy button..
player _build = build ;
build _powders = powders ;
return encodeBuild ( build , powders ) ;
}
}
/ * *
* Update the window ' s URL .
*
* Signature : URLUpdateNode ( build _str : str ) => null
* /
class URLUpdateNode extends ComputeNode {
constructor ( ) { super ( "builder-url-update" ) ; }
2022-06-19 07:42:49 +00:00
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "URLUpdateNode accepts exactly one input (build_str)" ; }
const [ build _str ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
location . hash = build _str ;
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Create a "build" object from a set of equipments .
* Returns a new Build object , or null if all items are NONE items .
*
* TODO : add tomes
*
* Signature : BuildAssembleNode ( helmet - input : Item ,
* chestplate - input : Item ,
* leggings - input : Item ,
* boots - input : Item ,
* ring1 - input : Item ,
* ring2 - input : Item ,
* bracelet - input : Item ,
* necklace - input : Item ,
* weapon - input : Item ,
* level - input : int ) => Build | null
* /
2022-06-19 07:42:49 +00:00
class BuildAssembleNode extends ComputeNode {
2022-06-19 20:44:02 +00:00
constructor ( ) { super ( "builder-make-build" ) ; }
2022-06-19 07:42:49 +00:00
compute _func ( input _map ) {
let equipments = [
input _map . get ( 'helmet-input' ) ,
input _map . get ( 'chestplate-input' ) ,
input _map . get ( 'leggings-input' ) ,
input _map . get ( 'boots-input' ) ,
input _map . get ( 'ring1-input' ) ,
input _map . get ( 'ring2-input' ) ,
input _map . get ( 'bracelet-input' ) ,
input _map . get ( 'necklace-input' )
] ;
let weapon = input _map . get ( 'weapon-input' ) ;
let level = input _map . get ( 'level-input' ) ;
2022-06-19 16:49:04 +00:00
let all _none = weapon . statMap . has ( 'NONE' ) ;
for ( const item of equipments ) {
all _none = all _none && item . statMap . has ( 'NONE' ) ;
}
if ( all _none ) {
return null ;
}
2022-06-19 07:42:49 +00:00
return new Build ( level , equipments , [ ] , weapon ) ;
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Read an input field and parse into a list of powderings .
* Every two characters makes one powder . If parsing fails , NULL is returned .
*
* Signature : PowderInputNode ( ) => List [ powder ] | null
* /
2022-06-19 16:49:04 +00:00
class PowderInputNode extends InputNode {
2022-06-19 20:44:02 +00:00
constructor ( name , input _field ) { super ( name , input _field ) ; }
2022-06-19 16:49:04 +00:00
compute _func ( input _map ) {
// TODO: haha improve efficiency to O(n) dumb
// also, error handling is missing
let input = this . input _field . value . trim ( ) ;
let powdering = [ ] ;
let errorederrors = [ ] ;
while ( input ) {
let first = input . slice ( 0 , 2 ) ;
let powder = powderIDs . get ( first ) ;
if ( powder === undefined ) {
return null ;
} else {
powdering . push ( powder ) ;
}
input = input . slice ( 2 ) ;
}
//console.log("POWDERING: " + powdering);
return powdering ;
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Select a spell + spell "variation" based on a build / spell idx .
* Right now this isn ' t much logic and is only used to abstract away major id interactions
* but will become significantly more complex in wynn2 .
*
* Signature : SpellSelectNode < int > ( build : Build ) => [ Spell , SpellParts ]
* /
2022-06-19 18:02:28 +00:00
class SpellSelectNode extends ComputeNode {
constructor ( spell _num ) {
super ( "builder-spell" + spell _num + "-select" ) ;
this . spell _idx = spell _num ;
}
compute _func ( input _map ) {
const build = input _map . get ( 'build' ) ;
const i = this . spell _idx ;
let spell = spell _table [ build . weapon . statMap . get ( "type" ) ] [ i ] ;
let stats = build . statMap ;
let spell _parts ;
if ( spell . parts ) {
spell _parts = spell . parts ;
}
else {
spell _parts = spell . variants . DEFAULT ;
for ( const majorID of stats . get ( "activeMajorIDs" ) ) {
if ( majorID in spell . variants ) {
spell _parts = spell . variants [ majorID ] ;
break ;
}
}
}
return [ spell , spell _parts ] ;
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Compute spell damage of spell parts .
* Currently kinda janky / TODO while we rework the internal rep . of spells .
*
* Signature : SpellDamageCalcNode ( weapon - input : Item ,
* build : Build ,
* weapon - powder : List [ powder ] ,
* spell - info : [ Spell , SpellParts ] ) => List [ SpellDamage ]
* /
2022-06-19 16:49:04 +00:00
class SpellDamageCalcNode extends ComputeNode {
constructor ( spell _num ) {
super ( "builder-spell" + spell _num + "-calc" ) ;
}
compute _func ( input _map ) {
2022-06-19 18:02:28 +00:00
const weapon = new Map ( input _map . get ( 'weapon-input' ) . statMap ) ;
const build = input _map . get ( 'build' ) ;
const weapon _powder = input _map . get ( 'weapon-powder' ) ;
const damage _mult = 1 ; // TODO: hook up
const spell _info = input _map . get ( 'spell-info' ) ;
const spell _parts = spell _info [ 1 ] ;
2022-06-19 16:49:04 +00:00
weapon . set ( "powders" , weapon _powder ) ;
2022-06-19 18:02:28 +00:00
let spell _results = [ ]
let stats = build . statMap ;
for ( const part of spell _parts ) {
if ( part . type === "damage" ) {
let results = calculateSpellDamage ( stats , part . conversion ,
stats . get ( "sdRaw" ) + stats . get ( "rainbowRaw" ) , stats . get ( "sdPct" ) ,
part . multiplier / 100 , weapon , build . total _skillpoints , damage _mult ) ;
spell _results . push ( results ) ;
} else if ( part . type === "heal" ) {
// TODO: wynn2 formula
let heal _amount = ( part . strength * build . getDefenseStats ( ) [ 0 ] * Math . max ( 0.5 , Math . min ( 1.75 , 1 + 0.5 * stats . get ( "wDamPct" ) / 100 ) ) ) . toFixed ( 2 ) ;
spell _results . push ( heal _amount ) ;
} else if ( part . type === "total" ) {
// TODO: remove "total" type
spell _results . push ( null ) ;
}
}
return spell _results ;
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Display spell damage from spell parts .
* Currently kinda janky / TODO while we rework the internal rep . of spells .
*
* Signature : SpellDisplayNode ( build : Build ,
* spell - info : [ Spell , SpellParts ] ,
* spell - damage : List [ SpellDamage ] ) => null
* /
2022-06-19 18:02:28 +00:00
class SpellDisplayNode extends ComputeNode {
constructor ( spell _num ) {
super ( "builder-spell" + spell _num + "-display" ) ;
this . spell _idx = spell _num ;
}
compute _func ( input _map ) {
const build = input _map . get ( 'build' ) ;
const spell _info = input _map . get ( 'spell-info' ) ;
const damages = input _map . get ( 'spell-damage' ) ;
const spell = spell _info [ 0 ] ;
const spell _parts = spell _info [ 1 ] ;
2022-06-19 16:49:04 +00:00
const i = this . spell _idx ;
let parent _elem = document . getElementById ( "spell" + i + "-info" ) ;
let overallparent _elem = document . getElementById ( "spell" + i + "-infoAvg" ) ;
2022-06-19 18:02:28 +00:00
displaySpellDamage ( parent _elem , overallparent _elem , build , spell , i + 1 , spell _parts , damages ) ;
2022-06-19 16:49:04 +00:00
}
}
2022-06-19 20:44:02 +00:00
/ * *
* Display build stats .
*
* Signature : BuildDisplayNode ( build : Build ) => null
* /
class BuildDisplayNode extends ComputeNode {
constructor ( spell _num ) { super ( "builder-stats-display" ) ; }
compute _func ( input _map ) {
if ( input _map . size !== 1 ) { throw "BuildDisplayNode accepts exactly one input (build)" ; }
const [ build ] = input _map . values ( ) ; // Extract values, pattern match it into size one list and bind to first element
displayBuildStats ( 'overall-stats' , build , build _all _display _commands ) ;
displayBuildStats ( "offensive-stats" , build , build _offensive _display _commands ) ;
displaySetBonuses ( "set-info" , build ) ;
2022-06-19 20:59:16 +00:00
let meleeStats = build . getMeleeStats ( ) ;
displayMeleeDamage ( document . getElementById ( "build-melee-stats" ) , document . getElementById ( "build-melee-statsAvg" ) , meleeStats ) ;
displayDefenseStats ( document . getElementById ( "defensive-stats" ) , build ) ;
displayPoisonDamage ( document . getElementById ( "build-poison-stats" ) , build ) ;
2022-06-20 02:07:59 +00:00
displayEquipOrder ( document . getElementById ( "build-order" ) , build . equip _order ) ;
2022-06-19 20:44:02 +00:00
}
}
2022-06-20 02:07:59 +00:00
/ * *
* Set the editble id fields .
* /
class EditableIDSetterNode extends ComputeNode {
}
2022-05-22 10:21:34 +00:00
let item _nodes = [ ] ;
2022-06-19 16:49:04 +00:00
let powder _nodes = [ ] ;
2022-06-19 18:02:28 +00:00
let spelldmg _nodes = [ ] ;
2022-05-22 10:21:34 +00:00
2022-06-19 20:44:02 +00:00
function builder _graph _init ( ) {
2022-06-20 03:20:22 +00:00
// Phase 1/2: Set up item input, propagate updates, etc.
2022-06-19 16:49:04 +00:00
// Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff).
2022-06-19 20:44:02 +00:00
for ( const [ eq , display _elem , none _item ] of zip3 ( equipment _fields , build _fields , none _items ) ) {
2022-05-22 10:21:34 +00:00
let input _field = document . getElementById ( eq + "-choice" ) ;
let item _image = document . getElementById ( eq + "-img" ) ;
let item _input = new ItemInputNode ( eq + '-input' , input _field , none _item ) ;
item _nodes . push ( item _input ) ;
2022-06-19 20:44:02 +00:00
new ItemInputDisplayNode ( eq + '-input-display' , eq , item _image ) . link _to ( item _input ) ;
new ItemDisplayNode ( eq + '-item-display' , display _elem ) . link _to ( item _input ) ;
2022-06-19 16:49:04 +00:00
//new PrintNode(eq+'-debug').link_to(item_input);
2022-05-22 10:21:34 +00:00
//document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm');
}
2022-06-19 16:49:04 +00:00
// weapon image changer node.
2022-05-22 10:21:34 +00:00
let weapon _image = document . getElementById ( "weapon-img" ) ;
2022-06-19 20:44:02 +00:00
new WeaponInputDisplayNode ( 'weapon-type' , weapon _image ) . link _to ( item _nodes [ 8 ] ) ;
2022-06-19 16:49:04 +00:00
// Level input node.
2022-06-19 07:42:49 +00:00
let level _input = new InputNode ( 'level-input' , document . getElementById ( 'level-choice' ) ) ;
2022-06-19 16:49:04 +00:00
// "Build" now only refers to equipment and level (no powders). Powders are injected before damage calculation / stat display.
2022-06-19 07:42:49 +00:00
let build _node = new BuildAssembleNode ( ) ;
for ( const input of item _nodes ) {
build _node . link _to ( input ) ;
}
build _node . link _to ( level _input ) ;
2022-06-19 20:44:02 +00:00
new BuildDisplayNode ( ) . link _to ( build _node , 'build' ) ;
let build _encode _node = new BuildEncodeNode ( ) ;
build _encode _node . link _to ( build _node , 'build' ) ;
let url _update _node = new URLUpdateNode ( ) ;
url _update _node . link _to ( build _encode _node , 'build-str' ) ;
2022-06-19 16:49:04 +00:00
for ( const input of powder _inputs ) {
2022-06-19 20:44:02 +00:00
let powder _node = new PowderInputNode ( input , document . getElementById ( input ) ) ;
powder _nodes . push ( powder _node ) ;
build _encode _node . link _to ( powder _node , input ) ;
2022-06-19 16:49:04 +00:00
}
2022-06-20 03:20:22 +00:00
for ( const input _node of item _nodes . concat ( powder _nodes ) ) {
input _node . update ( ) ;
}
level _input . update ( ) ;
// Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage
// Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap)
// let stat_agg_node =
for ( const field of editable _elems ) {
// Create nodes that listens to each editable id input, the node name should match the "id"
// stat_agg_node.link_to( ... )
}
// Also do something similar for skill points
2022-06-19 16:49:04 +00:00
for ( let i = 0 ; i < 4 ; ++ i ) {
2022-06-19 18:02:28 +00:00
let spell _node = new SpellSelectNode ( i ) ;
2022-06-19 16:49:04 +00:00
spell _node . link _to ( build _node , 'build' ) ;
2022-06-20 03:20:22 +00:00
// link and rewrite spell_node to the stat agg node
// spell_node.link_to(stat_agg_node, 'stats')
2022-06-19 18:02:28 +00:00
let calc _node = new SpellDamageCalcNode ( i ) ;
calc _node . link _to ( item _nodes [ 8 ] , 'weapon-input' ) ;
calc _node . link _to ( build _node , 'build' ) ;
calc _node . link _to ( powder _nodes [ 4 ] , 'weapon-powder' ) ;
calc _node . link _to ( spell _node , 'spell-info' ) ;
spelldmg _nodes . push ( calc _node ) ;
let display _node = new SpellDisplayNode ( i ) ;
display _node . link _to ( build _node , 'build' ) ;
display _node . link _to ( spell _node , 'spell-info' ) ;
display _node . link _to ( calc _node , 'spell-damage' ) ;
2022-06-19 16:49:04 +00:00
}
2022-06-20 03:20:22 +00:00
// Create a node that binds the build to the edit ids text boxes
// (make it export to the textboxes)
// This node is a bit tricky since it has to make a "weak link" (directly mark dirty and call update() for each of the stat nodes).
// IMPORTANT: mark all children dirty, then update each child, in that order (2 loops). else performance issues will be bad
// let id_exporter_node = ...
// id_exporter_node.link_to(build_node, 'build')
// call node.update() for each skillpoint node and stat edit listener node manually
// NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them
// this will propagate the update to the `stat_agg_node`, and then to damage calc
2022-06-20 02:07:59 +00:00
2022-05-22 10:21:34 +00:00
console . log ( "Set up graph" ) ;
2022-06-12 14:45:48 +00:00
}
2022-06-19 20:44:02 +00:00