1
0
Fork 0

Fix meal 999 cap and durability transfer (#9)

* fix meal 999 cap and durability transfer

* clean up

* add test for stackable key items

* add ref link
This commit is contained in:
Michael 2022-07-30 22:52:32 -07:00 committed by GitHub
parent 8623617f3c
commit 98782ac94c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 253 additions and 37 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "botw-hundo-dupl", "name": "botw-hundo-dupl",
"version": "2.1.1", "version": "2.1.2",
"homepage": "https://dupl.itntpiston.app/", "homepage": "https://dupl.itntpiston.app/",
"private": true, "private": true,
"dependencies": { "dependencies": {

View file

@ -23,5 +23,7 @@ export {};
1. Open the `e2e.ts` file of the failed test 1. Open the `e2e.ts` file of the failed test
2. replace `it` with `it.only` 2. replace `it` with `it.only`
3. replace `toPassE2ESimulation()` with `toPassE2ESimulation(true)` (i.e. type `true` in the parenthese) 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 5. After fixing the test, revert the changes to `e2e.ts` you made

View file

@ -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 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 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 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 break 10 slots

View file

@ -276,36 +276,34 @@ export class Slots {
if(slot < 0 || slot >= this.internalSlots.length){ if(slot < 0 || slot >= this.internalSlots.length){
return; 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); 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}); this.modifySlot(slot, {count: life});
//}
} }
// shoot count arrows. return the slot that was updated, or -1 // shoot count arrows. return the slot that was updated, or -1
public shootArrow(count: number): number { public shootArrow(count: number): number {
// first find equipped arrow, search entire inventory // first find equipped arrow, search entire inventory
// this is the last equipped arrow before armor const lastEquippedArrowSlot = this.findLastEquippedSlot(ItemType.Arrow);
let i=0; if(lastEquippedArrowSlot < 0){
let equippedArrow: Item | undefined = undefined;
// [needs confirm] does this check entire inventory?
for(;i<this.internalSlots.length;i++){
if(this.internalSlots[i].item.type > 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
return -1; return -1;
} }
const equippedArrow: Item = this.internalSlots[lastEquippedArrowSlot].item;
// now find the first slot of that arrow and update // now find the first slot of that arrow and update
for(let j=0;j<this.internalSlots.length;j++){ for(let j=0;j<this.internalSlots.length;j++){
if(this.internalSlots[j].item === equippedArrow){ if(this.internalSlots[j].item === equippedArrow){
@ -315,7 +313,26 @@ export class Slots {
} }
//for some reason cannot find that arrow now? //for some reason cannot find that arrow now?
return -1; return -1;
}
public findLastEquippedSlot(type: ItemType): number {
let i = 0;
let result = -1;
// [needs confirm] does this check entire inventory?
for(;i<this.internalSlots.length;i++){
// [needs confirm] does this break when == type+1?
// [needs confirm] does this matter when tabs are undiscovered?
if(this.internalSlots[i].item.type > 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 // set item metadata

View file

@ -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 {};

View file

@ -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 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 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 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 createEquipmentMockItem = (id: string, type: ItemType): Item => new MockItem(id, type, false, true);
export const equalsExceptEquip = (a: ItemStack, b: ItemStack): boolean => a.equalsExceptForEquipped(b); export const equalsExceptEquip = (a: ItemStack, b: ItemStack): boolean => a.equalsExceptForEquipped(b);

1
src/core/Slots/index.ts Normal file
View file

@ -0,0 +1 @@
export * from "./Slots";

View file

@ -63,27 +63,38 @@ export class VisibleInventory implements DisplayableInventory{
} }
public updateEquipmentDurability(gameData: GameData) { 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 // find first weapon/bow/shield. this one searches entire inventory
let foundWeapon = false; let firstEquippedWeaponSlot = -1;
let foundBow = false; let firstEquippedBowSlot = -1;
let foundShield = false; let firstEquippedShieldSlot = -1;
this.slots.getSlotsRef().forEach(({item, count, equipped}, i)=>{ this.slots.getSlotsRef().forEach(({item, equipped}, i)=>{
if(equipped){ if(equipped){
const type = item.type; const type = item.type;
if(type === ItemType.Weapon && !foundWeapon){ if(type === ItemType.Weapon && firstEquippedWeaponSlot === -1){
gameData.updateLife(count, i); firstEquippedWeaponSlot = i;
foundWeapon = true;
} }
if(type === ItemType.Bow && !foundBow){ if(type === ItemType.Bow && firstEquippedBowSlot === -1){
gameData.updateLife(count, i); firstEquippedBowSlot = i;
foundBow = true;
} }
if(type === ItemType.Shield && !foundShield){ if(type === ItemType.Shield && firstEquippedShieldSlot === -1){
gameData.updateLife(count, i); firstEquippedShieldSlot = i;
foundShield = true;
} }
} }
}); });
// 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) { public shootArrow(count: number, gameData: GameData) {

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// jest-dom adds custom jest matchers for asserting on DOM nodes. // jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
@ -69,6 +70,73 @@ const runE2ESimulation = (str: string): SimulationState => {
return state; 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<obj1.length;i++){
const obj1Str = JSON.stringify(obj1[i]);
const obj2Str = JSON.stringify(obj2[i]);
if(obj1Str !== obj2Str){
diffObjects(obj1[i], obj2[i], path+`[${i}]`, out);
}
}
}
return;
}
if(typeof obj1 === "object" && typeof obj2 === "object") {
// find out if any sub-thing mismatch
const mismatches = new Set<string>();
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 runE2ETest = (name: string, debug: boolean): [string, string]=>{
const script = fs.readFileSync(`src/__tests__/${name}.in.txt`, "utf-8"); const script = fs.readFileSync(`src/__tests__/${name}.in.txt`, "utf-8");
const result = runE2ESimulation(script); const result = runE2ESimulation(script);
@ -80,6 +148,15 @@ const runE2ETest = (name: string, debug: boolean): [string, string]=>{
if(debug){ if(debug){
fs.writeFileSync("src/__tests__/debug.actual.log", resultString, "utf-8"); fs.writeFileSync("src/__tests__/debug.actual.log", resultString, "utf-8");
fs.writeFileSync("src/__tests__/debug.expected.log", expectedString, "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]; return [resultString, expectedString];
}; };