diff --git a/src/App.tsx b/src/App.tsx
index 65d3467..9614a4d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,7 +5,6 @@ import "./App.css";
import { CommandItem } from "./components/CommandItem";
import { DisplayPane } from "surfaces/DisplayPane";
-import { saveAs } from "data/FileSaver";
import { parseCommand } from "core/Parser";
import { ItemList } from "components/ItemList";
import { TitledList } from "components/TitledList";
@@ -69,16 +68,25 @@ export const App: React.FC = () => {
useEffect(()=>{
window.onkeydown=(e)=>{
if(e.code==="ArrowDown"){
- if(displayIndex===commands.length-1){
+ let nextCommandIndex = displayIndex+1;
+ while(nextCommandIndex=0 && !commands[nextCommandIndex].isValid()){
+ nextCommandIndex--;
+ }
+ setDisplayIndex(Math.max(0, nextCommandIndex));
}
};
}, [commands, displayIndex]);
@@ -219,7 +227,6 @@ export const App: React.FC = () => {
setCommands(arrCopy);
}}>(new)
-
@@ -306,7 +313,7 @@ export const App: React.FC = () => {
commandText={commandText}
setCommandText={(value)=>{
if(value !== commandText){
- const commands = value.split("\n").map(parseCommand)
+ const commands = value.split("\n").map(parseCommand);
setCommands(commands);
}
}}
@@ -341,32 +348,32 @@ export const App: React.FC = () => {
paddingInlineStart: 0
}}>
- {
+ {
+ const arrCopy = [...commands];
+ arrCopy.splice(contextIndex, 0, new CommandNop(""));
+ setCommands(arrCopy);
+ setContextIndex(-1);
+ }}>Insert Above
+ {
+ if(contextIndex > 0){
const arrCopy = [...commands];
- arrCopy.splice(contextIndex, 0, new CommandNop(""));
+ const temp = arrCopy[contextIndex];
+ arrCopy[contextIndex] = arrCopy[contextIndex-1];
+ arrCopy[contextIndex-1] = temp;
setCommands(arrCopy);
setContextIndex(-1);
- }}>Insert Above
- {
- if(contextIndex > 0){
- const arrCopy = [...commands];
- const temp = arrCopy[contextIndex];
- arrCopy[contextIndex] = arrCopy[contextIndex-1];
- arrCopy[contextIndex-1] = temp;
- setCommands(arrCopy);
- setContextIndex(-1);
- }
+ }
- }}>Move Up
- {
- if(confirm("Delete?")){
- setCommands(commands.filter((_,i)=>i!==contextIndex));
- if(displayIndex >= commands.length){
- setDisplayIndex(commands.length-1);
- }
- setContextIndex(-1);
+ }}>Move Up
+ {
+ if(confirm("Delete?")){
+ setCommands(commands.filter((_,i)=>i!==contextIndex));
+ if(displayIndex >= commands.length){
+ setDisplayIndex(commands.length-1);
}
- }}>Delete
+ setContextIndex(-1);
+ }
+ }}>Delete
diff --git a/src/components/CommandItem.tsx b/src/components/CommandItem.tsx
index dc7366e..dea8f28 100644
--- a/src/components/CommandItem.tsx
+++ b/src/components/CommandItem.tsx
@@ -30,7 +30,7 @@ export const CommandItem: React.FC = ({
);
const clickHandler = useCallback((e: React.MouseEvent)=>{
- onClick(e.clientX, e.clientY)
+ onClick(e.clientX, e.clientY);
}, [onClick]);
const contextMenuHandler = useCallback((e: React.MouseEvent)=>{
if(onContextMenu){
diff --git a/src/core/Command.ts b/src/core/Command.ts
index a9ba2db..167df5a 100644
--- a/src/core/Command.ts
+++ b/src/core/Command.ts
@@ -232,20 +232,21 @@ const joinItemStackString = (initial: string, stacks: ItemStack[]): string => {
};
export class CommandDaP extends CommandImpl {
- private count: number;
- private item: Item;
+ private stacks: ItemStack[];
- constructor(count: number, item: Item){
+ constructor(stacks: ItemStack[]){
super();
- this.count = count;
- this.item = item;
+ this.stacks = stacks;
}
public execute(state: SimulationState): void {
- state.remove(this.item, this.count, 0);
- state.obtain(this.item, this.count);
+ this.stacks.forEach(({item,count})=>{
+ state.remove(item, count, 0);
+ state.obtain(item, count);
+ });
+
}
public getDisplayString(): string {
- return `D&P ${this.count} ${this.item}`;
+ return joinItemStackString("D&P", this.stacks);
}
}
@@ -328,6 +329,21 @@ export class CommandSync extends CommandImpl {
}
}
+export class CommandEventide extends CommandImpl {
+ private enter: boolean;
+ constructor(enter: boolean){
+ super();
+ this.enter = enter;
+ }
+
+ public execute(state: SimulationState): void {
+ state.setEventide(this.enter);
+ }
+ public getDisplayString(): string {
+ return `${this.enter? "Enter":"Exit"} Eventide`;
+ }
+}
+
export class CommandNop extends CommandImpl {
private text: string;
constructor(text: string){
diff --git a/src/core/Parser.ts b/src/core/Parser.ts
index f546b2b..de74340 100644
--- a/src/core/Parser.ts
+++ b/src/core/Parser.ts
@@ -7,6 +7,7 @@ import {
CommandCloseGame,
CommandDaP,
CommandEquip,
+ CommandEventide,
CommandInitialize,
CommandNop,
CommandReload,
@@ -124,11 +125,10 @@ export const parseCommand = (cmdString: string): Command => {
}
}
//Shortcut for drop and pick up
- if (tokens.length === 3 && tokens[0] === "D&P" ){
- const count = parseInt(tokens[1]);
- const item = tokens[2];
- if(Number.isInteger(count) && item in Item){
- return new CommandDaP(count, Item[item as keyof typeof Item]);
+ if (tokens.length >2 && tokens[0] === "D&P" ){
+ const stacks = parseItemStacks(tokens, 1);
+ if(stacks){
+ return new CommandDaP(stacks);
}
}
@@ -182,6 +182,9 @@ export const parseCommand = (cmdString: string): Command => {
if(tokens.length===2 && tokens[0] === "Sync" && tokens[1] === "GameData"){
return new CommandSync("Sync GameData");
}
+ if(tokens.length===2 && (tokens[0] === "Enter" || tokens[0] === "Exit") && tokens[1] === "Eventide"){
+ return new CommandEventide(tokens[0] === "Enter");
+ }
return new CommandNop(cmdString);
};
diff --git a/src/core/SimulationState.ts b/src/core/SimulationState.ts
index 47e4ea4..6a1061e 100644
--- a/src/core/SimulationState.ts
+++ b/src/core/SimulationState.ts
@@ -85,6 +85,7 @@ export class SimulationState {
this.pouch.clearForReload();
this.gameData.addAllToPouchOnReload(this.pouch);
this.pouch.updateEquipmentDurability(this.gameData);
+ this.isOnEventide = false;
}
public useSaveForNextReload(name: string){
@@ -126,6 +127,22 @@ export class SimulationState {
this.isOnEventide = false;
}
+ public setEventide(onEventide: boolean){
+ if(this.isOnEventide !== onEventide){
+ if(onEventide){
+ // clear everything except for key items
+ this.pouch.clearForEventide();
+ // game data is not updated (?)
+
+ }else{
+ // reload pouch from gamedata as if reloading a save
+ this.reloadFrom(this.gameData);
+ }
+ this.isOnEventide = onEventide;
+ }
+
+ }
+
public syncGameDataWithPouch() {
if(!this.isOnEventide){
this.gameData.syncWith(this.pouch);
diff --git a/src/core/Slots.ts b/src/core/Slots.ts
index 9ca7ac1..c72219e 100644
--- a/src/core/Slots.ts
+++ b/src/core/Slots.ts
@@ -264,4 +264,12 @@ export class Slots {
}
+ // return how many slots are removed
+ public clearAllButKeyItems(): number {
+ const newslots = this.internalSlots.filter(stack=>itemToItemData(stack.item).type === ItemType.Key);
+ const removedCount = this.internalSlots.length - newslots.length;
+ this.internalSlots = newslots;
+ return removedCount;
+ }
+
}
diff --git a/src/core/VisibleInventory.ts b/src/core/VisibleInventory.ts
index f2adda5..f3b0e47 100644
--- a/src/core/VisibleInventory.ts
+++ b/src/core/VisibleInventory.ts
@@ -105,4 +105,8 @@ export class VisibleInventory implements DisplayableInventory{
public resetCount(): void {
this.count = this.slots.length;
}
+
+ public clearForEventide(): void {
+ this.count-=this.slots.clearAllButKeyItems();
+ }
}
diff --git a/src/surfaces/DisplayPane.tsx b/src/surfaces/DisplayPane.tsx
index dd77df7..7c3c1c7 100644
--- a/src/surfaces/DisplayPane.tsx
+++ b/src/surfaces/DisplayPane.tsx
@@ -58,12 +58,8 @@ export const DisplayPane: React.FC = ({command,editCommand,dis
const cmdString = e.target.value;
setCommandString(cmdString);
const parsedCommand = parseCommand(cmdString);
- if(parsedCommand){
- editCommand(parsedCommand);
- setHasError(false);
- }else{
- setHasError(true);
- }
+ editCommand(parsedCommand);
+ setHasError(cmdString!=="" &&!cmdString.startsWith("#") && !parsedCommand.isValid());
}}>
diff --git a/src/surfaces/OptionPage.tsx b/src/surfaces/OptionPage.tsx
index fdccd61..b422d99 100644
--- a/src/surfaces/OptionPage.tsx
+++ b/src/surfaces/OptionPage.tsx
@@ -1,107 +1,106 @@
-import { TitledList } from "components/TitledList"
-import { parseCommand } from "core/Parser";
-import { saveAs } from "data/FileSaver";
-import { useRef, useState } from "react";
-
-type OptionPageProps = {
- interlaceInventory: boolean,
- setInterlaceInventory: (value: boolean)=>void,
- commandText: string,
- setCommandText: (value: string)=>void,
-}
-
-export const OptionPage: React.FC = ({
- interlaceInventory,
- setInterlaceInventory,
- commandText,
- setCommandText
-}) => {
- const [currentText, setCurrentText] = useState(commandText);
- const [fileName, setFileName] = useState("");
- const uploadRef = useRef(null);
-
- return (
-
-
-
{
- const files = e.target.files;
- if(files?.length && files[0]){
- const file = files[0];
- const fileName = file.name.endsWith(".txt") ? file.name.substring(0, file.name.length-4) : file.name;
- setFileName(fileName);
- file.text().then(text=>{
- setCurrentText(text);
- setCommandText(text);
- });
- }
- }}/>
-
-
-
-
-
- Interlace Inventory with GameData
- {
- setInterlaceInventory(!interlaceInventory);
- }}>
- {interlaceInventory ? "ON" : "OFF"}
-
-
-
- Toggle whether Visible Inventory should be displayed separetely from Game Data or interlaced.
-
-
-
Import / Export
-
- You can also directly copy, paste, or edit the commands here
-
-
- {
- if(uploadRef.current){
- uploadRef.current.click();
- }
- }}>
- Import
-
- {
- saveAs(currentText, fileName+".txt" || "dupe.txt");
- }}>
- Export
-
- {
- setFileName(e.target.value);
- }}
- placeholder="File name"
- />
-
-
-
-
-
- )
-}
\ No newline at end of file
+import { TitledList } from "components/TitledList";
+import { saveAs } from "data/FileSaver";
+import { useRef, useState } from "react";
+
+type OptionPageProps = {
+ interlaceInventory: boolean,
+ setInterlaceInventory: (value: boolean)=>void,
+ commandText: string,
+ setCommandText: (value: string)=>void,
+}
+
+export const OptionPage: React.FC = ({
+ interlaceInventory,
+ setInterlaceInventory,
+ commandText,
+ setCommandText
+}) => {
+ const [currentText, setCurrentText] = useState(commandText);
+ const [fileName, setFileName] = useState("");
+ const uploadRef = useRef(null);
+
+ return (
+
+
+
{
+ const files = e.target.files;
+ if(files?.length && files[0]){
+ const file = files[0];
+ const fileName = file.name.endsWith(".txt") ? file.name.substring(0, file.name.length-4) : file.name;
+ setFileName(fileName);
+ file.text().then(text=>{
+ setCurrentText(text);
+ setCommandText(text);
+ });
+ }
+ }}/>
+
+
+
+
+
+ Interlace Inventory with GameData
+ {
+ setInterlaceInventory(!interlaceInventory);
+ }}>
+ {interlaceInventory ? "ON" : "OFF"}
+
+
+
+ Toggle whether Visible Inventory should be displayed separetely from Game Data or interlaced.
+
+
+
Import / Export
+
+ You can also directly copy, paste, or edit the commands here
+
+
+ {
+ if(uploadRef.current){
+ uploadRef.current.click();
+ }
+ }}>
+ Import
+
+ {
+ saveAs(currentText, fileName+".txt" || "dupe.txt");
+ }}>
+ Export
+
+ {
+ setFileName(e.target.value);
+ }}
+ placeholder="File name"
+ />
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/surfaces/ReferencePage.tsx b/src/surfaces/ReferencePage.tsx
index 52498d1..ab70e4c 100644
--- a/src/surfaces/ReferencePage.tsx
+++ b/src/surfaces/ReferencePage.tsx
@@ -13,6 +13,9 @@ export const ReferencePage: React.FC = React.memo(()=>{
getAllItems().map((item, i)=>{item} )
}
Commands
+
+ This is a list of available commands. All commands and items are case-sensitive
+
Initialize X item1 Y item2 Z item3 ...
Used for initializing inventory before simulation
@@ -29,12 +32,17 @@ export const ReferencePage: React.FC = React.memo(()=>{
Example: Initialize 1 Apple 2 Axe 3 Slate 4 SpiritOrb
- Save / Save As NAME
+ Save
+ Save As NAME
+
Simulates a hard save or auto save action
Writes Game Data to the corresponding save slot. The auto saves are specified by NAME.
You can have as many auto saves as you want in the simulator.
+
+ You cannot save on Eventide/ToTS. However, the simulator does not enforce that.
+
Example 1: Save
Example 2: Save As MySave
@@ -128,12 +136,18 @@ export const ReferencePage: React.FC = React.memo(()=>{
Example 3: Sell 10 Apple 5 Diamond
Example 4: Sell 5 Apple From Slot 3
- D&P X item
+ D&P X item1 Y item2 Z item3 ...
Shortcut for drop and pick up, for sorting inventory
- This command drops X item from the first slot, then pick them up
+ This command drops and pick up each item stack in the specified order.
+ You can also repeat items if you are combining more than 2 slots.
- Example: D&P 5 Diamond
+
+ You can only drop from slot 1 with this shortcut.
+
+ Example 1: D&P 5 Diamond
+ Example 2: D&P 20 Shaft 5 Diamond
+ Example 3: D&P 5 Diamond 10 Diamond
Equip item
Equip item In Slot X
@@ -170,7 +184,7 @@ export const ReferencePage: React.FC = React.memo(()=>{
Example: Close Game
- Sync GameData
+ Sync GameData
Copy Visible Inventory to Game Data
Usually done in game by opening and closing inventory.
@@ -182,7 +196,30 @@ export const ReferencePage: React.FC = React.memo(()=>{
This command is currently broken
+
+ Shoot X Arrow
+ Simulates shooting arrow without opening inventory
+
+ When reloading a save with desynced game data, the equipped weapon/bow/shield are automatically corrupted, but not the arrows.
+ To corrupt the equipped arrow slot, you need to shoot an arrow.
+
+
+ This command does not let you select which arrow to shoot.
+ When you reload a save, Link should have the last equipped arrow slot equipped in the overworld.
+ [needs confirmation]
+
+ Example: Shoot 1 Arrow
+ Enter/Exit Eventide
+ Simulates entering/exiting Eventide or Trial of the Sword
+
+ When entering Eventide or TotS, the entire inventory is cleared except for key items regardless of inventory count.
+ While the challenge is active, none of the inventory changes are synced to game data.
+
+
+ When exiting the challenge, the game reloads the game data as if reloading a save
+
+ Example: Enter Eventide