diff --git a/package.json b/package.json index 3378bd1..6a58a56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "botw-hundo-dupl", - "version": "2.1.1", + "version": "2.1.2", "homepage": "https://dupl.itntpiston.app/", "private": true, "dependencies": { diff --git a/src/__tests__/README.md b/src/__tests__/README.md index 96cc828..3baec21 100644 --- a/src/__tests__/README.md +++ b/src/__tests__/README.md @@ -23,5 +23,7 @@ export {}; 1. Open the `e2e.ts` file of the failed test 2. replace `it` with `it.only` 3. replace `toPassE2ESimulation()` with `toPassE2ESimulation(true)` (i.e. type `true` in the parenthese) -4. Run the test again, you should now see `debug.expected.log` and `debug.actual.log` containing the expected and actual content of the simuation states. +4. Run the test again, you should see 3 files in the test folder: + - `debug.expected.log` and `debug.actual.log` containing the expected and actual content of the simuation states. + - `debug.mismatch.log` containing parts of the states that are different 5. After fixing the test, revert the changes to `e2e.ts` you made diff --git a/src/__tests__/aqFinished1.out.txt b/src/__tests__/aqFinished1.out.txt index 19cbec8..e5e50ff 100644 --- a/src/__tests__/aqFinished1.out.txt +++ b/src/__tests__/aqFinished1.out.txt @@ -1,6 +1,6 @@ -initialize 1 hammer[equip] 1 royal bow[equip] 11 normal arrow[equip] 4700 ancient arrow 1 potlid [equip] 999 wood 999 rush 999 Spring 10 amber 1 core 999 fairy 4 Shaft 1 Hasty Elixir[life=1000] 1 slate 1 Glider +initialize 1 hammer[equip] 1 royal bow[equip] 11 normal arrow[equip] 4000 ancient arrow 1 potlid [equip] 999 wood 999 rush 999 Spring 10 amber 1 core 999 fairy 4 Shaft 1 Hasty Elixir[life=999] 1 slate 1 Glider Save initialize 1 hammer 2 tree 1 boko spear 1 boko club 1 wood*axe 1 royal bow[equip] 11 normal arrow[equip] 0 ancient arrow 1 slate 1 Glider Save as AutoSave -initialize 1 hammer[equip] 1 royal bow[equip] 1 normal arrow[equip] 10 normal arrow[equip] 4700 ancient arrow 1 potlid [equip] 5 fairy 999 rush 699 Spring 1 Shaft 969 Wood 10 amber 994 fairy 4 Shaft 1 sapphire 1 Hasty Elixir[life=1000] 1 Hasty Elixir[life=1000] 1 seafood paella 1 slate 1 Glider 5 orb 5 orb +initialize 1 hammer[equip] 1 royal bow[equip] 1 normal arrow[equip] 10 normal arrow[equip] 4000 ancient arrow 1 potlid [equip] 5 fairy 999 rush 699 Spring 1 Shaft 969 Wood 10 amber 994 fairy 4 Shaft 1 sapphire 1 Hasty Elixir[life=999] 1 Hasty Elixir[life=999] 1 seafood paella 1 slate 1 Glider 5 orb 5 orb break 10 slots diff --git a/src/core/Slots.add.test.ts b/src/core/Slots/Slots.add.test.ts similarity index 100% rename from src/core/Slots.add.test.ts rename to src/core/Slots/Slots.add.test.ts diff --git a/src/core/Slots.remove.test.ts b/src/core/Slots/Slots.remove.test.ts similarity index 100% rename from src/core/Slots.remove.test.ts rename to src/core/Slots/Slots.remove.test.ts diff --git a/src/core/Slots.ts b/src/core/Slots/Slots.ts similarity index 90% rename from src/core/Slots.ts rename to src/core/Slots/Slots.ts index 76fb9d2..1770bc8 100644 --- a/src/core/Slots.ts +++ b/src/core/Slots/Slots.ts @@ -276,36 +276,34 @@ export class Slots { if(slot < 0 || slot >= this.internalSlots.length){ return; } - if(this.internalSlots[slot].item.stackable && this.internalSlots[slot].item.type !== ItemType.Arrow){ + const type = this.internalSlots[slot].item.type; + const stackable = this.internalSlots[slot].item.stackable; + // [confirmed] material and meals are capped at 999 + // meals: https://discord.com/channels/269611402854006785/269616041435332608/1000253331668742265 + const isMaterialOrMeal = type === ItemType.Material || type === ItemType.Food; + // [confirmed] arrows are not capped at 999 + const isArrow = type === ItemType.Arrow; + // [confirmed] stackble key items are capped + // https://discord.com/channels/269611402854006785/269616041435332608/1003165317125656586 + const isStackableKey = type === ItemType.Key && stackable; + + const shouldCapAt999 = !isArrow && (isMaterialOrMeal || isStackableKey); + if(shouldCapAt999){ life = Math.min(999, life); } - //const thisData = itemToItemData(this.internalSlots[slot].item); - // Currently only supports corrupting arrows, material, food and key items as durability values are not simulated on equipments - //if(this.internalSlots[slot].item.type >= ItemType.Material || this.internalSlots[slot].item.stackable){ - //const newLife = Math.min(999, life); + this.modifySlot(slot, {count: life}); - //} + } // shoot count arrows. return the slot that was updated, or -1 public shootArrow(count: number): number { // first find equipped arrow, search entire inventory - // this is the last equipped arrow before armor - let i=0; - let equippedArrow: Item | undefined = undefined; - // [needs confirm] does this check entire inventory? - for(;i ItemType.Shield){ - break; - } - if(this.internalSlots[i].equipped && this.internalSlots[i].item.type === ItemType.Arrow){ - equippedArrow = this.internalSlots[i].item; - } - } - if(i>=this.internalSlots.length){ - //can't find equipped arrow + const lastEquippedArrowSlot = this.findLastEquippedSlot(ItemType.Arrow); + if(lastEquippedArrowSlot < 0){ return -1; } + const equippedArrow: Item = this.internalSlots[lastEquippedArrowSlot].item; // now find the first slot of that arrow and update for(let j=0;j type+1){ + break; + } + if(this.internalSlots[i].equipped && this.internalSlots[i].item.type === type){ + // constantly update result as long as a new slot is found + // In the end, this will be the last equipped slots of that type + result = i; + } + } + // will be -1 if never found + return result; } // set item metadata diff --git a/src/core/Slots/Slots.updateLife.test.ts b/src/core/Slots/Slots.updateLife.test.ts new file mode 100644 index 0000000..ace36ae --- /dev/null +++ b/src/core/Slots/Slots.updateLife.test.ts @@ -0,0 +1,107 @@ +import { createEquipmentStack, createMaterialStack, ItemStack, ItemType } from "data/item"; +import { Slots } from "./Slots"; +import { createArrowMockItem, createEquipmentMockItem, createFoodMockItem, createKeyMockItemStackable, createMaterialMockItem } from "./SlotsTestHelpers"; + +describe.only("Slots.updateLife", ()=>{ + it("should update life", ()=>{ + const mockItem1 = createMaterialMockItem("MaterialA"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(2, 0); + + const expected = [slot.modify({count: 2})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should update life in the correct slot", ()=>{ + const mockItem1 = createMaterialMockItem("MaterialA"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot, slot]; + const slots = new Slots(stacks); + slots.updateLife(2, 1); + + const expected = [slot, slot.modify({count: 2})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should not 999 cap for weapon", ()=>{ + const mockItem1 = createEquipmentMockItem("Weapon", ItemType.Weapon); + const slot = createEquipmentStack(mockItem1, 10, false); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({durability: 10})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should not 999 cap for bow", ()=>{ + const mockItem1 = createEquipmentMockItem("Bow", ItemType.Bow); + const slot = createEquipmentStack(mockItem1, 10, false); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({durability: 10})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should not 999 cap for shield", ()=>{ + const mockItem1 = createEquipmentMockItem("Shield", ItemType.Shield); + const slot = createEquipmentStack(mockItem1, 10, false); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({durability: 10})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should not 999 cap for arrow", ()=>{ + const mockItem1 = createArrowMockItem("Arrow"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({count: 1000})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should 999 cap for material", ()=>{ + const mockItem1 = createMaterialMockItem("A"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({count: 999})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should 999 cap for food", ()=>{ + const mockItem1 = createFoodMockItem("A"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({count: 999})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); + it("should 999 cap for stackable key items", ()=>{ + const mockItem1 = createKeyMockItemStackable("A"); + const slot = createMaterialStack(mockItem1, 1); + + const stacks: ItemStack[] = [slot]; + const slots = new Slots(stacks); + slots.updateLife(1000, 0); + + const expected = [slot.modify({count: 999})]; + expect(slots.getSlotsRef()).toEqualItemStacks(expected); + }); +}); + +export {}; diff --git a/src/core/SlotsTestHelpers.ts b/src/core/Slots/SlotsTestHelpers.ts similarity index 91% rename from src/core/SlotsTestHelpers.ts rename to src/core/Slots/SlotsTestHelpers.ts index 7baec21..4bd4265 100644 --- a/src/core/SlotsTestHelpers.ts +++ b/src/core/Slots/SlotsTestHelpers.ts @@ -27,6 +27,7 @@ export const createArrowMockItem = (id: string): Item => new MockItem(id, ItemTy export const createMaterialMockItem = (id: string): Item => new MockItem(id, ItemType.Material, true, true); export const createFoodMockItem = (id: string): Item => new MockItem(id, ItemType.Food, false, true); export const createKeyMockItem = (id: string): Item => new MockItem(id, ItemType.Key, false, false); +export const createKeyMockItemStackable = (id: string): Item => new MockItem(id, ItemType.Key, true, true); export const createEquipmentMockItem = (id: string, type: ItemType): Item => new MockItem(id, type, false, true); export const equalsExceptEquip = (a: ItemStack, b: ItemStack): boolean => a.equalsExceptForEquipped(b); diff --git a/src/core/Slots/index.ts b/src/core/Slots/index.ts new file mode 100644 index 0000000..1aebcf2 --- /dev/null +++ b/src/core/Slots/index.ts @@ -0,0 +1 @@ +export * from "./Slots"; diff --git a/src/core/VisibleInventory.ts b/src/core/VisibleInventory.ts index ab14596..28a5b94 100644 --- a/src/core/VisibleInventory.ts +++ b/src/core/VisibleInventory.ts @@ -63,27 +63,38 @@ export class VisibleInventory implements DisplayableInventory{ } public updateEquipmentDurability(gameData: GameData) { + // find last equipped weapon/bow/shield, but update the durability on first equipped slot // find first weapon/bow/shield. this one searches entire inventory - let foundWeapon = false; - let foundBow = false; - let foundShield = false; - this.slots.getSlotsRef().forEach(({item, count, equipped}, i)=>{ + let firstEquippedWeaponSlot = -1; + let firstEquippedBowSlot = -1; + let firstEquippedShieldSlot = -1; + this.slots.getSlotsRef().forEach(({item, equipped}, i)=>{ if(equipped){ const type = item.type; - if(type === ItemType.Weapon && !foundWeapon){ - gameData.updateLife(count, i); - foundWeapon = true; + if(type === ItemType.Weapon && firstEquippedWeaponSlot === -1){ + firstEquippedWeaponSlot = i; } - if(type === ItemType.Bow && !foundBow){ - gameData.updateLife(count, i); - foundBow = true; + if(type === ItemType.Bow && firstEquippedBowSlot === -1){ + firstEquippedBowSlot = i; } - if(type === ItemType.Shield && !foundShield){ - gameData.updateLife(count, i); - foundShield = true; + if(type === ItemType.Shield && firstEquippedShieldSlot === -1){ + firstEquippedShieldSlot = i; } } }); + // get life value from last equipped + const lastEquippedWeaponSlot = this.slots.findLastEquippedSlot(ItemType.Weapon); + if(firstEquippedWeaponSlot >=0 && lastEquippedWeaponSlot >=0){ + gameData.updateLife(this.slots.getSlotsRef()[lastEquippedWeaponSlot].count, firstEquippedWeaponSlot); + } + const lastEquippedBowSlot = this.slots.findLastEquippedSlot(ItemType.Bow); + if(firstEquippedBowSlot >=0 && lastEquippedBowSlot >=0){ + gameData.updateLife(this.slots.getSlotsRef()[lastEquippedBowSlot].count, firstEquippedBowSlot); + } + const lastEquippedShieldSlot = this.slots.findLastEquippedSlot(ItemType.Shield); + if(firstEquippedShieldSlot >=0 && lastEquippedShieldSlot >=0){ + gameData.updateLife(this.slots.getSlotsRef()[lastEquippedShieldSlot].count, firstEquippedShieldSlot); + } } public shootArrow(count: number, gameData: GameData) { diff --git a/src/setupTests.ts b/src/setupTests.ts index 13c2fc2..00d2172 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) @@ -69,6 +70,73 @@ const runE2ESimulation = (str: string): SimulationState => { return state; }; +// input should already be different +const diffObjects = (obj1: any, obj2: any, path: string, out: string[]) => { + if(Array.isArray(obj1) && Array.isArray(obj2)){ + if(obj1.length !== obj2.length){ + out.push("Array length mismatch"); + out.push("path : "+path); + out.push("expected: "+JSON.stringify(obj1)); + out.push("actual : "+JSON.stringify(obj2)); + out.push(""); + }else{ + for (let i=0;i(); + for(const key in obj1) { + if (key in obj2){ + const obj1Str = JSON.stringify(obj1[key]); + const obj2Str = JSON.stringify(obj2[key]); + if(obj1Str !== obj2Str){ + mismatches.add(key); + } + }else{ + // keys are not the same, output entire diff + out.push("Object key set mismatch"); + out.push("path : "+path); + out.push("expected: "+JSON.stringify(obj1)); + out.push("actual : "+JSON.stringify(obj2)); + out.push(""); + return; + } + } + for(const key in obj2) { + // every key in obj2 either is also in obj1 (already checked), or not + if(mismatches.has(key)){ + continue; + } + if (!(key in obj1)){ + out.push("Object key set mismatch"); + out.push("path : "+path); + out.push("expected: "+JSON.stringify(obj1)); + out.push("actual : "+JSON.stringify(obj2)); + out.push(""); + return; + } + } + // output each mismatch + mismatches.forEach(key=>{ + diffObjects(obj1[key], obj2[key], path+"."+key, out); + }); + return; + } + out.push("Value mismatch"); + out.push("path : "+path); + out.push("expected: "+JSON.stringify(obj1)); + out.push("actual : "+JSON.stringify(obj2)); + out.push(""); +}; + const runE2ETest = (name: string, debug: boolean): [string, string]=>{ const script = fs.readFileSync(`src/__tests__/${name}.in.txt`, "utf-8"); const result = runE2ESimulation(script); @@ -80,6 +148,15 @@ const runE2ETest = (name: string, debug: boolean): [string, string]=>{ if(debug){ fs.writeFileSync("src/__tests__/debug.actual.log", resultString, "utf-8"); fs.writeFileSync("src/__tests__/debug.expected.log", expectedString, "utf-8"); + if(expectedString !== resultString){ + const out: string[] = []; + diffObjects(expectedState, result, "", out); + fs.writeFileSync( + "src/__tests__/debug.mismatches.log", + out.join("\n"), + "utf-8"); + } + } return [resultString, expectedString]; };