Support rolling mystery seeds

This commit is contained in:
2026-05-25 13:25:08 -05:00
parent 48213fe301
commit def69c430a
10 changed files with 847 additions and 218 deletions
+1 -1
View File
@@ -261,7 +261,7 @@ export default defineComponent({
<MsuResumePicker ref="msu_resume" />
<ReduceFlashingPicker ref="reduce_flashing" />
<QuickswapPicker ref="quickswap" />
<CollectionRatePicker v-if="allow_cr" ref="cr" />
<CollectionRatePicker v-if="allow_cr || settings.mystery == 'mystery'" ref="cr" />
<UltraFastJunkPicker :created="created" :race="settings.race" ref="ufj" />
</div>
</li>
+66 -59
View File
@@ -58,76 +58,83 @@ export default defineComponent({
{{ settingsDisplay.randomizer[settings.randomizer] }}
<hr class="mt-2 mb-2" />
</div>
<div v-if="settings.mode">
Mode: {{ settingsDisplay.mode[settings.mode] }}
</div>
<div v-if="settings.tile_swap && settings.tile_swap != 'vanilla'">
{{ settingsDisplay.tile_swap[settings.tile_swap] }}
</div>
<div v-if="settings.overworld_layout && settings.overworld_layout != 'vanilla'">
{{ settingsDisplay.overworld_layout[settings.overworld_layout] }}
</div>
<div v-if="settings.goal">
Goal: {{ settingsDisplay.goal[settings.goal] }}
</div>
<div v-if="settings.crystals_gt">
Ganon's Tower Entry: {{ settingsDisplay.crystals_gt[settings.crystals_gt] }}
</div>
<div v-if="settings.weapons">
Weapons: {{ settingsDisplay.weapons[settings.weapons] }}
</div>
<div v-if="settings.entrance_shuffle && settings.entrance_shuffle != 'vanilla'">
<hr class="mt-2 mb-2" />
<template v-if="settings.mystery == 'mystery'">
<div>
{{ settingsDisplay.entrance_shuffle[settings.entrance_shuffle] }}
Mystery Settings
</div>
</div>
<div v-if="settings.door_shuffle && settings.door_shuffle != 'vanilla'">
<hr class="mt-2 mb-2" />
<div>
{{ settingsDisplay.door_shuffle[settings.door_shuffle] }}
</template>
<template v-else>
<div v-if="settings.mode">
Mode: {{ settingsDisplay.mode[settings.mode] }}
</div>
<div v-if="settings.lobbies && settings.lobbies != 'vanilla'">
{{ settingsDisplay.lobbies[settings.lobbies] }}
<div v-if="settings.tile_swap && settings.tile_swap != 'vanilla'">
{{ settingsDisplay.tile_swap[settings.tile_swap] }}
</div>
</div>
<div v-if="isEnemizer">
<hr class="mt-2 mb-2" />
<div v-if="settings.enemy_shuffle && settings.enemy_shuffle != 'vanilla'">
{{ settingsDisplay.enemy_shuffle[settings.enemy_shuffle] }}
<div v-if="settings.overworld_layout && settings.overworld_layout != 'vanilla'">
{{ settingsDisplay.overworld_layout[settings.overworld_layout] }}
</div>
<div v-if="settings.boss_shuffle && settings.boss_shuffle != 'vanilla'">
{{ settingsDisplay.boss_shuffle[settings.boss_shuffle] }}
<div v-if="settings.goal">
Goal: {{ settingsDisplay.goal[settings.goal] }}
</div>
</div>
<div v-if="dungeonItemsShuffled">
<hr class="mt-2 mb-2" />
<div v-if="settings.small_keys && settings.small_keys != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.small_keys] }} Small Keys
<div v-if="settings.crystals_gt">
Ganon's Tower Entry: {{ settingsDisplay.crystals_gt[settings.crystals_gt] }}
</div>
<div v-if="settings.big_keys && settings.big_keys != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.big_keys] }} Big Keys
<div v-if="settings.weapons">
Weapons: {{ settingsDisplay.weapons[settings.weapons] }}
</div>
<div v-if="settings.maps && settings.maps != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.maps] }} Maps
<div v-if="settings.entrance_shuffle && settings.entrance_shuffle != 'vanilla'">
<hr class="mt-2 mb-2" />
<div>
{{ settingsDisplay.entrance_shuffle[settings.entrance_shuffle] }}
</div>
</div>
<div v-if="settings.compasses && settings.compasses != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.compasses] }} Compasses
<div v-if="settings.door_shuffle && settings.door_shuffle != 'vanilla'">
<hr class="mt-2 mb-2" />
<div>
{{ settingsDisplay.door_shuffle[settings.door_shuffle] }}
</div>
<div v-if="settings.lobbies && settings.lobbies != 'vanilla'">
{{ settingsDisplay.lobbies[settings.lobbies] }}
</div>
</div>
<div v-if="settings.prize_shuffle && settings.prize_shuffle != 'vanilla'">
{{ settingsDisplay.prize_shuffle[settings.prize_shuffle] }}
<div v-if="isEnemizer">
<hr class="mt-2 mb-2" />
<div v-if="settings.enemy_shuffle && settings.enemy_shuffle != 'vanilla'">
{{ settingsDisplay.enemy_shuffle[settings.enemy_shuffle] }}
</div>
<div v-if="settings.boss_shuffle && settings.boss_shuffle != 'vanilla'">
{{ settingsDisplay.boss_shuffle[settings.boss_shuffle] }}
</div>
</div>
</div>
<div v-if="extraPool">
<hr class="mt-2 mb-2" />
<div v-if="settings.shop_shuffle && settings.shop_shuffle != 'vanilla'">
{{ settingsDisplay.shop_shuffle[settings.shop_shuffle] }}
<div v-if="dungeonItemsShuffled">
<hr class="mt-2 mb-2" />
<div v-if="settings.small_keys && settings.small_keys != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.small_keys] }} Small Keys
</div>
<div v-if="settings.big_keys && settings.big_keys != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.big_keys] }} Big Keys
</div>
<div v-if="settings.maps && settings.maps != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.maps] }} Maps
</div>
<div v-if="settings.compasses && settings.compasses != 'dungeon'">
{{ settingsDisplay.dungeon_items[settings.compasses] }} Compasses
</div>
<div v-if="settings.prize_shuffle && settings.prize_shuffle != 'vanilla'">
{{ settingsDisplay.prize_shuffle[settings.prize_shuffle] }}
</div>
</div>
<div v-if="settings.drop_shuffle && settings.drop_shuffle != 'vanilla'">
{{ settingsDisplay.drop_shuffle[settings.drop_shuffle] }}
<div v-if="extraPool">
<hr class="mt-2 mb-2" />
<div v-if="settings.shop_shuffle && settings.shop_shuffle != 'vanilla'">
{{ settingsDisplay.shop_shuffle[settings.shop_shuffle] }}
</div>
<div v-if="settings.drop_shuffle && settings.drop_shuffle != 'vanilla'">
{{ settingsDisplay.drop_shuffle[settings.drop_shuffle] }}
</div>
<div v-if="settings.pot_shuffle && settings.pot_shuffle != 'vanilla'">
{{ settingsDisplay.pot_shuffle[settings.pot_shuffle] }}
</div>
</div>
<div v-if="settings.pot_shuffle && settings.pot_shuffle != 'vanilla'">
{{ settingsDisplay.pot_shuffle[settings.pot_shuffle] }}
</div>
</div>
</template>
</template>
+1 -1
View File
@@ -76,7 +76,7 @@ export default defineComponent({
<b>{{ settings.display ?? name }}:</b>
<div class="nav nav-pills nav-fill mt-1 mb-1">
<template v-for="value of settings.order ?? Object.keys(settings.values)">
<template v-if="settings.values[value]">
<template v-if="settings.values[value] && !settings.values[value].hidden">
<input type="radio" class="btn-check" :name="`${prefix}${name}`" :id="`${prefix}${name}_${value}`"
autocomplete="off" :value="value" v-model="selected" @change="change" />
<label :class="`btn btn-outline-${color} nav-item m-1`" :for="`${prefix}${name}_${value}`" data-bs-toggle="tooltip" data-bs-placement="bottom" :title="settings.values[value].tip" ref="tips">
+14
View File
@@ -84,6 +84,20 @@ base:
beta:
default:
display: Default
wjb_cross:
display: wjbCross
goal: fast_ganon
show_loot: always
show_map: always
show_loot_hud: never
entrance_shuffle: crossed
skull_woods: vanilla_drops
small_keys: wild
big_keys: wild
maps: wild
compasses: wild
boots: pseudoboots
flute: preactivated
pikit:
default:
display: Default
+37 -1
View File
@@ -93,21 +93,57 @@ bosses_ganon:
display: Bosses for Ganon
default: "8of10"
values:
"0of10":
hidden: true
"1of10":
hidden: true
"2of10":
hidden: true
"3of10":
hidden: true
"4of10":
hidden: true
"5of10":
hidden: true
"6of10":
hidden: true
"7of10":
display: "7 Prize Bosses"
tip: To defeat Ganon, you must defeat 7 of the 10 bosses (not including either form of Agahnim).
"8of10":
display: "8 Prize Bosses"
tip: To defeat Ganon, you must defeat 8 of the 10 bosses (not including either form of Agahnim).
"9of10":
hidden: true
"10of10":
display: "10 Prize Bosses"
tip: To defeat Ganon, you must defeat all 10 bosses (not including either form of Agahnim).
"0of12":
hidden: true
"1of12":
hidden: true
"2of12":
hidden: true
"3of12":
hidden: true
"4of12":
hidden: true
"5of12":
hidden: true
"6of12":
hidden: true
"7of12":
hidden: true
"8of12":
display: "8 Total Bosses"
tip: To defeat Ganon, you must defeat 8 of the 12 dungeon bosses.
"9of12":
hidden: true
"10of12":
display: "10 Total Bosses"
tip: To defeat Ganon, you must defeat 10 of the 12 dungeon bosses.
"11of12":
hidden: true
"12of12":
display: "12 Total Bosses"
tip: To defeat Ganon, you must defeat all 12 of the dungeon bosses.
@@ -460,7 +496,7 @@ dark_rooms:
no_dark_rooms:
display: All Rooms Lit
tip: There are no dark rooms; all rooms that would normally be dark (except for Ganon) are instead fully lit, regardless of whether the player has the lamp.
always_in_logic:
dark_in_logic:
display: Dark Rooms in Logic
tip: Dark rooms are always considered to be in logic, even if the player does not have the lamp.
starting_lamp:
+13 -1
View File
@@ -3,6 +3,7 @@ import SeedView from "@/views/SeedView.vue";
import MultiView from "@/views/MultiView.vue";
import GenerateView from "@/views/GenerateView.vue";
import GenerateMulti from "@/views/GenerateMulti.vue";
import GenerateMystery from "@/views/GenerateMystery.vue";
import GenerateApr2025View from "@/views/GenerateApr2025View.vue";
const router = createRouter({
@@ -38,10 +39,21 @@ const router = createRouter({
props: { generator: "pikit", prefix: "pikit-" },
},
{
path: '/multibeta',
path: '/multiworld/beta',
component: GenerateMulti,
alias: '/multibeta',
props: { generator: "beta", prefix: "dungeon-maps-" },
},
{
path: '/mystery',
component: GenerateMystery,
props: { generator: "base", prefix: "" },
},
{
path: '/mystery/beta',
component: GenerateMystery,
props: { generator: "beta", prefix: "beta-" },
},
{
path: '/seed/:id',
name: 'seed',
+174
View File
@@ -0,0 +1,174 @@
<script>
import { defineComponent } from "vue";
import axios from "axios";
import localforage from "localforage";
import yaml from "js-yaml";
import settingsData from "@/data/settings.yaml";
import generatorSettings from "@/data/generator-settings.yaml";
import VueMarkdown from "vue-markdown-render";
export default defineComponent({
components: {
VueMarkdown,
},
props: {
prefix: null,
generator: null,
},
data() {
return {
uploaded_yaml: null,
yaml_error: null,
};
},
async mounted() {
document.title = "ALttPR Mystery"
},
computed: {
settings() {
const settingsList = generatorSettings[this.generator];
const validSettings = {};
for (const setting of Object.keys(settingsList)) {
var generatorValue = settingsList[setting];
if (generatorValue == "all") {
validSettings[setting] = Object.keys(settingsData[setting].values);
} else if (Array.isArray(generatorValue)) {
validSettings[setting] = generatorValue;
} else {
validSettings[setting] = Object.keys(generatorValue.values);
}
}
return validSettings;
},
},
methods: {
async generate(race) {
await axios.post(`/mystery/${this.generator}`, this.uploaded_yaml, {
headers: {
"Content-Type": "text/yaml",
},
params: {
race: race,
},
})
.then(response => {
const id = response.data;
this.$router.push(`/seed/${id}`);
})
.catch(error => {
this.yaml_error = "Bad YAML";
console.log(error.response?.data);
});
},
yamlErrors(yaml) {
const errorList = [];
for (const key of Object.keys(yaml)) {
if (!(key in this.settings)) {
errorList.push(`Invalid setting \`${key}\``);
continue;
}
if (yaml[key] instanceof Object) {
for (const option of Object.keys(yaml[key])) {
if (!this.settings[key].includes(option)) {
errorList.push(`Invalid setting \`${key}.${option}\``);
continue;
}
const weight = yaml[key][option];
if (!/^\d+$/.test(weight)) {
errorList.push(`Invalid setting weight \`${yaml[key][option]}\` for \`${key}.${option}\``);
}
}
} else {
const option = yaml[key];
if (!this.settings[key].includes(option)) {
errorList.push(`Invalid setting \`${key}.${option}\``);
}
}
}
return errorList;
},
uploadYaml(file) {
if (!file) {
this.yaml_error = null;
this.uploaded_yaml = null;
return;
}
const reader = new FileReader();
reader.onload = async function() {
try {
const parsed_yaml = yaml.load(reader.result, { schema: yaml.FAILSAFE_SCHEMA });
const errors = this.yamlErrors(parsed_yaml);
if (errors) {
this.uploaded_yaml = null;
this.yaml_error = errors.join("\\\n");
} else {
this.uploaded_yaml = reader.result;
this.yaml_error = null;
}
} catch (error) {
this.uploaded_yaml = null;
this.yaml_error = "Error parsing YAML";
if (error.reason) {
this.yaml_error += `: ${error.reason}`;
}
console.log(error);
}
}.bind(this);
reader.readAsText(file);
},
},
});
</script>
<template>
<div class="mw-30">
<div class="card content-div m-3">
<div class="card-header">
Generate Mystery Seed
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
Upload Mystery YAML:
<a class="example-yaml-link" href="/sample_mystery.yaml">(example)</a>
<div class="input-group mt-2 mb-2">
<input id="yaml-input" class="form-control" type="file" accept=".yaml,.yml" @change="uploadYaml($event.target.files[0], true)" />
</div>
<div v-if="yaml_error" class="invalid mt-3">
<vue-markdown :source="yaml_error" />
</div>
</li>
</ul>
<div class="card-footer">
<div class="nav nav-pills nav-fill" role="group">
<button type="button" class="m-1 nav-item btn btn-outline-danger"
@click="generate(true);" :disabled="uploaded_yaml == null || yaml_error">
Generate Race Mystery
</button>
<button type="button" class="m-1 nav-item btn btn-outline-primary"
@click="generate(false);" :disabled="uploaded_yaml == null || yaml_error">
Generate Mystery
</button>
</div>
</div>
</div>
</div>
</template>
<style>
.example-yaml-link {
position: absolute;
right: 0;
padding-right: var(--bs-list-group-item-padding-x);
}
</style>