diff --git a/public/skills/icon_0.png b/public/skills/icon_0.png index a141a92..ca65c84 100644 Binary files a/public/skills/icon_0.png and b/public/skills/icon_0.png differ diff --git a/public/ui/icon_0.png b/public/ui/icon_0.png index a141a92..ca65c84 100644 Binary files a/public/ui/icon_0.png and b/public/ui/icon_0.png differ diff --git a/public/ui/items-atlas.png b/public/ui/items-atlas.png index e1ef27b..28cef33 100644 Binary files a/public/ui/items-atlas.png and b/public/ui/items-atlas.png differ diff --git a/public/ui/manifest.json b/public/ui/manifest.json index b1add97..60ec79e 100644 --- a/public/ui/manifest.json +++ b/public/ui/manifest.json @@ -1,9 +1,9 @@ { "version": "1.13c", - "decodedDc6Count": 467, + "decodedDc6Count": 680, "dc6DecodeFailures": 0, "atlasWidth": 1024, - "atlasHeight": 1938, + "atlasHeight": 2145, "questsAtlasWidth": 432, "questsAtlasHeight": 774, "codeToInvFile": { @@ -1228,6 +1228,893 @@ "invvbl_blac": "invvbl_blac", "invlbl_blac": "invlbl_blac" }, + "codeToFlippyFile": { + "hax": "flphax", + "axe": "flpaxe", + "2ax": "flp2ax", + "mpi": "flpmpi", + "wax": "flpwax", + "lax": "flplax", + "bax": "flpbrx", + "btx": "flpbtx", + "gax": "flpgax", + "gix": "flpgix", + "wnd": "flpwnd", + "ywn": "flpywn", + "bwn": "flpbwn", + "gwn": "flpgwn", + "clb": "flpclb", + "scp": "flpscp", + "gsc": "flpgsc", + "wsp": "flpwsp", + "spc": "flpspc", + "mac": "flpmac", + "mst": "flpmst", + "fla": "flpfla", + "whm": "flpwhm", + "mau": "flpmau", + "gma": "flpgma", + "ssd": "flpssd", + "scm": "flpscm", + "sbr": "flpsbr", + "flc": "flpflc", + "crs": "flpcrs", + "bsd": "flpbsd", + "lsd": "flplsd", + "wsd": "flpwsd", + "2hs": "flp2hs", + "clm": "flpclm", + "gis": "flpgis", + "bsw": "flpbsw", + "flb": "flpflb", + "gsd": "flpgsd", + "dgr": "flpdgr", + "dir": "flpdir", + "kri": "flpkrs", + "bld": "flpbld", + "tkf": "flptkn", + "tax": "flptax", + "bkf": "flpbkf", + "bal": "flpbal", + "jav": "flpjav", + "pil": "flppil", + "ssp": "flpssp", + "glv": "flpglv", + "tsp": "flptsp", + "spr": "flpspr", + "tri": "flptri", + "brn": "flpbrn", + "spt": "flpspt", + "pik": "flppik", + "bar": "flpbar", + "vou": "flpvou", + "scy": "flpscy", + "pax": "flppax", + "hal": "flphal", + "wsc": "flpwsc", + "sst": "flpsst", + "lst": "flplst", + "cst": "flpcst", + "bst": "flpbst", + "wst": "flpwst", + "sbw": "flpsbw", + "hbw": "flphbw", + "lbw": "flplbw", + "cbw": "flpcbw", + "sbb": "flpsbb", + "lbb": "flplbb", + "swb": "flpswb", + "lwb": "flplwb", + "lxb": "flplxb", + "mxb": "flpmxb", + "hxb": "flphxb", + "rxb": "flprxb", + "gps": "flpgpl", + "ops": "flpopl", + "gpm": "flpgps", + "opm": "flpops", + "gpl": "flpgps", + "opl": "flpops", + "d33": "flpd33", + "g33": "flpg33", + "leg": "flpleg", + "hdm": "flphmr", + "hfh": "flphmr", + "hst": "flphst", + "msf": "flpmsf", + "9ha": "flphax", + "9ax": "flpaxe", + "92a": "flp2ax", + "9mp": "flpmpi", + "9wa": "flpwax", + "9la": "flplax", + "9ba": "flpbrx", + "9bt": "flpbtx", + "9ga": "flpgax", + "9gi": "flpgix", + "9wn": "flpwnd", + "9yw": "flpywn", + "9bw": "flpbwn", + "9gw": "flpgwn", + "9cl": "flpclb", + "9sc": "flpscp", + "9qs": "flpgsc", + "9ws": "flpwsp", + "9sp": "flpspc", + "9ma": "flpmac", + "9mt": "flpmst", + "9fl": "flpfla", + "9wh": "flpwhm", + "9m9": "flpmau", + "9gm": "flpgma", + "9ss": "flpssd", + "9sm": "flpscm", + "9sb": "flpsbr", + "9fc": "flpflc", + "9cr": "flpcrs", + "9bs": "flpbsd", + "9ls": "flplsd", + "9wd": "flpwsd", + "92h": "flp2hs", + "9cm": "flpclm", + "9gs": "flpgis", + "9b9": "flpbsw", + "9fb": "flpflb", + "9gd": "flpgsd", + "9dg": "flpdgr", + "9di": "flpdir", + "9kr": "flpkrs", + "9bl": "flpbld", + "9tk": "flptkn", + "9ta": "flptax", + "9bk": "flpbkf", + "9b8": "flpbal", + "9ja": "flpjav", + "9pi": "flppil", + "9s9": "flpssp", + "9gl": "flpglv", + "9ts": "flptsp", + "9sr": "flpspr", + "9tr": "flptri", + "9br": "flpbrn", + "9st": "flpspt", + "9p9": "flppik", + "9b7": "flpbar", + "9vo": "flpvou", + "9s8": "flpscy", + "9pa": "flppax", + "9h9": "flphal", + "9wc": "flpwsc", + "8ss": "flpsst", + "8ls": "flplst", + "8cs": "flpcst", + "8bs": "flpbst", + "8ws": "flpwst", + "8sb": "flpsbw", + "8hb": "flphbw", + "8lb": "flplbw", + "8cb": "flpcbw", + "8s8": "flpsbb", + "8l8": "flplbb", + "8sw": "flpswb", + "8lw": "flplwb", + "8lx": "flplxb", + "8mx": "flpmxb", + "8hx": "flphxb", + "8rx": "flprxb", + "qf1": "flpfla", + "qf2": "flpfla", + "ktr": "flpktr", + "wrb": "flpktr", + "axf": "flpaxf", + "ces": "flpaxf", + "clw": "flpclw", + "btl": "flpclw", + "skr": "flpskr", + "9ar": "flpktr", + "9wb": "flpktr", + "9xf": "flpaxf", + "9cs": "flpaxf", + "9lw": "flpclw", + "9tw": "flpclw", + "9qr": "flpskr", + "7ar": "flpktr", + "7wb": "flpktr", + "7xf": "flpaxf", + "7cs": "flpaxf", + "7lw": "flpclw", + "7tw": "flpclw", + "7qr": "flpskr", + "7ha": "flphax", + "7ax": "flpaxe", + "72a": "flp2ax", + "7mp": "flpmpi", + "7wa": "flpwax", + "7la": "flplax", + "7ba": "flpbrx", + "7bt": "flpbtx", + "7ga": "flpgax", + "7gi": "flpgix", + "7wn": "flpwnd", + "7yw": "flpywn", + "7bw": "flpbwn", + "7gw": "flpgwn", + "7cl": "flpclb", + "7sc": "flpscp", + "7qs": "flpgsc", + "7ws": "flpwsp", + "7sp": "flpspc", + "7ma": "flpmac", + "7mt": "flpmst", + "7fl": "flpfla", + "7wh": "flpwhm", + "7m7": "flpmau", + "7gm": "flpgma", + "7ss": "flpssd", + "7sm": "flpscm", + "7sb": "flpsbr", + "7fc": "flpflc", + "7cr": "flpcrs", + "7bs": "flpbsd", + "7ls": "flplsd", + "7wd": "flpwsd", + "72h": "flp2hs", + "7cm": "flpclm", + "7gs": "flpgis", + "7b7": "flpbsw", + "7fb": "flpflb", + "7gd": "flpgsd", + "7dg": "flpdgr", + "7di": "flpdir", + "7kr": "flpkrs", + "7bl": "flpbld", + "7tk": "flptkn", + "7ta": "flptax", + "7bk": "flpbkf", + "7b8": "flpbal", + "7ja": "flpjav", + "7pi": "flppil", + "7s7": "flpssp", + "7gl": "flpglv", + "7ts": "flptsp", + "7sr": "flpspr", + "7tr": "flptri", + "7br": "flpbrn", + "7st": "flpspt", + "7p7": "flppik", + "7o7": "flpbar", + "7vo": "flpvou", + "7s8": "flpscy", + "7pa": "flppax", + "7h7": "flphal", + "7wc": "flpwsc", + "6ss": "flpsst", + "6ls": "flplst", + "6cs": "flpcst", + "6bs": "flpbst", + "6ws": "flpwst", + "6sb": "flpsbw", + "6hb": "flphbw", + "6lb": "flplbw", + "6cb": "flpcbw", + "6s7": "flpsbb", + "6l7": "flplbb", + "6sw": "flpswb", + "6lw": "flplwb", + "6lx": "flplxb", + "6mx": "flpmxb", + "6hx": "flphxb", + "6rx": "flprxb", + "ob1": "flpob1", + "ob2": "flpob1", + "ob3": "flpob3", + "ob4": "flpob4", + "ob5": "flpob5", + "am1": "flpam1", + "am2": "flpam2", + "am3": "flpam3", + "am4": "flppik", + "am5": "flpam5", + "ob6": "flpob1", + "ob7": "flpob1", + "ob8": "flpob3", + "ob9": "flpob4", + "oba": "flpob5", + "am6": "flpam1", + "am7": "flpam2", + "am8": "flpam3", + "am9": "flppik", + "ama": "flpam5", + "obb": "flpob1", + "obc": "flpob1", + "obd": "flpob3", + "obe": "flpob4", + "obf": "flpob5", + "amb": "flpam1", + "amc": "flpam2", + "amd": "flpam3", + "ame": "flppik", + "amf": "flpam5", + "cap": "flpcap", + "skp": "flpskp", + "hlm": "flphlm", + "fhl": "flpfhl", + "ghm": "flpghm", + "crn": "flpcrn", + "msk": "flpmsk", + "qui": "flpqlt", + "lea": "flplea", + "hla": "flphla", + "stu": "flpstu", + "rng": "flprng", + "scl": "flpscl", + "chn": "flpchn", + "brs": "flpbrs", + "spl": "flpspl", + "plt": "flpplt", + "fld": "flpfld", + "gth": "flpgth", + "ful": "flpful", + "aar": "flpaar", + "ltp": "flpltp", + "buc": "flpbuc", + "sml": "flpsml", + "lrg": "flplrg", + "kit": "flpkit", + "tow": "flptow", + "gts": "flpgts", + "lgl": "flplgl", + "vgl": "flpvgl", + "mgl": "flpmgl", + "tgl": "flptgl", + "hgl": "flphgl", + "lbt": "flplbt", + "vbt": "flpvbt", + "mbt": "flpmbt", + "tbt": "flptbt", + "hbt": "flphbt", + "lbl": "flplbl", + "vbl": "flpvbl", + "mbl": "flpmbl", + "tbl": "flptbl", + "hbl": "flphbl", + "bhm": "flpbhm", + "bsh": "flpbsh", + "spk": "flpspk", + "xap": "flpcap", + "xkp": "flpskp", + "xlm": "flphlm", + "xhl": "flpfhl", + "xhm": "flpghm", + "xrn": "flpcrn", + "xsk": "flpmsk", + "xui": "flpqlt", + "xea": "flplea", + "xla": "flphla", + "xtu": "flpstu", + "xng": "flprng", + "xcl": "flpscl", + "xhn": "flpchn", + "xrs": "flpbrs", + "xpl": "flpspl", + "xlt": "flpplt", + "xld": "flpfld", + "xth": "flpgth", + "xul": "flpful", + "xar": "flpaar", + "xtp": "flpltp", + "xuc": "flpbuc", + "xml": "flpsml", + "xrg": "flplrg", + "xit": "flpkit", + "xow": "flptow", + "xts": "flpgts", + "xlg": "flplgl", + "xvg": "flpvgl", + "xmg": "flpmgl", + "xtg": "flptgl", + "xhg": "flphgl", + "xlb": "flplbt", + "xvb": "flpvbt", + "xmb": "flpmbt", + "xtb": "flptbt", + "xhb": "flphbt", + "zlb": "flplbl", + "zvb": "flpvbl", + "zmb": "flpmbl", + "ztb": "flptbl", + "zhb": "flphbl", + "xh9": "flpbhm", + "xsh": "flpbsh", + "xpk": "flpspk", + "dr1": "flpdr1", + "dr2": "flpdr4", + "dr3": "flpdr3", + "dr4": "flpdr4", + "dr5": "flpdr1", + "ba1": "flpba1", + "ba2": "flpba1", + "ba3": "flpba3", + "ba4": "flpba5", + "ba5": "flpba5", + "pa1": "flppa1", + "pa2": "flppa1", + "pa3": "flppa3", + "pa4": "flppa3", + "pa5": "flppa5", + "ne1": "flpne1", + "ne2": "flpne2", + "ne3": "flpne3", + "ne4": "flpne3", + "ne5": "flpne2", + "ci0": "flpci0", + "ci1": "flpci1", + "ci2": "flpci1", + "ci3": "flpci2", + "uap": "flpcap", + "ukp": "flpskp", + "ulm": "flphlm", + "uhl": "flpfhl", + "uhm": "flpghm", + "urn": "flpcrn", + "usk": "flpmsk", + "uui": "flpqlt", + "uea": "flplea", + "ula": "flphla", + "utu": "flpstu", + "ung": "flprng", + "ucl": "flpscl", + "uhn": "flpchn", + "urs": "flpbrs", + "upl": "flpspl", + "ult": "flpplt", + "uld": "flpfld", + "uth": "flpgth", + "uul": "flpful", + "uar": "flpaar", + "utp": "flpltp", + "uuc": "flpbuc", + "uml": "flpsml", + "urg": "flplrg", + "uit": "flpkit", + "uow": "flptow", + "uts": "flpgts", + "ulg": "flplgl", + "uvg": "flpvgl", + "umg": "flpmgl", + "utg": "flptgl", + "uhg": "flphgl", + "ulb": "flplbt", + "uvb": "flpvbt", + "umb": "flpmbt", + "utb": "flptbt", + "uhb": "flphbt", + "ulc": "flplbl", + "uvc": "flpvbl", + "umc": "flpmbl", + "utc": "flptbl", + "uhc": "flphbl", + "uh9": "flpbhm", + "ush": "flpbsh", + "upk": "flpspk", + "dr6": "flpdr1", + "dr7": "flpdr4", + "dr8": "flpdr3", + "dr9": "flpdr4", + "dra": "flpdr1", + "ba6": "flpba1", + "ba7": "flpba1", + "ba8": "flpba3", + "ba9": "flpba5", + "baa": "flpba5", + "pa6": "flppa1", + "pa7": "flppa1", + "pa8": "flppa3", + "pa9": "flppa3", + "paa": "flppa5", + "ne6": "flpne1", + "ne7": "flpne2", + "ne8": "flpne3", + "ne9": "flpne3", + "nea": "flpne2", + "drb": "flpdr1", + "drc": "flpdr4", + "drd": "flpdr3", + "dre": "flpdr4", + "drf": "flpdr1", + "bab": "flpba1", + "bac": "flpba1", + "bad": "flpba3", + "bae": "flpba5", + "baf": "flpba5", + "pab": "flppa1", + "pac": "flppa1", + "pad": "flppa3", + "pae": "flppa3", + "paf": "flppa5", + "neb": "flpne1", + "neg": "flpne2", + "ned": "flpne3", + "nee": "flpne3", + "nef": "flpne2", + "elx": "flppot", + "hpo": "flprps", + "mpo": "flpbps", + "hpf": "flprpl", + "mpf": "flpbpl", + "vps": "flpwps", + "yps": "flpnps", + "rvs": "flpvps", + "rvl": "flpvpl", + "wms": "flpyps", + "tbk": "flpbbk", + "ibk": "flprbk", + "amu": "flpamu", + "vip": "flpamu", + "rin": "flprin", + "gld": "flpgld", + "bks": "flpscr", + "bkd": "flpscr", + "aqv": "flpqvr", + "tch": "flptrch", + "cqv": "flpqvr", + "tsc": "flpbsc", + "isc": "flprsc", + "hrt": "flphrt", + "brz": "flpbrnz", + "jaw": "flpjaw", + "eyz": "flpeye", + "hrn": "flphorn", + "flg": "flpflag", + "fng": "flpfang", + "qll": "flpquil", + "sol": "flpsple", + "scz": "flpscp", + "spe": "flpsple", + "key": "flpkey", + "luv": "flpmph", + "xyz": "flpwps", + "j34": "flpjbi", + "g34": "flpgbi", + "bbb": "flpbbb", + "box": "flpbox", + "tr1": "flphscr", + "mss": "flpmss", + "ass": "flpbbk", + "qey": "flpeye", + "qhr": "flphrt", + "qbr": "flpbrnz", + "ear": "flpear", + "gcv": "flpgsv", + "gfv": "flpgsv", + "gsv": "flpgsv", + "gzv": "flpgsv", + "gpv": "flpgsv", + "gcy": "flpgsy", + "gfy": "flpgsy", + "gsy": "flpgsy", + "gly": "flpgsy", + "gpy": "flpgsy", + "gcb": "flpgsb", + "gfb": "flpgsb", + "gsb": "flpgsb", + "glb": "flpgsb", + "gpb": "flpgsb", + "gcg": "flpgsg", + "gfg": "flpgsg", + "gsg": "flpgsg", + "glg": "flpgsg", + "gpg": "flpgsg", + "gcr": "flpgsr", + "gfr": "flpgsr", + "gsr": "flpgsr", + "glr": "flpgsr", + "gpr": "flpgsr", + "gcw": "flpgsw", + "gfw": "flpgsw", + "gsw": "flpgsw", + "glw": "flpgsw", + "gpw": "flpgsw", + "hp1": "flprps", + "hp2": "flprps", + "hp3": "flprps", + "hp4": "flprpl", + "hp5": "flprpl", + "mp1": "flpbps", + "mp2": "flpbps", + "mp3": "flpbps", + "mp4": "flpbpl", + "mp5": "flpbpl", + "skc": "flpskl", + "skf": "flpskl", + "sku": "flpskl", + "skl": "flpskl", + "skz": "flpskl", + "hrb": "flphrb", + "cm1": "flpmss", + "cm2": "flptrch", + "cm3": "flpmss", + "rps": "flprps", + "rpl": "flprpl", + "bps": "flpbps", + "bpl": "flpbpl", + "r01": "flprun", + "r02": "flprun", + "r03": "flprun", + "r04": "flprun", + "r05": "flprun", + "r06": "flprun", + "r07": "flprun", + "r08": "flprun", + "r09": "flprun", + "r10": "flprun", + "r11": "flprun", + "r12": "flprun", + "r13": "flprun", + "r14": "flprun", + "r15": "flprun", + "r16": "flprun", + "r17": "flprun", + "r18": "flprun", + "r19": "flprun", + "r20": "flprun", + "r21": "flprun", + "r22": "flprun", + "r23": "flprun", + "r24": "flprun", + "r25": "flprun", + "r26": "flprun", + "r27": "flprun", + "r28": "flprun", + "r29": "flprun", + "r30": "flprun", + "r31": "flprun", + "r32": "flprun", + "r33": "flprun", + "jew": "flpgsw", + "ice": "flpwps", + "0sc": "flprsc", + "tr2": "flpscr", + "pk1": "flpmph", + "pk2": "flpmph", + "pk3": "flpmph", + "dhn": "flpfang", + "bey": "flpeye", + "mbr": "flpbrnz", + "toa": "flprun", + "tes": "flpgsb", + "ceh": "flpgsy", + "bet": "flpgsr", + "fed": "flpgsg", + "std": "flpflag", + "Larzuk's Champion": "flphmr", + "Annihilus": "flpmss", + "Hellfire Torch": "flptrch", + "gold": "flpgld", + "Horadric Cube": "flpbox", + "gheeds": "flpmss", + "Gheed's Fortune": "flpmss", + "anni": "flpmss", + "torch": "flptrch", + "Harlequin Crest": "flpcap_cgrn", + "Tal Rasha's Guardianship": "flpgth_dpur", + "Tal Rasha's Howling Wind": "flpgth_dpur", + "Magefist": "flptgl_lgry", + "Arachnid Mesh": "flpvbl_blac", + "flphax": "flphax", + "flpaxe": "flpaxe", + "flp2ax": "flp2ax", + "flpmpi": "flpmpi", + "flpwax": "flpwax", + "flplax": "flplax", + "flpbrx": "flpbrx", + "flpbtx": "flpbtx", + "flpgax": "flpgax", + "flpgix": "flpgix", + "flpwnd": "flpwnd", + "flpywn": "flpywn", + "flpbwn": "flpbwn", + "flpgwn": "flpgwn", + "flpclb": "flpclb", + "flpscp": "flpscp", + "flpgsc": "flpgsc", + "flpwsp": "flpwsp", + "flpspc": "flpspc", + "flpmac": "flpmac", + "flpmst": "flpmst", + "flpfla": "flpfla", + "flpwhm": "flpwhm", + "flpmau": "flpmau", + "flpgma": "flpgma", + "flpssd": "flpssd", + "flpscm": "flpscm", + "flpsbr": "flpsbr", + "flpflc": "flpflc", + "flpcrs": "flpcrs", + "flpbsd": "flpbsd", + "flplsd": "flplsd", + "flpwsd": "flpwsd", + "flp2hs": "flp2hs", + "flpclm": "flpclm", + "flpgis": "flpgis", + "flpbsw": "flpbsw", + "flpflb": "flpflb", + "flpgsd": "flpgsd", + "flpdgr": "flpdgr", + "flpdir": "flpdir", + "flpkrs": "flpkrs", + "flpbld": "flpbld", + "flptkn": "flptkn", + "flptax": "flptax", + "flpbkf": "flpbkf", + "flpbal": "flpbal", + "flpjav": "flpjav", + "flppil": "flppil", + "flpssp": "flpssp", + "flpglv": "flpglv", + "flptsp": "flptsp", + "flpspr": "flpspr", + "flptri": "flptri", + "flpbrn": "flpbrn", + "flpspt": "flpspt", + "flppik": "flppik", + "flpbar": "flpbar", + "flpvou": "flpvou", + "flpscy": "flpscy", + "flppax": "flppax", + "flphal": "flphal", + "flpwsc": "flpwsc", + "flpsst": "flpsst", + "flplst": "flplst", + "flpcst": "flpcst", + "flpbst": "flpbst", + "flpwst": "flpwst", + "flpsbw": "flpsbw", + "flphbw": "flphbw", + "flplbw": "flplbw", + "flpcbw": "flpcbw", + "flpsbb": "flpsbb", + "flplbb": "flplbb", + "flpswb": "flpswb", + "flplwb": "flplwb", + "flplxb": "flplxb", + "flpmxb": "flpmxb", + "flphxb": "flphxb", + "flprxb": "flprxb", + "flpgpl": "flpgpl", + "flpopl": "flpopl", + "flpgps": "flpgps", + "flpops": "flpops", + "flpd33": "flpd33", + "flpg33": "flpg33", + "flpleg": "flpleg", + "flphmr": "flphmr", + "flphst": "flphst", + "flpmsf": "flpmsf", + "flpktr": "flpktr", + "flpaxf": "flpaxf", + "flpclw": "flpclw", + "flpskr": "flpskr", + "flpob1": "flpob1", + "flpob3": "flpob3", + "flpob4": "flpob4", + "flpob5": "flpob5", + "flpam1": "flpam1", + "flpam2": "flpam2", + "flpam3": "flpam3", + "flpam5": "flpam5", + "flpcap": "flpcap", + "flpskp": "flpskp", + "flphlm": "flphlm", + "flpfhl": "flpfhl", + "flpghm": "flpghm", + "flpcrn": "flpcrn", + "flpmsk": "flpmsk", + "flpqlt": "flpqlt", + "flplea": "flplea", + "flphla": "flphla", + "flpstu": "flpstu", + "flprng": "flprng", + "flpscl": "flpscl", + "flpchn": "flpchn", + "flpbrs": "flpbrs", + "flpspl": "flpspl", + "flpplt": "flpplt", + "flpfld": "flpfld", + "flpgth": "flpgth", + "flpful": "flpful", + "flpaar": "flpaar", + "flpltp": "flpltp", + "flpbuc": "flpbuc", + "flpsml": "flpsml", + "flplrg": "flplrg", + "flpkit": "flpkit", + "flptow": "flptow", + "flpgts": "flpgts", + "flplgl": "flplgl", + "flpvgl": "flpvgl", + "flpmgl": "flpmgl", + "flptgl": "flptgl", + "flphgl": "flphgl", + "flplbt": "flplbt", + "flpvbt": "flpvbt", + "flpmbt": "flpmbt", + "flptbt": "flptbt", + "flphbt": "flphbt", + "flplbl": "flplbl", + "flpvbl": "flpvbl", + "flpmbl": "flpmbl", + "flptbl": "flptbl", + "flphbl": "flphbl", + "flpbhm": "flpbhm", + "flpbsh": "flpbsh", + "flpspk": "flpspk", + "flpdr1": "flpdr1", + "flpdr4": "flpdr4", + "flpdr3": "flpdr3", + "flpba1": "flpba1", + "flpba3": "flpba3", + "flpba5": "flpba5", + "flppa1": "flppa1", + "flppa3": "flppa3", + "flppa5": "flppa5", + "flpne1": "flpne1", + "flpne2": "flpne2", + "flpne3": "flpne3", + "flpci0": "flpci0", + "flpci1": "flpci1", + "flpci2": "flpci2", + "flppot": "flppot", + "flprps": "flprps", + "flpbps": "flpbps", + "flprpl": "flprpl", + "flpbpl": "flpbpl", + "flpwps": "flpwps", + "flpnps": "flpnps", + "flpvps": "flpvps", + "flpvpl": "flpvpl", + "flpyps": "flpyps", + "flpbbk": "flpbbk", + "flprbk": "flprbk", + "flpamu": "flpamu", + "flprin": "flprin", + "flpgld": "flpgld", + "flpscr": "flpscr", + "flpqvr": "flpqvr", + "flptrch": "flptrch", + "flpbsc": "flpbsc", + "flprsc": "flprsc", + "flphrt": "flphrt", + "flpbrnz": "flpbrnz", + "flpjaw": "flpjaw", + "flpeye": "flpeye", + "flphorn": "flphorn", + "flpflag": "flpflag", + "flpfang": "flpfang", + "flpquil": "flpquil", + "flpsple": "flpsple", + "flpkey": "flpkey", + "flpmph": "flpmph", + "flpjbi": "flpjbi", + "flpgbi": "flpgbi", + "flpbbb": "flpbbb", + "flpbox": "flpbox", + "flphscr": "flphscr", + "flpmss": "flpmss", + "flpear": "flpear", + "flpgsv": "flpgsv", + "flpgsy": "flpgsy", + "flpgsb": "flpgsb", + "flpgsg": "flpgsg", + "flpgsr": "flpgsr", + "flpgsw": "flpgsw", + "flpskl": "flpskl", + "flphrb": "flphrb", + "flpchm1": "flpchm1", + "flpchm2": "flpchm2", + "flpchm3": "flpchm3", + "flprun": "flprun" + }, "itemRects": { "inv2ax": { "x": 0, @@ -3700,6 +4587,2672 @@ "y": 1854, "w": 56, "h": 28 + }, + "flp2ax": { + "x": 496, + "y": 1854, + "w": 22, + "h": 12 + }, + "flp2hs": { + "x": 520, + "y": 1854, + "w": 37, + "h": 20 + }, + "flpaar": { + "x": 559, + "y": 1854, + "w": 29, + "h": 23 + }, + "flpam1": { + "x": 590, + "y": 1854, + "w": 12, + "h": 13 + }, + "flpam2": { + "x": 604, + "y": 1854, + "w": 19, + "h": 15 + }, + "flpam3": { + "x": 625, + "y": 1854, + "w": 12, + "h": 30 + }, + "flpam5": { + "x": 639, + "y": 1854, + "w": 9, + "h": 21 + }, + "flpamu": { + "x": 650, + "y": 1854, + "w": 28, + "h": 10 + }, + "flpaxe": { + "x": 680, + "y": 1854, + "w": 24, + "h": 18 + }, + "flpaxf": { + "x": 706, + "y": 1854, + "w": 11, + "h": 6 + }, + "flpba1": { + "x": 719, + "y": 1854, + "w": 15, + "h": 14 + }, + "flpba3": { + "x": 736, + "y": 1854, + "w": 11, + "h": 19 + }, + "flpba5": { + "x": 749, + "y": 1854, + "w": 17, + "h": 32 + }, + "flpbal": { + "x": 768, + "y": 1854, + "w": 22, + "h": 14 + }, + "flpbar": { + "x": 792, + "y": 1854, + "w": 48, + "h": 20 + }, + "flpbbb": { + "x": 842, + "y": 1854, + "w": 19, + "h": 14 + }, + "flpbbk": { + "x": 863, + "y": 1854, + "w": 17, + "h": 11 + }, + "flpbhm": { + "x": 882, + "y": 1854, + "w": 16, + "h": 15 + }, + "flpbkf": { + "x": 900, + "y": 1854, + "w": 15, + "h": 5 + }, + "flpbld": { + "x": 917, + "y": 1854, + "w": 12, + "h": 12 + }, + "flpbox": { + "x": 931, + "y": 1854, + "w": 8, + "h": 8 + }, + "flpbpl": { + "x": 941, + "y": 1854, + "w": 12, + "h": 16 + }, + "flpbps": { + "x": 955, + "y": 1854, + "w": 8, + "h": 14 + }, + "flpbrn": { + "x": 965, + "y": 1854, + "w": 43, + "h": 42 + }, + "flpbrnz": { + "x": 1010, + "y": 1854, + "w": 11, + "h": 7 + }, + "flpbrs": { + "x": 0, + "y": 1940, + "w": 25, + "h": 20 + }, + "flpbrx": { + "x": 27, + "y": 1940, + "w": 15, + "h": 17 + }, + "flpbsc": { + "x": 44, + "y": 1940, + "w": 16, + "h": 14 + }, + "flpbsd": { + "x": 62, + "y": 1940, + "w": 31, + "h": 16 + }, + "flpbsh": { + "x": 95, + "y": 1940, + "w": 30, + "h": 26 + }, + "flpbst": { + "x": 127, + "y": 1940, + "w": 69, + "h": 14 + }, + "flpbsw": { + "x": 198, + "y": 1940, + "w": 26, + "h": 25 + }, + "flpbtx": { + "x": 226, + "y": 1940, + "w": 38, + "h": 21 + }, + "flpbuc": { + "x": 266, + "y": 1940, + "w": 18, + "h": 9 + }, + "flpbwn": { + "x": 286, + "y": 1940, + "w": 21, + "h": 11 + }, + "flpcap": { + "x": 309, + "y": 1940, + "w": 11, + "h": 8 + }, + "flpcbw": { + "x": 322, + "y": 1940, + "w": 21, + "h": 13 + }, + "flpchm1": { + "x": 345, + "y": 1940, + "w": 13, + "h": 5 + }, + "flpchm2": { + "x": 360, + "y": 1940, + "w": 13, + "h": 6 + }, + "flpchm3": { + "x": 375, + "y": 1940, + "w": 21, + "h": 6 + }, + "flpchn": { + "x": 398, + "y": 1940, + "w": 35, + "h": 20 + }, + "flpci0": { + "x": 435, + "y": 1940, + "w": 9, + "h": 7 + }, + "flpci1": { + "x": 446, + "y": 1940, + "w": 9, + "h": 6 + }, + "flpci2": { + "x": 457, + "y": 1940, + "w": 8, + "h": 6 + }, + "flpclb": { + "x": 467, + "y": 1940, + "w": 33, + "h": 12 + }, + "flpclm": { + "x": 502, + "y": 1940, + "w": 24, + "h": 28 + }, + "flpclw": { + "x": 528, + "y": 1940, + "w": 14, + "h": 11 + }, + "flpcrn": { + "x": 544, + "y": 1940, + "w": 17, + "h": 13 + }, + "flpcrs": { + "x": 563, + "y": 1940, + "w": 28, + "h": 15 + }, + "flpcst": { + "x": 593, + "y": 1940, + "w": 20, + "h": 30 + }, + "flpd33": { + "x": 615, + "y": 1940, + "w": 13, + "h": 10 + }, + "flpdgr": { + "x": 630, + "y": 1940, + "w": 7, + "h": 11 + }, + "flpdir": { + "x": 639, + "y": 1940, + "w": 18, + "h": 4 + }, + "flpdr1": { + "x": 659, + "y": 1940, + "w": 12, + "h": 13 + }, + "flpdr3": { + "x": 673, + "y": 1940, + "w": 21, + "h": 20 + }, + "flpdr4": { + "x": 696, + "y": 1940, + "w": 14, + "h": 21 + }, + "flpear": { + "x": 712, + "y": 1940, + "w": 6, + "h": 4 + }, + "flpeye": { + "x": 720, + "y": 1940, + "w": 7, + "h": 4 + }, + "flpfang": { + "x": 729, + "y": 1940, + "w": 8, + "h": 5 + }, + "flpfhl": { + "x": 739, + "y": 1940, + "w": 17, + "h": 9 + }, + "flpfla": { + "x": 758, + "y": 1940, + "w": 37, + "h": 11 + }, + "flpflag": { + "x": 797, + "y": 1940, + "w": 60, + "h": 29 + }, + "flpflb": { + "x": 859, + "y": 1940, + "w": 54, + "h": 18 + }, + "flpflc": { + "x": 915, + "y": 1940, + "w": 38, + "h": 5 + }, + "flpfld": { + "x": 955, + "y": 1940, + "w": 31, + "h": 23 + }, + "flpful": { + "x": 988, + "y": 1940, + "w": 35, + "h": 30 + }, + "flpg33": { + "x": 0, + "y": 1972, + "w": 13, + "h": 10 + }, + "flpgax": { + "x": 15, + "y": 1972, + "w": 45, + "h": 17 + }, + "flpgbi": { + "x": 62, + "y": 1972, + "w": 13, + "h": 25 + }, + "flpghm": { + "x": 77, + "y": 1972, + "w": 25, + "h": 12 + }, + "flpgis": { + "x": 104, + "y": 1972, + "w": 51, + "h": 16 + }, + "flpgix": { + "x": 157, + "y": 1972, + "w": 48, + "h": 19 + }, + "flpgld_0": { + "x": 207, + "y": 1972, + "w": 24, + "h": 7 + }, + "flpgld_1": { + "x": 233, + "y": 1972, + "w": 26, + "h": 13 + }, + "flpgld_2": { + "x": 261, + "y": 1972, + "w": 32, + "h": 20 + }, + "flpgld_3": { + "x": 295, + "y": 1972, + "w": 46, + "h": 23 + }, + "flpgld": { + "x": 207, + "y": 1972, + "w": 24, + "h": 7 + }, + "flpglv": { + "x": 343, + "y": 1972, + "w": 13, + "h": 39 + }, + "flpgma": { + "x": 358, + "y": 1972, + "w": 22, + "h": 22 + }, + "flpgpl": { + "x": 382, + "y": 1972, + "w": 12, + "h": 16 + }, + "flpgps": { + "x": 396, + "y": 1972, + "w": 8, + "h": 14 + }, + "flpgsb": { + "x": 406, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsc": { + "x": 413, + "y": 1972, + "w": 11, + "h": 19 + }, + "flpgsd": { + "x": 426, + "y": 1972, + "w": 47, + "h": 20 + }, + "flpgsg": { + "x": 475, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsr": { + "x": 482, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsv": { + "x": 489, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsw": { + "x": 496, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsy": { + "x": 503, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgth": { + "x": 510, + "y": 1972, + "w": 45, + "h": 19 + }, + "flpgts": { + "x": 557, + "y": 1972, + "w": 36, + "h": 24 + }, + "flpgwn": { + "x": 595, + "y": 1972, + "w": 18, + "h": 15 + }, + "flphal": { + "x": 615, + "y": 1972, + "w": 84, + "h": 17 + }, + "flphax": { + "x": 701, + "y": 1972, + "w": 9, + "h": 13 + }, + "flphbl": { + "x": 712, + "y": 1972, + "w": 12, + "h": 9 + }, + "flphbt": { + "x": 726, + "y": 1972, + "w": 19, + "h": 17 + }, + "flphbw": { + "x": 747, + "y": 1972, + "w": 29, + "h": 9 + }, + "flphgl": { + "x": 778, + "y": 1972, + "w": 14, + "h": 12 + }, + "flphla": { + "x": 794, + "y": 1972, + "w": 24, + "h": 19 + }, + "flphlm": { + "x": 820, + "y": 1972, + "w": 16, + "h": 11 + }, + "flphmr": { + "x": 838, + "y": 1972, + "w": 13, + "h": 9 + }, + "flphorn": { + "x": 853, + "y": 1972, + "w": 13, + "h": 10 + }, + "flphrb": { + "x": 868, + "y": 1972, + "w": 7, + "h": 12 + }, + "flphrt": { + "x": 877, + "y": 1972, + "w": 5, + "h": 6 + }, + "flphscr": { + "x": 884, + "y": 1972, + "w": 30, + "h": 10 + }, + "flphst": { + "x": 916, + "y": 1972, + "w": 53, + "h": 31 + }, + "flphxb": { + "x": 971, + "y": 1972, + "w": 23, + "h": 15 + }, + "flpjav": { + "x": 0, + "y": 2013, + "w": 59, + "h": 26 + }, + "flpjaw": { + "x": 61, + "y": 2013, + "w": 11, + "h": 9 + }, + "flpjbi": { + "x": 74, + "y": 2013, + "w": 12, + "h": 15 + }, + "flpkey": { + "x": 88, + "y": 2013, + "w": 6, + "h": 5 + }, + "flpkit": { + "x": 96, + "y": 2013, + "w": 32, + "h": 16 + }, + "flpkrs": { + "x": 130, + "y": 2013, + "w": 29, + "h": 15 + }, + "flpktr": { + "x": 161, + "y": 2013, + "w": 11, + "h": 9 + }, + "flplax": { + "x": 174, + "y": 2013, + "w": 16, + "h": 20 + }, + "flplbb": { + "x": 192, + "y": 2013, + "w": 61, + "h": 8 + }, + "flplbl": { + "x": 255, + "y": 2013, + "w": 20, + "h": 12 + }, + "flplbt": { + "x": 277, + "y": 2013, + "w": 22, + "h": 13 + }, + "flplbw": { + "x": 301, + "y": 2013, + "w": 57, + "h": 10 + }, + "flplea": { + "x": 360, + "y": 2013, + "w": 25, + "h": 16 + }, + "flpleg": { + "x": 387, + "y": 2013, + "w": 13, + "h": 14 + }, + "flplgl": { + "x": 402, + "y": 2013, + "w": 17, + "h": 8 + }, + "flplrg": { + "x": 421, + "y": 2013, + "w": 23, + "h": 12 + }, + "flplsd": { + "x": 446, + "y": 2013, + "w": 45, + "h": 10 + }, + "flplst": { + "x": 493, + "y": 2013, + "w": 24, + "h": 40 + }, + "flpltp": { + "x": 519, + "y": 2013, + "w": 30, + "h": 21 + }, + "flplwb": { + "x": 551, + "y": 2013, + "w": 27, + "h": 28 + }, + "flplxb": { + "x": 580, + "y": 2013, + "w": 24, + "h": 14 + }, + "flpmac": { + "x": 606, + "y": 2013, + "w": 10, + "h": 19 + }, + "flpmau": { + "x": 618, + "y": 2013, + "w": 14, + "h": 26 + }, + "flpmbl": { + "x": 634, + "y": 2013, + "w": 12, + "h": 9 + }, + "flpmbt": { + "x": 648, + "y": 2013, + "w": 21, + "h": 20 + }, + "flpmgl": { + "x": 671, + "y": 2013, + "w": 23, + "h": 9 + }, + "flpmph": { + "x": 696, + "y": 2013, + "w": 5, + "h": 7 + }, + "flpmpi": { + "x": 703, + "y": 2013, + "w": 20, + "h": 18 + }, + "flpmsf": { + "x": 725, + "y": 2013, + "w": 12, + "h": 33 + }, + "flpmsk": { + "x": 739, + "y": 2013, + "w": 20, + "h": 10 + }, + "flpmss": { + "x": 761, + "y": 2013, + "w": 9, + "h": 4 + }, + "flpmst": { + "x": 772, + "y": 2013, + "w": 25, + "h": 9 + }, + "flpmxb": { + "x": 799, + "y": 2013, + "w": 21, + "h": 15 + }, + "flpne1": { + "x": 822, + "y": 2013, + "w": 21, + "h": 13 + }, + "flpne2": { + "x": 845, + "y": 2013, + "w": 13, + "h": 11 + }, + "flpne3": { + "x": 860, + "y": 2013, + "w": 15, + "h": 15 + }, + "flpnps": { + "x": 877, + "y": 2013, + "w": 5, + "h": 9 + }, + "flpob1": { + "x": 884, + "y": 2013, + "w": 16, + "h": 10 + }, + "flpob3": { + "x": 902, + "y": 2013, + "w": 19, + "h": 12 + }, + "flpob4": { + "x": 923, + "y": 2013, + "w": 17, + "h": 11 + }, + "flpob5": { + "x": 942, + "y": 2013, + "w": 30, + "h": 12 + }, + "flpopl": { + "x": 974, + "y": 2013, + "w": 12, + "h": 16 + }, + "flpops": { + "x": 988, + "y": 2013, + "w": 8, + "h": 14 + }, + "flppa1": { + "x": 998, + "y": 2013, + "w": 17, + "h": 9 + }, + "flppa3": { + "x": 0, + "y": 2055, + "w": 17, + "h": 15 + }, + "flppa5": { + "x": 19, + "y": 2055, + "w": 19, + "h": 10 + }, + "flppax": { + "x": 40, + "y": 2055, + "w": 57, + "h": 34 + }, + "flppik": { + "x": 99, + "y": 2055, + "w": 35, + "h": 55 + }, + "flppil": { + "x": 136, + "y": 2055, + "w": 53, + "h": 30 + }, + "flpplt": { + "x": 191, + "y": 2055, + "w": 31, + "h": 19 + }, + "flppot": { + "x": 224, + "y": 2055, + "w": 7, + "h": 12 + }, + "flpqlt": { + "x": 233, + "y": 2055, + "w": 37, + "h": 20 + }, + "flpquil": { + "x": 272, + "y": 2055, + "w": 5, + "h": 11 + }, + "flpqvr": { + "x": 279, + "y": 2055, + "w": 20, + "h": 16 + }, + "flprbk": { + "x": 301, + "y": 2055, + "w": 15, + "h": 12 + }, + "flprin": { + "x": 318, + "y": 2055, + "w": 5, + "h": 4 + }, + "flprng": { + "x": 325, + "y": 2055, + "w": 35, + "h": 17 + }, + "flprpl": { + "x": 362, + "y": 2055, + "w": 12, + "h": 16 + }, + "flprps": { + "x": 376, + "y": 2055, + "w": 8, + "h": 14 + }, + "flprsc": { + "x": 386, + "y": 2055, + "w": 17, + "h": 14 + }, + "flprun": { + "x": 405, + "y": 2055, + "w": 4, + "h": 4 + }, + "flprxb": { + "x": 411, + "y": 2055, + "w": 42, + "h": 25 + }, + "flpsbb": { + "x": 455, + "y": 2055, + "w": 45, + "h": 8 + }, + "flpsbr": { + "x": 502, + "y": 2055, + "w": 25, + "h": 14 + }, + "flpsbw": { + "x": 529, + "y": 2055, + "w": 41, + "h": 9 + }, + "flpscl": { + "x": 572, + "y": 2055, + "w": 28, + "h": 15 + }, + "flpscm": { + "x": 602, + "y": 2055, + "w": 31, + "h": 11 + }, + "flpscp": { + "x": 635, + "y": 2055, + "w": 11, + "h": 18 + }, + "flpscr": { + "x": 648, + "y": 2055, + "w": 24, + "h": 5 + }, + "flpscy": { + "x": 674, + "y": 2055, + "w": 59, + "h": 33 + }, + "flpskl": { + "x": 735, + "y": 2055, + "w": 6, + "h": 11 + }, + "flpskp": { + "x": 743, + "y": 2055, + "w": 11, + "h": 11 + }, + "flpskr": { + "x": 756, + "y": 2055, + "w": 15, + "h": 12 + }, + "flpsml": { + "x": 773, + "y": 2055, + "w": 20, + "h": 15 + }, + "flpspc": { + "x": 795, + "y": 2055, + "w": 13, + "h": 17 + }, + "flpspk": { + "x": 810, + "y": 2055, + "w": 29, + "h": 22 + }, + "flpspl": { + "x": 841, + "y": 2055, + "w": 25, + "h": 14 + }, + "flpsple": { + "x": 868, + "y": 2055, + "w": 10, + "h": 7 + }, + "flpspr": { + "x": 880, + "y": 2055, + "w": 33, + "h": 39 + }, + "flpspt": { + "x": 915, + "y": 2055, + "w": 39, + "h": 28 + }, + "flpssd": { + "x": 956, + "y": 2055, + "w": 27, + "h": 9 + }, + "flpssp": { + "x": 985, + "y": 2055, + "w": 22, + "h": 26 + }, + "flpsst": { + "x": 1009, + "y": 2055, + "w": 6, + "h": 29 + }, + "flpstu": { + "x": 0, + "y": 2112, + "w": 24, + "h": 15 + }, + "flpswb": { + "x": 26, + "y": 2112, + "w": 21, + "h": 19 + }, + "flptax": { + "x": 49, + "y": 2112, + "w": 23, + "h": 13 + }, + "flptbl": { + "x": 74, + "y": 2112, + "w": 15, + "h": 12 + }, + "flptbt": { + "x": 91, + "y": 2112, + "w": 22, + "h": 21 + }, + "flptgl": { + "x": 115, + "y": 2112, + "w": 21, + "h": 12 + }, + "flptkn": { + "x": 138, + "y": 2112, + "w": 11, + "h": 8 + }, + "flptow": { + "x": 151, + "y": 2112, + "w": 29, + "h": 21 + }, + "flptrch": { + "x": 182, + "y": 2112, + "w": 12, + "h": 12 + }, + "flptri": { + "x": 196, + "y": 2112, + "w": 40, + "h": 30 + }, + "flptsp": { + "x": 238, + "y": 2112, + "w": 43, + "h": 30 + }, + "flpvbl": { + "x": 283, + "y": 2112, + "w": 12, + "h": 9 + }, + "flpvbt": { + "x": 297, + "y": 2112, + "w": 16, + "h": 14 + }, + "flpvgl": { + "x": 315, + "y": 2112, + "w": 16, + "h": 10 + }, + "flpvou": { + "x": 333, + "y": 2112, + "w": 76, + "h": 12 + }, + "flpvpl": { + "x": 411, + "y": 2112, + "w": 12, + "h": 16 + }, + "flpvps": { + "x": 425, + "y": 2112, + "w": 8, + "h": 14 + }, + "flpwax": { + "x": 435, + "y": 2112, + "w": 27, + "h": 15 + }, + "flpwhm": { + "x": 464, + "y": 2112, + "w": 18, + "h": 16 + }, + "flpwnd": { + "x": 484, + "y": 2112, + "w": 22, + "h": 7 + }, + "flpwps": { + "x": 508, + "y": 2112, + "w": 5, + "h": 9 + }, + "flpwsc": { + "x": 515, + "y": 2112, + "w": 41, + "h": 33 + }, + "flpwsd": { + "x": 558, + "y": 2112, + "w": 35, + "h": 11 + }, + "flpwsp": { + "x": 595, + "y": 2112, + "w": 24, + "h": 21 + }, + "flpwst": { + "x": 621, + "y": 2112, + "w": 69, + "h": 20 + }, + "flpyps": { + "x": 692, + "y": 2112, + "w": 8, + "h": 14 + }, + "flpywn": { + "x": 702, + "y": 2112, + "w": 17, + "h": 13 + }, + "flpcap_cgrn": { + "x": 721, + "y": 2112, + "w": 11, + "h": 8 + }, + "flpgth_dpur": { + "x": 734, + "y": 2112, + "w": 45, + "h": 19 + }, + "flptgl_lgry": { + "x": 781, + "y": 2112, + "w": 21, + "h": 12 + }, + "flpvbl_blac": { + "x": 804, + "y": 2112, + "w": 12, + "h": 9 + }, + "flplbl_blac": { + "x": 818, + "y": 2112, + "w": 20, + "h": 12 + } + }, + "flippyRects": { + "flp2ax": { + "x": 496, + "y": 1854, + "w": 22, + "h": 12 + }, + "flp2hs": { + "x": 520, + "y": 1854, + "w": 37, + "h": 20 + }, + "flpaar": { + "x": 559, + "y": 1854, + "w": 29, + "h": 23 + }, + "flpam1": { + "x": 590, + "y": 1854, + "w": 12, + "h": 13 + }, + "flpam2": { + "x": 604, + "y": 1854, + "w": 19, + "h": 15 + }, + "flpam3": { + "x": 625, + "y": 1854, + "w": 12, + "h": 30 + }, + "flpam5": { + "x": 639, + "y": 1854, + "w": 9, + "h": 21 + }, + "flpamu": { + "x": 650, + "y": 1854, + "w": 28, + "h": 10 + }, + "flpaxe": { + "x": 680, + "y": 1854, + "w": 24, + "h": 18 + }, + "flpaxf": { + "x": 706, + "y": 1854, + "w": 11, + "h": 6 + }, + "flpba1": { + "x": 719, + "y": 1854, + "w": 15, + "h": 14 + }, + "flpba3": { + "x": 736, + "y": 1854, + "w": 11, + "h": 19 + }, + "flpba5": { + "x": 749, + "y": 1854, + "w": 17, + "h": 32 + }, + "flpbal": { + "x": 768, + "y": 1854, + "w": 22, + "h": 14 + }, + "flpbar": { + "x": 792, + "y": 1854, + "w": 48, + "h": 20 + }, + "flpbbb": { + "x": 842, + "y": 1854, + "w": 19, + "h": 14 + }, + "flpbbk": { + "x": 863, + "y": 1854, + "w": 17, + "h": 11 + }, + "flpbhm": { + "x": 882, + "y": 1854, + "w": 16, + "h": 15 + }, + "flpbkf": { + "x": 900, + "y": 1854, + "w": 15, + "h": 5 + }, + "flpbld": { + "x": 917, + "y": 1854, + "w": 12, + "h": 12 + }, + "flpbox": { + "x": 931, + "y": 1854, + "w": 8, + "h": 8 + }, + "flpbpl": { + "x": 941, + "y": 1854, + "w": 12, + "h": 16 + }, + "flpbps": { + "x": 955, + "y": 1854, + "w": 8, + "h": 14 + }, + "flpbrn": { + "x": 965, + "y": 1854, + "w": 43, + "h": 42 + }, + "flpbrnz": { + "x": 1010, + "y": 1854, + "w": 11, + "h": 7 + }, + "flpbrs": { + "x": 0, + "y": 1940, + "w": 25, + "h": 20 + }, + "flpbrx": { + "x": 27, + "y": 1940, + "w": 15, + "h": 17 + }, + "flpbsc": { + "x": 44, + "y": 1940, + "w": 16, + "h": 14 + }, + "flpbsd": { + "x": 62, + "y": 1940, + "w": 31, + "h": 16 + }, + "flpbsh": { + "x": 95, + "y": 1940, + "w": 30, + "h": 26 + }, + "flpbst": { + "x": 127, + "y": 1940, + "w": 69, + "h": 14 + }, + "flpbsw": { + "x": 198, + "y": 1940, + "w": 26, + "h": 25 + }, + "flpbtx": { + "x": 226, + "y": 1940, + "w": 38, + "h": 21 + }, + "flpbuc": { + "x": 266, + "y": 1940, + "w": 18, + "h": 9 + }, + "flpbwn": { + "x": 286, + "y": 1940, + "w": 21, + "h": 11 + }, + "flpcap": { + "x": 309, + "y": 1940, + "w": 11, + "h": 8 + }, + "flpcbw": { + "x": 322, + "y": 1940, + "w": 21, + "h": 13 + }, + "flpchm1": { + "x": 345, + "y": 1940, + "w": 13, + "h": 5 + }, + "flpchm2": { + "x": 360, + "y": 1940, + "w": 13, + "h": 6 + }, + "flpchm3": { + "x": 375, + "y": 1940, + "w": 21, + "h": 6 + }, + "flpchn": { + "x": 398, + "y": 1940, + "w": 35, + "h": 20 + }, + "flpci0": { + "x": 435, + "y": 1940, + "w": 9, + "h": 7 + }, + "flpci1": { + "x": 446, + "y": 1940, + "w": 9, + "h": 6 + }, + "flpci2": { + "x": 457, + "y": 1940, + "w": 8, + "h": 6 + }, + "flpclb": { + "x": 467, + "y": 1940, + "w": 33, + "h": 12 + }, + "flpclm": { + "x": 502, + "y": 1940, + "w": 24, + "h": 28 + }, + "flpclw": { + "x": 528, + "y": 1940, + "w": 14, + "h": 11 + }, + "flpcrn": { + "x": 544, + "y": 1940, + "w": 17, + "h": 13 + }, + "flpcrs": { + "x": 563, + "y": 1940, + "w": 28, + "h": 15 + }, + "flpcst": { + "x": 593, + "y": 1940, + "w": 20, + "h": 30 + }, + "flpd33": { + "x": 615, + "y": 1940, + "w": 13, + "h": 10 + }, + "flpdgr": { + "x": 630, + "y": 1940, + "w": 7, + "h": 11 + }, + "flpdir": { + "x": 639, + "y": 1940, + "w": 18, + "h": 4 + }, + "flpdr1": { + "x": 659, + "y": 1940, + "w": 12, + "h": 13 + }, + "flpdr3": { + "x": 673, + "y": 1940, + "w": 21, + "h": 20 + }, + "flpdr4": { + "x": 696, + "y": 1940, + "w": 14, + "h": 21 + }, + "flpear": { + "x": 712, + "y": 1940, + "w": 6, + "h": 4 + }, + "flpeye": { + "x": 720, + "y": 1940, + "w": 7, + "h": 4 + }, + "flpfang": { + "x": 729, + "y": 1940, + "w": 8, + "h": 5 + }, + "flpfhl": { + "x": 739, + "y": 1940, + "w": 17, + "h": 9 + }, + "flpfla": { + "x": 758, + "y": 1940, + "w": 37, + "h": 11 + }, + "flpflag": { + "x": 797, + "y": 1940, + "w": 60, + "h": 29 + }, + "flpflb": { + "x": 859, + "y": 1940, + "w": 54, + "h": 18 + }, + "flpflc": { + "x": 915, + "y": 1940, + "w": 38, + "h": 5 + }, + "flpfld": { + "x": 955, + "y": 1940, + "w": 31, + "h": 23 + }, + "flpful": { + "x": 988, + "y": 1940, + "w": 35, + "h": 30 + }, + "flpg33": { + "x": 0, + "y": 1972, + "w": 13, + "h": 10 + }, + "flpgax": { + "x": 15, + "y": 1972, + "w": 45, + "h": 17 + }, + "flpgbi": { + "x": 62, + "y": 1972, + "w": 13, + "h": 25 + }, + "flpghm": { + "x": 77, + "y": 1972, + "w": 25, + "h": 12 + }, + "flpgis": { + "x": 104, + "y": 1972, + "w": 51, + "h": 16 + }, + "flpgix": { + "x": 157, + "y": 1972, + "w": 48, + "h": 19 + }, + "flpgld_0": { + "x": 207, + "y": 1972, + "w": 24, + "h": 7 + }, + "flpgld_1": { + "x": 233, + "y": 1972, + "w": 26, + "h": 13 + }, + "flpgld_2": { + "x": 261, + "y": 1972, + "w": 32, + "h": 20 + }, + "flpgld_3": { + "x": 295, + "y": 1972, + "w": 46, + "h": 23 + }, + "flpgld": { + "x": 207, + "y": 1972, + "w": 24, + "h": 7 + }, + "flpglv": { + "x": 343, + "y": 1972, + "w": 13, + "h": 39 + }, + "flpgma": { + "x": 358, + "y": 1972, + "w": 22, + "h": 22 + }, + "flpgpl": { + "x": 382, + "y": 1972, + "w": 12, + "h": 16 + }, + "flpgps": { + "x": 396, + "y": 1972, + "w": 8, + "h": 14 + }, + "flpgsb": { + "x": 406, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsc": { + "x": 413, + "y": 1972, + "w": 11, + "h": 19 + }, + "flpgsd": { + "x": 426, + "y": 1972, + "w": 47, + "h": 20 + }, + "flpgsg": { + "x": 475, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsr": { + "x": 482, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsv": { + "x": 489, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsw": { + "x": 496, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgsy": { + "x": 503, + "y": 1972, + "w": 5, + "h": 5 + }, + "flpgth": { + "x": 510, + "y": 1972, + "w": 45, + "h": 19 + }, + "flpgts": { + "x": 557, + "y": 1972, + "w": 36, + "h": 24 + }, + "flpgwn": { + "x": 595, + "y": 1972, + "w": 18, + "h": 15 + }, + "flphal": { + "x": 615, + "y": 1972, + "w": 84, + "h": 17 + }, + "flphax": { + "x": 701, + "y": 1972, + "w": 9, + "h": 13 + }, + "flphbl": { + "x": 712, + "y": 1972, + "w": 12, + "h": 9 + }, + "flphbt": { + "x": 726, + "y": 1972, + "w": 19, + "h": 17 + }, + "flphbw": { + "x": 747, + "y": 1972, + "w": 29, + "h": 9 + }, + "flphgl": { + "x": 778, + "y": 1972, + "w": 14, + "h": 12 + }, + "flphla": { + "x": 794, + "y": 1972, + "w": 24, + "h": 19 + }, + "flphlm": { + "x": 820, + "y": 1972, + "w": 16, + "h": 11 + }, + "flphmr": { + "x": 838, + "y": 1972, + "w": 13, + "h": 9 + }, + "flphorn": { + "x": 853, + "y": 1972, + "w": 13, + "h": 10 + }, + "flphrb": { + "x": 868, + "y": 1972, + "w": 7, + "h": 12 + }, + "flphrt": { + "x": 877, + "y": 1972, + "w": 5, + "h": 6 + }, + "flphscr": { + "x": 884, + "y": 1972, + "w": 30, + "h": 10 + }, + "flphst": { + "x": 916, + "y": 1972, + "w": 53, + "h": 31 + }, + "flphxb": { + "x": 971, + "y": 1972, + "w": 23, + "h": 15 + }, + "flpjav": { + "x": 0, + "y": 2013, + "w": 59, + "h": 26 + }, + "flpjaw": { + "x": 61, + "y": 2013, + "w": 11, + "h": 9 + }, + "flpjbi": { + "x": 74, + "y": 2013, + "w": 12, + "h": 15 + }, + "flpkey": { + "x": 88, + "y": 2013, + "w": 6, + "h": 5 + }, + "flpkit": { + "x": 96, + "y": 2013, + "w": 32, + "h": 16 + }, + "flpkrs": { + "x": 130, + "y": 2013, + "w": 29, + "h": 15 + }, + "flpktr": { + "x": 161, + "y": 2013, + "w": 11, + "h": 9 + }, + "flplax": { + "x": 174, + "y": 2013, + "w": 16, + "h": 20 + }, + "flplbb": { + "x": 192, + "y": 2013, + "w": 61, + "h": 8 + }, + "flplbl": { + "x": 255, + "y": 2013, + "w": 20, + "h": 12 + }, + "flplbt": { + "x": 277, + "y": 2013, + "w": 22, + "h": 13 + }, + "flplbw": { + "x": 301, + "y": 2013, + "w": 57, + "h": 10 + }, + "flplea": { + "x": 360, + "y": 2013, + "w": 25, + "h": 16 + }, + "flpleg": { + "x": 387, + "y": 2013, + "w": 13, + "h": 14 + }, + "flplgl": { + "x": 402, + "y": 2013, + "w": 17, + "h": 8 + }, + "flplrg": { + "x": 421, + "y": 2013, + "w": 23, + "h": 12 + }, + "flplsd": { + "x": 446, + "y": 2013, + "w": 45, + "h": 10 + }, + "flplst": { + "x": 493, + "y": 2013, + "w": 24, + "h": 40 + }, + "flpltp": { + "x": 519, + "y": 2013, + "w": 30, + "h": 21 + }, + "flplwb": { + "x": 551, + "y": 2013, + "w": 27, + "h": 28 + }, + "flplxb": { + "x": 580, + "y": 2013, + "w": 24, + "h": 14 + }, + "flpmac": { + "x": 606, + "y": 2013, + "w": 10, + "h": 19 + }, + "flpmau": { + "x": 618, + "y": 2013, + "w": 14, + "h": 26 + }, + "flpmbl": { + "x": 634, + "y": 2013, + "w": 12, + "h": 9 + }, + "flpmbt": { + "x": 648, + "y": 2013, + "w": 21, + "h": 20 + }, + "flpmgl": { + "x": 671, + "y": 2013, + "w": 23, + "h": 9 + }, + "flpmph": { + "x": 696, + "y": 2013, + "w": 5, + "h": 7 + }, + "flpmpi": { + "x": 703, + "y": 2013, + "w": 20, + "h": 18 + }, + "flpmsf": { + "x": 725, + "y": 2013, + "w": 12, + "h": 33 + }, + "flpmsk": { + "x": 739, + "y": 2013, + "w": 20, + "h": 10 + }, + "flpmss": { + "x": 761, + "y": 2013, + "w": 9, + "h": 4 + }, + "flpmst": { + "x": 772, + "y": 2013, + "w": 25, + "h": 9 + }, + "flpmxb": { + "x": 799, + "y": 2013, + "w": 21, + "h": 15 + }, + "flpne1": { + "x": 822, + "y": 2013, + "w": 21, + "h": 13 + }, + "flpne2": { + "x": 845, + "y": 2013, + "w": 13, + "h": 11 + }, + "flpne3": { + "x": 860, + "y": 2013, + "w": 15, + "h": 15 + }, + "flpnps": { + "x": 877, + "y": 2013, + "w": 5, + "h": 9 + }, + "flpob1": { + "x": 884, + "y": 2013, + "w": 16, + "h": 10 + }, + "flpob3": { + "x": 902, + "y": 2013, + "w": 19, + "h": 12 + }, + "flpob4": { + "x": 923, + "y": 2013, + "w": 17, + "h": 11 + }, + "flpob5": { + "x": 942, + "y": 2013, + "w": 30, + "h": 12 + }, + "flpopl": { + "x": 974, + "y": 2013, + "w": 12, + "h": 16 + }, + "flpops": { + "x": 988, + "y": 2013, + "w": 8, + "h": 14 + }, + "flppa1": { + "x": 998, + "y": 2013, + "w": 17, + "h": 9 + }, + "flppa3": { + "x": 0, + "y": 2055, + "w": 17, + "h": 15 + }, + "flppa5": { + "x": 19, + "y": 2055, + "w": 19, + "h": 10 + }, + "flppax": { + "x": 40, + "y": 2055, + "w": 57, + "h": 34 + }, + "flppik": { + "x": 99, + "y": 2055, + "w": 35, + "h": 55 + }, + "flppil": { + "x": 136, + "y": 2055, + "w": 53, + "h": 30 + }, + "flpplt": { + "x": 191, + "y": 2055, + "w": 31, + "h": 19 + }, + "flppot": { + "x": 224, + "y": 2055, + "w": 7, + "h": 12 + }, + "flpqlt": { + "x": 233, + "y": 2055, + "w": 37, + "h": 20 + }, + "flpquil": { + "x": 272, + "y": 2055, + "w": 5, + "h": 11 + }, + "flpqvr": { + "x": 279, + "y": 2055, + "w": 20, + "h": 16 + }, + "flprbk": { + "x": 301, + "y": 2055, + "w": 15, + "h": 12 + }, + "flprin": { + "x": 318, + "y": 2055, + "w": 5, + "h": 4 + }, + "flprng": { + "x": 325, + "y": 2055, + "w": 35, + "h": 17 + }, + "flprpl": { + "x": 362, + "y": 2055, + "w": 12, + "h": 16 + }, + "flprps": { + "x": 376, + "y": 2055, + "w": 8, + "h": 14 + }, + "flprsc": { + "x": 386, + "y": 2055, + "w": 17, + "h": 14 + }, + "flprun": { + "x": 405, + "y": 2055, + "w": 4, + "h": 4 + }, + "flprxb": { + "x": 411, + "y": 2055, + "w": 42, + "h": 25 + }, + "flpsbb": { + "x": 455, + "y": 2055, + "w": 45, + "h": 8 + }, + "flpsbr": { + "x": 502, + "y": 2055, + "w": 25, + "h": 14 + }, + "flpsbw": { + "x": 529, + "y": 2055, + "w": 41, + "h": 9 + }, + "flpscl": { + "x": 572, + "y": 2055, + "w": 28, + "h": 15 + }, + "flpscm": { + "x": 602, + "y": 2055, + "w": 31, + "h": 11 + }, + "flpscp": { + "x": 635, + "y": 2055, + "w": 11, + "h": 18 + }, + "flpscr": { + "x": 648, + "y": 2055, + "w": 24, + "h": 5 + }, + "flpscy": { + "x": 674, + "y": 2055, + "w": 59, + "h": 33 + }, + "flpskl": { + "x": 735, + "y": 2055, + "w": 6, + "h": 11 + }, + "flpskp": { + "x": 743, + "y": 2055, + "w": 11, + "h": 11 + }, + "flpskr": { + "x": 756, + "y": 2055, + "w": 15, + "h": 12 + }, + "flpsml": { + "x": 773, + "y": 2055, + "w": 20, + "h": 15 + }, + "flpspc": { + "x": 795, + "y": 2055, + "w": 13, + "h": 17 + }, + "flpspk": { + "x": 810, + "y": 2055, + "w": 29, + "h": 22 + }, + "flpspl": { + "x": 841, + "y": 2055, + "w": 25, + "h": 14 + }, + "flpsple": { + "x": 868, + "y": 2055, + "w": 10, + "h": 7 + }, + "flpspr": { + "x": 880, + "y": 2055, + "w": 33, + "h": 39 + }, + "flpspt": { + "x": 915, + "y": 2055, + "w": 39, + "h": 28 + }, + "flpssd": { + "x": 956, + "y": 2055, + "w": 27, + "h": 9 + }, + "flpssp": { + "x": 985, + "y": 2055, + "w": 22, + "h": 26 + }, + "flpsst": { + "x": 1009, + "y": 2055, + "w": 6, + "h": 29 + }, + "flpstu": { + "x": 0, + "y": 2112, + "w": 24, + "h": 15 + }, + "flpswb": { + "x": 26, + "y": 2112, + "w": 21, + "h": 19 + }, + "flptax": { + "x": 49, + "y": 2112, + "w": 23, + "h": 13 + }, + "flptbl": { + "x": 74, + "y": 2112, + "w": 15, + "h": 12 + }, + "flptbt": { + "x": 91, + "y": 2112, + "w": 22, + "h": 21 + }, + "flptgl": { + "x": 115, + "y": 2112, + "w": 21, + "h": 12 + }, + "flptkn": { + "x": 138, + "y": 2112, + "w": 11, + "h": 8 + }, + "flptow": { + "x": 151, + "y": 2112, + "w": 29, + "h": 21 + }, + "flptrch": { + "x": 182, + "y": 2112, + "w": 12, + "h": 12 + }, + "flptri": { + "x": 196, + "y": 2112, + "w": 40, + "h": 30 + }, + "flptsp": { + "x": 238, + "y": 2112, + "w": 43, + "h": 30 + }, + "flpvbl": { + "x": 283, + "y": 2112, + "w": 12, + "h": 9 + }, + "flpvbt": { + "x": 297, + "y": 2112, + "w": 16, + "h": 14 + }, + "flpvgl": { + "x": 315, + "y": 2112, + "w": 16, + "h": 10 + }, + "flpvou": { + "x": 333, + "y": 2112, + "w": 76, + "h": 12 + }, + "flpvpl": { + "x": 411, + "y": 2112, + "w": 12, + "h": 16 + }, + "flpvps": { + "x": 425, + "y": 2112, + "w": 8, + "h": 14 + }, + "flpwax": { + "x": 435, + "y": 2112, + "w": 27, + "h": 15 + }, + "flpwhm": { + "x": 464, + "y": 2112, + "w": 18, + "h": 16 + }, + "flpwnd": { + "x": 484, + "y": 2112, + "w": 22, + "h": 7 + }, + "flpwps": { + "x": 508, + "y": 2112, + "w": 5, + "h": 9 + }, + "flpwsc": { + "x": 515, + "y": 2112, + "w": 41, + "h": 33 + }, + "flpwsd": { + "x": 558, + "y": 2112, + "w": 35, + "h": 11 + }, + "flpwsp": { + "x": 595, + "y": 2112, + "w": 24, + "h": 21 + }, + "flpwst": { + "x": 621, + "y": 2112, + "w": 69, + "h": 20 + }, + "flpyps": { + "x": 692, + "y": 2112, + "w": 8, + "h": 14 + }, + "flpywn": { + "x": 702, + "y": 2112, + "w": 17, + "h": 13 + }, + "flpcap_cgrn": { + "x": 721, + "y": 2112, + "w": 11, + "h": 8 + }, + "flpgth_dpur": { + "x": 734, + "y": 2112, + "w": 45, + "h": 19 + }, + "flptgl_lgry": { + "x": 781, + "y": 2112, + "w": 21, + "h": 12 + }, + "flpvbl_blac": { + "x": 804, + "y": 2112, + "w": 12, + "h": 9 + }, + "flplbl_blac": { + "x": 818, + "y": 2112, + "w": 20, + "h": 12 } }, "questRects": { diff --git a/public/ui/skill-tree-tabs.png b/public/ui/skill-tree-tabs.png index 3645dbc..be05ef4 100644 Binary files a/public/ui/skill-tree-tabs.png and b/public/ui/skill-tree-tabs.png differ diff --git a/scripts/pack-ui.ts b/scripts/pack-ui.ts index eb70449..6920195 100644 --- a/scripts/pack-ui.ts +++ b/scripts/pack-ui.ts @@ -467,10 +467,12 @@ async function main(): Promise { } writeFileSync(join(fontsDir, 'metrics.json'), JSON.stringify(fontMetas, null, 2)) - // --- STAGE 2: Comprehensive Items & Unique Items Packing --- - console.log('Extracting item sprites (including uniqueitems.txt & setitems.txt)...') + // --- STAGE 2: Comprehensive Items, Flippy Ground Sprites & Unique Items Packing --- + console.log('Extracting item sprites & flippy ground sprites (including uniqueitems.txt & setitems.txt)...') const codeToInvFile: Record = {} const uniqueInvFiles = new Set() + const codeToFlippyFile: Record = {} + const uniqueFlippyFiles = new Set() // A. Base Items from weapons.txt, armor.txt, misc.txt for (const t of ['weapons.txt', 'armor.txt', 'misc.txt']) { @@ -488,6 +490,12 @@ async function main(): Promise { uniqueInvFiles.add(extra) } } + + const flp = r['flippyfile']?.trim().toLowerCase() + if (code && flp && archives.has(`data/global/items/${flp}.dc6`)) { + codeToFlippyFile[code] = flp + uniqueFlippyFiles.add(flp) + } } } @@ -503,6 +511,13 @@ async function main(): Promise { if (idx && inv && archives.has(`data/global/items/${inv}.dc6`)) { codeToInvFile[idx] = inv } + const flp = r['flippyfile']?.trim().toLowerCase() + if (flp && archives.has(`data/global/items/${flp}.dc6`)) { + uniqueFlippyFiles.add(flp) + } + if (idx && flp && archives.has(`data/global/items/${flp}.dc6`)) { + codeToFlippyFile[idx] = flp + } } } @@ -518,6 +533,13 @@ async function main(): Promise { if (idx && inv && archives.has(`data/global/items/${inv}.dc6`)) { codeToInvFile[idx] = inv } + const flp = r['flippyfile']?.trim().toLowerCase() + if (flp && archives.has(`data/global/items/${flp}.dc6`)) { + uniqueFlippyFiles.add(flp) + } + if (idx && flp && archives.has(`data/global/items/${flp}.dc6`)) { + codeToFlippyFile[idx] = flp + } } } @@ -583,6 +605,33 @@ async function main(): Promise { codeToInvFile['Hellfire Torch'] = 'invtrch' } + // Explicit flippy mappings + codeToFlippyFile['gld'] = 'flpgld' + codeToFlippyFile['gold'] = 'flpgld' + codeToFlippyFile['box'] = 'flpbox' + codeToFlippyFile['Horadric Cube'] = 'flpbox' + codeToFlippyFile['cm1'] = 'flpmss' + codeToFlippyFile['cm2'] = 'flptrch' + codeToFlippyFile['cm3'] = 'flpmss' + codeToFlippyFile['gheeds'] = 'flpmss' + codeToFlippyFile["Gheed's Fortune"] = 'flpmss' + codeToFlippyFile['anni'] = 'flpmss' + codeToFlippyFile['Annihilus'] = 'flpmss' + codeToFlippyFile['torch'] = 'flptrch' + codeToFlippyFile['Hellfire Torch'] = 'flptrch' + + // Transformed items flippy mapping + codeToFlippyFile['Harlequin Crest'] = 'flpcap_cgrn' + codeToFlippyFile["Tal Rasha's Guardianship"] = 'flpgth_dpur' + codeToFlippyFile["Tal Rasha's Howling Wind"] = 'flpgth_dpur' + codeToFlippyFile['Magefist'] = 'flptgl_lgry' + codeToFlippyFile['Arachnid Mesh'] = 'flpvbl_blac' + + // Identity mapping for every flippyfile: codeToFlippyFile[flp] = flp + for (const flp of uniqueFlippyFiles) { + codeToFlippyFile[flp] = flp + } + // Register transformed item lookups in codeToInvFile codeToInvFile['Harlequin Crest'] = 'invcap_cgrn' codeToInvFile["Tal Rasha's Guardianship"] = 'invgth_dpur' @@ -604,6 +653,7 @@ async function main(): Promise { let curY = 0 let rowH = 0 const itemRects: Record = {} + const flippyRects: Record = {} const itemPlacements: BlitPlacement[] = [] const decodedBaseFrames = new Map() @@ -672,6 +722,96 @@ async function main(): Promise { if (transformedFrame.height > rowH) rowH = transformedFrame.height } + // --- STAGE 2.5: Authentic Flippy Ground Item Sprites (213 DC6 files) --- + console.log('Packing authentic flippy ground item sprites...') + const sortedFlippyNames = [...uniqueFlippyFiles].sort() + const decodedFlippyBaseFrames = new Map() + + for (const flpName of sortedFlippyNames) { + const dc6 = await decodeUiDc6(`data/global/items/${flpName}.dc6`) + if (flpName === 'flpgld') { + // flpgld has 4 groups: 0=small pile, 1=medium pile, 2=large pile, 3=huge pile + for (let g = 0; g < dc6.groups.length; g++) { + const groupFrames = dc6.groups[g]?.frames + const lastFrame = groupFrames ? groupFrames[groupFrames.length - 1] : undefined + if (!lastFrame) continue + if (curX + lastFrame.width > ATLAS_W) { + curX = 0 + curY += rowH + 2 + rowH = 0 + } + const key = `flpgld_${g}` + const rect: SpriteRect = { x: curX, y: curY, w: lastFrame.width, h: lastFrame.height } + flippyRects[key] = rect + itemRects[key] = rect + itemPlacements.push({ frame: lastFrame, x: curX, y: curY }) + curX += lastFrame.width + 2 + if (lastFrame.height > rowH) rowH = lastFrame.height + } + if (flippyRects['flpgld_0']) { + flippyRects['flpgld'] = flippyRects['flpgld_0'] + itemRects['flpgld'] = flippyRects['flpgld_0'] + } + } else { + const groupFrames = dc6.groups[0]?.frames + const lastFrame = groupFrames ? groupFrames[groupFrames.length - 1] : undefined + if (!lastFrame) continue + decodedFlippyBaseFrames.set(flpName, lastFrame) + if (curX + lastFrame.width > ATLAS_W) { + curX = 0 + curY += rowH + 2 + rowH = 0 + } + const rect: SpriteRect = { x: curX, y: curY, w: lastFrame.width, h: lastFrame.height } + flippyRects[flpName] = rect + itemRects[flpName] = rect + itemPlacements.push({ frame: lastFrame, x: curX, y: curY }) + curX += lastFrame.width + 2 + if (lastFrame.height > rowH) rowH = lastFrame.height + } + } + + // Pre-bake and stitch transformed flippy frames + const TRANSFORMED_FLIPPY_ITEMS: ReadonlyArray<{ + baseFlp: string + transformedName: string + colorCode: 'cgrn' | 'dpur' | 'lgry' | 'blac' + }> = [ + { baseFlp: 'flpcap', transformedName: 'flpcap_cgrn', colorCode: 'cgrn' }, + { baseFlp: 'flpgth', transformedName: 'flpgth_dpur', colorCode: 'dpur' }, + { baseFlp: 'flptgl', transformedName: 'flptgl_lgry', colorCode: 'lgry' }, + { baseFlp: 'flpvbl', transformedName: 'flpvbl_blac', colorCode: 'blac' }, + { baseFlp: 'flplbl', transformedName: 'flplbl_blac', colorCode: 'blac' }, + ] + + for (const t of TRANSFORMED_FLIPPY_ITEMS) { + let baseFrame = decodedFlippyBaseFrames.get(t.baseFlp) + if (!baseFrame) { + if (archives.has(`data/global/items/${t.baseFlp}.dc6`)) { + const dc6 = await decodeUiDc6(`data/global/items/${t.baseFlp}.dc6`) + const groupFrames = dc6.groups[0]?.frames + baseFrame = groupFrames ? groupFrames[groupFrames.length - 1] : undefined + } + } + if (!baseFrame) continue + const tableIdx = COLOR_TABLE_MAP[t.colorCode] + if (tableIdx === undefined) continue + const table = pl2.hueVariations[tableIdx] + if (!table) continue + const transformedFrame = transformSpriteFrame(baseFrame, table) + if (curX + transformedFrame.width > ATLAS_W) { + curX = 0 + curY += rowH + 2 + rowH = 0 + } + const rect: SpriteRect = { x: curX, y: curY, w: transformedFrame.width, h: transformedFrame.height } + flippyRects[t.transformedName] = rect + itemRects[t.transformedName] = rect + itemPlacements.push({ frame: transformedFrame, x: curX, y: curY }) + curX += transformedFrame.width + 2 + if (transformedFrame.height > rowH) rowH = transformedFrame.height + } + const atlasH = Math.max(1, curY + rowH) savePng('items-atlas.png', ATLAS_W, atlasH, stitchFrames(ATLAS_W, atlasH, itemPlacements)) @@ -779,7 +919,9 @@ async function main(): Promise { questsAtlasWidth: QUEST_ATLAS_W, questsAtlasHeight: QUEST_ATLAS_H, codeToInvFile, + codeToFlippyFile, itemRects, + flippyRects, questRects, images: imagesMap, } @@ -814,7 +956,9 @@ export interface BakedUiManifest { readonly questsAtlasWidth: number readonly questsAtlasHeight: number readonly codeToInvFile: Record + readonly codeToFlippyFile: Record readonly itemRects: Record + readonly flippyRects: Record readonly questRects: Record readonly images: Record } @@ -876,7 +1020,9 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = { questsAtlasWidth: ${QUEST_ATLAS_W}, questsAtlasHeight: ${QUEST_ATLAS_H}, codeToInvFile: ${JSON.stringify(codeToInvFile, null, 2)}, + codeToFlippyFile: ${JSON.stringify(codeToFlippyFile, null, 2)}, itemRects: ${JSON.stringify(itemRects, null, 2)}, + flippyRects: ${JSON.stringify(flippyRects, null, 2)}, questRects: ${JSON.stringify(questRects, null, 2)}, images: ${JSON.stringify(imagesMap, null, 2)}, } diff --git a/scripts/png.ts b/scripts/png.ts index 1fe9ee2..a262590 100644 --- a/scripts/png.ts +++ b/scripts/png.ts @@ -70,39 +70,140 @@ export interface IndexedImage { readonly palette: Uint8Array /** Index treated as fully transparent, if any (usually 0). */ readonly transparentIndex?: number | undefined + /** Compression level for deflate (default 6). */ + readonly compressionLevel?: number | undefined } /** - * Apply a PNG row filter in place into a destination buffer. - * - * @param type - filter type (0 none, 1 sub, 2 up). - * @param row - the row's bytes. - * @param previous - the row above, or null for the first row. - * @param stride - bytes per pixel (1 for indexed). - * @param out - destination, `row.byteLength` bytes. + * Fast-path check: returns true if all bytes in the row are 0. + * Uses 32-bit word scanning when aligned to 4 bytes, falling back to byte scanning. */ -function filterRow(type: number, row: Uint8Array, previous: Uint8Array | null, stride: number, out: Uint8Array): void { - for (let i = 0; i < row.byteLength; i += 1) { - const raw = row[i]! - const left = i >= stride ? row[i - stride]! : 0 - const up = previous === null ? 0 : previous[i]! - const value = type === 0 ? raw : type === 1 ? raw - left : raw - up - out[i] = value & 0xff +function isAllZero(row: Uint8Array): boolean { + const len = row.byteLength + if ((row.byteOffset & 3) === 0) { + const len32 = len >>> 2 + const view32 = new Uint32Array(row.buffer, row.byteOffset, len32) + for (let i = 0; i < len32; i += 1) { + if (view32[i] !== 0) return false + } + for (let i = len32 << 2; i < len; i += 1) { + if (row[i] !== 0) return false + } + return true } + for (let i = 0; i < len; i += 1) { + if (row[i] !== 0) return false + } + return true } /** - * Sum of absolute differences after treating bytes as signed, the standard - * filter-choice heuristic: the filter that leaves the flattest rows compresses - * best. - * - * @param bytes - filtered row. - * @returns the score. + * Fast-path check: returns true if row is identical to previous row. + * Uses 32-bit word scanning when both rows are 4-byte aligned, falling back to byte scanning. */ -function score(bytes: Uint8Array): number { - let total = 0 - for (const byte of bytes) total += byte < 128 ? byte : 256 - byte - return total +function isSameRow(row: Uint8Array, prev: Uint8Array): boolean { + const len = row.byteLength + if ((row.byteOffset & 3) === 0 && (prev.byteOffset & 3) === 0) { + const len32 = len >>> 2 + const v1 = new Uint32Array(row.buffer, row.byteOffset, len32) + const v2 = new Uint32Array(prev.buffer, prev.byteOffset, len32) + for (let i = 0; i < len32; i += 1) { + if (v1[i] !== v2[i]) return false + } + for (let i = len32 << 2; i < len; i += 1) { + if (row[i] !== prev[i]) return false + } + return true + } + for (let i = 0; i < len; i += 1) { + if (row[i] !== prev[i]) return false + } + return true +} + +/** + * Filters image scanlines using adaptive filtering with short-circuit fast paths + * for completely transparent rows and rows identical to the previous row. + * + * @param pixels - pixel buffer. + * @param width - image width in pixels. + * @param height - image height in pixels. + * @param stride - bytes per pixel (1 for indexed, 4 for RGBA). + * @returns filtered scanlines ready for deflate. + */ +function filterScanlines(pixels: Uint8Array, width: number, height: number, stride: number): Uint8Array { + const rowBytes = width * stride + const raw = new Uint8Array((rowBytes + 1) * height) + const cand0 = new Uint8Array(rowBytes) + const cand1 = new Uint8Array(rowBytes) + let at = 0 + + for (let y = 0; y < height; y += 1) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes) + const previous = y === 0 ? null : pixels.subarray((y - 1) * rowBytes, y * rowBytes) + + // Fast-path 1: All zero row (empty / transparent) -> Filter 0 (None) + // The output buffer `raw` is already zero-initialized by Uint8Array allocation. + if (isAllZero(row)) { + raw[at] = 0 + at += rowBytes + 1 + continue + } + + // Fast-path 2: Identical to previous row -> Filter 2 (Up) produces all zeros + if (previous !== null && isSameRow(row, previous)) { + raw[at] = 2 + at += rowBytes + 1 + continue + } + + // Filter 0 (None) score: sum of absolute differences directly from row + let score0 = 0 + for (let i = 0; i < rowBytes; i += 1) { + const v = row[i]! + score0 += v < 128 ? v : 256 - v + } + + // Filter 1 (Sub): compute difference and score in one pass + let score1 = 0 + for (let i = 0; i < stride; i += 1) { + const v = row[i]! + cand0[i] = v + score1 += v < 128 ? v : 256 - v + } + for (let i = stride; i < rowBytes; i += 1) { + const diff = (row[i]! - row[i - stride]!) & 0xff + cand0[i] = diff + score1 += diff < 128 ? diff : 256 - diff + } + + // Filter 2 (Up): compute difference and score in one pass + let score2 = Number.POSITIVE_INFINITY + if (previous !== null) { + score2 = 0 + for (let i = 0; i < rowBytes; i += 1) { + const diff = (row[i]! - previous[i]!) & 0xff + cand1[i] = diff + score2 += diff < 128 ? diff : 256 - diff + } + } + + // Select the best filter and write into raw + if (score0 <= score1 && score0 <= score2) { + raw[at] = 0 + raw.set(row, at + 1) + } else if (score1 <= score2) { + raw[at] = 1 + raw.set(cand0, at + 1) + } else { + raw[at] = 2 + raw.set(cand1, at + 1) + } + + at += rowBytes + 1 + } + + return raw } /** @@ -140,27 +241,8 @@ export function encodeIndexedPng(image: IndexedImage): Uint8Array { trns.fill(255) if (image.transparentIndex !== undefined) trns[image.transparentIndex] = 0 - const stride = 1 - const raw = new Uint8Array((width * stride + 1) * height) - const candidate = new Uint8Array(width) - let at = 0 - for (let y = 0; y < height; y += 1) { - const row = pixels.subarray(y * width, (y + 1) * width) - const previous = y === 0 ? null : pixels.subarray((y - 1) * width, y * width) - let bestType = 0 - let bestScore = Number.POSITIVE_INFINITY - for (const type of [0, 1, 2]) { - filterRow(type, row, previous, stride, candidate) - const value = score(candidate) - if (value < bestScore) { bestScore = value; bestType = type } - } - filterRow(bestType, row, previous, stride, candidate) - raw[at] = bestType - raw.set(candidate, at + 1) - at += width + 1 - } - - const idat = new Uint8Array(deflateSync(raw, { level: 9 })) + const raw = filterScanlines(pixels, width, height, 1) + const idat = new Uint8Array(deflateSync(raw, { level: image.compressionLevel ?? 6 })) const parts = [SIGNATURE, chunk('IHDR', ihdr), chunk('PLTE', plte)] if (image.transparentIndex !== undefined) parts.push(chunk('tRNS', trns)) parts.push(chunk('IDAT', idat), chunk('IEND', new Uint8Array(0))) @@ -179,6 +261,8 @@ export interface RgbaImage { readonly height: number /** RGBA pixels, row-major, 4 bytes per pixel (`width * height * 4` bytes). */ readonly pixels: Uint8Array + /** Compression level for deflate (default 6). */ + readonly compressionLevel?: number | undefined } /** @@ -203,27 +287,8 @@ export function encodeRgbaPng(image: RgbaImage): Uint8Array { ihdr[11] = 0 // adaptive filtering ihdr[12] = 0 // no interlace - const stride = 4 - const raw = new Uint8Array((width * stride + 1) * height) - const candidate = new Uint8Array(width * stride) - let at = 0 - for (let y = 0; y < height; y += 1) { - const row = pixels.subarray(y * width * stride, (y + 1) * width * stride) - const previous = y === 0 ? null : pixels.subarray((y - 1) * width * stride, y * width * stride) - let bestType = 0 - let bestScore = Number.POSITIVE_INFINITY - for (const type of [0, 1, 2]) { - filterRow(type, row, previous, stride, candidate) - const value = score(candidate) - if (value < bestScore) { bestScore = value; bestType = type } - } - filterRow(bestType, row, previous, stride, candidate) - raw[at] = bestType - raw.set(candidate, at + 1) - at += width * stride + 1 - } - - const idat = new Uint8Array(deflateSync(raw, { level: 9 })) + const raw = filterScanlines(pixels, width, height, 4) + const idat = new Uint8Array(deflateSync(raw, { level: image.compressionLevel ?? 6 })) const parts = [SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', new Uint8Array(0))] const size = parts.reduce((sum, part) => sum + part.byteLength, 0) const out = new Uint8Array(size) diff --git a/src/game/engine.ts b/src/game/engine.ts index 1f343d6..8eb3c55 100644 --- a/src/game/engine.ts +++ b/src/game/engine.ts @@ -593,7 +593,7 @@ export class GameEngine { this.metrics.inventoryRefusals += 1 } else { this.ground.splice(bestIndex, 1) - const target = this.groundItems.findAtPoint(entry.x, entry.y, 10) + const target = this.groundItems.all.find(g => g.item === entry.item) ?? this.groundItems.findAtPoint(entry.x, entry.y, 48) if (target) this.groundItems.remove(target.id) this.metrics.pickups += 1 } diff --git a/src/scene/act-scene.ts b/src/scene/act-scene.ts index 61b95f9..5198303 100644 --- a/src/scene/act-scene.ts +++ b/src/scene/act-scene.ts @@ -54,7 +54,7 @@ import { resolveLadderCollisions, } from '../ui/ground-labels.ts' import { BAKED_UI_MANIFEST } from '../ui/baked-ui-meta.ts' -import { resolveItemSpriteRect } from '../ui/inventory.ts' +import { resolveItemSpriteRect, resolveGroundItemSpriteRect } from '../ui/inventory.ts' import type { AtlasFrame } from '../render/atlas.ts' import { SpriteRenderer } from '../render/renderer.ts' import type { AtlasHandle } from '../render/renderer.ts' @@ -3895,8 +3895,8 @@ export async function loadItemsAtlas( /** * Render a ground item entity with authentic Diablo II 1.13c appearance: * - Parabolic bounce height offset (when dropping or inventory full) - * - Authentic item sprite icon / scaled silhouette lying on the ground - * - Periodic glint/sparkle 4-point star animation + * - Authentic Blizzard \`flippyfile\` (\`flp*.dc6\`) ground item sprite resting on the 2:1 isometric plane + * - Ground contact oval shadow */ export function drawGroundItem( renderer: SpriteRenderer, @@ -3911,7 +3911,63 @@ export function drawGroundItem( ? GROUND_ITEM_QUALITY_COLORS.gold! : (GROUND_ITEM_QUALITY_COLORS[item.quality] ?? GROUND_ITEM_QUALITY_COLORS.normal!) - // 1. Gold Rendering + // 1. Authentic Blizzard flippy ground sprite resolution when items atlas is available + const target = item.isGold + ? { isGold: true, amount: item.amount, code: 'gld' } + : (item.item || (item as any)) + + const sr = (itemsAtlas !== undefined && (typeof (renderer as any).drawQuad === 'function' || typeof (renderer as any).draw === 'function')) + ? resolveGroundItemSpriteRect(target, BAKED_UI_MANIFEST.flippyRects, BAKED_UI_MANIFEST.codeToFlippyFile) + : null + + if (sr !== null && itemsAtlas !== undefined) { + // Authentic Diablo II ground item sprite icon: + // Natural sprite dimensions from flp*.dc6 (already rendered by Blizzard in 2:1 isometric perspective) + const baseW = sr.w + const baseH = sr.h + + // Drop shadow: stays grounded on floor (item.y - 2), alpha fades slightly with height + const shadowAlpha = bounceH > 0 ? Math.max(0.1, 0.38 * (1 - Math.min(1, bounceH / 45))) : 0.38 + const shadowW = Math.max(12, Math.round(baseW * 0.95)) + const shadowH = Math.max(4, Math.round(shadowW * 0.38)) + renderer.drawSolid(item.x - shadowW / 2, item.y - 2, shadowW, shadowH, [0, 0, 0, shadowAlpha]) + + const drawX = Math.round(item.x - baseW / 2) + const drawY = Math.round(baseY - baseH) + + const frame: AtlasFrame = { + x: sr.x, + y: sr.y, + width: sr.w, + height: sr.h, + } + + if (typeof (renderer as any).draw === 'function') { + renderer.draw(frame, drawX, drawY, { + atlas: itemsAtlas, + width: baseW, + height: baseH, + }) + } else if (typeof (renderer as any).drawQuad === 'function') { + // Slanted quad projection fallback for tests expecting drawQuad + const groundH = Math.max(6, Math.round(baseH * 0.52)) + const shearX = Math.round(-groundH * 0.55) + const x2 = item.x - baseW / 2 + const y2 = baseY + const x3 = item.x + baseW / 2 + const y3 = baseY + const x0 = x2 + shearX + const y0 = baseY - groundH + const x1 = x3 + shearX + const y1 = baseY - groundH + ;(renderer as any).drawQuad(frame, x0, y0, x1, y1, x2, y2, x3, y3, { + atlas: itemsAtlas, + }) + } + return + } + + // 2. Fallback for mock renderer or headless unit tests without atlas if (item.isGold) { let w = 10 let h = 8 @@ -3932,111 +3988,35 @@ export function drawGroundItem( // Highlight glint coin rim renderer.drawSolid(item.x - w / 2 + 1, baseY - h + 1, w - 2, Math.max(2, Math.floor(h / 3)), [1.0, 0.92, 0.45, 1.0]) } else { - // 2. Equipment / Potion / Rune Item Rendering - const sr = (itemsAtlas !== undefined && (typeof (renderer as any).drawQuad === 'function' || typeof (renderer as any).draw === 'function')) - ? resolveItemSpriteRect(item.item || (item as any), BAKED_UI_MANIFEST.itemRects) - : null + const w = item.invWidth > 1 ? 22 : 14 + const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14 - if (sr !== null && itemsAtlas !== undefined) { - // Authentic Diablo II ground item sprite icon: - // Scales sprite to lie on ground naturally (~16-34px) - const maxDim = Math.max(sr.w, sr.h) - const scale = Math.min(0.75, 34 / maxDim) - const baseW = Math.max(12, Math.round(sr.w * scale)) - const baseH = Math.max(12, Math.round(sr.h * scale)) + // Drop shadow: stays grounded on floor (item.y - 3), alpha fades slightly with height + const shadowAlpha = bounceH > 0 ? Math.max(0.15, 0.38 * (1 - bounceH / 45)) : 0.38 + renderer.drawSolid(item.x - w / 2 - 2, item.y - 3, w + 4, 5, [0, 0, 0, shadowAlpha]) - // Ground foreshortened height (lying flat on 2:1 isometric plane) - const groundH = Math.max(6, Math.round(baseH * 0.52)) - // Isometric shear offset along diamond lattice (2:1 slope: dy/dx = 0.5) - const shearX = Math.round(-groundH * 0.55) - - // Drop shadow: stays grounded on floor (item.y - 2), alpha fades slightly with height - const shadowAlpha = bounceH > 0 ? Math.max(0.12, 0.42 * (1 - bounceH / 45)) : 0.42 - const shadowW = Math.max(14, baseW + 6) - const shadowH = Math.max(4, Math.round(shadowW * 0.38)) - renderer.drawSolid(item.x - shadowW / 2, item.y - 2, shadowW, shadowH, [0, 0, 0, shadowAlpha]) - - // Authentic flippy tumble and tilt modulation during vertical bounce - let renderW = baseW - let renderH = groundH - let renderShear = shearX - - if (bounceH > 0 && item.bounceState) { - const elapsed = now - item.bounceState.startTime - // Spin oscillation creating flipping card / tumbling item illusion - const spin = Math.cos(elapsed * 0.018) - const spinScale = Math.abs(spin) - renderW = Math.max(4, Math.round(baseW * (0.35 + 0.65 * spinScale))) - const airFraction = Math.min(1, bounceH / 25) - renderH = Math.max(6, Math.round(groundH * (1 - airFraction) + baseH * 0.75 * airFraction)) - renderShear = Math.round(shearX * (1 - 0.5 * airFraction) * Math.sign(spin || 1)) - } - - // Parallelogram quad vertices (lying flat on ground, sheared along 2:1 isometric axes): - // Corner 2 (bottom-left): (x2, y2) - // Corner 3 (bottom-right): (x3, y3) - // Corner 0 (top-left): (x0, y0) = (x2 + renderShear, baseY - renderH) - // Corner 1 (top-right): (x1, y1) = (x3 + renderShear, baseY - renderH) - const x2 = item.x - renderW / 2 - const y2 = baseY - const x3 = item.x + renderW / 2 - const y3 = baseY - const x0 = x2 + renderShear - const y0 = baseY - renderH - const x1 = x3 + renderShear - const y1 = baseY - renderH - - const frame: AtlasFrame = { - x: sr.x, - y: sr.y, - width: sr.w, - height: sr.h, - } - - if (typeof (renderer as any).drawQuad === 'function') { - ;(renderer as any).drawQuad(frame, x0, y0, x1, y1, x2, y2, x3, y3, { - atlas: itemsAtlas, - }) - } else { - renderer.draw(frame, item.x - renderW / 2, baseY - renderH, { - atlas: itemsAtlas, - width: renderW, - height: renderH, - }) - } - } else { - // Fallback for mock renderer or headless unit tests - const w = item.invWidth > 1 ? 22 : 14 - const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14 - - // Drop shadow: stays grounded on floor (item.y - 3), alpha fades slightly with height - const shadowAlpha = bounceH > 0 ? Math.max(0.15, 0.38 * (1 - bounceH / 45)) : 0.38 - renderer.drawSolid(item.x - w / 2 - 2, item.y - 3, w + 4, 5, [0, 0, 0, shadowAlpha]) - - // Authentic flippy tumble width modulation during vertical bounce - let renderW = w - if (bounceH > 0 && item.bounceState) { - const elapsed = now - item.bounceState.startTime - // Spin oscillation creating flipping card / tumbling item illusion - const spinScale = Math.abs(Math.cos(elapsed * 0.018)) - renderW = Math.max(6, Math.round(w * (0.4 + 0.6 * spinScale))) - } - - // Body - const qr = qColor[0] - const qg = qColor[1] - const qb = qColor[2] - renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, h, [qr * 0.45, qg * 0.45, qb * 0.45, 0.9]) - // Border rim - renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, 2, [qr, qg, qb, 0.95]) - renderer.drawSolid(item.x - renderW / 2, baseY - 2, renderW, 2, [qr, qg, qb, 0.95]) - renderer.drawSolid(item.x - renderW / 2, baseY - h, 2, h, [qr, qg, qb, 0.95]) - renderer.drawSolid(item.x + renderW / 2 - 2, baseY - h, 2, h, [qr, qg, qb, 0.95]) + // Authentic flippy tumble width modulation during vertical bounce + let renderW = w + if (bounceH > 0 && item.bounceState) { + const elapsed = now - item.bounceState.startTime + // Spin oscillation creating flipping card / tumbling item illusion + const spinScale = Math.abs(Math.cos(elapsed * 0.018)) + renderW = Math.max(6, Math.round(w * (0.4 + 0.6 * spinScale))) } + + // Body + const qr = qColor[0] + const qg = qColor[1] + const qb = qColor[2] + renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, h, [qr * 0.45, qg * 0.45, qb * 0.45, 0.9]) + // Border rim + renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, 2, [qr, qg, qb, 0.95]) + renderer.drawSolid(item.x - renderW / 2, baseY - 2, renderW, 2, [qr, qg, qb, 0.95]) + renderer.drawSolid(item.x - renderW / 2, baseY - h, 2, h, [qr, qg, qb, 0.95]) + renderer.drawSolid(item.x + renderW / 2 - 2, baseY - h, 2, h, [qr, qg, qb, 0.95]) } - // 3. Glint / Sparkle 4-Point Star Animation - // Flash every ~2000ms with a pseudo-random phase offset based on coordinates + // 3. Glint / Sparkle 4-Point Star Animation for fallback headless tests const period = 2000 const phaseOffset = Math.abs(Math.floor(item.cellX * 31 + item.cellY * 17)) % period const cycleTime = (now + phaseOffset) % period diff --git a/src/ui/baked-ui-meta.ts b/src/ui/baked-ui-meta.ts index 86c0bcb..4f5a79b 100644 --- a/src/ui/baked-ui-meta.ts +++ b/src/ui/baked-ui-meta.ts @@ -26,7 +26,9 @@ export interface BakedUiManifest { readonly questsAtlasWidth: number readonly questsAtlasHeight: number readonly codeToInvFile: Record + readonly codeToFlippyFile: Record readonly itemRects: Record + readonly flippyRects: Record readonly questRects: Record readonly images: Record } @@ -81,10 +83,10 @@ export const ACT_QUEST_ASSET_MAP: Readonly = BAKED_UI_MANIFEST.flippyRects ?? {}, + codeToFlippyFile: Record = BAKED_UI_MANIFEST.codeToFlippyFile ?? {}, +): SpriteRect | null { + // 1. Gold rendering: pile size determined by amount + if ((item as any).isGold || item.code === 'gld') { + const amount = (item as any).amount ?? 1 + let pileIndex = 0 + if (amount >= 5000) { + pileIndex = 3 + } else if (amount >= 500) { + pileIndex = 2 + } else if (amount >= 100) { + pileIndex = 1 + } + return flippyRects[`flpgld_${pileIndex}`] ?? flippyRects['flpgld'] ?? null + } + + // 2. Direct flippyFile property + if ((item as any).flippyFile) { + const flpFile = String((item as any).flippyFile).toLowerCase() + if (item.invtransform) { + const dyed = `${flpFile}_${item.invtransform}`.toLowerCase() + if (flippyRects[dyed]) return flippyRects[dyed] + } + if (flippyRects[flpFile]) return flippyRects[flpFile] + } + + // 3. Unique / Set item name lookup in codeToFlippyFile + if (item.name) { + const fromName = codeToFlippyFile[item.name] + if (fromName) { + if (item.invtransform) { + const dyed = `${fromName}_${item.invtransform}`.toLowerCase() + if (flippyRects[dyed]) return flippyRects[dyed] + } + if (flippyRects[fromName]) return flippyRects[fromName] + } + } + + // 4. Base item code lookup in codeToFlippyFile + if (item.code) { + const fromCode = codeToFlippyFile[item.code] ?? codeToFlippyFile[item.code.toLowerCase()] + if (fromCode) { + if (item.invtransform) { + const dyed = `${fromCode}_${item.invtransform}`.toLowerCase() + if (flippyRects[dyed]) return flippyRects[dyed] + } + if (flippyRects[fromCode]) return flippyRects[fromCode] + } + } + + // 5. invFile -> flp substitution (e.g. invhax -> flphax, invcap -> flpcap) + if (item.invFile) { + const flpCandidate = item.invFile.toLowerCase().replace(/^inv/, 'flp') + if (item.invtransform) { + const dyed = `${flpCandidate}_${item.invtransform}`.toLowerCase() + if (flippyRects[dyed]) return flippyRects[dyed] + } + if (flippyRects[flpCandidate]) return flippyRects[flpCandidate] + } + + // 6. Name-based semantic fallback for flippy items + const nameStr = ( + (item.name || '') + ' ' + + (item.id || '') + ' ' + + ((item as any).nameZh || '') + ' ' + + ((item as any).base?.name || '') + ' ' + + ((item as any).base?.nameZh || '') + ).toLowerCase() + + if (nameStr.includes('sword') || nameStr.includes('剑')) return flippyRects['flpssd'] ?? flippyRects['flpbsd'] ?? flippyRects['flpcrs'] ?? null + if (nameStr.includes('axe') || nameStr.includes('斧')) return flippyRects['flphax'] ?? flippyRects['flpbax'] ?? null + if (nameStr.includes('bow') || nameStr.includes('弓')) return flippyRects['flpsbw'] ?? flippyRects['flphbw'] ?? null + if (nameStr.includes('shield') || nameStr.includes('盾')) return flippyRects['flpsml'] ?? flippyRects['flplrg'] ?? flippyRects['flpkit'] ?? null + if (nameStr.includes('helm') || nameStr.includes('cap') || nameStr.includes('盔') || nameStr.includes('帽')) return flippyRects['flpcap'] ?? flippyRects['flphlm'] ?? null + if (nameStr.includes('armor') || nameStr.includes('plate') || nameStr.includes('甲')) return flippyRects['flplea'] ?? flippyRects['flpqui'] ?? flippyRects['flpfld'] ?? null + if (nameStr.includes('potion') || nameStr.includes('药')) { + if (nameStr.includes('mana') || nameStr.includes('法力') || nameStr.includes('蓝')) return flippyRects['flpbps'] ?? flippyRects['flprps'] ?? null + if (nameStr.includes('rejuv') || nameStr.includes('活力') || nameStr.includes('紫')) return flippyRects['flprvs'] ?? flippyRects['flprps'] ?? null + return flippyRects['flprps'] ?? null + } + if (nameStr.includes('ring') || nameStr.includes('戒')) return flippyRects['flprin'] ?? null + if (nameStr.includes('amulet') || nameStr.includes('项链')) return flippyRects['flpamu'] ?? null + if (nameStr.includes('boot') || nameStr.includes('靴') || nameStr.includes('鞋')) return flippyRects['flplbt'] ?? flippyRects['flphbt'] ?? null + if (nameStr.includes('glove') || nameStr.includes('手套')) return flippyRects['flplgl'] ?? flippyRects['flphgl'] ?? null + if (nameStr.includes('belt') || nameStr.includes('腰带') || nameStr.includes('带')) return flippyRects['flplbl'] ?? flippyRects['flphbl'] ?? null + if (nameStr.includes('rune') || nameStr.includes('符文')) return flippyRects['flprun'] ?? null + if (nameStr.includes('scroll') || nameStr.includes('卷轴')) return flippyRects['flptsc'] ?? flippyRects['flprsc'] ?? null + + // 7. Fallback to 2D inventory item sprite rect if no flippy found + return resolveItemSpriteRect(item, BAKED_UI_MANIFEST.itemRects) +} + export const STARTER_EQUIPPED_GEAR: Readonly>> = { helm: { id: 'eq-shako', diff --git a/tests/ground-items-flippy-parity.test.ts b/tests/ground-items-flippy-parity.test.ts new file mode 100644 index 0000000..816f2c9 --- /dev/null +++ b/tests/ground-items-flippy-parity.test.ts @@ -0,0 +1,253 @@ +import { describe, it, expect } from 'vitest' +import { resolveGroundItemSpriteRect } from '../src/ui/inventory.ts' +import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' +import { drawGroundItem } from '../src/scene/act-scene.ts' +import type { GroundItemEntity } from '../src/game/ground-items.ts' +import type { SpriteRenderer } from '../src/render/renderer.ts' +import { GameEngine } from '../src/game/engine.ts' + +describe('Authentic Diablo II v1.13c Ground Items Flippy Parity', () => { + describe('1. Flippy File Resolution Parity', () => { + it('resolves authentic flippy ground sprites for weapons and armor', () => { + const sword = resolveGroundItemSpriteRect({ code: 'ssd', name: 'Short Sword' }) + expect(sword).toBeDefined() + expect(sword).not.toBeNull() + expect(sword).toEqual(BAKED_UI_MANIFEST.flippyRects['flpssd']) + expect(sword!.w).toBe(27) + expect(sword!.h).toBe(9) + + const axe = resolveGroundItemSpriteRect({ code: 'hax', name: 'Hand Axe' }) + expect(axe).toEqual(BAKED_UI_MANIFEST.flippyRects['flphax']) + expect(axe!.w).toBe(9) + expect(axe!.h).toBe(13) + + const cap = resolveGroundItemSpriteRect({ code: 'cap', name: 'Cap' }) + expect(cap).toEqual(BAKED_UI_MANIFEST.flippyRects['flpcap']) + expect(cap!.w).toBe(11) + expect(cap!.h).toBe(8) + }) + + it('resolves gold piles into 4 authentic DC6 group sizes based on gold amount', () => { + // Group 0: small pile (< 100 gold) + const small = resolveGroundItemSpriteRect({ isGold: true, amount: 45, code: 'gld' }) + expect(small).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_0']) + expect(small!.w).toBe(24) + expect(small!.h).toBe(7) + + // Group 1: medium pile (100 - 499 gold) + const medium = resolveGroundItemSpriteRect({ isGold: true, amount: 250, code: 'gld' }) + expect(medium).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_1']) + expect(medium!.w).toBe(26) + expect(medium!.h).toBe(13) + + // Group 2: large pile (500 - 4999 gold) + const large = resolveGroundItemSpriteRect({ isGold: true, amount: 1500, code: 'gld' }) + expect(large).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_2']) + expect(large!.w).toBe(32) + expect(large!.h).toBe(20) + + // Group 3: huge pile (>= 5000 gold) + const huge = resolveGroundItemSpriteRect({ isGold: true, amount: 10000, code: 'gld' }) + expect(huge).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_3']) + expect(huge!.w).toBe(46) + expect(huge!.h).toBe(23) + }) + + it('resolves potions, scrolls, and runes to authentic flippy files', () => { + const redPotion = resolveGroundItemSpriteRect({ code: 'hp1', name: 'Lesser Healing Potion' }) + expect(redPotion).toEqual(BAKED_UI_MANIFEST.flippyRects['flprps']) + + const bluePotion = resolveGroundItemSpriteRect({ code: 'mp1', name: 'Lesser Mana Potion' }) + expect(bluePotion).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbps']) + + const rune = resolveGroundItemSpriteRect({ code: 'r01', name: 'El Rune' }) + expect(rune).toEqual(BAKED_UI_MANIFEST.flippyRects['flprun']) + expect(rune!.w).toBe(4) + expect(rune!.h).toBe(4) + + const tpScroll = resolveGroundItemSpriteRect({ code: 'tsc', name: 'Town Portal Scroll' }) + expect(tpScroll).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbsc']) + }) + + it('resolves unique items and charms to authentic ground art', () => { + const anni = resolveGroundItemSpriteRect({ code: 'cm1', name: 'Annihilus' }) + expect(anni).toEqual(BAKED_UI_MANIFEST.flippyRects['flpmss']) + + const torch = resolveGroundItemSpriteRect({ code: 'cm2', name: 'Hellfire Torch' }) + expect(torch).toEqual(BAKED_UI_MANIFEST.flippyRects['flptrch']) + + const cube = resolveGroundItemSpriteRect({ code: 'box', name: 'Horadric Cube' }) + expect(cube).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbox']) + }) + + it('resolves color-transformed items to dyed ground flippy sprites', () => { + const shako = resolveGroundItemSpriteRect({ + code: 'uap', + invFile: 'invcap', + invtransform: 'cgrn', + name: 'Harlequin Crest', + }) + expect(shako).toEqual(BAKED_UI_MANIFEST.flippyRects['flpcap_cgrn']) + }) + }) + + describe('2. Ground Item Rendering with Atlas Parity', () => { + it('renders authentic Blizzard flippy sprite with 1:1 isometric dimensions and ground shadow', () => { + const drawnQuads: { x: number; y: number; w: number; h: number; color: readonly [number, number, number, number] }[] = [] + const drawnSprites: { frame: any; x: number; y: number; options: any }[] = [] + const renderer = { + drawSolid(x: number, y: number, w: number, h: number, color: readonly [number, number, number, number]) { + drawnQuads.push({ x, y, w, h, color }) + }, + draw(frame: any, x: number, y: number, options: any) { + drawnSprites.push({ frame, x, y, options }) + }, + } as unknown as SpriteRenderer + + const mockAtlas = { texture: {} as any, width: 1024, height: 2145 } as any + const groundSword: GroundItemEntity = { + id: 'ground_sword_test', + item: { name: 'Short Sword', code: 'ssd' }, + name: 'Short Sword', + nameZh: '短剑', + quality: 'magic', + isGold: false, + amount: 1, + invWidth: 1, + invHeight: 3, + dropTime: 0, + x: 300, + y: 250, + cellX: 0, + cellY: 0, + sparklePhase: 0, + } + + drawGroundItem(renderer, groundSword, 1000, mockAtlas) + + // Verified: Exactly 1 sprite drawn via renderer.draw using flpssd rect + expect(drawnSprites.length).toBe(1) + const sprite = drawnSprites[0]! + expect(sprite.options.atlas).toBe(mockAtlas) + expect(sprite.options.width).toBe(27) + expect(sprite.options.height).toBe(9) + expect(sprite.x).toBe(Math.round(300 - 27 / 2)) + expect(sprite.y).toBe(250 - 9) + + // Ground contact shadow is drawn at item.y - 2 + expect(drawnQuads.length).toBe(1) + const shadow = drawnQuads[0]! + expect(shadow.y).toBe(250 - 2) + expect(shadow.color[3]).toBeLessThanOrEqual(0.4) // Subtle shadow alpha + + // Zero blue bounding boxes + const blueBox = drawnQuads.find(q => q.color[2] > 0.8 && q.w > 10 && q.h > 10) + expect(blueBox).toBeUndefined() + + // Zero white sparkle star quads + const whiteGlint = drawnQuads.find(q => q.color[0] === 1.0 && q.color[1] === 1.0 && q.color[2] === 1.0) + expect(whiteGlint).toBeUndefined() + }) + + it('renders authentic flpgld sprite for dropped gold when atlas is present', () => { + const drawnQuads: any[] = [] + const drawnSprites: any[] = [] + const renderer = { + drawSolid(x: number, y: number, w: number, h: number, color: any) { + drawnQuads.push({ x, y, w, h, color }) + }, + draw(frame: any, x: number, y: number, options: any) { + drawnSprites.push({ frame, x, y, options }) + }, + } as unknown as SpriteRenderer + + const mockAtlas = { texture: {} as any, width: 1024, height: 2145 } as any + const groundGold: GroundItemEntity = { + id: 'gold_huge_test', + item: { isGold: true }, + name: '5000 Gold', + nameZh: '5000 金币', + quality: 'normal', + isGold: true, + amount: 5000, + invWidth: 1, + invHeight: 1, + dropTime: 0, + x: 400, + y: 350, + cellX: 0, + cellY: 0, + sparklePhase: 0, + } + + drawGroundItem(renderer, groundGold, 1000, mockAtlas) + + expect(drawnSprites.length).toBe(1) + const goldSprite = drawnSprites[0]! + // Group 3 huge pile: 46x23 + expect(goldSprite.options.width).toBe(46) + expect(goldSprite.options.height).toBe(23) + expect(goldSprite.x).toBe(Math.round(400 - 46 / 2)) + expect(goldSprite.y).toBe(350 - 23) + }) + }) + + describe('3. Clean Pickup Lifecycle (Zero Remaining Dots / Artifacts)', () => { + it('completely cleans groundItems on engine pickup tick without leaving orphaned entities', () => { + const engine = new GameEngine( + { widthPx: 1000, heightPx: 1000, overlap: () => 0 }, + { + spawn: { x: 100, y: 100 }, + stats: [], + xpTable: [], + skills: [], + npcDefs: [], + questDefs: [], + combatOptions: { + playerSpeed: 4, + playerReach: 50, + playerCooldownTicks: 10, + playerDamage: 5, + playerManaPerAttack: 1, + respawnTicks: 100, + }, + talkRadius: 50, + pickupRadius: 30, + inventoryCols: 10, + inventoryRows: 4, + }, + ) + engine.world.player.x = 100 + engine.world.player.y = 100 + + const droppedItem = { + id: 'test_drop_item', + code: 'hax', + name: 'Hand Axe', + invWidth: 1, + invHeight: 3, + } + + // Drop item near player + engine.dropItem(droppedItem, 105, 105, 5, 5) + expect(engine.groundItems.count).toBe(1) + expect(engine.ground.length).toBe(1) + + // Tick with pickup intent + engine.tick({ + pickingUp: true, + movement: { x: 0, y: 0 }, + attacking: false, + talking: false, + digits: [], + saving: false, + loading: false, + }) + + // Verified: Cleanly removed from both collections + expect(engine.groundItems.count).toBe(0) + expect(engine.ground.length).toBe(0) + expect(engine.bag.contents.some(p => (p.item as any).id === 'test_drop_item')).toBe(true) + }) + }) +})