Skip to content

Commit

Permalink
Welcome to the lab quest (#137)
Browse files Browse the repository at this point in the history
## Summary
Adds welcome to the lab quest

## Implementation Notes
* This provides the infrastructure needed for displaying collections with the puzzle list, and also specifically quests which are implemented through collections
* Yeah, the duplicate request of the collection is rather silly and an easy place to introduce a race condition (if you go to load the puzzle list before the collection info is available), but the real fix for that is to rip out the unnecessary VueX usage entirely

## Testing
* Loaded test version of quest on dev
* Loaded other quest
* Tested unsolved filter on both WTTL quest and general puzzle list

## Related Issues
Resolves #136
  • Loading branch information
luxaritas authored Sep 24, 2024
1 parent 806fe69 commit d382afe
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 35 deletions.
64 changes: 37 additions & 27 deletions src/components/BasePuzzleListPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export default Vue.extend({
quest: {
type: Object,
default: null
},
collection: {
type: String,
default: null
}
},
data() {
Expand All @@ -99,7 +103,6 @@ export default Vue.extend({
this.availableFilters = [
{ value: 'notcleared', text: 'Uncleared' },
];
if (!this.$route.query.switch) this.availableFilters.push({ value: 'single', text: 'Single State' });
} else {
this.availableFilters = [
{ value: 'challenge', text: 'Challenges' },
Expand Down Expand Up @@ -140,24 +143,27 @@ export default Vue.extend({
cleared: puzzleList.cleared ? puzzleList.cleared.some(cleared => cleared.nid === puzzle.id) : false,
}));
// When we are displaying a collection, the API does not have a filter for uncleared puzzles, so we do that here
const filters = `${this.$route.query.filters}`.split(',');
const filteredPuzzles = filters.includes("notcleared") ? puzzlesWithCleared.filter((puz) => !puz.cleared) : puzzlesWithCleared;
if (this.$route.query.progression) {
// Sort such that puzzle A which specifies its next puzzle is puzzle B is sorted before puzzle A
// The first puzzle is the one that has no other puzzle pointing to it
const orderedPuzzles: (PuzzleItem & {cleared: boolean})[] = [];
let puzzle = puzzlesWithCleared.find(
candidatePuzzle => !puzzlesWithCleared.some(otherPuzzle => otherPuzzle['next-puzzle'] === candidatePuzzle.id)
let puzzle = filteredPuzzles.find(
candidatePuzzle => !filteredPuzzles.some(otherPuzzle => otherPuzzle['next-puzzle'] === candidatePuzzle.id)
);
while (puzzle) {
orderedPuzzles.push(puzzle);
const nextPuzzle = puzzle['next-puzzle'];
puzzle = puzzlesWithCleared.find(candidatePuzzle => candidatePuzzle.id === nextPuzzle);
puzzle = filteredPuzzles.find(candidatePuzzle => candidatePuzzle.id === nextPuzzle);
}
// Add any additional puzzles not part of the next puzzle "chain"
orderedPuzzles.push(...puzzlesWithCleared.filter(candidatePuzzle => !orderedPuzzles.includes(candidatePuzzle)));
orderedPuzzles.push(...filteredPuzzles.filter(candidatePuzzle => !orderedPuzzles.includes(candidatePuzzle)));
return orderedPuzzles;
} else {
console.log('unordered');
return puzzlesWithCleared;
return filteredPuzzles;
}
} else {
return [];
Expand All @@ -167,7 +173,7 @@ export default Vue.extend({
return this.$store.state.user.lab_access;
},
morePuzzlesAvailable(): boolean {
return this.numberOfPuzzles < parseInt(this.$store.state.puzzle_list.num_puzzles);
return this.$store.state.puzzle_list.puzzles.length < parseInt(this.$store.state.puzzle_list.num_puzzles);
},
firstQuest(): boolean {
return !!this.$route.query.firstQuest;
Expand All @@ -176,33 +182,37 @@ export default Vue.extend({
methods: {
async fetchNewPuzzles() {
// Get filters from query, then convert to API's expected parameters
// Will change with Eterna-Next API
const query = this.$route.query;
const filters = `${query.filters}`.split(',');
const queryParams = new URLSearchParams({type: 'puzzles'});
if (filters.includes("challenge") && !filters.includes("player")) queryParams.append('puzzle_type', 'Challenge');
else if (filters.includes("player") && !filters.includes("challenge")) queryParams.append('puzzle_type', 'PlayerPuzzle');
else if (query.progression) queryParams.append('puzzle_type', 'Progression');
else queryParams.append('puzzle_type', 'AllChallengesPuzzle')
if (filters.includes("single")) queryParams.append('single', 'checked');
if (query.switch) queryParams.append('switch', 'checked');
if (this.collection) {
await this.$store.dispatch(Action.GET_COLLECTION, {id: this.collection});
} else {
const queryParams = new URLSearchParams({type: 'puzzles'});
const filters = `${query.filters}`.split(',');
if(filters.includes("notcleared")) queryParams.append('notcleared', 'true');
if (filters.includes("challenge") && !filters.includes("player")) queryParams.append('puzzle_type', 'Challenge');
else if (filters.includes("player") && !filters.includes("challenge")) queryParams.append('puzzle_type', 'PlayerPuzzle');
else if (query.progression) queryParams.append('puzzle_type', 'Progression');
else queryParams.append('puzzle_type', 'AllChallengesPuzzle')
if (filters.includes("single")) queryParams.append('single', 'checked');
if (query.switch) queryParams.append('switch', 'checked');
if (filters.includes("notcleared")) queryParams.append('notcleared', 'true');
if (this.$store.state.uid) queryParams.append('uid', this.$store.state.uid);
if (this.$store.state.uid) queryParams.append('uid', this.$store.state.uid);
if (query.tags) queryParams.append('tags', query.tags as string);
if (query.tags) queryParams.append('tags', query.tags as string);
if (query.search) queryParams.append('search', query.search as string);
queryParams.append('sort', query.sort ? query.sort as string : 'date');
if (query.search) queryParams.append('search', query.search as string);
queryParams.append('sort', query.sort ? query.sort as string : 'date');
if (!query.progression) queryParams.append('size', this.numberOfPuzzles.toString(10));
if (!query.progression) queryParams.append('size', this.numberOfPuzzles.toString(10));
await this.$store.dispatch(Action.GET_PUZZLES, queryParams.toString());
await this.$store.dispatch(Action.GET_PUZZLES, queryParams.toString());
}
},
async fetchMorePuzzles() {
this.numberOfPuzzles += 9;
Expand Down
51 changes: 50 additions & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ export interface ClearedPuzzle {
export interface PuzzleList {
puzzles: PuzzleItem[];
cleared: ClearedPuzzle[];
num_puzzles: number;
}


export interface Puzzle {
title: string;
created: string;
Expand Down Expand Up @@ -88,6 +88,24 @@ export interface Puzzle {
// "max-votes": 0
}

export interface Collection {
nid: string;
desc: string;
title: string;
userpicture: string;
username: string;
puzzles: string;
quest: string;
achievement: string;
created: string;
image: string;
}

export interface CollectionList {
collections: Collection[];
num_collections: string;
}

export interface LabCardData {
affiliation: string;
cover_image?: string;
Expand Down Expand Up @@ -211,6 +229,8 @@ export const Action = {
GET_QUEST_ACHIEVEMENT_ROADMAP: 'GET_QUEST_ACHIEVEMENT_ROADMAP',
GET_LABS: 'GET_LABS',
GET_LAB: 'GET_LAB',
GET_QUESTS: 'GET_QUESTS',
GET_COLLECTION: 'GET_COLLECTION',
GET_PUZZLES: 'GET_PUZZLES',
GET_PUZZLE: 'GET_PUZZLE',
GET_PROFILE: 'GET_PROFILE'
Expand All @@ -234,6 +254,7 @@ export default function createStore(http: AxiosInstance) {
current_lab: <LabData | null>null,
puzzle_list: <PuzzleList | null> null,
current_puzzle: <(Puzzle & { cleared: boolean }) | null>null,
quests: <CollectionList | null>null,
},
getters: {
isLoading({isLoadingCount}) {
Expand Down Expand Up @@ -284,6 +305,9 @@ export default function createStore(http: AxiosInstance) {
},
setUserData(state, user) {
state.user = user
},
setQuests(state, quests) {
state.quests = quests
}
},
actions: {
Expand Down Expand Up @@ -382,6 +406,31 @@ export default function createStore(http: AxiosInstance) {
commit('popIsLoading');
}
},
async [Action.GET_QUESTS]({ commit }) {
commit('pushIsLoading');
try{
const { data } = (await http.get('/get/?type=collections&quest=true&sort=title&size=30')).data;
commit('setQuests', data);
}
finally{
commit('popIsLoading');
}
},
async [Action.GET_COLLECTION]({ commit }, { id }: { id: string}) {
commit('pushIsLoading');
try{
const { data: collectionData } = (await http.get(`/get/?type=collection&nid=${id}`)).data;
const { data: clearedData } = (await http.get(`/get/?type=puzzle&nid=${collectionData.puzzles[0].id}`)).data;
commit('setPuzzles', {
puzzles: collectionData.puzzles,
num_puzzles: collectionData.puzzles.length,
cleared: clearedData?.cleared ?? [],
});
}
finally{
commit('popIsLoading');
}
},
async [Action.GET_PUZZLES]({ commit }, queryString: string){
// commit('pushIsLoading');
try{
Expand Down
17 changes: 12 additions & 5 deletions src/views/Quest.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<BasePuzzleListPage :forQuest="true" :quest="achievement">
<BasePuzzleListPage :forQuest="true" :quest="achievement" :collection="collection?.nid" v-if="!$store.state.isLoading">
<template v-slot:left-block>
<img :src="resolveUrl(achievement.image)" :alt="achievement.title" style="object-fit: contain; width: 100%; height: 100%; max-height: 40vh;"/>
<p v-html="description"></p>
Expand All @@ -12,22 +12,29 @@ import Vue from 'vue'
import DOMPurify from 'dompurify'
import BasePuzzleListPage from '../components/BasePuzzleListPage.vue'
import { Achievement, Action } from '../store';
import { Achievement, Action, CollectionList } from '../store';
export default Vue.extend({
async mounted() {
await this.$store.dispatch(Action.GET_QUEST_ACHIEVEMENT_ROADMAP);
await Promise.all([
this.$store.dispatch(Action.GET_QUEST_ACHIEVEMENT_ROADMAP),
this.$store.dispatch(Action.GET_QUESTS)
]);
},
components: {
BasePuzzleListPage
},
computed: {
achievement(): Achievement {
return (this.$store.state.quest_roadmap as Achievement[] || []).find(a => a.key == this.$route.params.id && a.level == +this.$route.params.level)!;
return (this.$store.state.quest_roadmap as Achievement[] || [])?.find(a => a.key == this.$route.params.id && a.level == +this.$route.params.level)!;
},
collection() {
const achievement = this.achievement as Achievement;
return (this.$store.state.quests as CollectionList)?.collections.find((collection) => collection.achievement === achievement.key)
},
description(): string{
return DOMPurify.sanitize(this.achievement.desc);
return DOMPurify.sanitize(this.collection?.desc || this.achievement.desc);
}
},
methods: {
Expand Down
8 changes: 6 additions & 2 deletions src/views/QuestExplore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<b-container id="page-scroll-content">
<div id="scroll-card-wrapper">
<TutorialCard
:imgSrc="resolveUrl(getAchAtCurrentLevel('side_quest_eterna3d_expo').image)"
@play="$router.push(`/quests/side_quest_eterna3d_expo/${getAchAtCurrentLevel('side_quest_eterna3d_expo').level}?search=:Eterna3D Expo&sort=date_asc`)"
:imgSrc="resolveUrl(getAchAtCurrentLevel('side_quest_welcome_to_the_lab').image)"
@play="$router.push(`/quests/side_quest_welcome_to_the_lab/${getAchAtCurrentLevel('side_quest_welcome_to_the_lab').level}`)"
/>
<TutorialCard
:imgSrc="resolveUrl(getAchAtCurrentLevel('side_quest_best_lab_practices').image)"
Expand All @@ -25,6 +25,10 @@
: `/quests/side_quest_best_lab_practices/${getAchAtCurrentLevel('side_quest_best_lab_practices').level}?search=%5BLab%20Tutorial%5D&sort=date`
)"
/>
<TutorialCard
:imgSrc="resolveUrl(getAchAtCurrentLevel('side_quest_eterna3d_expo').image)"
@play="$router.push(`/quests/side_quest_eterna3d_expo/${getAchAtCurrentLevel('side_quest_eterna3d_expo').level}?search=:Eterna3D Expo&sort=date_asc`)"
/>
<TutorialCard
:imgSrc="resolveUrl(getAchAtCurrentLevel('side_quest_eterna100').image)"
@play="$router.push(`/quests/side_quest_eterna100/${getAchAtCurrentLevel('side_quest_eterna100').level}?search=:Eterna100&sort=solved`)"
Expand Down

0 comments on commit d382afe

Please sign in to comment.