<template>
<header>
<h1>
Persona 4 Golden
<br />
Guide |
<span class="sub" v-if="routeTitle">{{ routeTitle }}</span>
</h1>
<nav>
<div
class="nav-item clickable"
v-for="item in config.nav.items"
:class="{ 'nav-active': isActiveRoute(item.link) }"
@click="changeRoute(item)"
>
{{ item.name }}
</div>
</nav>
</header>
<main>
<section id="home" v-if="isActiveRoute('home')" class="persona-banner">
<div class="welcome card">
<h2>What's all this about?</h2>
<p>
<span class="key">This is a guide for Persona 4 Golden.</span>
With 23 social links, 69 quests, and countless other things to do in
Persona 4, deciding how to spend time can feel overwhelming. My aim
with this guide was primarily to show the user which social links are
available on any given day so they so they can plan appropriately.
</p>
<br />
<p>
<span class="key">
My intention was to be as spoiler-lite as possible
</span>
, but to also include other things they might otherwise miss, such as
shopping channel rewards, social link openings, and general advice.
I've also included test answers under a spoiler button and social link
dialog 'answers' that can help the player maximize social link points.
</p>
</div>
<div class="card">
<h2>Updates</h2>
<ul>
<li>
<span class="key">October 17th, 2021</span>
Got calendar page, all dates, navigation between them, social links
list page and detail pages for each done as well.
Toggle for dialog on the social link ranks is implemented as well.
This puts us essentially back up to previous functionality
</li>
<li>
<span class="key">October 14th, 2021</span>
Got calendar most of the way there, not quite satisfied with the
implementation, but it's getting there.
</li>
<li>
<span class="key">October 13th, 2021</span>
Porting the site to a vue app. Did home page and data page
</li>
</ul>
<h3>Legacy Updates</h3>
<ul>
<li>
<span class="key">August 30th, 2015</span>
Rebuilt the site again using AngularJS, running much quicker now.
</li>
<li>
<span class="key">July 28th, 2015</span>
Rebuilt the site from the ground up using Express and Swig
</li>
<li>
<span class="key">July 13th, 2015</span>
Added seperate pages for the calendar and social links and added a
small trivia game, which is still in development.
</li>
<li>
<span class="key">July 6th, 2015</span>
Finished Social Link pages including dialog, availablity, and bike
ride days for party characters
</li>
<li>
<span class="key">June 29th, 2015</span>
Finished SLinks availablity on dates pages and added notes for Boxed
Lunches, Bike Trips, and the Shopping Program.
</li>
<li>
<span class="key">June 28th, 2015</span>
Added class and exam answers to dates pages as spoilers.
</li>
<li>
<span class="key">June 26th, 2015</span>
Added a calendar page, with all the dates available for the game,
and added a 404 page as well. Added visual transitions between the
pages. Dates completed through November.
</li>
<li>
<span class="key">June 24th, 2015</span>
Dates pages completed for first month of the game, including social
link options, weather, and events during day and night. Added arrow
navigation to transition between dates pages.
</li>
<li>
<span class="key">June 22nd, 2015</span>
Working Server, dates pages started
</li>
</ul>
</div>
</section>
<section id="calendar" v-if="isActiveRoute('calendar')">
<article class="card" v-for="month in orderedMonths">
<h2>{{ month.name }}</h2>
<ul class="cal-grid">
<li class="day key">Su</li>
<li class="day key">Mo</li>
<li class="day key">Tu</li>
<li class="day key">We</li>
<li class="day key">Th</li>
<li class="day key">Fr</li>
<li class="day key">Sa</li>
<li class="day spacer" v-for="day in offsetDays(month.name)"></li>
<li class="day" v-for="day in month.days">
<span
:class="{ clickable: day.data.date }"
@click="selectDate(day.dateCode)"
>
{{ day.day }}
</span>
</li>
</ul>
</article>
</section>
<section
id="calendar-detail"
v-if="isActiveRoute('calendar-detail') && getRouteItem('date')">
<nav class="button-nav">
<button class="prev clickable" @click="changeDate('prev')">
Previous Date
</button>
<button class="back clickable" @click="config.nav.active = 'calendar'">
Back to Calendar
</button>
<button class="next clickable" @click="changeDate('next')">
Next Date
</button>
</nav>
<div class="card full" v-if="activeDay">
<div class="title">
<img
:src="getWeatherImage(activeDay)"
:alt="`weather icon for ${activeDay.weather}`"
/>
<h2>{{ activeDay.weekDay }}, {{ activeDay.date }}</h2>
</div>
<div class="detail">
<h3>
<span class="key">After School</span>
{{ activeDay.dayAvail }}
</h3>
<section v-if="hasNotes">
<h4>Notes</h4>
<div class="notes" v-if="hasNotes">
<p
class="note"
v-for="note in activeDay?.dayNotes?.split('<br>')"
>
{{ note }}
</p>
</div>
</section>
<section v-if="hasSpoilers">
<h4>Spoilers</h4>
<div class="spoilers">
<article class="spoiler" v-for="spoiler in activeDay.spoilers">
<div class="question clickable" @click="(e) => showSpoiler(e)">
{{ spoiler.question }}
</div>
<div class="answer">{{ spoiler.answer }}</div>
</article>
</div>
</section>
<section v-if="hasDayLinks">
<h4>Available Links</h4>
<div class="slinks detail">
<div
class="arcana clickable"
v-for="arcana in activeDay.socialLinks"
@click="selectArcana(arcana)"
>
<img :src="sLinkImg(arcana)" alt="" />
</div>
</div>
</section>
</div>
</div>
<div class="card full dark">
<div class="title">
<img
:src="getWeatherImage(activeDay)"
:alt="`weather icon for ${activeDay.weather}`"
/>
<h2>{{ activeDay.weekDay }} Night Options</h2>
</div>
<div class="detail">
<h3>
<span class="key">Night</span>
{{ activeDay.nightAvail }}
</h3>
<section v-if="hasNightNotes">
<h4>Night Notes</h4>
<div class="notes">
<p
class="note"
v-for="note in activeDay?.nightNotes?.split('<br>')"
>
{{ note }}
</p>
</div>
</section>
<section v-if="hasNightLinks">
<h4>Available Links</h4>
<div class="slinks detail">
<div
class="arcana night clickable"
v-for="arcana in activeDay.nightLinks"
@click="selectArcana(arcana)"
>
<img :src="sLinkImg(arcana)" alt="" />
</div>
</div>
</section>
</div>
</div>
</section>
<section id="socials" v-if="isActiveRoute('socials')">
<article
class="tile clickable"
v-for="arcana in orderedArcanas"
@click="selectArcana(arcana.img)"
>
<img :src="sLinkImg(arcana.img)" alt="" />
<div class="detail">
<h2>{{ arcana.arcana }} Arcana</h2>
<p class="key">{{ arcana.name }}</p>
</div>
<img :src="sLinkCard(arcana.img)" alt="" />
</article>
</section>
<section
id="social-detail"
v-if="isActiveRoute('social-detail') && getRouteItem('arcana')"
>
<nav class="button-nav">
<button class="prev clickable" @click="changeArcana('prev')">
Previous Arcana
</button>
<button class="back clickable" @click="config.nav.active = 'socials'">
Back to Social Links
</button>
<button class="next clickable" @click="changeArcana('next')">
Next Arcana
</button>
</nav>
<div class="card dark full" v-if="activeArcana">
<div class="title">
<img :src="sLinkImg(activeArcana.img)" alt="" />
<h1>
<div class="key">{{ activeArcana.arcana }} Arcana</div>
{{ activeArcana.name }}
</h1>
<img :src="sLinkCard(activeArcana.img)" alt="" />
</div>
<div class="detail">
<section class="notes" v-if="activeArcana.notes">
<p>{{ activeArcana.notes }}</p>
</section>
<h2 class="key">Rank Dialog Options and Notes</h2>
<section class="ranks">
<article class="rank" v-for="rank in activeArcana.rank">
<h3 class="key title clickable" @click="showSpoiler">
{{ rank.rank || "Rank 1" }}
</h3>
<section class="rank-detail dialog">
<span v-if="rank.notes">
{{ rank.notes }}
</span>
<span v-if="typeof rank === 'string'">
{{ rank }}
</span>
<div v-for="dialog in rank.dialog" v-if="rank.dialog">
<h4>{{ dialog.question }}</h4>
<ul class="answers" v-if="dialog.answers">
<li class="answer" v-for="answer in dialog.answers">
<span class="response">{{ answer[0] }}</span>
<span class="points without">{{ answer[1] }}</span>
<span class="points with">{{ answer[2] }}</span>
</li>
</ul>
</div>
</section>
</article>
</section>
</div>
</div>
</section>
<section id="trivia" v-if="isActiveRoute('trivia')"></section>
<section id="data" v-if="isActiveRoute('data')">
<article class="card">
<h2>Social Link Data</h2>
<ul>
<li>
<span class="key">arcanas</span>
Array of {{ arcanas.length }} Items, Keys for slinks, in order
</li>
<li>
<span class="key">slinks</span>
Object with {{ Object.keys(slinks).length }} Items, Slinks Data
</li>
</ul>
<h3>Slinks Data Format</h3>
<ul>
<li>
<span class="key">name</span>
string
</li>
<li>
<span class="key">arcana</span>
string
</li>
<li>
<span class="key">img</span>
string, lowercase name
</li>
<li>
<span class="key">notes</span>
string
</li>
<li>
<span class="key">rank</span>
Array of Rank Data
</li>
<li>
<h4>Rank Data Format</h4>
<ul>
<li>
<span class="key">rank</span>
string
</li>
<li>
<span class="key">points</span>
number
</li>
<li>
<span class="key">notes</span>
string
</li>
<li>
<span class="key">dialog</span>
Array of Dialog Data
</li>
<li>
<h5>Dialog Data Format</h5>
<ul>
<li>
<span class="key">question</span>
string
</li>
<li>
<span class="key">answers</span>
Array of answers with point values
</li>
</ul>
</li>
</ul>
</li>
</ul>
</article>
<article class="card data">
<h4>Social Links Live Data</h4>
<div class="data-list">
<div v-for="link of slinks">
<h5>{{ link.arcana }} | {{ link.name }}</h5>
<p v-for="rank in link.rank">
<span class="key">{{ rank.rank || "Initial" }}</span>
{{ rank }}
</p>
</div>
</div>
</article>
<article class="card">
<h2>Calendar Data</h2>
<ul>
<li>
<span class="key">dates</span>
Array of {{ dates.length }} Items. Keys for calendar, in order
</li>
<li>
<span class="key">calendar</span>
Object with {{ Object.keys(calendar).length }} Items, Calendar Data
</li>
</ul>
<h3>Calendar Data Format</h3>
<ul>
<li>
<span class="key">date</span>
string, eg: "January 1st"
</li>
<li>
<span class="key">dayAvail</span>
string, eg: "Event - New Years"
</li>
<li>
<span class="key">dayNotes</span>
string, eg: ""
</li>
<li>
<span class="key">fusionForecast</span>
string, eg: ""
</li>
<li>
<span class="key">nightAvail</span>
string, eg: "Restricted"
</li>
<li>
<span class="key">nightLinks</span>
Array of names of available links at night
</li>
<li>
<span class="key">nightNotes</span>
string, eg: "Boxed Lunch ..."
</li>
<li>
<span class="key">socialLinks</span>
Array of names of available links during day
</li>
<li>
<span class="key">spoilers</span>
Array of potential spoilers and quiz answers
</li>
<li>
<span class="key">weather</span>
string, eg: "snowy"
</li>
<li>
<span class="key">weekDay</span>
string, eg: "Sunday"
</li>
</ul>
</article>
<article class="card data">
<h4>Calendar Live Data</h4>
<div class="data-list">
<div v-for="date of dates">
<span class="key">{{ date }}:</span>
{{ calendar[date] }}
</div>
</div>
</article>
</section>
</main>
</template>
<script>
export default {
data() {
return {
config: {
dataUrl:
"https://raw.githubusercontent.com/Lawlheart/Persona-4-Golden-Guide/master/js/data.json",
bifrostUrl: "https://bifrost.loreheart.com/arcade/persona",
nav: {
active: 'socials',
activeItem: {
date: null,
arcana: null
},
items: [
{
name: "Home",
link: "home"
},
{
name: "Calendar",
link: "calendar"
},
{
name: "Social Links",
link: "socials"
},
{
name: "Data",
link: "data"
}
]
}
},
arcanas: [],
calendar: {},
dates: [],
names: {},
slinks: {}
};
},
async created() {
await this.loadPersonaData();
},
computed: {
routeTitle() {
const {
active,
activeItem: { date: dateCode }
} = this.config.nav;
switch (active) {
case "calendar-detail":
const { date } = this.getDateData(dateCode);
return date;
case "social-detail":
return this.activeArcana.arcana;
default:
return active;
break;
}
},
orderedMonthNames() {
return this.dates
.map((dateCode) => {
let { date } = this.calendar[dateCode];
let month = date.split(" ")[0];
return month;
})
.filter((month, i, a) => a.indexOf(month) === i);
},
orderedMonths() {
return this.orderedMonthNames.map((name) => {
return {
name,
days: this.monthDays(name)
};
});
},
activeDay() {
const { date: dateCode } = this.config.nav.activeItem;
return { ...this.getDateData(dateCode) };
},
hasSpoilers() {
const spoilers = { ...this.activeDay.spoilers };
return !!spoilers[0];
},
hasNotes() {
return !!this.activeDay.dayNotes;
},
hasNightNotes() {
return !!this.activeDay.nightNotes;
},
hasDayLinks() {
const socialLinks = { ...this.activeDay.socialLinks };
return !!socialLinks[0];
},
hasNightLinks() {
const socialLinks = { ...this.activeDay.nightLinks };
return !!socialLinks[0];
},
activeArcana() {
const { arcana } = this.config.nav.activeItem;
return { ...this.slinks[arcana] };
},
orderedArcanas() {
return this.arcanas.map((arcana) => ({ ...this.slinks[arcana] }));
}
},
methods: {
changeRoute(item) {
this.config.nav.active = item.link;
},
isActiveRoute(route) {
return this.config.nav.active === route;
},
getRouteItem(itemName) {
return this.config.nav.activeItem[itemName];
},
showSpoiler(e) {
e.target.nextSibling.classList.toggle("show");
},
resetSpoiler() {
try {
let spoilers = document.querySelectorAll(".show");
if (spoilers.length) {
spoilers.forEach((s) => {
s.classList.toggle("show");
});
}
} catch (e) {
console.log(e);
}
},
offsetDays(monthName) {
const date = this.getPersonaDate(monthName, 1);
const offset = date.getDay();
return Array.from(Array(offset).keys());
},
monthDays(monthName) {
const possible = Array.from(Array(31).keys()).map((n) => n + 1);
const days = possible.filter((day) => this.isValidDate(monthName, day));
return days.map((day) => {
const dateCode = this.getDateCode(monthName, day);
const data = this.getDateData(dateCode);
return { data, dateCode, day };
});
},
isValidDate(month, day) {
const date = this.getPersonaDate(month, day);
return date.getDate() === day;
},
getPersonaDate(month, day) {
let year;
switch (month) {
case "January":
case "February":
case "March":
year = "2017";
break;
default:
year = "2016";
break;
}
return new Date(`${month} ${day}, ${year}`);
},
getDateCode(month, day) {
const date = this.getPersonaDate(month, day);
const first = `${date.getMonth() + 1 < 10 ? "0" : ""}${
date.getMonth() + 1
}`;
const second = `${day < 10 ? "0" : ""}${day}`;
return `${first}${second}`;
},
getDateData(dateCode) {
return this.calendar[dateCode] ? { ...this.calendar[dateCode] } : {};
},
selectDate(dateCode) {
this.config.nav.activeItem.date = dateCode;
this.config.nav.active = "calendar-detail";
},
changeDate(direction = "prev") {
const { date: dateCode } = this.config.nav.activeItem;
let index = this.dates.indexOf(dateCode);
switch (direction) {
case "prev":
index--;
break;
case "next":
index++;
break;
}
const newDateCode = this.dates[index];
if (newDateCode) {
this.selectDate(newDateCode);
this.resetSpoiler();
}
},
selectArcana(arcana) {
this.config.nav.activeItem.arcana = arcana;
this.config.nav.active = "social-detail";
},
changeArcana(direction = "prev") {
const { arcana } = this.config.nav.activeItem;
let index = this.arcanas.indexOf(arcana);
switch (direction) {
case "prev":
index--;
break;
case "next":
index++;
break;
}
const newArcana = this.arcanas[index];
if (newArcana) {
this.selectArcana(newArcana);
this.resetSpoiler();
}
},
async loadPersonaData() {
const url = this.config.dataUrl;
let {
data: { arcanaList: arcanas, calendar, dates, names, slinks }
} = await axios.get(url);
this.arcanas = arcanas;
this.calendar = calendar;
this.dates = dates;
this.names = names;
this.slinks = slinks;
},
sLinkImg(arcana = "fool") {
return this.getBifrostAsset(`/arcanas/${arcana}.png`);
},
sLinkCard(arcana = "fool") {
return this.getBifrostAsset(`/cards/${arcana}-card.png`);
},
getWeatherImage({ weather = "rain" }) {
return this.getBifrostAsset(`/weather/${weather}.png`);
},
getBifrostAsset(endpoint = "") {
return `${this.config.bifrostUrl}${endpoint}`;
}
}
};
</script>
<style lang="scss">
* {
box-sizing: border-box;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
color: #fff;
font-family: "Bubblegum Sans", cursive;
letter-spacing: 0.1rem;
}
ul {
margin: 0 1rem 1rem;
padding: 0;
li {
list-style: none;
}
}
p {
margin: 0;
}
$gold-0: #b39b00;
$gold-1: #f1dc53;
$gold-2: #e5c813;
$gold-3: #8b7900;
$gold-4: #574b00;
$blue-0: #035b6f;
$blue-1: #368395;
$blue-2: #0f768e;
$blue-3: #024656;
$blue-4: #002c36;
$orange-0: #b35900;
$orange-1: #f1a253;
$orange-2: #e57b13;
$orange-3: #8b4500;
$orange-4: #572b00;
$purple-0: #310d7b;
$purple-1: #6444a5;
$purple-2: #461b9c;
$purple-3: #24065f;
$purple-4: #15023c;
.clickable {
color: #fff;
cursor: pointer;
transition: transform 0.25s, background-color 0.25s, color 0.25s;
&:hover {
color: $gold-1;
transform: scale(1.05);
}
}
button {
background-color: $orange-2;
border: none;
color: #fff;
padding: 1rem 2rem;
margin: 0.5rem 1rem;
}
.button-nav {
width: 100vw;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
button {
flex-grow: 1;
}
@media (max-width: 42rem) {
flex-direction: column;
padding: 0 2rem;
}
}
.card {
margin: 1rem 1rem 2rem;
padding: 1rem;
max-width: 32rem;
background-color: $orange-2;
color: $orange-4;
font-size: 1.2rem;
&.dark {
background-color: $purple-2;
color: $purple-3;
&.full .detail {
background-color: $purple-1;
}
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
h2 {
padding-left: 2rem;
justify-self: start;
flex-grow: 2;
}
img {
height: 6rem;
margin: -1rem;
}
}
&.data {
max-width: 48rem;
border-radius: 0.5rem;
}
.data-list {
font-size: 1rem;
background-color: $orange-1;
margin: 0 -1rem -1rem;
padding: 1rem;
max-height: 28rem;
overflow-y: scroll;
border-radius: 0 0 0.5rem 0.5rem;
p {
margin: 0;
}
}
&.full {
max-width: 52rem;
.detail {
background-color: $orange-1;
margin: 1rem -1rem -2rem;
padding: 1rem;
flex-grow: 2;
h4 {
margin: 0.5rem 0 0;
}
}
}
}
.tile {
margin: 1rem;
min-height: 8rem;
min-width: 28rem;
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
img {
height: 8rem;
}
}
header {
position: fixed;
width: 100vw;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background-color: $orange-2;
color: #fff;
font-family: "Rubik", sans-serif;
@media (max-width: 32rem) {
text-align: center;
flex-direction: column;
nav {
flex-direction: column;
}
}
h1 {
font-size: 1.4rem;
.sub {
text-transform: capitalize;
color: $gold-1;
font-family: "Rubik", sans-serif;
}
}
nav {
display: flex;
justify-content: end;
.nav-item {
font-weight: bold;
padding: 0.5rem;
border-radius: 0.5rem;
margin: 0 0.5rem;
&.nav-active {
color: $gold-1;
}
}
}
}
.key {
color: $gold-1;
font-weight: bold;
}
main {
min-height: 100vh;
background-color: $orange-1;
font-family: "Rubik", sans-serif;
background-image: url("https://assets.codepen.io/328538/p4banner.jpg");
background-size: cover;
background-attachment: fixed;
.banner img {
width: 100vw;
}
> section {
min-height: 100vh;
padding: 6rem 0.5rem 4rem;
display: flex;
align-items: start;
justify-content: space-between;
flex-wrap: wrap;
@media (max-width: 32rem) {
padding-top: 16rem;
}
}
::-webkit-scrollbar {
width: 20px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px $orange-0;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: $gold-2;
border-radius: 10px;
}
}
#home {
.detail {
padding: 1rem;
}
}
#calendar {
text-align: center;
.card {
flex-basis: 26rem;
}
.cal-grid {
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
background-color: $orange-1;
margin: 0 -1rem -1rem;
background-image: repeating-linear-gradient(
45deg,
$orange-1,
$orange-1 35px,
$orange-2 35px,
$orange-2 70px
);
.day {
flex-basis: 14.28%;
margin: 0.25rem 0;
&.key {
background-color: $orange-2;
margin: 0;
}
}
}
@media (max-width: 32rem) {
flex-direction: column;
justify-content: start;
.card {
flex-basis: inherit;
}
.cal-grid .day {
margin: 0.5rem 0;
}
}
}
#calendar-detail {
align-items: start;
justify-content: center;
}
.notes {
padding: 1rem;
.note + .note {
margin-top: 1rem;
}
}
.slinks {
display: flex;
flex-wrap: wrap;
justify-content: center;
.arcana {
height: 10rem;
width: 10rem;
margin: 0 1rem 1rem;
border-radius: 0.5rem;
background-color: $orange-0;
overflow: hidden;
box-shadow: inset 1rem 1rem 1rem $orange-2, inset 1rem -1rem 1rem $orange-2,
inset -1rem 1rem 1rem $orange-2, inset -1rem -1rem 1rem $orange-2;
&.night {
background-color: $purple-0;
box-shadow: inset 1rem 1rem 1rem $purple-2,
inset 1rem -1rem 1rem $purple-2, inset -1rem 1rem 1rem $purple-2,
inset -1rem -1rem 1rem $purple-2;
}
}
img {
height: 10rem;
width: auto;
margin: auto;
}
}
.spoilers {
display: flex;
flex-direction: column;
.spoiler {
display: flex;
align-items: stretch;
justify-content: space-between;
position: relative;
color: #fff;
& + .spoiler {
margin-top: 1rem;
}
&:after {
position: absolute;
display: block;
content: "Q";
left: -0.5rem;
font-size: 3rem;
color: #fff;
}
&:before {
position: absolute;
display: block;
content: "A";
right: -0.5rem;
font-size: 3rem;
}
}
.question {
padding: 1rem 0 1rem 3rem;
width: 70%;
background-color: $orange-2;
}
.answer {
padding: 1rem;
display: none;
width: 30%;
background-color: $gold-2;
&.show {
display: block;
}
}
}
#socials {
display: flex;
justify-content: center;
.tile {
background-color: $blue-3;
}
}
#social-detail {
.card {
width: 100vw;
margin: auto;
img {
height: 10rem;
}
}
h2.key {
text-align: center;
margin-top: 4rem;
font-size: 1.2rem;
}
.rank {
margin: 1rem 0;
.title {
background-color: $purple-2;
padding: 1rem;
margin: 1rem 0;
}
.rank-detail {
display: none;
&.show {
display: block;
}
}
.answer {
margin: 1rem 0;
display: flex;
justify-content: space-between;
align-items: center;
.response {
flex-grow: 6;
}
.points {
padding: 1rem;
color: #fff;
&.with {
background-color: $blue-1;
}
}
}
}
}
</style>