Multiworld stuff

This commit is contained in:
2025-03-13 21:51:34 -05:00
parent c148272d38
commit 054fc9231c
12 changed files with 465 additions and 164 deletions

93
src/components/Multi.vue Normal file
View File

@@ -0,0 +1,93 @@
<script>
import { defineComponent } from "vue";
import { Base64 } from "js-base64";
import axios from "axios";
export default defineComponent({
data() {
return {
multidata: null,
worlds: {},
error: null,
};
},
props: {
id: "",
},
async mounted() {
document.title = `ALttPR Multiworld: ${this.id}`;
await this.fetchMulti();
},
computed: {
permalink() {
return `/multi/${this.id}`;
},
},
methods: {
async fetchMulti() {
await axios.get(`/multi/${this.id}`)
.then(response => {
if (response && response.data && response.data.multidata) {
const multi = response.data;
this.multidata = Base64.toUint8Array(multi.multidata);
this.worlds = multi.worlds;
} else {
this.error = "Error loading multiworld.";
}
})
.catch(error => {
if (error.response?.status == 409) {
// still generating, try again
setTimeout(this.fetchMulti.bind(this), 10000);
} else {
this.error = "Multiworld not found. :(";
}
});
},
async downloadMultidata() {
const blob = new Blob([this.multidata], { type: 'octet/stream' });
const link = document.getElementById('downloader');
link.href = URL.createObjectURL(blob);
link.download = `GK_${this.id}_multidata`;
link.click();
},
}
});
</script>
<template>
<div class="mw-30">
<div v-if="multidata" class="card content-div m-3">
<div class="card-header">
Permalink: <a :href="permalink">{{ permalink }}</a>
</div>
<ul class="list-group list-group-flush">
<li v-for="world in worlds" class="list-group-item">
{{ world.name }}: <a :href="`/seed/${world.id}`">/seed/{{ world.id }}</a>
</li>
<li class="list-group-item">
<button type="submit" class="btn btn-primary submit-btn" :disabled="!multidata" @click="downloadMultidata">
Download Multidata!
</button>
<a id="downloader" style="display: none;" />
</li>
</ul>
</div>
<div v-else-if="error" class="error-message">
{{ error }}
</div>
<div v-else>
<img class="center" src="/bludormspinbig.gif" />
</div>
</div>
</template>
<style scoped>
.error-message {
text-align: center;
font-size: xx-large;
font-weight: bold;
margin-top: 3rem;
}
</style>

View File

@@ -69,6 +69,9 @@ export default defineComponent({
},
settingsMatch(newSettings, preset) {
for (const settingName of Object.keys(newSettings)) {
if (settingName == "player_name") {
continue;
}
if (preset[settingName] != newSettings[settingName]) {
return false;
}

View File

@@ -36,6 +36,7 @@ export default defineComponent({
patch: null,
error: null,
settings: null,
multi: null,
};
},
props: {
@@ -63,6 +64,7 @@ export default defineComponent({
dataLoaded(patch, seedData) {
this.patch = patch;
this.settings = seedData.settings;
this.multi = seedData.parent;
},
async fetchSeed() {
await axios.get(`/seed/${this.id}`)
@@ -175,7 +177,7 @@ export default defineComponent({
</li>
<li v-if="settings" class="list-group-item">
<div class="mb-1">
<SeedSettings :settings="settings" />
<SeedSettings :settings="settings" :multi="multi" />
</div>
</li>
<li class="list-group-item">

View File

@@ -7,6 +7,7 @@ import mustache from "mustache";
export default defineComponent({
props: {
settings: {},
multi: null,
},
computed: {
settingsDisplay() {
@@ -29,7 +30,14 @@ export default defineComponent({
|| (this.settings.maps && this.settings.maps != "dungeon")
|| (this.settings.compasses && this.settings.compasses != "dungeon")
|| (this.settings.prize_shuffle && this.settings.prize_shuffle != "vanilla");
}
},
multilink() {
if (this.multi) {
return `/multi/${this.multi}`;
} else {
return null;
}
},
},
methods: {
}
@@ -37,6 +45,10 @@ export default defineComponent({
</script>
<template>
<div v-if="multi">
Part of Multiworld: <a :href="multilink">{{ multilink }}</a>
<hr class="mt-2 mb-2" />
</div>
<div v-if="settings.randomizer && settingsDisplay.randomizer[settings.randomizer]">
{{ settingsDisplay.randomizer[settings.randomizer] }}
<hr class="mt-2 mb-2" />

View File

@@ -19,6 +19,7 @@ export default defineComponent({
name: null,
color: "primary",
generator: null,
prefix: null,
},
watch: {
async modelValue(newValue, oldValue) {
@@ -44,7 +45,7 @@ export default defineComponent({
},
},
async mounted() {
this.selected = await localforage.getItem(`setting_${this.name}`) ?? this.settings.default;
this.selected = await localforage.getItem(`${this.prefix}setting_${this.name}`) ?? this.settings.default;
this.change();
},
methods: {
@@ -53,7 +54,7 @@ export default defineComponent({
await this.updateLocalForage();
},
async updateLocalForage() {
await localforage.setItem(`setting_${this.name}`, this.selected);
await localforage.setItem(`${this.prefix}setting_${this.name}`, this.selected);
},
},
});
@@ -64,9 +65,9 @@ export default defineComponent({
<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]">
<input type="radio" class="btn-check" :name="name" :id="`${name}_${value}`"
<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="`${name}_${value}`">
<label :class="`btn btn-outline-${color} nav-item m-1`" :for="`${prefix}${name}_${value}`">
{{ settings.values[value].display }}
</label>
</template>

View File

@@ -0,0 +1,209 @@
<script>
import { defineComponent } from "vue";
import axios from "axios";
import { Modal } from "bootstrap";
import AccordionItem from "@/components/AccordionItem.vue";
import PresetPicker from "@/components/PresetPicker.vue";
import SettingPicker from "@/components/SettingPicker.vue";
export default defineComponent({
components: {
AccordionItem,
PresetPicker,
SettingPicker,
},
emits: [
'update:modelValue'
],
props: {
modelValue: {},
prefix: null,
},
data() {
return {
set: {},
existingLocalPresets: [],
newPresetName: "",
replacePreset: null,
modal: null,
};
},
mounted() {
this.modal = new Modal(document.getElementById("savePresetModal"), {});
},
watch: {
set: {
handler(newValue, oldValue) {
this.$refs.preset.settingChanged(newValue);
this.$emit("update:modelValue", this.set);
},
deep: true,
}
},
methods: {
async generate(race) {
const settings = {
randomizer: "base",
race: race ? "race" : "normal"
};
for (const setting of Object.keys(this.set)) {
settings[setting] = this.set[setting];
}
await axios.post("/generate", settings)
.then(response => {
const id = response.data;
this.$router.push(`/seed/${id}`);
})
.catch(error => {
console.log(error);
});
},
presetSelected(preset) {
for (const setting of Object.keys(this.set)) {
if (preset[setting] != undefined) {
this.set[setting] = preset[setting];
}
}
},
savePreset(names) {
this.newPresetName = "";
this.existingLocalPresets = names;
this.modal.show();
},
async modalSavePreset() {
const preset = JSON.parse(JSON.stringify(this.set));
preset.display = this.newPresetName;
if (this.replacePreset != null) {
preset.display = this.existingLocalPresets[this.replacePreset];
}
await this.$refs.preset.savePreset(this.replacePreset, preset);
this.modal.hide();
},
},
});
</script>
<template>
<div class="modal" tabindex="-1" id="savePresetModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Save Preset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<select v-model="replacePreset" class="form-select">
<option :value="null" selected>New</option>
<optgroup v-if="existingLocalPresets.length" label="Overwrite Preset:">
<template v-for="(preset, idx) of existingLocalPresets">
<option :value="idx">
{{ preset }}
</option>
</template>
</optgroup>
</select>
</div>
<div class="input-group mb-3" v-if="replacePreset == null">
<label class="input-group-text" for="new-preset-name">
Name
</label>
<input type="text" class="form-control" placeholder="Display Name"
id="new-preset-name" v-model="newPresetName" autocomplete="off" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button :disabled="replacePreset == null && newPresetName.length == 0" type="button"
class="btn btn-primary" @click="modalSavePreset">
Save Preset
</button>
</div>
</div>
</div>
</div>
<div id="settings" class="accordion accordion-flush">
<AccordionItem>
<PresetPicker ref="preset" generator="base" @selected="presetSelected"
@save="savePreset" />
</AccordionItem>
<AccordionItem :expanded="true">
<template #header>
<b>World:</b>
</template>
<SettingPicker color="primary" v-model="set.mode" name="mode" generator="base" :prefix="prefix" />
<SettingPicker color="primary" v-model="set.weapons" name="weapons" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="true">
<template #header>
<b>Goal:</b>
</template>
<SettingPicker color="success" v-model="set.goal" name="goal" generator="base" :prefix="prefix" />
<SettingPicker color="success" v-if="['ganon', 'fast_ganon'].includes(this.set.goal)"
v-model="set.crystals_ganon" name="crystals_ganon" generator="base" :prefix="prefix" />
<SettingPicker color="success" v-model="set.crystals_gt" name="crystals_gt" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Entrance Shuffle:</b>
</template>
<SettingPicker color="warning" v-model="set.entrance_shuffle" name="entrance_shuffle" generator="base" :prefix="prefix" />
<SettingPicker color="warning" v-if="this.set.entrance_shuffle != 'vanilla'"
v-model="set.skull_woods" name="skull_woods" generator="base" :prefix="prefix" />
<SettingPicker color="warning" v-if="this.set.entrance_shuffle != 'vanilla'"
v-model="set.linked_drops" name="linked_drops" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Dungeon Shuffle:</b>
</template>
<SettingPicker color="danger" v-model="set.door_shuffle" name="door_shuffle" generator="base" />
<SettingPicker color="danger" v-if="this.set.door_shuffle != 'vanilla'"
v-model="set.door_lobbies" name="door_lobbies" generator="base" :prefix="prefix" />
<SettingPicker color="danger" v-if="this.set.door_shuffle != 'vanilla'"
v-model="set.door_type_mode" name="door_type_mode" generator="base" :prefix="prefix" />
<SettingPicker color="danger" v-if="this.set.door_shuffle != 'vanilla'"
v-model="set.trap_door_mode" name="trap_door_mode" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Enemizer:</b>
</template>
<SettingPicker color="success" v-model="set.boss_shuffle" name="boss_shuffle" generator="base" :prefix="prefix" />
<SettingPicker color="success" v-model="set.enemy_shuffle" name="enemy_shuffle" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Dungeon Item Shuffle:</b>
</template>
<SettingPicker color="warning" v-model="set.small_keys" name="small_keys" generator="base" :prefix="prefix" />
<SettingPicker color="warning" v-model="set.big_keys" name="big_keys" generator="base" :prefix="prefix" />
<SettingPicker color="warning" v-model="set.maps" name="maps" generator="base" :prefix="prefix" />
<SettingPicker color="warning" v-model="set.compasses" name="compasses" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Other Pool Settings:</b>
</template>
<SettingPicker color="danger" v-model="set.shop_shuffle" name="shop_shuffle" generator="base" :prefix="prefix" />
<SettingPicker color="danger" v-model="set.drop_shuffle" name="drop_shuffle" generator="base" :prefix="prefix" />
<SettingPicker color="danger" v-model="set.pottery" name="pottery" generator="base" :prefix="prefix" />
<SettingPicker color="danger" v-model="set.prize_shuffle" name="prize_shuffle" generator="base" :prefix="prefix" />
</AccordionItem>
<AccordionItem :expanded="false">
<template #header>
<b>Item Settings:</b>
</template>
<SettingPicker color="primary" v-model="set.boots" name="boots" generator="base" :prefix="prefix" />
<SettingPicker color="primary" v-model="set.flute" name="flute" generator="base" :prefix="prefix" />
<SettingPicker color="primary" v-model="set.dark_rooms" name="dark_rooms" generator="base" :prefix="prefix" />
<SettingPicker color="primary" v-model="set.bombs" name="bombs" generator="base" :prefix="prefix" />
<SettingPicker color="primary" v-model="set.book" name="book" generator="base" :prefix="prefix" />
</AccordionItem>
</div>
</template>