diff options
author | Freeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace> | 2013-05-13 00:35:11 +1200 |
---|---|---|
committer | Freeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace> | 2013-05-13 00:41:08 +1200 |
commit | 5e332bfac3dfe2075dde30568498bc6c42ca27ce (patch) | |
tree | caaa5d48e3f448527e3e62359a6b4453bdf3655f /public | |
parent | 2b3bf882d6b53f09e0dddf3f8d4f159832471fb3 (diff) | |
download | manavis-5e332bfac3dfe2075dde30568498bc6c42ca27ce.tar.gz manavis-5e332bfac3dfe2075dde30568498bc6c42ca27ce.tar.bz2 manavis-5e332bfac3dfe2075dde30568498bc6c42ca27ce.tar.xz manavis-5e332bfac3dfe2075dde30568498bc6c42ca27ce.zip |
Move served files to a public/ directory
Diffstat (limited to 'public')
-rw-r--r-- | public/css/style.css | 134 | ||||
-rw-r--r-- | public/index.html | 96 | ||||
-rw-r--r-- | public/js/comp/item.js | 809 | ||||
-rwxr-xr-x | public/js/comp/makeitem.sed | 16 | ||||
-rwxr-xr-x | public/js/comp/makemap.sed | 17 | ||||
-rwxr-xr-x | public/js/comp/makemob.sed | 14 | ||||
-rw-r--r-- | public/js/comp/map.js | 114 | ||||
-rw-r--r-- | public/js/comp/mob.js | 101 | ||||
-rw-r--r-- | public/js/comp/stat.js | 153 | ||||
m--------- | public/js/crossfilter | 0 | ||||
m--------- | public/js/d3 | 0 | ||||
m--------- | public/js/dc | 0 | ||||
-rw-r--r-- | public/js/mv/chart.js | 131 | ||||
-rw-r--r-- | public/js/mv/connect.js | 156 | ||||
-rw-r--r-- | public/js/mv/heap.js | 63 | ||||
-rw-r--r-- | public/js/mv/load.js | 60 | ||||
-rw-r--r-- | public/js/mv/main.js | 59 | ||||
-rw-r--r-- | public/js/mv/parse.js | 109 | ||||
-rw-r--r-- | public/js/util/memoize.js | 45 | ||||
-rw-r--r-- | public/js/util/progress.js | 37 | ||||
-rw-r--r-- | public/js/util/trellis-chart.js | 140 |
21 files changed, 2254 insertions, 0 deletions
diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..2690b1f --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,134 @@ +/* Ensure sane defaults */ + +body { + padding: 0; + margin: 0; +} + +/* Top-level layout */ +.side { + float: right; +} +#main { + overflow: hidden; +} + +#mask { + position: absolute; + width: 100%; + height: 100%; + background-color: #333; + opacity: .8; + -moz-transition: opacity 1s linear; + -o-transition: opacity 1s linear; + -webkit-transition: opacity 1s linear; + transition: opacity 1s linear; + top: 0; +} + +body, #main, #status, .side { + min-height: 40px; +} + +body { + min-width: 1350px; +} + +#status { + width: 100%; + font-size: smaller; +} + +/* Columns */ + +.med { + width: 400px; +} + +.thin { + width: 250px; +} + +#main, .side { + font-size: smaller; +} + +/* Titles */ +h3 { + margin: 0.3em 0.8em; +} + +/* Loadinfo panel */ + +#loadinfo { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + width: 647px; + height: 400px; + border: 1px grey solid; + margin: auto; + background-color: #fff; + padding: 20px; +} + +/* Hide charts while loadinfo is shown */ + +.vis-hide { + display: none; + opacity: 0; +} + +/* Loading bars */ +.progressbar { + margin: 10px 0; + padding: 3px; + border: 1px solid #000; + font-size: 14px; + clear: both; + opacity: 0; +} + +.progressbar.loading { + opacity: 1.0; +} + +.progressbar .percent { + background-color: #99ccff; + height: auto; + width: 0; + white-space: nowrap; +} + +/* Stat chart */ + +#stat-chart svg g g.column .border-line { + fill: none; + stroke: #ccc; + opacity: .5; + shape-rendering: crispEdges; +} + +/* User list */ + +#users-status { + padding: 0; +} + +#users-status li.user { + list-style: none; +} + +/* Utility */ +.fader { + -moz-transition: opacity 1s linear; + -o-transition: opacity 1s linear; + -webkit-transition: opacity 1s linear; + transition: opacity 1s linear; +} + +.help { + cursor: help; +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..baebbb2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Manavis</title> +<link rel="stylesheet" type="text/css" href="css/style.css" /> +<link rel="stylesheet" type="text/css" href="js/dc/test/dc.css" /> + +<body onload="mv.init();"> + <div class="side med"> + <div class="vis-hide"> + <div id="connect-status"> + <h3>Users</h3> + <ul id="users-status"></ul> + </div> + <div id="blvl-chart"> + <h3>Instance breakdown by Character Base Level <a class="reset" style="display: none;" href="javascript:mv.charts.blvl.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="stat-chart"> + <h3>Instance breakdown by Stat allocation</h3> + </div> + </div> + </div> + <div class="side thin"> + <div class="vis-hide"> + <div id="target-chart"> + <h3>Instance breakdown by Target <a class="reset" style="display: none;" href="javascript:mv.charts.target.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="wpn-chart"> + <h3>Instance breakdown by Weapon <a class="reset" style="display: none;" href="javascript:mv.charts.wpn.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="type-chart"> + <h3>Instance breakdown by Type <a class="reset" style="display: none;" href="javascript:mv.charts.type.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="def-chart"> + <h3>Definedness of records <span class="help" title="If logs are missing between server boot and the logs provided, not all information will be available for all records. Definedness of records falls into three categories. Records with undefined data; records with well defined data, but mixed in with records having undefined data, limiting validity of inferences; and records after any uncertain times, which are fully well defined. It is highly recommended that you filter results to only include records which are in fully well defined times; however, this may not always be possible.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.def.filterAll();dc.redrawAll();">clear</a></h3> + </div> + </div> + </div> + <div id="main"> +<!-- <div id="status"> + Manavis + TODO: Load icons et al for when new records need loading? + </div>--> + <div class="vis-hide"> + <div id="map-chart"> + <h3>Breakdown by Map <span class="help" title="Bubble size indicates instances of experience gain for that map. X axis position indicates the sum of level experience gain for that map. Y axis position indicates the sum of job experience gain for that map.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.map.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="date-chart"> + <h3>Instance breakdown by Date <a class="reset" style="display: none;" href="javascript:mv.charts.date.filterAll();dc.redrawAll();">clear</a></h3> + </div> + <div id="player-chart"> + <h3>Instance breakdown by Character ID <span class="help" title="Older to newer characters appear left to right, respectively.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.pc.filterAll();dc.redrawAll();">clear</a></h3> + </div> + </div> + </div> + <div id="mask"><noscript><h1>Javascript is required for this website.</h1></noscript> + <div id="loadinfo" class="fader"> + <h1>Manavis</h1> + <input id="connect-option" name="connect" type="checkbox" checked></input> + <label for="connect">Connect to server</label> + <h3>Select records to load and display</h3> + <p>You can load any number of files at once.</p> + <input type="file" id="input" name="records[]" multiple /> + <output id="list"></output> + <div id="filesbar" class="progressbar fader"> + <div class="percent">0%</div> + </div> + <div id="loadbar" class="progressbar fader"> + <div class="percent">0%</div> + </div> + </div> + </div> +</body> + +<!-- Libs --> +<script src="js/util/memoize.js"></script> +<script src="js/util/progress.js"></script> +<script src="js/d3/d3.js"></script> +<script src="js/crossfilter/crossfilter.js"></script> +<script src="js/dc/dc.js"></script> + +<script src="/socket.io/socket.io.js"></script> + +<!-- Components --> +<script src="js/comp/item.js"></script> +<script src="js/comp/map.js"></script> +<script src="js/comp/mob.js"></script> +<script src="js/comp/stat.js"></script><!-- Depends on crossfilter for crossfilter.bisect --> + +<!-- Processing --> +<script src="js/mv/chart.js"></script> +<script src="js/mv/connect.js"></script> +<script src="js/mv/heap.js"></script> +<script src="js/mv/load.js"></script> +<script src="js/mv/main.js"></script> +<script src="js/mv/parse.js"></script> +<script src="js/util/trellis-chart.js"></script> diff --git a/public/js/comp/item.js b/public/js/comp/item.js new file mode 100644 index 0000000..65596be --- /dev/null +++ b/public/js/comp/item.js @@ -0,0 +1,809 @@ +var item = function(){ + var item = {}; + var items = { + 0:"DEFAULT", + 501:"CactusDrink", + 502:"CactusPotion", + 503:"CasinoCoins", + 504:"DecorCandy", + 505:"MaggotSlime", + 506:"CandyCane", + 507:"ScorpionStinger", + 508:"XmasCake", + 509:"ChocolateBar", + 510:"Candy", + 511:"SantaHat", + 512:"GingerBreadMan", + 513:"Cake", + 514:"XmasCandyCane", + 515:"PurplePresentBox", + 516:"BluePresentBox", + 517:"RedScorpionStinger", + 518:"BugLeg", + 519:"CherryCake", + 520:"EasterEgg", + 521:"Dagger", + 522:"SharpKnife", + 523:"LeatherShirt", + 524:"FancyHat", + 525:"MinersHat", + 526:"CoinBag", + 527:"Milk", + 528:"Boots", + 529:"IronArrow", + 530:"ShortBow", + 531:"MinerGloves", + 532:"LeatherGloves", + 533:"RoastedMaggot", + 534:"OrangeCupcake", + 535:"RedApple", + 536:"ShortSword", + 537:"TreasureKey", + 538:"GreenPresentBox", + 539:"Beer", + 540:"EmptyBottle", + 541:"BottleOfWater", + 542:"BottleOfSand", + 543:"StandardHeadband", + 544:"SilkHeadband", + 545:"ForestBow", + 546:"DesertShirt", + 547:"Bardiche", + 548:"Halberd", + 549:"Axe", + 550:"BlacksmithsAxe", + 551:"AquaHint", + 552:"MagentaHint", + 553:"YellowHint", + 554:"GreenHint", + 555:"TealHint", + 556:"PurpleHint", + 557:"RedHint", + 558:"BlueHint", + 559:"OrangeHint", + 560:"GrayHint", + 561:"Sabre", + 562:"ChickenLeg", + 563:"WinterGloves", + 564:"TurtleneckSweater", + 565:"PinkPetal", + 566:"SmallMushroom", + 567:"IronPotion", + 568:"ConcentrationPotion", + 569:"RawLog", + 570:"BoneKnife", + 571:"Setzer", + 572:"Scimitar", + 573:"Falchion", + 574:"Scorpion", + 575:"DesertBow", + 576:"Beheader", + 577:"BoneDarts", + 578:"SandCutter", + 579:"RockKnife", + 580:"StaffOfLife", + 581:"CrescentRod", + 582:"StaffOfFire", + 583:"StaffOfIce", + 584:"Jackal", + 585:"ScarabArmlet", + 586:"CottonShorts", + 587:"Sword", + 594:"Spear", + 601:"SteelShield", + 602:"WoodenShield", + 603:"LeatherShield", + 610:"JeansShorts", + 611:"WhiteFur", + 612:"CaveSnakeLamp", + 613:"HardSpike", + 614:"PinkAntenna", + 615:"PumpkinHelmet", + 616:"AxeHat", + 617:"PirateHat", + 618:"Goggles", + 619:"LeatherGoggles", + 620:"Circlet", + 621:"Eyepatch", + 622:"Bandana", + 623:"Scythe", + 624:"VNeckSweater", + 625:"ChainmailShirt", + 626:"LightPlatemail", + 627:"TopHat", + 628:"FunkyHat", + 629:"MushHat", + 630:"ShroomHat", + 631:"DarkCrystal", + 632:"CottonSkirt", + 633:"ChristmasElfHat", + 634:"FaceMask", + 635:"SantaCookie", + 636:"WarlordHelmet", + 637:"KnightsHelmet", + 638:"InfantryHelmet", + 639:"CrusadeHelmet", + 640:"IronOre", + 641:"SnakeSkin", + 642:"JeansChaps", + 643:"WhiteCowboyHat", + 644:"BlackCowboyHat", + 645:"GoldenPlatemail", + 646:"Crown", + 647:"DevelopersCap", + 648:"CottonTrousers", + 649:"WhiteEvokersRobeBlue", + 650:"BlackEvokersRobeBlue", + 651:"WhiteWizardRobe", + 652:"BlackWizardRobe", + 653:"ApprenticeRobe", + 654:"Cap", + 655:"FurBoots", + 656:"SerfHat", + 657:"Orange", + 658:"WarlordPlate", + 659:"GoldenWarlordPlate", + 660:"CottonCloth", + 661:"RedRose", + 662:"WhiteRose", + 663:"DarkRedRose", + 664:"PinkRose", + 665:"YellowRose", + 666:"BlackRose", + 667:"OrangeRose", + 668:"BlueRose", + 669:"YellowTulip", + 670:"PurpleTulip", + 671:"RedTulip", + 672:"WhiteTulip", + 673:"PinkTulip", + 674:"OrangeTulip", + 675:"GraduationCap", + 676:"Steak", + 677:"HeartNecklace", + 678:"NohMask", + 679:"DemonMask", + 680:"MauveHerb", + 681:"CobaltHerb", + 682:"GambogeHerb", + 683:"AlizarinHerb", + 684:"TinyHealingPotion", + 685:"SmallHealingPotion", + 686:"MediumHealingPotion", + 687:"LargeHealingPotion", + 688:"TankTop", + 689:"ShortTankTop", + 690:"RedDye", + 691:"GreenDye", + 692:"DarkBlueDye", + 693:"YellowDye", + 694:"LightBlueDye", + 695:"PinkDye", + 696:"BlackDye", + 697:"OrangeDye", + 698:"PurpleDye", + 699:"DarkGreenDye", + 700:"Pearl", + 701:"PileOfAsh", + 702:"WeddingRing", + 703:"SulphurPowder", + 704:"IronPowder", + 705:"ManaPotion", + 706:"GoldenScorpionStinger", + 707:"MonsterOilPotion", + 708:"LeatherPatch", + 709:"BlackScorpionStinger", + 710:"SnakeTongue", + 711:"MountainSnakeTongue", + 712:"GrassSnakeTongue", + 713:"CaveSnakeTongue", + 714:"SnakeEgg", + 715:"MountainSnakeEgg", + 716:"GrassSnakeEgg", + 717:"CaveSnakeEgg", + 718:"SilkCocoon", + 719:"GreenApple", + 720:"SilkRobe", + 721:"HighPriestCrown", + 722:"MonsterSkullHelmet", + 723:"DesertHat", + 724:"CottonHeadband", + 725:"GMCap", + 726:"GMRobe", + 727:"Iten", + 728:"MoubooFigurine", + 729:"WarpedLog", + 730:"Lifestone", + 731:"AssassinPants", + 732:"DruidTreeBranch", + 733:"PurificationPotion", + 734:"BlackBoots", + 735:"CottonBoots", + 736:"WhiteCake", + 737:"ChocolateCake", + 738:"OrangeCake", + 739:"AppleCake", + 740:"Root", + 741:"CottonGloves", + 742:"FourLeafClover", + 743:"Acorn", + 744:"DilutedConcentrationPotion", + 745:"DarkConcentrationPotion", + 746:"MopoxCurePotion", + 747:"LacedChocolateCake", + 748:"LacedOrangeCupcake", + 749:"Towel", + 750:"SlowPoisonPotion", + 751:"PinkieHat", + 752:"FluffyHat", + 753:"BatWing", + 754:"BatTeeth", + 755:"AssassinShirt", + 756:"AssassinGloves", + 757:"AssassinBoots", + 758:"WoodenStaff", + 762:"TerraniteArrow", + 763:"TerraniteOre", + 766:"TerraniteHelmet", + 767:"TerraniteChestArmor", + 768:"TerraniteLegs", + 769:"GuyFawkesMask", + 770:"FairyHat", + 771:"Miniskirt", + 772:"WispPowder", + 773:"SpectrePowder", + 774:"PoltergeistPowder", + 775:"Bone", + 776:"Skull", + 777:"RottenRags", + 778:"DiseasedHeart", + 779:"UndeadEar", + 780:"UndeadEye", + 782:"ForestArmor", + 783:"PlatynaRedDress", + 784:"ZombieNachos", + 785:"LadyFingers", + 786:"JellAhh", + 787:"Snapple", + 788:"BeetleJuice", + 789:"GutBuster", + 790:"BloodWine", + 791:"YetiSkinShirt", + 792:"BromenalBoots", + 793:"BromenalChest", + 794:"BromenalGloves", + 795:"BromenalHelmet", + 796:"BromenalLegs", + 797:"BromenalShield", + 798:"SorcererRobeRed", + 799:"MylarinDust", + 800:"BowlerHatBrown", + 801:"PinkieHelmet", + 802:"EasterBasket", + 803:"GrassLiner", + 804:"JellyBeans", + 805:"ChocolateMouboo", + 806:"ReedBundle", + 807:"GrassSeed", + 808:"HitchhikersTowel", + 809:"WhiteHitchhikersTowel", + 810:"RedHitchhikersTowel", + 811:"GreenHitchhikersTowel", + 812:"BlueHitchhikersTowel", + 813:"YellowHitchhikersTowel", + 814:"PurpleHitchhikersTowel", + 815:"OrangeHitchhikersTowel", + 816:"PinkHitchhikersTowel", + 817:"TealHitchhikersTowel", + 818:"LimeHitchhikersTowel", + 819:"DiamondPowder", + 820:"RubyPowder", + 821:"EmeraldPowder", + 822:"SapphirePowder", + 823:"TopazPowder", + 824:"AmethystPowder", + 825:"TinyManaElixir", + 826:"SmallManaElixir", + 827:"MediumManaElixir", + 828:"LargeManaElixir", + 829:"CrozeniteFourLeafAmulet", + 830:"BromenalFourLeafAmulet", + 831:"SilverFourLeafAmulet", + 832:"GoldenFourLeafAmulet", + 833:"BrokenFourLeafAmulet", + 834:"BrokenDoll", + 835:"HyvernStinger", + 836:"GrubSlime", + 838:"CranberryLollipop", + 839:"GrapeLollipop", + 840:"OrangeLollipop", + 841:"RedDottedWrap", + 842:"YellowDottedWrap", + 843:"BlueDottedWrap", + 844:"PurpleStripedWrap", + 845:"RedGoldenStripedWrap", + 846:"GreenRedStripedWrap", + 847:"PlushMouboo", + 848:"Earmuffs", + 849:"OpenPresentBox", + 850:"ClosedChristmasBox", + 851:"StickReinboo", + 852:"LeatherBall", + 853:"Doll", + 854:"ElfNightcap", + 855:"Sunglasses", + 856:"KnitCap", + 857:"LeatherTrousers", + 858:"WolvernTooth", + 859:"WolvernPelt", + 860:"SquirrelPelt", + 861:"WhiteBellTuber", + 862:"IcedWater", + 863:"SilverMirror", + 864:"BookPage", + 865:"Grimoire", + 866:"LeatherSuitcase", + 867:"IceGladius", + 868:"SilkGloves", + 869:"Antlers", + 870:"FineDress", + 871:"SealedSoul", + 872:"LockPicks", + 873:"LazuriteShard", + 874:"LazuriteCrystal", + 875:"HeartOfLazurite", + 876:"WarlordBoots", + 877:"BullHelmet", + 878:"BansheeBow", + 879:"HeartOfIsis", + 880:"LazuriteRobe", + 881:"RaggedShorts", + 882:"RedEggshellHat", + 883:"BlueEggshellHat", + 884:"YellowEggshellHat", + 885:"GreenEggshellHat", + 886:"OrangeEggshellHat", + 887:"DarkEggshellHat", + 888:"MagicGMTopHat", + 889:"MurdererCrown", + 890:"BeanieCopter", + 1198:"JackOSoul", + 1199:"Arrow", + 1200:"Bow", + 1201:"Knife", + 1202:"CottonShirt", + 1203:"RangerHat", + 1204:"AntlerHat", + 1205:"ChristmasTreeHat", + 1206:"SantaBeardHat", + 1207:"RedChristmasStocking", + 1208:"RedEasterEgg", + 1209:"GreenEasterEgg", + 1210:"BlueEasterEgg", + 1211:"YellowEasterEgg", + 1212:"PinkEasterEgg", + 1213:"TealEasterEgg", + 1214:"BunnyEars", + 1215:"ToySabre", + 1216:"MoubooHead", + 1217:"CatEars", + 1218:"PaperBag", + 1219:"MoubootaurHead", + 1220:"BunchOfParsley", + 1221:"SkullMask", + 1228:"LightCrystal", + 1229:"CaramelApple", + 1230:"LollipopColor1", + 1231:"LollipopColor2", + 1232:"LollipopColor3", + 1233:"FakeFangs", + 1234:"RedOrnament", + 1235:"YellowOrnament", + 1236:"GreenOrnament", + 1237:"AquaOrnament", + 1238:"BlueOrnament", + 1239:"MagentaOrnament", + 1240:"SantaSnowGlobe", + 1241:"SnowmanSnowGlobe", + 1242:"SnowGoggles", + 1244:"DarkTalisman", + 1245:"BentNeedle", + 1246:"DarkEasterEgg", + 1247:"HeartGlasses", + 1248:"Blueberries", + 1249:"StrangeCoin", + 1250:"Pear", + 1251:"Plum", + 1252:"Cherry", + 1253:"GoldenDeliciousApple", + 1254:"DarkPetal", + 1255:"WhiteRabbitEars", + 1256:"EggshellHat", + 1257:"FlawedLens", + 1258:"Honey", + 1276:"OperaMask", + 1277:"JesterMask", + 1278:"WitchHat", + 1279:"GoblinMask", + 1280:"Scissors", + 1281:"ShockSweet", + 1282:"BoneArrows", + 2050:"RedCottonShirt", + 2051:"GreenCottonShirt", + 2052:"DarkBlueCottonShirt", + 2053:"YellowCottonShirt", + 2054:"LightBlueCottonShirt", + 2055:"PinkCottonShirt", + 2056:"BlackCottonShirt", + 2057:"OrangeCottonShirt", + 2058:"PurpleCottonShirt", + 2059:"DarkGreenCottonShirt", + 2060:"RedVNeckSweater", + 2061:"GreenVNeckSweater", + 2062:"DarkBlueVNeckSweater", + 2063:"YellowVNeckSweater", + 2064:"LightBlueVNeckSweater", + 2065:"PinkVNeckSweater", + 2066:"BlackVNeckSweater", + 2067:"OrangeVNeckSweater", + 2068:"PurpleVNeckSweater", + 2069:"DarkGreenVNeckSweater", + 2070:"RedTurtleneckSweater", + 2071:"GreenTurtleneckSweater", + 2072:"DarkBlueTurtleneckSweater", + 2073:"YellowTurtleneckSweater", + 2074:"LightBlueTurtleneckSweater", + 2075:"PinkTurtleneckSweater", + 2076:"BlackTurtleneckSweater", + 2077:"OrangeTurtleneckSweater", + 2078:"PurpleTurtleneckSweater", + 2079:"DarkGreenTurtleneckSweater", + 2080:"RedSilkRobe", + 2081:"GreenSilkRobe", + 2082:"DarkBlueSilkRobe", + 2083:"YellowSilkRobe", + 2084:"LightBlueSilkRobe", + 2085:"PinkSilkRobe", + 2086:"BlackSilkRobe", + 2087:"OrangeSilkRobe", + 2088:"PurpleSilkRobe", + 2089:"DarkGreenSilkRobe", + 2090:"RedTankTop", + 2091:"GreenTankTop", + 2092:"DarkBlueTankTop", + 2093:"YellowTankTop", + 2094:"LightBlueTankTop", + 2095:"PinkTankTop", + 2096:"BlackTankTop", + 2097:"OrangeTankTop", + 2098:"PurpleTankTop", + 2099:"DarkGreenTankTop", + 2100:"RedCottonSkirt", + 2101:"GreenCottonSkirt", + 2102:"DarkBlueCottonSkirt", + 2103:"YellowCottonSkirt", + 2104:"LightBlueCottonSkirt", + 2105:"PinkCottonSkirt", + 2106:"BlackCottonSkirt", + 2107:"OrangeCottonSkirt", + 2108:"PurpleCottonSkirt", + 2109:"DarkGreenCottonSkirt", + 2110:"RedCottonShorts", + 2111:"GreenCottonShorts", + 2112:"DarkBlueCottonShorts", + 2113:"YellowCottonShorts", + 2114:"LightBlueCottonShorts", + 2115:"PinkCottonShorts", + 2116:"BlackCottonShorts", + 2117:"OrangeCottonShorts", + 2118:"PurpleCottonShorts", + 2119:"DarkGreenCottonShorts", + 2120:"RedShortTankTop", + 2121:"GreenShortTankTop", + 2122:"DarkBlueShortTankTop", + 2123:"YellowShortTankTop", + 2124:"LightBlueShortTankTop", + 2125:"PinkShortTankTop", + 2126:"BlackShortTankTop", + 2127:"OrangeShortTankTop", + 2128:"PurpleShortTankTop", + 2129:"DarkGreenShortTankTop", + 2130:"RedDesertHat", + 2131:"GreenDesertHat", + 2132:"DarkBlueDesertHat", + 2133:"YellowDesertHat", + 2134:"LightBlueDesertHat", + 2135:"PinkDesertHat", + 2136:"BlackDesertHat", + 2137:"OrangeDesertHat", + 2138:"PurpleDesertHat", + 2139:"DarkGreenDesertHat", + 2140:"RedCottonHeadband", + 2141:"GreenCottonHeadband", + 2142:"DarkBlueCottonHeadband", + 2143:"YellowCottonHeadband", + 2144:"LightBlueCottonHeadband", + 2145:"PinkCottonHeadband", + 2146:"BlackCottonHeadband", + 2147:"OrangeCottonHeadband", + 2148:"PurpleCottonHeadband", + 2149:"DarkGreenCottonHeadband", + 2150:"RedCottonBoots", + 2151:"GreenCottonBoots", + 2152:"DarkBlueCottonBoots", + 2153:"YellowCottonBoots", + 2154:"LightBlueCottonBoots", + 2155:"PinkCottonBoots", + 2156:"BlackCottonBoots", + 2157:"OrangeCottonBoots", + 2158:"PurpleCottonBoots", + 2159:"DarkGreenCottonBoots", + 2160:"RedCottonGloves", + 2161:"GreenCottonGloves", + 2162:"DarkBlueCottonGloves", + 2163:"YellowCottonGloves", + 2164:"LightBlueCottonGloves", + 2165:"PinkCottonGloves", + 2166:"BlackCottonGloves", + 2167:"OrangeCottonGloves", + 2168:"PurpleCottonGloves", + 2169:"DarkGreenCottonGloves", + 2170:"RedMiniskirt", + 2171:"GreenMiniskirt", + 2172:"DarkBlueMiniskirt", + 2173:"YellowMiniskirt", + 2174:"LightBlueMiniskirt", + 2175:"PinkMiniskirt", + 2176:"BlackMiniskirt", + 2177:"OrangeMiniskirt", + 2178:"PurpleMiniskirt", + 2179:"DarkGreenMiniskirt", + 2180:"RedCottonTrousers", + 2181:"GreenCottonTrousers", + 2182:"DarkBlueCottonTrousers", + 2183:"YellowCottonTrousers", + 2184:"LightBlueCottonTrousers", + 2185:"PinkCottonTrousers", + 2186:"BlackCottonTrousers", + 2187:"OrangeCottonTrousers", + 2188:"PurpleCottonTrousers", + 2189:"DarkGreenCottonTrousers", + 2190:"RedRabbitEars", + 2191:"GreenRabbitEars", + 2192:"DarkBlueRabbitEars", + 2193:"YellowRabbitEars", + 2194:"LightBlueRabbitEars", + 2195:"PinkRabbitEars", + 2196:"BlackRabbitEars", + 2197:"OrangeRabbitEars", + 2198:"PurpleRabbitEars", + 2199:"DarkGreenRabbitEars", + 2200:"RedWizardHat", + 2201:"GreenWizardHat", + 2202:"DarkBlueWizardHat", + 2203:"YellowWizardHat", + 2204:"LightBlueWizardHat", + 2205:"PinkWizardHat", + 2206:"BlackWizardHat", + 2207:"OrangeWizardHat", + 2208:"PurpleWizardHat", + 2209:"DarkGreenWizardHat", + 2210:"RedBowlerHat", + 2211:"GreenBowlerHat", + 2212:"DarkBlueBowlerHat", + 2213:"YellowBowlerHat", + 2214:"LightBlueBowlerHat", + 2215:"PinkBowlerHat", + 2216:"BlackBowlerHat", + 2217:"OrangeBowlerHat", + 2218:"PurpleBowlerHat", + 2219:"DarkGreenBowlerHat", + 2220:"RedSorcererRobeRed", + 2221:"GreenSorcererRobeRed", + 2222:"DarkBlueSorcererRobeRed", + 2223:"YellowSorcererRobeRed", + 2224:"LightBlueSorcererRobeRed", + 2225:"PinkSorcererRobeRed", + 2226:"BlackSorcererRobeRed", + 2227:"OrangeSorcererRobeRed", + 2228:"PurpleSorcererRobeRed", + 2229:"DarkGreenSorcererRobeRed", + 2230:"RedBowlerHatBrown", + 2231:"GreenBowlerHatBrown", + 2232:"DarkBlueBowlerHatBrown", + 2233:"YellowBowlerHatBrown", + 2234:"LightBlueBowlerHatBrown", + 2235:"PinkBowlerHatBrown", + 2236:"BlackBowlerHatBrown", + 2237:"OrangeBowlerHatBrown", + 2238:"PurpleBowlerHatBrown", + 2239:"DarkGreenBowlerHatBrown", + 2240:"FineRedDress", + 2241:"FineGreenDress", + 2242:"FineDarkBlueDress", + 2243:"FineYellowDress", + 2244:"FineLightBlueDress", + 2245:"FinePinkDress", + 2246:"FineBlackDress", + 2247:"FineOrangeDress", + 2248:"FinePurpleDress", + 2249:"FineDarkGreenDress", + 2250:"RedCottonCloth", + 2251:"GreenCottonCloth", + 2252:"DarkBlueCottonCloth", + 2253:"YellowCottonCloth", + 2254:"LightBlueCottonCloth", + 2255:"PinkCottonCloth", + 2256:"BlackCottonCloth", + 2257:"OrangeCottonCloth", + 2258:"PurpleCottonCloth", + 2259:"DarkGreenCottonCloth", + 3000:"JackOLantern", + 3001:"RubberBat", + 3002:"RealisticBrain", + 3003:"JarofBlood", + 3004:"Tongue", + 3006:"TonoriDelight", + 3007:"Marshmallow", + 3009:"JellySkull", + 3010:"CandyPumpkin", + 3011:"PumpkinSeeds", + 4000:"AngryScorpionStinger", + 4001:"Coal", + 4002:"Diamond", + 4003:"Ruby", + 4004:"Emerald", + 4005:"Sapphire", + 4006:"Topaz", + 4007:"Amethyst", + 4008:"DiamondRing", + 4009:"RubyRing", + 4010:"EmeraldRing", + 4011:"SapphireRing", + 4012:"TopazRing", + 4013:"AmethystRing", + 4014:"SimpleRing", + 4015:"IronIngot", + 4016:"BanditHood", + 4017:"RedPowder", + 4018:"YellowPowder", + 4019:"BluePowder", + 4020:"CandleHelmet", + 4021:"YellowPresentBox", + 4022:"WhitePresentBox", + 4023:"AnimalBones", + 4024:"FrozenYetiTear", + 4025:"YetiClaw", + 4026:"IceCube", + 4027:"YetiMask", + 4028:"WizardHat", + 4029:"GrimaceOfDementia", + 4030:"BowlerHat", + 4031:"Monocle", + 4032:"PanHat", + 4033:"ChefHat", + 4034:"BlackPearl", + 4035:"PickledBeets", + 4036:"RoastedAcorn", + 4037:"WhiteBlanket", + 4038:"WhiteSaddleRug", + 4039:"RedSaddleRug", + 4040:"RawTalisman", + 4041:"FlightTalisman", + 4042:"RedNose", + 5000:"RedSorcererRobeGreen", + 5001:"GreenSorcererRobeGreen", + 5002:"DarkBlueSorcererRobeGreen", + 5003:"YellowSorcererRobeGreen", + 5004:"LightBlueSorcererRobeGreen", + 5005:"PinkSorcererRobeGreen", + 5006:"BlackSorcererRobeGreen", + 5007:"OrangeSorcererRobeGreen", + 5008:"PurpleSorcererRobeGreen", + 5009:"DarkGreenSorcererRobeGreen", + 5010:"SorcererRobeGreen", + 5011:"RedSorcererRobeDarkBlue", + 5012:"GreenSorcererRobeDarkBlue", + 5013:"DarkBlueSorcererRobeDarkBlue", + 5014:"YellowSorcererRobeDarkBlue", + 5015:"LightBlueSorcererRobeDarkBlue", + 5016:"PinkSorcererRobeDarkBlue", + 5017:"BlackSorcererRobeDarkBlue", + 5018:"OrangeSorcererRobeDarkBlue", + 5019:"PurpleSorcererRobeDarkBlue", + 5020:"DarkGreenSorcererRobeDarkBlue", + 5021:"SorcererRobeDarkBlue", + 5022:"RedSorcererRobeYellow", + 5023:"GreenSorcererRobeYellow", + 5024:"DarkBlueSorcererRobeYellow", + 5025:"YellowSorcererRobeYellow", + 5026:"LightBlueSorcererRobeYellow", + 5027:"PinkSorcererRobeYellow", + 5028:"BlackSorcererRobeYellow", + 5029:"OrangeSorcererRobeYellow", + 5030:"PurpleSorcererRobeYellow", + 5031:"DarkGreenSorcererRobeYellow", + 5032:"SorcererRobeYellow", + 5033:"RedSorcererRobeLightBlue", + 5034:"GreenSorcererRobeLightBlue", + 5035:"DarkBlueSorcererRobeLightBlue", + 5036:"YellowSorcererRobeLightBlue", + 5037:"LightBlueSorcererRobeLightBlue", + 5038:"PinkSorcererRobeLightBlue", + 5039:"BlackSorcererRobeLightBlue", + 5040:"OrangeSorcererRobeLightBlue", + 5041:"PurpleSorcererRobeLightBlue", + 5042:"DarkGreenSorcererRobeLightBlue", + 5043:"SorcererRobeLightBlue", + 5044:"RedSorcererRobePink", + 5045:"GreenSorcererRobePink", + 5046:"DarkBlueSorcererRobePink", + 5047:"YellowSorcererRobePink", + 5048:"LightBlueSorcererRobePink", + 5049:"PinkSorcererRobePink", + 5050:"BlackSorcererRobePink", + 5051:"OrangeSorcererRobePink", + 5052:"PurpleSorcererRobePink", + 5053:"DarkGreenSorcererRobePink", + 5054:"SorcererRobePink", + 5055:"RedSorcererRobeBlack", + 5056:"GreenSorcererRobeBlack", + 5057:"DarkBlueSorcererRobeBlack", + 5058:"YellowSorcererRobeBlack", + 5059:"LightBlueSorcererRobeBlack", + 5060:"PinkSorcererRobeBlack", + 5061:"BlackSorcererRobeBlack", + 5062:"OrangeSorcererRobeBlack", + 5063:"PurpleSorcererRobeBlack", + 5064:"DarkGreenSorcererRobeBlack", + 5065:"SorcererRobeBlack", + 5066:"RedSorcererRobeOrange", + 5067:"GreenSorcererRobeOrange", + 5068:"DarkBlueSorcererRobeOrange", + 5069:"YellowSorcererRobeOrange", + 5070:"LightBlueSorcererRobeOrange", + 5071:"PinkSorcererRobeOrange", + 5072:"BlackSorcererRobeOrange", + 5073:"OrangeSorcererRobeOrange", + 5074:"PurpleSorcererRobeOrange", + 5075:"DarkGreenSorcererRobeOrange", + 5076:"SorcererRobeOrange", + 5077:"RedSorcererRobePurple", + 5078:"GreenSorcererRobePurple", + 5079:"DarkBlueSorcererRobePurple", + 5080:"YellowSorcererRobePurple", + 5081:"LightBlueSorcererRobePurple", + 5082:"PinkSorcererRobePurple", + 5083:"BlackSorcererRobePurple", + 5084:"OrangeSorcererRobePurple", + 5085:"PurpleSorcererRobePurple", + 5086:"DarkGreenSorcererRobePurple", + 5087:"SorcererRobePurple", + 5088:"RedSorcererRobeDarkGreen", + 5089:"GreenSorcererRobeDarkGreen", + 5090:"DarkBlueSorcererRobeDarkGreen", + 5091:"YellowSorcererRobeDarkGreen", + 5092:"LightBlueSorcererRobeDarkGreen", + 5093:"PinkSorcererRobeDarkGreen", + 5094:"BlackSorcererRobeDarkGreen", + 5095:"OrangeSorcererRobeDarkGreen", + 5096:"PurpleSorcererRobeDarkGreen", + 5097:"DarkGreenSorcererRobeDarkGreen", + 5098:"SorcererRobeDarkGreen", + 5099:"RedSorcererRobeWhite", + 5100:"GreenSorcererRobeWhite", + 5101:"DarkBlueSorcererRobeWhite", + 5102:"YellowSorcererRobeWhite", + 5103:"LightBlueSorcererRobeWhite", + 5104:"PinkSorcererRobeWhite", + 5105:"BlackSorcererRobeWhite", + 5106:"OrangeSorcererRobeWhite", + 5107:"PurpleSorcererRobeWhite", + 5108:"DarkGreenSorcererRobeWhite", + 5109:"SorcererRobeWhite", + }; + item.nameByServerID = function(serverID) { + return serverID in items ? items[serverID] : "UNDEFINED"; + } + return item; +}(); diff --git a/public/js/comp/makeitem.sed b/public/js/comp/makeitem.sed new file mode 100755 index 0000000..45e5a91 --- /dev/null +++ b/public/js/comp/makeitem.sed @@ -0,0 +1,16 @@ +#!/bin/sed -nf +# Usage: ./makeitem.sed < item.in > item.js +1i\ +var item = function(){\ + var item = {};\ + var items = { +# /(?<!,0)[\t ]\+,[^,]\+,[^,]\+$/ { +s/^\([0-9]\+\),[\t ]\+\([^,]\+\).*/ \1:"\2",/p +# } +$i\ + };\ + item.nameByServerID = function(serverID) {\ + return serverID in items ? items[serverID] : "UNDEFINED";\ + }\ + return item;\ +}(); diff --git a/public/js/comp/makemap.sed b/public/js/comp/makemap.sed new file mode 100755 index 0000000..561530b --- /dev/null +++ b/public/js/comp/makemap.sed @@ -0,0 +1,17 @@ +#!/bin/sed -nf +# Usage: ./makemap.sed < map.in > map.js +1i\ +var map = function(){\ + var map = {};\ + var maps = { +/^Loading Maps/,/^Maps Loaded/ { + s/^Loading Maps \[\([0-9]\+\)\/[0-9]\+\]: data\\\(.*\)\.gat/ "\1": "\2",/p; +} +$i\ + };\ + map.nameByServerID = function(serverID, date) {\ + /* TODO: Merged output format suitable for converting records running under different data */\ + return maps[serverID];\ + }\ + return map;\ +}(); diff --git a/public/js/comp/makemob.sed b/public/js/comp/makemob.sed new file mode 100755 index 0000000..c934a4e --- /dev/null +++ b/public/js/comp/makemob.sed @@ -0,0 +1,14 @@ +#!/bin/sed -nf +# Usage: ./makemob.sed < mob.in > mob.js +1i\ +var mob = function(){\ + var mob = {};\ + var mobs = { +s/^\([0-9]\+\),[\t ]\+\([^\t ]\+\),.*/ \1:"\2",/p +$i\ + };\ + mob.nameByServerID = function(serverID) {\ + return serverID in mobs ? mobs[serverID] : "UNDEFINED";\ + }\ + return mob;\ +}(); diff --git a/public/js/comp/map.js b/public/js/comp/map.js new file mode 100644 index 0000000..d845031 --- /dev/null +++ b/public/js/comp/map.js @@ -0,0 +1,114 @@ +var map = function(){ + var map = {}; + var maps = { + "0": "001-1", + "1": "001-2", + "2": "001-3", + "3": "002-1", + "4": "002-3", + "5": "002-4", + "6": "003-1", + "7": "003-2", + "8": "004-1", + "9": "004-2", + "10": "005-1", + "11": "005-3", + "12": "005-4", + "13": "006-1", + "14": "006-3", + "15": "007-1", + "16": "008-1", + "17": "009-1", + "18": "009-2", + "19": "009-3", + "20": "009-4", + "21": "009-5", + "22": "009-6", + "23": "010-1", + "24": "010-2", + "25": "011-1", + "26": "011-3", + "27": "011-4", + "28": "011-6", + "29": "012-1", + "30": "012-3", + "31": "012-4", + "32": "013-1", + "33": "013-2", + "34": "013-3", + "35": "014-1", + "36": "014-3", + "37": "015-1", + "38": "015-3", + "39": "016-1", + "40": "017-1", + "41": "017-2", + "42": "017-3", + "43": "017-4", + "44": "017-9", + "45": "018-1", + "46": "018-2", + "47": "018-3", + "48": "019-1", + "49": "019-3", + "50": "019-4", + "51": "020-1", + "52": "020-2", + "53": "020-3", + "54": "021-1", + "55": "021-2", + "56": "022-1", + "57": "024-1", + "58": "024-2", + "59": "024-3", + "60": "024-4", + "61": "025-1", + "62": "025-3", + "63": "025-4", + "64": "026-1", + "65": "027-1", + "66": "027-2", + "67": "027-3", + "68": "027-4", + "69": "028-1", + "70": "028-3", + "71": "029-1", + "72": "029-3", + "73": "030-1", + "74": "030-2", + "75": "031-1", + "76": "031-2", + "77": "031-3", + "78": "031-4", + "79": "032-1", + "80": "032-3", + "81": "033-1", + "82": "034-1", + "83": "041-1", + "84": "042-1", + "85": "042-2", + "86": "044-1", + "87": "044-3", + "88": "045-1", + "89": "046-1", + "90": "046-3", + "91": "047-1", + "92": "048-1", + "93": "048-2", + "94": "051-1", + "95": "051-3", + "96": "052-1", + "97": "052-2", + "98": "055-1", + "99": "055-3", + "100": "056-1", + "101": "056-2", + "102": "057-1", + "103": "botcheck", + }; + map.nameByServerID = function(serverID, date) { + /* TODO: Merged output format suitable for converting records running under different data */ + return maps[serverID]; + } + return map; +}(); diff --git a/public/js/comp/mob.js b/public/js/comp/mob.js new file mode 100644 index 0000000..022494e --- /dev/null +++ b/public/js/comp/mob.js @@ -0,0 +1,101 @@ +var mob = function(){ + var mob = {}; + var mobs = { + 1002:"Maggot", + 1003:"Scorpion", + 1004:"RedScorpion", + 1005:"GreenSlime", + 1006:"GiantMaggot", + 1007:"YellowSlime", + 1008:"RedSlime", + 1009:"BlackScorpion", + 1010:"Snake", + 1011:"FireGoblin", + 1012:"Spider", + 1013:"EvilMushroom", + 1014:"PinkFlower", + 1015:"SantaSlime", + 1016:"RudolphSlime", + 1017:"Bat", + 1018:"Pinkie", + 1019:"SpikyMushroom", + 1020:"Fluffy", + 1021:"CaveSnake", + 1022:"JackO", + 1023:"FireSkull", + 1024:"PoisonSkull", + 1025:"LogHead", + 1026:"MountainSnake", + 1027:"EasterFluffy", + 1028:"Mouboo", + 1029:"MauvePlant", + 1030:"CobaltPlant", + 1031:"GambogePlant", + 1032:"AlizarinPlant", + 1033:"SeaSlime", + 1034:"GrassSnake", + 1035:"Silkworm", + 1036:"Zombie", + 1037:"CloverPatch", + 1038:"Squirrel", + 1040:"Wisp", + 1041:"Snail", + 1042:"Spectre", + 1043:"Skeleton", + 1044:"LadySkeleton", + 1045:"Fallen", + 1046:"SnakeLord", + 1047:"Poltergeist", + 1049:"Bee", + 1055:"Butterfly", + 1056:"CaveMaggot", + 1057:"AngryScorpion", + 1058:"IceGoblin", + 1059:"GCMaggot", + 1060:"Archant", + 1061:"Moggun", + 1062:"Terranite", + 1063:"Pumpkin", + 1064:"Bandit", + 1065:"BanditLord", + 1066:"VampireBat", + 1067:"Reaper", + 1068:"Reaper2", + 1069:"Scythe", + 1070:"BallLightning", + 1071:"IceElement", + 1072:"Yeti", + 1073:"TheLost", + 1077:"DrunkenSkeleton", + 1078:"TipsySkeleton", + 1079:"DrunkenLadySkeleton", + 1080:"BlueSpark", + 1081:"RedSpark", + 1082:"Serqet", + 1083:"HuntsmanSpider", + 1084:"CrotcherScorpion", + 1085:"IceSkull", + 1086:"FeyElement", + 1087:"Larvern", + 1088:"Hyvern", + 1089:"HungryFluffy", + 1090:"Wolvern", + 1091:"BlueSlime", + 1092:"SlimeBlast", + 1093:"WhiteSlime", + 1094:"Reinboo", + 1095:"WhiteBell", + 1096:"SoulSnake", + 1097:"SoulEater", + 1098:"CopperSlime", + 1099:"SleepingBandit", + 1100:"AzulSlime", + 1101:"DemonicSpirit", + 1102:"Luvia", + 1103:"WitchGuard", + }; + mob.nameByServerID = function(serverID) { + return serverID in mobs ? mobs[serverID] : "UNDEFINED"; + } + return mob; +}(); diff --git a/public/js/comp/stat.js b/public/js/comp/stat.js new file mode 100644 index 0000000..cd75646 --- /dev/null +++ b/public/js/comp/stat.js @@ -0,0 +1,153 @@ +/** + * Make computations about tmwAthena's stat requirements. + * + * Amongst other things, this can be used to derive a minimum base level for a stat configuration. + */ +var stat = function(){ + var stat = {}; + var statpoint = [ + 48, + 52, + 56, + 60, + 64, + 69, + 74, + 79, + 84, + 90, + 96, + 102, + 108, + 115, + 122, + 129, + 136, + 144, + 152, + 160, + 168, + 177, + 186, + 195, + 204, + 214, + 224, + 234, + 244, + 255, + 266, + 277, + 288, + 300, + 312, + 324, + 336, + 349, + 362, + 375, + 388, + 402, + 416, + 430, + 444, + 459, + 474, + 489, + 504, + 520, + 536, + 552, + 568, + 585, + 602, + 619, + 636, + 654, + 672, + 690, + 708, + 727, + 746, + 765, + 784, + 804, + 824, + 844, + 864, + 885, + 906, + 927, + 948, + 970, + 992, + 1014, + 1036, + 1059, + 1082, + 1105, + 1128, + 1152, + 1176, + 1200, + 1224, + 1249, + 1274, + 1299, + 1324, + 1350, + 1376, + 1402, + 1428, + 1455, + 1482, + 1509, + 1536, + 1564, + 1592 + ]; + /* If a character is using a certain number of status points, what is the lowest level it could be? */ + stat.minLevelForStatusPoints = function(statusPoints) { + /* + * tmwAthena status points for a level are described by the following recurrence relation: + * p(1) = 48 + * p(l) = p(l - 1) + floor((l + 14) / 4) + * For whatever reason, the server also loads a cached copy from a file of all places - db/statpoint.txt. + * If this is out of sync with the equation, fun things can happen. + * For now, naively assume that statpoint.txt is correct and shamelessly dump it here (see above). + */ + return crossfilter.bisect.left(statpoint, statusPoints, 0, statpoint.length) + 1; + }; + var statusPointInc = [0, 0] + stat.statusPointsForStat = function(v) { + if (isNaN(v)) { + throw "Invalid input"; + } + /* First, convert the absolute stat to terms of increments. */ + /* This is as simple as removing the starting value - 1 - from it. */ + /* + * The status points needed to increase from a stat with value v can be calculated as follows: + * floor((v + 9) / 10) + 1 + */ + if (statusPointInc.length > v) { + return statusPointInc[v]; + } + var i = stat.statusPointsForStat(v - 1) + Math.floor(((v - 1) + 9) / 10) + 1; + statusPointInc.push(i); + return i; + } + /* If a character has a certain arrangement of attributes, how many status points are required for this configuration? */ + stat.statusPointsForStats = function(str, agi, vit, dex, int, luk) { + return stat.statusPointsForStat(str) + + stat.statusPointsForStat(agi) + + stat.statusPointsForStat(vit) + + stat.statusPointsForStat(dex) + + stat.statusPointsForStat(int) + + stat.statusPointsForStat(luk); + }; + /* Helper function currying minLevelForStatusPoints and stat.statusPointsForStats */ + stat.minLevelForStats = function(str, agi, vit, dex, int, luk) { + return stat.minLevelForStatusPoints(stat.statusPointsForStats(str, agi, vit, dex, int, luk)); + }; + return stat; +}(); diff --git a/public/js/crossfilter b/public/js/crossfilter new file mode 160000 +Subproject ae994e8f8014ad4304e0575973bb23c1fb1ac0b diff --git a/public/js/d3 b/public/js/d3 new file mode 160000 +Subproject 91d35b4205d4ef150c61c415b7379404bced726 diff --git a/public/js/dc b/public/js/dc new file mode 160000 +Subproject 649860c7f007ae41a89e1d6236748ba36292288 diff --git a/public/js/mv/chart.js b/public/js/mv/chart.js new file mode 100644 index 0000000..d06d40e --- /dev/null +++ b/public/js/mv/chart.js @@ -0,0 +1,131 @@ +var mv = function(mv) { + mv.charts = {}; + var thinWidth = 250; + var medWidth = 400; + var wideWidth = Math.max(700, document.width - thinWidth - medWidth); + mv.charter = function() { + var charter = {}; + charter.init = function() { + mv.charts.date = bar(monoGroup(wide(dc.barChart("#date-chart")), "date")) + .centerBar(true) + .elasticX(true) + .x(d3.time.scale().domain([mv.heap.date.dim.bottom(1)[0].date, mv.heap.date.dim.top(1)[0].date]).nice(d3.time.hour)) + .xUnits(d3.time.hours) + .xAxisPadding(2) + ; + mv.charts.pc = bar(monoGroup(wide(dc.barChart("#player-chart")), "pc")) + .x(d3.scale.linear().domain([mv.heap.pc.dim.bottom(1)[0].pc, mv.heap.pc.dim.top(1)[0].pc]).nice()) + ; + mv.charts.blvl = bar(monoGroup(med(dc.barChart("#blvl-chart")), "blvl")) + .x(d3.scale.linear().domain([0, mv.heap.blvl.dim.top(1)[0].pcstat.blvl])) + ; + mv.charts.type = pie(monoGroup(dc.pieChart("#type-chart"), "type")) + ; + mv.charts.target = pie(monoGroup(dc.pieChart("#target-chart"), "target")) + ; + mv.charts.wpn = pie(monoGroup(dc.pieChart("#wpn-chart"), "wpn")) + ; + mv.charts.def = pie(monoGroup(dc.pieChart("#def-chart"), "def")) + .label(function(d) { return defLevelVerbose(d.data.key); }) + .title(function(d) { return defLevelVerbose(d.data.key) + ": " + d.value; }) + .colorAccessor(function(d) { return d.data.key; }) + .colorCalculator(function(k) { switch(k) { + case 0: return "#fd350d"; + case 1: return "#fdae6b"; + case 2: return "#6baed6"; + default: throw "Definition chart: Color access key out of range!"; + }}) + .filter(2) + ; + mv.charts.map = monoGroup(margined(wide(dc.bubbleChart("#map-chart"))), "map") + .height(500) + .colorCalculator(d3.scale.category20c()) + /* X */ + .keyAccessor(function(d) { return d.value.e + 1; }) + /* Y */ + .valueAccessor(function(d) { return d.value.j + 1; }) + /* R */ + .radiusValueAccessor(function(d) { return Math.sqrt(d.value.r); }) + .maxBubbleRelativeSize(0.045) + .x(d3.scale.log().domain([1, 100000])) + .y(d3.scale.log().domain([1, 300000])) + .axisPixelPadding({left:5, top: 10, right: 15, bottom: 5}) + .elasticX(true) + .elasticY(true) + .renderHorizontalGridLines(true) + .renderVerticalGridLines(true) + .title(function(d) { return "Map " + d.key + ":" + d.value.r; }) + .renderTitle(true) + ; + mv.charts.stats = trellisChart("#stat-chart", ["str", "agi", "vit", "dex", "int", "luk"].map(function(d) { mv.heap[d].name = d; return mv.heap[d]; })); + dc.renderlet(function() { mv.charts.stats(); }); + dc.renderAll(); + } + charter.filters = function() { + var r = {}, f; + for (var k in mv.charts) { + f = mv.charts[k].filter(); + if (f != null) { + r[k] = f; + } + } + return r; + } + function defLevelVerbose(level) { + switch (level) { + case 0: return "Undefined"; + case 1: return "Mixed"; + case 2: return "Defined"; + default: console.log(d, d.data); throw "Unknown definedness case (" + d.data.key + "); this shouldn't happen"; + } + } + return charter; + }(); + function wide(chart) { + return chart + .width(wideWidth) + ; + } + function med(chart) { + return chart + .width(medWidth) + ; + } + function thin(chart) { + return chart + .width(thinWidth) + ; + } + function short(chart) { + return chart + .height(130) + ; + } + function margined(chart) { + return chart + .margins({left: 60, right: 18, top: 5, bottom: 30}) + } + function monoGroup(chart, name) { + return chart + .dimension(mv.heap[name].dim) + .group(mv.heap[name].group) + .transitionDuration(500) + ; + } + function bar(chart) { + return margined(short(chart)) + .elasticY(true) + .gap(1) + .renderHorizontalGridLines(true) + .title(function(d) { return d.key + ": " + d.value; }) + .brushOn(true) + ; + } + function pie(chart) { + return thin(chart) + .radius(90) + .colorCalculator(d3.scale.category20c()) + ; + } + return mv; +}(mv || {}); diff --git a/public/js/mv/connect.js b/public/js/mv/connect.js new file mode 100644 index 0000000..aba4c33 --- /dev/null +++ b/public/js/mv/connect.js @@ -0,0 +1,156 @@ +var mv = function(mv) { + mv.socket = { + connect: connect + }; + /* If we're updating due to something we received, then don't broadcast back. */ + var netrendering = false; + /* Our ID */ + var id = 0; + /* List of all users */ + var users = {}; + /* The user status box */ + var usersStatus = d3.select("#users-status"); + /* io.socket's socket */ + var socket; + function connect() { + socket = io.connect('http://localhost:3000'); + socket.on("connect", function() { console.log("CONNECT", arguments); }); + socket.on("disconnect", function() { console.log("DISCONNECT", arguments); }); + socket.emit('login'); + /* + * Protocol: + * selflogin -> id (I) + * login -> id (I), nick (S) + * nickset -> id (I), nick (S) + * users -> { id -> {nick (S), filters ({dim (S) -> filter (*)}) } } + * logout -> id (I) + */ + socket.on('selflogin', function(d) { + /* Acknowledged that we logged in */ + /* Take note of our ID. */ + id = d.id; + }); + socket.on('login', function(d) { + /* Someone else logging in */ + users[d.id] = { nick: d.nick, filters: {} }; + updateUsersStatus(); + }); + socket.on('users', function(d) { + /* We've got a list of all users. */ + /* The server is always right. */ + users = d.users; + updateUsersStatus(); + }); + socket.on('nickset', function(d) { + /* Someone, possibly us, changed their nick. */ + users[d.id].nick = d.nick; + updateUsersStatus(); + }); + socket.on('filterset', function(d) { + /* Someone changed their filter */ + users[d.id].filters = d.filters; + /* Use the variable netrendering to denote that we're rendering due to a change received from the network. */ + netrendering = true; + setOwnFilters(d.filters); + netrendering = false; + }); + socket.on('logout', function(d) { + /* Someone disconnected, take them off the list. */ + delete users[d.id]; + updateUsersStatus(); + }); + dc.renderlet(function() { + /* Hook a listener into dc's rendering routine. If it rerenders, broadcast the change. */ + if (netrendering) { + /* If we rendered due a change we received, don't broadcast it again. That would be A Bad Thing. */ + return; + } + socket.emit("filter", { filters: mv.charter.filters() }); + }); + } + function setOwnFilters(filters) { + /* See if there's any difference - if there isn't, don't update. */ + var change = false; + var key; + /* Check for keys in the filters to apply which are not in our charts. */ + for (key in filters) { + if (!(key in mv.charts)) + continue; + var filter = mv.charts[key].filter(); + if (typeof(filter) == "array") { + /* Crossfilter uses arrays to filter ranges. Exactly the first two elements are significant. */ + if (filter[0] == filters[key][0] && + filter[1] == filters[key][1]) { + continue; + } + } else if (filter == filters[key]) { + continue; + } + /* This filter differs. Apply it. */ + change = true; + mv.charts[key].filter(filters[key]); + } + /* Check for keys in our charts which are not in the filters to apply. */ + for (key in mv.charts) { + if (mv.charts[key].filter() != null) { + if (key in filters) { + /* This has already been handled above */ + continue; + } + /* There is no longer a filter applying here, clear it. */ + change = true; + mv.charts[key].filterAll(); + } + } + if (change) { + dc.redrawAll(); + } + } + function updateUsersStatus() { + /* Convert the user list to a form suitable for a d3 selection */ + var data = []; + for (var uid in users) { users[uid].id = uid; data.push(users[uid]); } + /* Data join */ + var userlist = usersStatus.selectAll(".user") + .data(data, function(d) { return d.id; }); + /* Enter */ + userlist + .enter().append("li").attr("class", "user") + ; + /* Update */ + userlist + .each(function(d,i) { + var elm = d3.select(this); + console.log("Userlist para appending", d,i); + var name = elm.select(".name"); + var nick = d.nick == null ? "Anonymous User " + d.id : d.nick; + if (d.id == id) { + /* This is us! We can edit our name. */ + if (name.empty()) { + console.log("Found our entry. id:", id, "datum", d); + name = elm.append("input").attr("class", "name") + .attr("type", "text") + .attr("placeholder", "Enter name here") + .on("change", function () { + /* A d3 select must be dereferenced twice to access the DOM entity */ + socket.emit("nick", { nick: name[0][0].value }); + }) + ; + } + name.attr("value", nick); + } else { + /* This is someone else. We can't edit their name. */ + if (name.empty()) { + name = elm.append("p").attr("class", "name"); + } + name.text(nick); + } + }) + ; + /* Remove */ + userlist + .exit().remove() + ; + } + return mv; +}(mv || {}); diff --git a/public/js/mv/heap.js b/public/js/mv/heap.js new file mode 100644 index 0000000..03f3c00 --- /dev/null +++ b/public/js/mv/heap.js @@ -0,0 +1,63 @@ +var mv = function(mv) { + mv.heap = function() { + var heap = {}; + var monoGroups = {}; + var statGran = 10; + heap.init = function() { + function ea(p, d) { p.e += d.e; p.j += d.j; p.r++; return p; } + function es(p, d) { p.e -= d.e; p.j -= d.j; p.r--; return p; } + function ez(p, d) { return { e: 0, j: 0, r: 0 }; } + heap.cfdata = crossfilter(mv.parser.records); + heap.all = heap.cfdata.groupAll().reduce(ea, es, ez); + monoGroup("date", function(d) { return d3.time.hour.round(d.date); }); + monoGroup("pc", function(d) { return d.pc; }); + monoGroup("map", function(d) { return d.map; }).reduce(ea, es, ez); + monoGroup("blvl", function(d) { return d.pcstat ? d.pcstat.blvl : 0; }); + monoGroup("type", function(d) { return d.type; }); + monoGroup("target", function(d) { return d.target; }); + monoGroup("wpn", function(d) { return d.wpn; }); + function sa(p, d) { + if (!d.pcstat) return p; + p.str[d.pcstat.str]++ || (p.str[d.pcstat.str] = 1); + p.agi[d.pcstat.agi]++ || (p.agi[d.pcstat.agi] = 1); + p.vit[d.pcstat.vit]++ || (p.vit[d.pcstat.vit] = 1); + p.dex[d.pcstat.dex]++ || (p.dex[d.pcstat.dex] = 1); + p.int[d.pcstat.int]++ || (p.int[d.pcstat.int] = 1); + p.luk[d.pcstat.luk]++ || (p.luk[d.pcstat.luk] = 1); + return p; + } + function ss(p, d) { + if (!d.pcstat) return p; + --p.str[d.pcstat.str] || (p.str[d.pcstat.str] = undefined); + --p.agi[d.pcstat.agi] || (p.agi[d.pcstat.agi] = undefined); + --p.vit[d.pcstat.vit] || (p.vit[d.pcstat.vit] = undefined); + --p.dex[d.pcstat.dex] || (p.dex[d.pcstat.dex] = undefined); + --p.int[d.pcstat.int] || (p.int[d.pcstat.int] = undefined); + --p.luk[d.pcstat.luk] || (p.luk[d.pcstat.luk] = undefined); + return p; + } + function sz(p, d) { return { str: [], agi: [], vit: [], dex: [], int: [], luk: [] }; } + monoGroup("str", function(d) { return d.pcstat ? d.pcstat.str : 0; }).reduce(sa, ss, sz); + monoGroup("agi", function(d) { return d.pcstat ? d.pcstat.agi : 0; }).reduce(sa, ss, sz); + monoGroup("vit", function(d) { return d.pcstat ? d.pcstat.vit : 0; }).reduce(sa, ss, sz); + monoGroup("dex", function(d) { return d.pcstat ? d.pcstat.dex : 0; }).reduce(sa, ss, sz); + monoGroup("int", function(d) { return d.pcstat ? d.pcstat.int : 0; }).reduce(sa, ss, sz); + monoGroup("luk", function(d) { return d.pcstat ? d.pcstat.luk : 0; }).reduce(sa, ss, sz); + /* Debugging group */ + /* + * How well defined a record is. + * 0 -> Record contains undefined data + * 1 -> Record is defined, but undefined records follow and may impede validity of findings + * 2 -> Record and all succeeding records are well defined + */ + monoGroup("def", function(d) { if (d.pcstat == undefined) { return 0; } if (d.date <= mv.parser.fullyDefinedCutoff()) { return 1; } return 2; }); + heap.def.dim.filterExact(2); + } + function monoGroup(name, mapping) { + heap[name] = {}; + return heap[name].group = (heap[name].dim = heap.cfdata.dimension(mapping)).group(); + } + return heap; + }(); + return mv; +}(mv || {}); diff --git a/public/js/mv/load.js b/public/js/mv/load.js new file mode 100644 index 0000000..39dd391 --- /dev/null +++ b/public/js/mv/load.js @@ -0,0 +1,60 @@ +var mv = function(mv) { + mv.loader = function() { + /* Set up handlers for file selector */ + var numfiles = 0; + var filenames = []; + var curfile = 0; + var loader = {}; + loader.onbulkstart = function(fevt) {}; + loader.onloadstart = function(evt) {}; + loader.onprogress = function(evt) {}; + loader.onabort = function(evt) { + alert('File load aborted!'); + }; + loader.onerror = function(evt) { + switch(evt.target.error.code) { + case evt.target.error.NOT_FOUND_ERR: + alert('File Not Found!'); + break; + case evt.target.error.NOT_READABLE_ERR: + alert('File is not readable'); + break; + case evt.target.error.ABORT_ERR: + break; // noop + default: + alert('An error occurred reading this file.'); + }; + }; + loader.numfiles = function() { return numfiles; }; + loader.filenames = function() { return filenames; }; + loader.curfile = function() { return curfile; }; + loader.init = function(each, after) { + document.getElementById('input').addEventListener('change', function(fevt) { + numfiles = fevt.target.files.length; + filenames = Array.prototype.map.call(fevt.target.files, function(d) { return d.name; }); + curfile = 0; + var reader = new FileReader(); + loader.onbulkstart(fevt); + reader.onerror = function() { loader.onerror.apply(null, arguments) }; + reader.onprogress = function() { loader.onprogress.apply(null, arguments) }; + reader.onabort = function() { loader.onabort.apply(null, arguments) }; + reader.onloadstart = function() { loader.onloadstart.apply(null, arguments) }; + reader.onload = function(evt) { + each(reader.result); + ++curfile; + if (curfile == numfiles) { + after(); + } else { + nextFile(); + } + }; + function nextFile() { + reader.readAsBinaryString(fevt.target.files[curfile]); + } + nextFile(); + }, false); + }; + return loader; + }(); + return mv; +}(mv || {}); diff --git a/public/js/mv/main.js b/public/js/mv/main.js new file mode 100644 index 0000000..b7fcb1e --- /dev/null +++ b/public/js/mv/main.js @@ -0,0 +1,59 @@ +var mv = function(mv) { + mv.init = function() { + console.log("Initialising"); + var loadbar = progress('loadbar'); + var filesbar = progress('filesbar'); + var lbase = loadbar.label; + loadbar.label = function() { + return lbase() == '100%' ? "Loaded '" + mv.loader.filenames()[mv.loader.curfile()]+ "' - Done!" : "Loading '" + mv.loader.filenames()[mv.loader.curfile()] + "' - " + lbase(); + }; + var fbase = filesbar.label; + filesbar.label = function () { + return fbase() == '100%' ? "Loaded " + mv.loader.numfiles() + " file(s) - Done!" : "Loading file " + mv.loader.curfile() + " of " + mv.loader.numfiles() + " - " + fbase(); + } + mv.loader.onbulkstart = function(fevt) { + loadbar.show(); + filesbar.show(); + }; + mv.loader.onloadstart = function(evt) { + filesbar.update(mv.loader.curfile(), mv.loader.numfiles()); + loadbar.reset(); + }; + mv.loader.onprogress = function(evt) { + if (evt.lengthComputable) { + loadbar.update(evt.loaded, evt.total); + } + }; + mv.loader.init(handleFile, postLoading); + function handleFile(data, curFileNum, numFiles) { + loadbar.complete(); + mv.parser.parseRecords(data); + } + function postLoading() { + filesbar.complete(); + /* TODO: This is still a total mess that doesn't really transition properly */ + setTimeout(function() { + loadbar.hide(); + }, 2000); + mv.heap.init(); + setTimeout(function() { + filesbar.hide(); + d3.select("#mask") + .transition() + .style("opacity", 0) + .remove(); + d3.selectAll(".vis-hide") + .style("display", "inline") + .transition() + .style("opacity", 1) + ; + mv.charter.init(); + console.log(document.getElementById("connect-option").checked); + if (document.getElementById("connect-option").checked) { + mv.socket.connect(); + } + }, 2000); + } + }; + return mv; +}(mv || {}); diff --git a/public/js/mv/parse.js b/public/js/mv/parse.js new file mode 100644 index 0000000..8094bc5 --- /dev/null +++ b/public/js/mv/parse.js @@ -0,0 +1,109 @@ +var mv = function(mv) { + mv.parser = function() { + var parser = {}; + var pcstat = {}; + var fullyDefinedCutoff = 0; + parser.records = []; + parser.fullyDefinedCutoff = function() { return fullyDefinedCutoff; }; + parser.parseRecords = function(data) { + var spl = data.split(/\r?\n/); + spl.forEach(function(e, i) { + var d; + d = e.match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/); + if (d) { + var mapSID = parseInt(d[3]); + var ts = new Date(0); + ts.setUTCSeconds(d[1]); + var rec = { + date: ts, + pc: parseInt(d[2]), + map: map.nameByServerID(parseInt(d[3]), ts), + x: parseInt(d[4]), + y: parseInt(d[5]), + e: parseInt(d[6]), + j: parseInt(d[7]), + type: d[8], + pcstat: pcstat[d[2]], + target: "UNKNOWN", + dmg: -1010, + wpn: "UNKNOWN", + atktype: "UNKNOWN" + }; + if (pcstat[d[2]] == undefined && (!fullyDefinedCutoff || ts > fullyDefinedCutoff)) { + fullyDefinedCutoff = ts; + } + /* XXX: Fragile horrible and unstructured, this whole thing needs a rewrite really */ + if (i >= 2 && rec.type == "KILLXP") { + d = spl[i - 1].match(/^(\d+\.\d+) MOB(\d+) DEAD/); + if (d) { + var mID = parseInt(d[2]); + /* There's a massive wealth of data that can be collected from this. Number of assailants, weapons used, the relationships with the assailants... this can't be done with a simple lookbehind. For now, just extract what mob it was, and what the killing weapon used was. */ + d = spl[i - 2].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) WPNDMG MOB(\d+) (\d+) FOR (\d+) WPN (\d+)/); + if (d) { + softAssert(mID == parseInt(d[6]), "Integrity error: MOB ID mismatch!"); + // softAssert(rec.pc == parseInt(d[2]), "Integrity error: PC ID mismatch!"); + rec.target = mob.nameByServerID(d[7]); + softAssert(rec.target, "Unknown target!") + rec.dmg = parseInt(d[8]); + rec.wpn = item.nameByServerID(d[9]); + rec.atktype = "Physical"; + } else { + /* Not weapon damage, perhaps it was spell damage? */ + d = spl[i - 2].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) SPELLDMG MOB(\d+) (\d+) FOR (\d+) BY ([^ ]+)/); + if (d) { + rec.target = mob.nameByServerID(d[7]); + rec.dmg = parseInt(d[8]); + rec.wpn = d[9]; + rec.atktype = "Magical"; + } +// console.error("No match (deathblow):", spl[i - 2]); + } + } else { + d = spl[i - 1].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/); + if (d) { + var clone = parser.records[parser.records.length - 1]; + softAssert(rec.map == clone.map, "Integrity error: MAP ID mismatch!"); + rec.target = clone.target; + softAssert(rec.target, "Unknown (cloned) target!"); + rec.dmg = clone.dmg; /* FIXME: Take into account actual assist damage */ + rec.wpn = clone.wpn; + rec.atktype = clone.atktype; /* FIXME: Take into account what the assists used */ + } else { +// console.error("No match (clone):", spl[i - 1]); + } + } + } + parser.records.push(rec); + return; + } + d = e.match(/^(?:\d+\.\d+) PC(\d+) (?:\d+):(?:\d+),(?:\d+) STAT (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) /); + if (d) { + var s = { + str: parseInt(d[2]), + agi: parseInt(d[3]), + vit: parseInt(d[4]), + int: parseInt(d[5]), + dex: parseInt(d[6]), + luk: parseInt(d[7]) + }; + s.blvl = stat.minLevelForStats(s.str, s.agi, s.vit, s.int, s.dex, s.luk); + s.str = Math.floor(s.str / 10); + s.agi = Math.floor(s.agi / 10); + s.vit = Math.floor(s.vit / 10); + s.int = Math.floor(s.int / 10); + s.dex = Math.floor(s.dex / 10); + s.luk = Math.floor(s.luk / 10); + pcstat[d[1]] = s; + return; + } + }); + }; + function softAssert(expr, msg) { + if (!expr) { + console.error("SOFTASSERT FAILURE: " + msg); + } + } + return parser; + }(); + return mv; +}(mv || {}); diff --git a/public/js/util/memoize.js b/public/js/util/memoize.js new file mode 100644 index 0000000..796c060 --- /dev/null +++ b/public/js/util/memoize.js @@ -0,0 +1,45 @@ +/* + * Helper function for memoization + * + * The passed labeller function must generate unique and exact names for entries that are naturally equal. + * IDs are persistent. + * + * names, idByName, and entries are accessible fields from the memoizer. + * Behaviour on external modification of these fields is undefined. + * + * An entry object passed to the memoizer has its field "id" set to the memoized id by default. + * The name of the ID field can be modified by calling the getter/setter idField() from the memoizer. + * Behaviour on changing the ID field used while the memoizer has already performed memoization is undefined. + */ +function memoize(labeller) { + var idField = "id"; + var names = []; + var idByName = {}; + var entries = []; + var _memoizer = function(entry) { + var name = labeller(entry); + if (name in idByName) { + /* We already have a suitable entry. */ + var id = idByName[name]; + return entries[id]; + } + /* New entry. */ + /* Set the entry's ID. */ + entry[idField] = entries.length; + /* Index the new entry. */ + idByName[name] = entry[idField]; + names[entry[idField]] = name; + entries.push(entry); + + return entry; + } + _memoizer.names = function() { return names; }; + _memoizer.idByName = function() { return idByName; }; + _memoizer.entries = function() { return entries; }; + _memoizer.idField = function(x) { + if (!arguments.length) return idField; + idField = x; + return _memoizer; + }; + return _memoizer; +} diff --git a/public/js/util/progress.js b/public/js/util/progress.js new file mode 100644 index 0000000..060e6dc --- /dev/null +++ b/public/js/util/progress.js @@ -0,0 +1,37 @@ +function progress(root) { + var _progress = {}; + var container = document.getElementById(root); + var _percent = '0%'; + var bar = document.querySelector('#' + root + ' .percent'); + _progress.label = function() { + return _percent; + } + /* Updates the progress bar to display a specific percentage. No range checking performed. */ + _progress.setPercent = function(percent) { + _percent = percent; + bar.style.width = _percent; + bar.textContent = _progress.label(); + } + /* Updates the progress bar to display a percentage based on the current proportion of items done. */ + _progress.update = function(current, total) { + var percentLoaded = Math.min(100, Math.round((current / total) * 100)); + _progress.setPercent(percentLoaded + '%'); + }; + /* Resets the progress bar to display nothing done. */ + _progress.reset = function() { + _progress.setPercent('0%'); + } + /* Resets the progress bar to display everything done. */ + _progress.complete = function() { + _progress.setPercent('100%'); + } + /* Shows the progress bar. */ + _progress.show = function() { + container.className += ' loading'; + } + /* Hides the progress bar */ + _progress.hide = function() { + container.className = container.className.replace(/\bloading\b/, ''); + } + return _progress; +} diff --git a/public/js/util/trellis-chart.js b/public/js/util/trellis-chart.js new file mode 100644 index 0000000..65e07a2 --- /dev/null +++ b/public/js/util/trellis-chart.js @@ -0,0 +1,140 @@ +function trellisChart(anchor, monoGroups) { + /* attr -> {dim, group} key -> str amount, value -> { str, agi, vit, dex, int, luk } */ + + var attrs = monoGroups.map(function(d) { return d.name; }); + var attrsIdByName = {}; + monoGroups.forEach(function(d, i) { attrsIdByName[d.name] = i; }); + + var cellWidth = 5; + var radius = cellWidth / 2; + var subChartLength = 57; + var subChartUnpaddedLength = 50; + var subChartPadding = 7; + var filler = d3.scale.log().domain([1, 2]).range([0, 255]); + + var margin = {top: 10, right: 10, bottom: 20, left: 10}; + var anchor = d3.select(anchor); + var g = anchor.select("g"); + + var _chart = function() { + if (g.empty()) { + /* Make stuff! */ + var svg = anchor.append("svg"); + g = svg + .append("g"); + attrs.forEach(function(d, i) { + g + .append("text") + .attr("transform", function(d) { return "translate(0," + ((attrs.length - i) * subChartLength + 10 - subChartLength / 2) + ")"; }) + .text(d) + ; + g + .append("text") + .attr("transform", function(d) { return "translate(" + (i * subChartLength + 25 + 22) + "," + (attrs.length * subChartLength + 18) + ")"; }) + .text(d) + ; + }) + g = svg + .append("g") + .attr("transform", "translate(" + (margin.left + 25) + "," + (margin.top) + ")"); + } + /* Group first into columns for each stat. We have one column for each of the stat monoGroups. */ + /* + * monoGroups is an array of each stat dimension. We can consider each column to have data in the following format: + * { group: function, dim: function, name: stat } + */ + var columns = g.selectAll(".column") + .data(monoGroups); + var colE = columns + .enter().append("g") + .attr("class", "column") + .attr("transform", function(d) { return "translate(" + (attrsIdByName[d.name] * subChartLength) + ",0)"; }) + ; + colE + .append("line") + .attr("x1", -cellWidth) + .attr("x2", -cellWidth) + .attr("y1", -cellWidth) + .attr("y2", subChartLength * attrs.length - subChartPadding) + .attr("class", "border-line") + ; + colE + .append("line") + .attr("x1", subChartUnpaddedLength) + .attr("x2", subChartUnpaddedLength) + .attr("y1", -cellWidth) + .attr("y2", subChartLength * attrs.length - subChartPadding) + .attr("class", "border-line") + ; + /* Each stat has an array for its value. Group these to find the x position. */ + /* + * The function transforms the data to take the grouping. We can consider each x position grouping to have data in the following format: + * { key: position, value: [{[stat] -> [y pos] -> count}] } + */ + var colposg = columns.selectAll(".colpos") + .data(function(d, i) { +// console.log("Incoming colposg format:", d, i, "Transformed to:", d.group.all().map(function(d2) { d2.name = d.name; return d2; })); + return d.group.all().map(function(d2) { d2.name = d.name; return d2; }); + }, function(d) { return d.key; }); + colposg + .enter().append("g") + .attr("class", "colpos") + .attr("transform", function(d) { return "translate(" + (d.key * cellWidth) + ",0)"; }) + ; + /* Next, split up each x position grouping into its y stat grouping. */ + /* + * We can consider each y stat grouping to have data in the following format: + * v[y pos] -> count; v.name -> name + */ + var rows = colposg.selectAll(".row") + .data(function(d, i) { +// console.log("Incoming row format:", d, i, "Transformed to:", attrs.map(function(d2) { return { name: d2, data: d.value[d2] }; })); + return attrs.map(function(d2) { return { name: d2, data: d.value[d2] }; }); + }); + rows + .enter().append("g") + .attr("class", "row") + .attr("transform", function(d) { return "translate(0," + ((attrs.length - attrsIdByName[d.name] - 1) * subChartLength) + ")"; }) + ; + /* Finally, split up each y stat grouping into x. */ + var vmax = 0; + var cells = rows.selectAll(".cell") + .data(function(d, i) { +// console.log("Incoming cells format:", d, i, "Transformed to:", d.data); + return d.data; + }); + cells + .enter().append("circle") + .attr("class", "cell") + .attr("r", radius) + ; + cells + .each(function(d) { + if (d > vmax) vmax = d; + }) + ; + filler.domain([1, vmax + 1]); + cells + .attr("fill", function(d) { + return d ? d3.rgb(255 - filler(d + 1), 255 - filler(d + 1) / 2, 255 - filler(d + 1) / 3) : "white"; + }) + .attr("transform", function(d, i) { return "translate(0," + ((10-i) * cellWidth) + ")"; }) + ; + cells + .exit() + .remove() + ; + } + + _chart.filter = function() { + /* + * TODO: + * This is going to be interesting. As the chart is not charting a single + * monogroup, and most code is built around this assumption, this might + * well end up being a messy special case. + */ + return null; + } + + return _chart; +} |