isBossDefeated,
isChestOpen,
} from './alttp-ram';
+import Logic from './logic';
export const BOOLEAN_STATES = [
'blue-boomerang',
];
export const INTEGER_STATES = [
- 'bottle',
+ 'bottle-1',
+ 'bottle-2',
+ 'bottle-3',
+ 'bottle-4',
'heart-piece',
'lift',
'mail',
mail: 1,
};
+export const BOTTLE_CONTENTS = [
+ 'mushroom',
+ 'bottle',
+ 'red-potion',
+ 'green-potion',
+ 'blue-potion',
+ 'fairy',
+ 'bottle-bee',
+ 'bottle-good-bee',
+];
+
export const BOSSES = [
'armos',
'lanmolas',
];
export const CONFIG = {
+ bossShuffle: false,
+ checkCalculation: 'room-data',
+ glitches: 'none',
+ mapLayout: 'horizontal',
+ showMap: 'situational',
+ showCompass: 'situational',
+ showSmall: 'always',
+ showBig: 'always',
wildMap: false,
wildCompass: false,
wildSmall: false,
wildBig: false,
- bossShuffle: false,
+ worldState: 'open',
};
export const DUNGEONS = [
{
id: 'hype-cave-bottom',
room: 0x11E,
- chest: 4,
+ chest: 3,
},
{
id: 'hype-cave-npc',
{
id: 'paradox-lower-far-right',
room: 0xEF,
- chest: 4,
+ chest: 3,
},
{
id: 'paradox-lower-mid',
room: 0xEF,
- chest: 5,
+ chest: 4,
},
{
id: 'paradox-upper-left',
},
];
+export const getConfigValue = (config, name, fallback) =>
+ Object.prototype.hasOwnProperty.call(config, name) ? config[name] : fallback;
+
+export const configureDungeons = config => DUNGEONS.map(dungeon => {
+ const newDungeon = JSON.parse(JSON.stringify(dungeon));
+ if (config.wildMap && dungeon.map) {
+ ++newDungeon.items;
+ }
+ if (config.wildCompass && dungeon.compass) {
+ ++newDungeon.items;
+ }
+ if (config.wildSmall) {
+ newDungeon.items += dungeon.sk;
+ }
+ if (config.wildBig && dungeon.bk && !dungeon.dropBk) {
+ ++newDungeon.items;
+ }
+ if (dungeon.boss) {
+ newDungeon.bosses = config.bossShuffle ? BOSSES : [dungeon.boss];
+ }
+ return newDungeon;
+});
+
+export const applyLogic = (config, dungeons, state) => {
+ const logic = Logic[config.worldState];
+ const map = {};
+ for (const name in logic) {
+ try {
+ map[name] = logic[name](config, dungeons, state);
+ } catch (e) {
+ console.error('error evaluating', name, e);
+ }
+ }
+ return map;
+};
+
+export const shouldShowDungeonItem = (config, which) => {
+ const show = config[`show${which}`] || 'always';
+ const wild = config[`wild${which}`] || false;
+ switch (show) {
+ default:
+ case 'always':
+ return true;
+ case 'situational':
+ return wild || (which === 'Compass' && config.bossShuffle);
+ case 'never':
+ return false;
+ }
+};
+
export const toggleBoolean = name => state => ({
...state,
[name]: !state[name],
return { ...state, ...changes };
};
-export const hasDungeonBoss = (state, dungeon) => !!state[`${dungeon.id}-boss-defeated`];
+export const countClearedLocations = (state, locations) =>
+ locations.reduce((acc, cur) => state[cur] ? acc + 1 : acc, 0);
+
+export const hasClearedLocations = (state, locations) =>
+ countClearedLocations(state, locations) === locations.length;
+
+export const getLocationStatus = (name, logic, state) => {
+ if (state[name]) return 'cleared';
+ if (logic[name]) return logic[name];
+ return logic.fallback;
+};
+
+export const getCombinedStatus = statuses => {
+ if (statuses.filter(s => s === 'cleared').length === statuses.length) {
+ return 'cleared';
+ }
+ if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) {
+ return 'available';
+ }
+ if (statuses.filter(s => ['unavailable', 'cleared'].includes(s)).length === statuses.length) {
+ return 'unavailable';
+ }
+ return 'partial';
+};
+
+export const aggregateLocationStatus = (names, logic, state) => {
+ const statuses = names.map(name => getLocationStatus(name, logic, state));
+ return getCombinedStatus(statuses);
+};
+
+export const countRemainingLocations = (state, locations) =>
+ locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
+
+export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7);
+
+export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7);
+
+export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
+
+export const hasDungeonBoss = (state, dungeon) =>
+ !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];
export const getDungeonBoss = (state, dungeon) =>
- state[`${dungeon.id}-boss`] || dungeon.boss || null;
+ dungeon.bosses.length > 1
+ ? state[`${dungeon.id}-boss`] || dungeon.boss || null
+ : dungeon.bosses[0];
-export const hasDungeonPrize = (state, dungeon) => !!state[`${dungeon.id}-prize-acquired`];
+export const hasDungeonPrize = (state, dungeon) =>
+ !dungeon.prize || !!state[`${dungeon.id}-prize-acquired`];
export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null;
+export const getDungeonClearedItems = (state, dungeon) => state[`${dungeon.id}-checks`] || 0;
+
+export const getDungeonRemainingItems = (state, dungeon) =>
+ Math.max(0, dungeon.items - getDungeonClearedItems(state, dungeon));
+
+export const getDungeonAcquiredSKs = (state, dungeon) => state[`${dungeon.id}-small-key`] || 0;
+
+export const isDungeonCleared = (state, dungeon) => {
+ const hasItems = !getDungeonRemainingItems(state, dungeon);
+ const hasBoss = hasDungeonBoss(state, dungeon);
+ const hasPrize = hasDungeonPrize(state, dungeon);
+ return hasItems && hasBoss && hasPrize;
+};
+
+export const aggregateDungeonStatus = (dungeon, logic, state) => {
+ if (isDungeonCleared(state, dungeon)) {
+ return 'cleared';
+ }
+ if (logic[dungeon.id] === 'unavailable') {
+ return 'unavailable';
+ }
+ const checks = [...dungeon.checks];
+ if (['ct', 'gt'].includes(dungeon.id)) {
+ checks.push(`${dungeon.id}-boss-killable`);
+ }
+ const statuses = checks.map(name => getLocationStatus(name, logic, state));
+ return getCombinedStatus(statuses);
+};
+
+export const toggleBossDefeated = dungeon => toggleBoolean(`${dungeon.id}-boss-defeated`);
+
+export const setBossDefeated = (dungeon, defeated) =>
+ state => ({ ...state, [`${dungeon.id}-boss-defeated`]: !!defeated });
+
+export const togglePrizeAcquired = dungeon => toggleBoolean(`${dungeon.id}-prize-acquired`);
+
+export const setPrizeAcquired = (dungeon, acquired) =>
+ state => ({ ...state, [`${dungeon.id}-prize-acquired`]: !!acquired });
+
+export const addDungeonCheck = dungeon => increment(`${dungeon.id}-checks`, dungeon.items);
+
+export const removeDungeonCheck = dungeon => decrement(`${dungeon.id}-checks`, dungeon.items);
+
+export const resetDungeonChecks = dungeon => state => ({ ...state, [`${dungeon.id}-checks`]: 0 });
+
+export const completeDungeonChecks = dungeon =>
+ state => ({ ...state, [`${dungeon.id}-checks`]: dungeon.items });
+
export const makeEmptyState = () => {
const state = {};
BOOLEAN_STATES.forEach(p => {
state[`${dungeon.id}-prize`] = 'crystal';
state[`${dungeon.id}-prize-acquired`] = false;
}
+ if (dungeon.id === 'gt') {
+ state['gt-bot-boss'] = 'armos';
+ state['gt-mid-boss'] = 'lanmolas';
+ state['gt-top-boss'] = 'moldorm';
+ }
});
OVERWORLD_LOCATIONS.forEach(location => {
state[location.id] = false;
state.duck = !!(data[INV_ADDR.RANDO_FLUTE] & 0x01);
state.bugnet = !!data[INV_ADDR.BUGNET];
state.book = !!data[INV_ADDR.BOOK];
- state.bottle = 0;
- if (data[INV_ADDR.BOTTLE_1]) {
- ++state.bottle;
- }
- if (data[INV_ADDR.BOTTLE_2]) {
- ++state.bottle;
- }
- if (data[INV_ADDR.BOTTLE_3]) {
- ++state.bottle;
- }
- if (data[INV_ADDR.BOTTLE_4]) {
- ++state.bottle;
- }
+ state['bottle-1'] = data[INV_ADDR.BOTTLE_1];
+ state['bottle-2'] = data[INV_ADDR.BOTTLE_2];
+ state['bottle-3'] = data[INV_ADDR.BOTTLE_3];
+ state['bottle-4'] = data[INV_ADDR.BOTTLE_4];
state.somaria = !!data[INV_ADDR.SOMARIA];
state.byrna = !!data[INV_ADDR.BYRNA];
state.cape = !!data[INV_ADDR.CAPE];
state[`${dungeon.id}-compass`] = !!(compass & dungeon.mask);
state[`${dungeon.id}-small-key`] = data[INV_ADDR.RANDO_KEY_START + dungeon.offset];
state[`${dungeon.id}-big-key`] = !!(bigKey & dungeon.mask);
+ state[`${dungeon.id}-checks-collected`] =
+ data[INV_ADDR.RANDO_CHECKS_START + dungeon.offset];
if (dungeon.prize) {
const isCrystal = prizeMap[dungeon.offset].isCrystal;
const prizeFlags = data[isCrystal ? INV_ADDR.CRYSTALS : INV_ADDR.PENDANTS];
- state[`${dungeon.id}-prize-acquired`] = !!(prizeFlags & prizeMap[dungeon.offset].mask);
+ const prizeAcquired = !!(prizeFlags & prizeMap[dungeon.offset].mask);
+ state[`${dungeon.id}-prize-acquired`] = prizeAcquired;
+ if (prizeAcquired) {
+ if (!isCrystal) {
+ if (prizeMap[dungeon.offset].mask === 1) {
+ state[`${dungeon.id}-prize`] = 'red-pendant';
+ } else if (prizeMap[dungeon.offset].mask === 2) {
+ state[`${dungeon.id}-prize`] = 'blue-pendant';
+ } else if (prizeMap[dungeon.offset].mask === 4) {
+ state[`${dungeon.id}-prize`] = 'green-pendant';
+ }
+ } else {
+ state[`${dungeon.id}-prize`] = 'crystal';
+ }
+ }
}
});
};
});
};
-export const computeState = (data, prizeMap) => {
- const state = {};
- collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap);
- collectOverworld(state, data);
- collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START));
- return state;
-};
-
const getDungeonAmounts = (config, state) => {
const amounts = {};
DUNGEONS.forEach(dungeon => {
let amount = 0;
let total = dungeon.checks.length;
- dungeon.checks.forEach(check => {
- if (state[check]) {
- ++amount;
- }
- });
+ if (config.checkCalculation === 'inventory') {
+ amount = state[`${dungeon.id}-checks-collected`];
+ } else {
+ dungeon.checks.forEach(check => {
+ if (state[check]) {
+ ++amount;
+ }
+ });
+ }
if (!config.wildMap && state[`${dungeon.id}-map`]) {
--amount;
--total;
return amounts;
};
-export const mergeStates = (config, cur, inc) => {
- const next = { ...cur, ...inc };
- const amounts = getDungeonAmounts(config, inc);
+export const computeState = (config, data, prizeMap) => {
+ const state = {};
+ collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap);
+ collectOverworld(state, data);
+ collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START));
+ const amounts = getDungeonAmounts(config, state);
+ DUNGEONS.forEach(dungeon => {
+ state[`${dungeon.id}-checks`] = amounts[dungeon.id];
+ });
+ return state;
+};
+
+export const mergeStates = (autoState, manualState) => {
+ const next = { ...autoState };
+ BOOLEAN_STATES.forEach(name => {
+ if (manualState[name]) {
+ next[name] = true;
+ }
+ });
+ INTEGER_STATES.forEach(name => {
+ next[name] = Math.max(autoState[name] || 0, manualState[name] || 0);
+ });
DUNGEONS.forEach(dungeon => {
- next[`${dungeon.id}-checks`] = amounts[dungeon.id];
+ next[`${dungeon.id}-small-key`] += manualState[`${dungeon.id}-small-key`] || 0;
+ next[`${dungeon.id}-checks`] += manualState[`${dungeon.id}-checks`] || 0;
+ if (manualState[`${dungeon.id}-big-key`]) {
+ next[`${dungeon.id}-big-key`] = true;
+ }
+ if (manualState[`${dungeon.id}-compass`]) {
+ next[`${dungeon.id}-compass`] = true;
+ }
+ if (manualState[`${dungeon.id}-map`]) {
+ next[`${dungeon.id}-map`] = true;
+ }
+ if (manualState[`${dungeon.id}-boss`]) {
+ next[`${dungeon.id}-boss`] = manualState[`${dungeon.id}-boss`];
+ }
+ if (manualState[`${dungeon.id}-boss-defeated`]) {
+ next[`${dungeon.id}-boss-defeated`] = true;
+ }
+ if (manualState[`${dungeon.id}-prize`] &&
+ manualState[`${dungeon.id}-prize`] !== 'crystal'
+ ) {
+ next[`${dungeon.id}-prize`] = manualState[`${dungeon.id}-prize`];
+ } else if (!next[`${dungeon.id}-prize`]) {
+ next[`${dungeon.id}-prize`] = 'crystal';
+ }
+ if (manualState[`${dungeon.id}-prize-acquired`]) {
+ next[`${dungeon.id}-prize-acquired`] = true;
+ }
+ });
+ OVERWORLD_LOCATIONS.forEach(loc => {
+ if (manualState[loc.id]) {
+ next[loc.id] = true;
+ }
+ });
+ UNDERWORLD_LOCATIONS.forEach(loc => {
+ if (manualState[loc.id]) {
+ next[loc.id] = true;
+ }
});
+ // prefer auto
+ next['bottle-1'] = autoState['bottle-1'] || manualState['bottle-1'] || 0;
+ next['bottle-2'] = autoState['bottle-2'] || manualState['bottle-2'] || 0;
+ next['bottle-3'] = autoState['bottle-3'] || manualState['bottle-3'] || 0;
+ next['bottle-4'] = autoState['bottle-4'] || manualState['bottle-4'] || 0;
+ // force manual
+ next['mm-medallion'] = manualState['mm-medallion'];
+ next['tr-medallion'] = manualState['tr-medallion'];
+ next['gt-crystals'] = manualState['gt-crystals'];
+ next['ganon-crystals'] = manualState['ganon-crystals'];
+ next['gt-bot-boss'] = manualState['gt-bot-boss'];
+ next['gt-mid-boss'] = manualState['gt-mid-boss'];
+ next['gt-top-boss'] = manualState['gt-top-boss'];
//console.log(next);
return next;
};