Enhance spoiler UI

This commit is contained in:
2026-05-25 21:41:50 -05:00
parent e60a4c4d57
commit 1e364f8af5
13 changed files with 664 additions and 68 deletions

View File

@@ -1,10 +1,20 @@
.mw-60 {
max-width: 60rem;
.mw-30 {
max-width: 30rem;
margin: 0 auto;
}
.mw-30 {
max-width: 30rem;
.mw-40 {
max-width: 40rem;
margin: 0 auto;
}
.mw-50 {
max-width: 50rem;
margin: 0 auto;
}
.mw-60 {
max-width: 60rem;
margin: 0 auto;
}
@@ -39,3 +49,33 @@
.sprite-preview-name {
padding-left: 0.7em;
}
.spoiler-table {
width: 100%;
--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.04);
--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.2);
margin-bottom: 1.5rem;
}
.spoiler-table th, .spoiler-table td {
border-top: none;
padding-top: 0;
padding-bottom: 2px;
border-bottom: 1px dotted #ccc;
}
.spoiler-table tr:first-child th {
padding-top: 0.75rem;
}
.spoiler-table th {
border-bottom: none;
}
.spoiler-table tr:last-child th {
border-bottom: 1px solid #bbb;
}
.spoiler-table tr:last-child td {
border-bottom: none;
}

View File

@@ -0,0 +1,50 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
label: "",
entrances: {},
entrance_arrow: null,
},
});
</script>
<template>
<template v-if="entrances.length > 0">
<thead>
<tr>
<th colspan="3">{{ label }}:</th>
</tr>
<tr>
<th>Interior:</th>
<th></th>
<th>Exterior:</th>
</tr>
</thead>
<tbody>
<template v-for="entrance in entrances">
<tr>
<td>{{ entrance.exit }}</td>
<td class="px-3">
<template v-if="entrance.direction == 'both'">
<i class="bi bi-arrow-left-right"></i>
</template>
<template v-else-if="entrance.direction == 'entrance'">
<template v-if="entrance_arrow != null">
<i class="bi" :class="entrance_arrow"></i>
</template>
<template v-else>
<i class="bi bi-arrow-left"></i>
</template>
</template>
<template v-else-if="entrance.direction == 'exit'">
<i class="bi bi-arrow-right"></i>
</template>
</td>
<td>{{ entrance.entrance }}</td>
</tr>
</template>
</tbody>
</template>
</template>

View File

@@ -0,0 +1,96 @@
<script>
import { defineComponent } from "vue";
import EntranceSection from "@/components/EntranceSection.vue";
import spoilerData from "@/data/spoiler.yaml";
export default defineComponent({
components: {
EntranceSection,
},
props: {
entrances: Array,
},
computed: {
mapping() {
const ret = {};
for (const entrance of this.entrances) {
if (!(entrance.exit in ret)) {
ret[entrance.exit] = {}
}
ret[entrance.exit][entrance.direction] = entrance;
}
return ret;
},
specific() {
return spoilerData.entrance_categories.dungeons
.concat(spoilerData.entrance_categories.dropdowns)
.concat(spoilerData.entrance_categories.connectors);
},
dungeons() {
return this.filter("dungeons");
},
dropdowns() {
return this.filter("dropdowns");
},
connectors() {
return this.filter("connectors");
},
other() {
const ret = [];
for (const exit of Object.keys(this.mapping)) {
if (this.specific.includes(exit)) {
continue;
}
const data = this.mapping[exit];
if (data.both) {
ret.push(data.both);
}
if (data.entrance) {
ret.push(data.entrance);
}
if (data.exit) {
ret.push(data.exit);
}
}
return ret;
},
},
methods: {
filter(category) {
const ret = [];
for (const exit of spoilerData.entrance_categories[category]) {
if (exit in this.mapping) {
const data = this.mapping[exit];
if (data.both) {
ret.push(data.both);
}
if (data.entrance) {
ret.push(data.entrance);
}
if (data.exit) {
ret.push(data.exit);
}
}
}
return ret;
},
},
});
</script>
<template>
<table class="spoiler-table table-striped table-hover">
<EntranceSection label="Dungeons" :entrances="dungeons" />
<EntranceSection label="Dropdowns" :entrances="dropdowns" />
<EntranceSection label="Connectors" :entrances="connectors" />
<EntranceSection label="Other" :entrances="other" entrance_arrow="bi-arrow-left-right" />
</table>
</template>

View File

@@ -0,0 +1,84 @@
<script>
import { defineComponent } from "vue";
import spoilerData from "@/data/spoiler.yaml";
export default defineComponent({
props: {
spoiler: {},
category: {},
player_name: null,
},
computed: {
item_locations() {
const ret = {}
for (const category of Object.keys(this.spoiler)) {
var found = false;
for (const categoryPrefix of spoilerData.item_sections) {
if (category.startsWith(categoryPrefix)) {
found = true;
break;
}
}
if (!found) {
continue;
}
const list = this.spoiler[category];
for (const loc of Object.keys(list)) {
ret[loc] = list[loc];
}
}
return ret;
},
suffix() {
if (this.player_name) {
return ` (${this.player_name})`;
} else {
return "";
}
},
item_mappings() {
const ret = {}
for (const item of this.category.items) {
ret[item + this.suffix] = { display: item, locations: [] };
}
for (const loc of Object.keys(this.item_locations)) {
const item = this.item_locations[loc];
if (item in ret) {
ret[item].locations.push(loc);
}
}
return ret;
},
},
});
</script>
<template>
<table class="spoiler-table table-striped table-hover">
<thead>
<tr>
<th colspan="2">{{ category.display }}:</th>
</tr>
</thead>
<tbody>
<template v-for="(data) in item_mappings">
<template v-if="data.locations.length > 0">
<tr>
<td>{{ data.display }}</td>
<td>
<template v-for="location in data.locations">
{{ location }}<br>
</template>
</td>
</tr>
</template>
</template>
</tbody>
</table>
</template>

View File

@@ -81,11 +81,11 @@ export default defineComponent({
<div class="mw-30">
<div v-if="multidata" class="card content-div m-3">
<div class="card-header">
Permalink: <a :href="permalink">{{ permalink }}</a>
Permalink: <router-link :to="permalink">{{ permalink }}</router-link>
</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>
{{ world.name }}: <router-link :to="`/seed/${world.id}`">/seed/{{ world.id }}</router-link>
</li>
<li class="list-group-item">
<button type="submit" class="btn btn-primary submit-btn" :disabled="!multidata" @click="downloadMultidata">

View File

@@ -0,0 +1,42 @@
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
spoiler: {},
},
});
</script>
<template>
<table class="spoiler-table table-striped table-hover">
<template v-for="(items, sphere) in spoiler.playthrough">
<template v-if="sphere == '0' && items.length > 0">
<thead>
<tr>
<th colspan="2">Starting Items:</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, location) in items">
<td></td>
<td>{{ item }}</td>
</tr>
</tbody>
</template>
<template v-if="sphere != '0'">
<thead>
<tr>
<th colspan="2">Sphere {{ sphere }}:</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, location) in items">
<td>{{ location }}</td>
<td>{{ item }}</td>
</tr>
</tbody>
</template>
</template>
</table>
</template>

View File

@@ -218,7 +218,7 @@ export default defineComponent({
<div class="mw-30">
<div v-if="patch" class="card content-div m-3">
<div class="card-header">
<b>Permalink</b>: <a :href="permalink">{{ permalink }}</a>
<b>Permalink</b>: <router-link :to="permalink">{{ permalink }}</router-link>
</div>
<div class="card-header" v-if="meta.hash">
<b>Hash</b>: {{ meta.hash }}
@@ -294,8 +294,8 @@ export default defineComponent({
<img class="block center" src="/bludormspinbig.gif" />
</div>
</div>
<div v-if="patch && spoiler && show_spoiler" class="mw-60">
<Spoiler :spoiler="spoiler" />
<div v-if="patch && spoiler && show_spoiler" class="mw-50">
<Spoiler :spoiler="spoiler" :settings="settings" />
</div>
</template>

View File

@@ -51,7 +51,7 @@ export default defineComponent({
<template>
<div v-if="multi">
{{ settings.player_name }} for Multiworld: <a :href="multilink">{{ multilink }}</a>
{{ settings.player_name }} for Multiworld: <router-link :to="multilink">{{ multilink }}</router-link>
<hr class="mt-2 mb-2" />
</div>
<div v-if="settings.randomizer && settingsDisplay.randomizer[settings.randomizer]">

View File

@@ -0,0 +1,46 @@
<script>
import { defineComponent } from "vue";
import settingsData from "@/data/settings.yaml";
import mustache from "mustache";
export default defineComponent({
props: {
settings: {},
},
computed: {
settingsDisplay() {
const rendered = {};
for (const setting of Object.keys(this.settings)) {
const data = settingsData[setting];
const nameDisplay = data?.display || setting;
const value = this.settings[setting];
const valueDisplay = data?.values && data.values[value]?.display || value;
if (valueDisplay) {
rendered[nameDisplay] = valueDisplay;
}
}
return rendered;
},
},
});
</script>
<template>
<table class="spoiler-table table-striped table-hover">
<thead>
<tr>
<th colspan="2">Settings:</th>
</tr>
</thead>
<tbody>
<template v-for="(value, setting) in settingsDisplay">
<tr>
<td>{{ setting }}</td>
<td>{{ value }}</td>
</tr>
</template>
</tbody>
</table>
</template>

View File

@@ -1,70 +1,89 @@
<script>
import { defineComponent } from "vue";
import SettingsSpoiler from "@/components/SettingsSpoiler.vue";
import PlaythroughSpoiler from "@/components/PlaythroughSpoiler.vue";
import ItemSpoiler from "@/components/ItemSpoiler.vue";
import EntranceSpoiler from "@/components/EntranceSpoiler.vue";
import spoilerData from "@/data/spoiler.yaml";
export default defineComponent({
components: {
SettingsSpoiler,
PlaythroughSpoiler,
ItemSpoiler,
EntranceSpoiler,
},
props: {
settings: {},
spoiler: {},
},
computed: {
item_categories() {
return spoilerData.item_categories;
},
player_slot() {
if (this.settings.player_slot) {
return this.settings.player_slot;
}
return null;
},
entrances() {
if (!this.player_slot) {
return this.spoiler.Entrances;
}
return this.spoiler.Entrances.filter(ent => !ent.player || ent.player == this.player_slot);
},
},
});
</script>
<template>
<div class="card content-div m-3">
<div class="card-header">
Playthrough
</div>
<table class="table table-striped table-hover">
<template v-for="(items, sphere) in spoiler.playthrough">
<template v-if="sphere == '0' && items.length > 0">
<thead>
<tr>
<th colspan="2">Starting Items:</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, location) in items">
<td></td>
<td>{{ item }}</td>
</tr>
</tbody>
</template>
<template v-if="sphere != '0'">
<thead>
<tr>
<th colspan="2">Sphere {{ sphere }}:</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, location) in items">
<td>{{ location }}</td>
<td>{{ item }}</td>
</tr>
</tbody>
</template>
<ul class="nav nav-tabs ps-3 pt-3 pe-3">
<li class="nav-item" role="presentation">
<button class="nav-link active" type="button" role="tab" ref="tabs"
data-bs-toggle="tab" data-bs-target="#settings">
Settings
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" type="button" role="tab" ref="tabs"
data-bs-toggle="tab" data-bs-target="#playthrough">
Playthrough
</button>
</li>
<li class="nav-item" role="presentation" v-for="(category, idx) in item_categories">
<button class="nav-link" type="button" role="tab" ref="tabs"
data-bs-toggle="tab" :data-bs-target="`#category_${idx}`">
{{ category.display }}
</button>
</li>
<li class="nav-item" role="presentation" v-if="entrances.length > 1">
<button class="nav-link" type="button" role="tab" ref="tabs"
data-bs-toggle="tab" data-bs-target="#entrances">
Entrances
</button>
</li>
</ul>
<div class="px-4 tab-content">
<div id="settings" class="tab-pane active show" role="tabpanel">
<SettingsSpoiler :settings="settings" />
</div>
<div id="playthrough" class="tab-pane" role="tabpanel">
<PlaythroughSpoiler :spoiler="spoiler" />
</div>
<template v-for="(category, idx) in item_categories">
<div :id="`category_${idx}`" class="tab-pane" role="tabpanel">
<ItemSpoiler :spoiler="spoiler" :category="category" :player_name="settings.player_name" />
</div>
</template>
</table>
<div id="entrances" class="tab-pane" role="tabpanel">
<EntranceSpoiler :entrances="entrances" />
</div>
</div>
</div>
</template>
<style scoped>
.table {
--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.04);
--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.2);
}
.table th, .table td {
border-top: none;
padding-top: 0;
padding-bottom: 0;
border-bottom: 1px dotted #ccc;
}
.table th {
border-bottom: 1px solid #bbb;
padding-top: 0.75rem;
}
.table tr:last-child td {
border-bottom: none;
}
</style>

View File

@@ -1,10 +1,35 @@
randomizer:
display: Randomizer
default: base
values:
base:
display: Base
beta:
display: Beta
apr2025:
display: April 2025
pikit:
display: Pikit Mode
player_name:
display: Player Name
player_slot:
display: Player Slot
race:
display: Race
default: normal
values:
normal:
display: Normal
race:
display: Race
mystery:
display: Mystery
default: normal
values:
normal:
display: Normal
mystery:
display: Mystery
mode:
display: Mode
default: open

192
src/data/spoiler.yaml Normal file
View File

@@ -0,0 +1,192 @@
item_sections:
- Light World
- Dark World
- Caves
- Hyrule Castle
- Eastern Palace
- Desert Palace
- Tower of Hera
- Agahnims Tower
- Palace of Darkness
- Swamp Palace
- Skull Woods
- Thieves Town
- Ice Palace
- Misery Mire
- Turtle Rock
- Ganons Tower
item_categories:
- display: Prizes
items:
- Green Pendant
- Blue Pendant
- Red Pendant
- Crystal 1
- Crystal 2
- Crystal 3
- Crystal 4
- Crystal 5
- Crystal 6
- Crystal 7
- display: Major Items
items:
- Sword and Shield
- Fighter Sword
- Master Sword
- Tempered Sword
- Golden Sword
- Progressive Sword
- Bow
- Progressive Bow
- Silver Arrows
- Hookshot
- Bomb Upgrade (+10)
- Fire Rod
- Ice Rod
- Bombos
- Ether
- Quake
- Lamp
- Hammer
- Ocarina
- Ocarina (Activated)
- Book of Mudora
- Cane of Somaria
- Magic Mirror
- Pegasus Boots
- Power Glove
- Titans Mitts
- Progressive Glove
- Flippers
- Moon Pearl
- display: Minor Items
items:
- Arrow Upgrade (+5)
- Arrow Upgrade (+10)
- Blue Boomerang
- Red Boomerang
- Bomb Upgrade (+5)
- Mushroom
- Powder
- Shovel
- Bug Catching Net
- Bottle
- Bottle (Green Potion)
- Bottle (Red Potion)
- Bottle (Blue Potion)
- Bottle (Fairy)
- Bottle (Bee)
- Bottle (Good Bee)
- Cane of Byrna
- Magic Cape
- Blue Mail
- Red Mail
- Progressive Armor
- Blue Shield
- Red Shield
- Mirror Shield
- Magic Upgrade (1/2)
- Magic Upgrade (1/4)
- display: Keys
items:
- Big Key (Escape)
- Big Key (Eastern Palace)
- Big Key (Desert Palace)
- Big Key (Tower of Hera)
- Big Key (Agahnims Tower)
- Big Key (Palace of Darkness)
- Big Key (Swamp Palace)
- Big Key (Skull Woods)
- Big Key (Thieves Town)
- Big Key (Ice Palace)
- Big Key (Misery Mire)
- Big Key (Turtle Rock)
- Big Key (Ganons Tower)
- Small Key (Escape)
- Small Key (Eastern Palace)
- Small Key (Desert Palace)
- Small Key (Tower of Hera)
- Small Key (Agahnims Tower)
- Small Key (Palace of Darkness)
- Small Key (Swamp Palace)
- Small Key (Skull Woods)
- Small Key (Thieves Town)
- Small Key (Ice Palace)
- Small Key (Misery Mire)
- Small Key (Turtle Rock)
- Small Key (Ganons Tower)
entrance_categories:
dungeons:
- Hyrule Castle Exit (South)
- Hyrule Castle Exit (East)
- Hyrule Castle Exit (West)
- Eastern Palace Exit
- Desert Palace Exit (South)
- Desert Palace Exit (East)
- Desert Palace Exit (West)
- Desert Palace Exit (North)
- Tower of Hera Exit
- Agahnims Tower Exit
- Palace of Darkness Exit
- Swamp Palace Exit
- Skull Woods First Section Exit
- Skull Woods Second Section Exit (East)
- Skull Woods Second Section Exit (West)
- Skull Woods Final Section Exit
- Thieves Town Exit
- Ice Palace Exit
- Misery Mire Exit
- Turtle Rock Exit (Front)
- Turtle Rock Ledge Exit (West)
- Turtle Rock Ledge Exit (East)
- Turtle Rock Isolated Ledge Exit
- Ganons Tower Exit
dropdowns:
- Hyrule Castle Secret Entrance
- Hyrule Castle Secret Entrance Exit
- Sewer Drop
- Sanctuary Exit
- Lumberjack Tree (top)
- Lumberjack Tree Exit
- Lost Woods Hideout (top)
- Lost Woods Hideout Exit
- Kakariko Well (top)
- Kakariko Well Exit
- Bat Cave (right)
- Bat Cave Exit
- North Fairy Cave
- North Fairy Cave Exit
- Skull Pinball
- Skull Left Drop
- Skull Pot Circle
- Skull Back Drop
- Pyramid
- Pyramid Exit
connectors:
- Elder House Exit (East)
- Elder House Exit (West)
- Two Brothers House Exit (East)
- Two Brothers House Exit (West)
- Old Man Cave Exit (West)
- Old Man Cave Exit (East)
- Old Man House Exit (Bottom)
- Old Man House Exit (Top)
- Spectacle Rock Cave Exit
- Spectacle Rock Cave Exit (Top)
- Spectacle Rock Cave Exit (Peak)
- Paradox Cave Exit (Middle)
- Paradox Cave Exit (Bottom)
- Paradox Cave Exit (Top)
- Spiral Cave Exit (Top)
- Spiral Cave Exit
- Fairy Ascension Cave Exit (Top)
- Fairy Ascension Cave Exit (Bottom)
- Death Mountain Return Cave Exit (East)
- Death Mountain Return Cave Exit (West)
- Bumper Cave Exit (Bottom)
- Bumper Cave Exit (Top)
- Superbunny Cave Exit (Bottom)
- Superbunny Cave Exit (Top)
- Hookshot Cave Front Exit
- Hookshot Cave Back Exit

View File

@@ -111,10 +111,12 @@ export default defineComponent({
try {
const parsed_yaml = yaml.load(reader.result, { schema: yaml.FAILSAFE_SCHEMA });
const errors = this.yamlErrors(parsed_yaml);
if (errors) {
console.log("GWAA");
if (errors.length > 0) {
this.uploaded_yaml = null;
this.yaml_error = errors.join("\\\n");
} else {
console.log("GWAA");
this.uploaded_yaml = reader.result;
this.yaml_error = null;
}
@@ -156,11 +158,11 @@ export default defineComponent({
<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">
@click="generate(true);" :disabled="uploaded_yaml == null">
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">
@click="generate(false);" :disabled="uploaded_yaml == null">
Generate Mystery
</button>
</div>