Compare commits

..

14 Commits

49 changed files with 7372 additions and 358 deletions
+16
View File
@@ -0,0 +1,16 @@
name: Build Vue
on:
release:
types: [published]
jobs:
build_vue:
runs-on: ubuntu-latest
name: Build Vue
steps:
- uses: actions/checkout@v2
- id: Build-Vue
uses: xRealNeon/VuePagesAction@1.0.1
with:
username: 'YouHaveTrouble'
reponame: 'GuildMaster'
token: ${{ secrets.GITHUB_TOKEN }}
+28
View File
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-1
View File
@@ -1 +0,0 @@
index.html
-1
View File
@@ -1 +0,0 @@
guildmaster.yht.one
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-106
View File
@@ -1,106 +0,0 @@
{
"ranks": {
"A": [
{
"title": "Ogre king",
"text": "Ogres have chosen a new king through democratic vote. They all voted for the strongest ogre."
},
{
"title": "Devilish dungeon",
"text": "New dungeon was discovered. It needs to be mapped and explored so lower rank adventurers can enter."
},
{
"title": "Eater of Worlds",
"text": "A giant worm emerged from the ground and appears to be consuming the ground itself."
}
],
"B": [
{
"title": "Undead horde",
"text": "Due to the spillage of necromancy potion at nearby graveyard we now have an undead army on our doorstep."
},
{
"title": "Runaway prisoner",
"text": "During the last prison guard strike a prisoner managed to escape. Bring them back to their cell."
},
{
"title": "The aristocrats",
"text": "Royalty wants an escort for one of their carriages."
}
],
"C": [
{
"title": "Scratchy, the butcher",
"text": "Scratchy turned evil and is terrorizing its victims. Put a stop to it!"
},
{
"title": "Hobgnoblin subjugation",
"text": "Gnoblins evolved and are back for vengeance."
},
{
"title": "Holy",
"text": "Gnoblins summoned their machine god and it started going haywire on everything around it. Destroy it!"
}
],
"D": [
{
"title": "Caravan escort",
"text": "Escort a merchant caravan."
},
{
"title": "Rare ore",
"text": "Obtain laudanium ore for town's blacksmith."
},
{
"title": "Demonic pests!",
"text": "Clear the fields from cabbage imps."
}
],
"E": [
{
"title": "Gnoblin subjugation",
"text": "Kill 3 gnoblins."
},
{
"title": "Phantom menace",
"text": "Exorcise ghosts out of someone's apartment."
},
{
"title": "Scratchy in peril",
"text": "Get Scratchy the cat from the tree safe onto the ground."
}
],
"F": [
{
"title": "Frog Frenzy",
"text": "Kill 10 demon frogs."
},
{
"title": "Rats!",
"text": "Get rid of the rats from someone's basement."
},
{
"title": "Herb gathering",
"text": "Collect medicinal herbs."
},
{
"title": "Big pile of rubble",
"text": "Tavern collapsed. Again. Help clean up the debris."
}
],
"S": [
{
"title": "The Demon King",
"text": "Demon King has awoken and is a threat to whole existence. Heroes needed."
},
{
"title": "Scratchy, Destruction Incarnate",
"text": "Scratchy was reborn as a machine of pure destruction and needs to be stopped."
},
{
"title": "Jiggly Jungle",
"text": "A jungle south began rapidly expanding and experts think arson is our only option."
}
]
}
}
Vendored
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

-40
View File
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 569.613 569.614"
xml:space="preserve">
<g>
<g>
<path d="M371.49,563.638l113.052-65.854c5.26-3.063,9.088-8.094,10.64-13.975c1.555-5.888,0.701-12.148-2.359-17.405
l-30.769-52.807c4.789-6.524,9.083-13.115,12.972-19.918c3.893-6.799,7.405-13.84,10.606-21.275l61.114-0.221
c6.086-0.021,11.915-2.464,16.202-6.781c4.287-4.32,6.687-10.165,6.665-16.255l-0.48-130.833
c-0.024-6.089-2.464-11.919-6.784-16.206c-4.299-4.269-10.113-6.662-16.166-6.662c-0.03,0-0.062,0-0.089,0l-61.182,0.242
c-6.444-14.462-14.428-28.14-23.871-40.913l30.417-53.143c6.294-11.001,2.481-25.025-8.52-31.316L369.403,5.335
c-5.281-3.023-11.545-3.822-17.424-2.231c-5.872,1.598-10.872,5.462-13.892,10.747L307.665,67
c-15.766-1.662-31.653-1.613-47.363,0.144l-30.796-52.892c-3.063-5.263-8.094-9.091-13.975-10.646
c-5.884-1.551-12.148-0.704-17.408,2.359L85.068,71.823c-10.949,6.38-14.657,20.429-8.28,31.38l30.765,52.831
c-4.761,6.484-9.048,13.076-12.953,19.899c-3.904,6.824-7.417,13.855-10.6,21.255l-61.139,0.235
C10.187,197.472-0.046,207.785,0,220.456L0.48,351.29c0.024,6.086,2.463,11.919,6.784,16.206
c4.299,4.269,10.11,6.661,16.166,6.661c0.028,0,0.058,0,0.086,0l61.203-0.229c6.432,14.452,14.413,28.131,23.868,40.915
l-30.413,53.141c-3.023,5.284-3.825,11.548-2.231,17.423c1.597,5.872,5.462,10.872,10.747,13.896l113.535,64.977
c3.596,2.056,7.513,3.032,11.38,3.032c7.962,0,15.701-4.146,19.942-11.552l30.417-53.149c15.799,1.671,31.684,1.619,47.348-0.144
l30.799,52.89c3.066,5.26,8.094,9.088,13.978,10.643C359.967,567.552,366.23,566.705,371.49,563.638z M341.129,465.911
c-4.902-8.418-14.599-12.815-24.137-10.994c-20.588,3.935-42.174,3.999-63.128,0.202c-9.572-1.735-19.184,2.741-24.015,11.181
l-26.748,46.745l-73.694-42.18l26.75-46.741c4.832-8.439,3.819-19.006-2.521-26.371c-13.978-16.239-24.685-34.594-31.818-54.554
c-3.265-9.131-11.918-15.227-21.61-15.227c-0.027,0-0.058,0-0.085,0l-53.825,0.199l-0.315-84.937l53.819-0.205
c9.722-0.04,18.366-6.197,21.576-15.374c3.69-10.557,7.962-20.019,13.06-28.917c5.101-8.914,11.089-17.387,18.311-25.897
c6.294-7.417,7.225-17.993,2.334-26.396l-27.081-46.509l73.385-42.754l27.078,46.497c4.893,8.4,14.544,12.821,24.095,11.004
c20.716-3.911,42.317-3.978,63.189-0.19c9.557,1.753,19.189-2.742,24.019-11.178l26.753-46.744l73.697,42.179l-26.753,46.742
c-4.826,8.437-3.816,19,2.521,26.368c13.956,16.221,24.669,34.587,31.842,54.59c3.271,9.119,11.919,15.202,21.604,15.202
c0.031,0,0.062,0,0.092,0l53.789-0.214l0.315,84.927l-53.783,0.192c-9.712,0.037-18.351,6.182-21.569,15.347
c-3.746,10.654-8.023,20.131-13.082,28.975c-5.064,8.847-11.067,17.338-18.356,25.958c-6.271,7.418-7.194,17.978-2.305,26.368
l27.078,46.472l-73.391,42.749L341.129,465.911z"/>
<path d="M392.531,346.458c16.472-28.773,20.746-62.24,12.047-94.232s-29.342-58.685-58.115-75.151
c-18.761-10.74-40.05-16.417-61.562-16.417c-44.446,0-85.762,23.944-107.822,62.485c-33.994,59.404-13.327,135.39,46.071,169.386
c18.764,10.737,40.052,16.411,61.564,16.411C329.158,408.943,370.475,385.001,392.531,346.458z M352.696,323.658
c-13.902,24.293-39.955,39.385-67.985,39.385c-13.528,0-26.934-3.58-38.764-10.349c-37.433-21.426-50.456-69.312-29.033-106.751
c13.905-24.291,39.958-39.385,67.987-39.385c13.528,0,26.932,3.58,38.762,10.355c18.136,10.379,31.142,27.197,36.628,47.359
C365.771,284.435,363.075,305.524,352.696,323.658z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

+9 -23
View File
@@ -1,33 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guild Master - Adventurer's guild management game</title> <title>Guild Master - Adventurer's guild management game</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=EB+Garamond&display=swap" rel="stylesheet">
<link rel="manifest" href="/manifest.json" />
<meta name="description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="twitter:title" content="Guild Master - Adventurer's guild management game"/>
<meta property="twitter:image" content="https://guildmaster.yht.one/img/app-icons/icon.png"/>
<meta property="twitter:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:title" content="Guild Master - Adventurer's guild management game"/>
<meta property="og:url" content="https://guildmaster.yht.one/"/>
<meta property="og:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:image" content="https://guildmaster.yht.one/img/app-icons/icon.png"/>
<script type="module" crossorigin src="/assets/index-981baea9.js"></script>
<link rel="stylesheet" href="/assets/index-87987ee1.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<noscript> <noscript>
This is a javascript game. You need to enable javascript for it to work. This is a javascript game. You need to enable javascript for it to work.
</noscript> </noscript>
<script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
-59
View File
@@ -1,59 +0,0 @@
{
"name": "Guild Master - Adventurer's guild management game",
"short_name": "Guild Master",
"theme_color": "#3C2114",
"background_color": "#d9c8b3",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "img/app-icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}
+5835
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "adventurers-guild",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"sass": "^1.59.3",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.36.0",
"eslint-plugin-vue": "^9.9.0",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+340
View File
@@ -0,0 +1,340 @@
<script setup lang="ts">
import {RouterLink, RouterView} from 'vue-router'</script>
<template>
<header>
<nav>
<RouterLink to="/">Guild</RouterLink>
<RouterLink to="/quests">Quests</RouterLink>
<RouterLink to="/adventurers">Adventurers</RouterLink>
</nav>
</header>
<RouterView
:guild="guild"
:adventurers="adventurers"
:quests="missives"
:lastRecruitTime="lastRecruitHandled"
@finalizeQuest="finalizeQuest($event)"
@wipeSave="resetSave()"
@recruitActionTaken="lastRecruitHandled = Number(new Date())"
/>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import {Adventurer} from "@/classes/Adventurer";
import {Quest} from "@/classes/Quest";
import {Guild} from "@/classes/Guild";
import {getFromString, QuestRank} from "@/classes/QuestRank";
export default defineComponent({
name: "GuildView",
watch: {
adventurers: {
deep: true,
handler(newAdventurers) {
for (const adventurerId in newAdventurers) {
const adventurer = newAdventurers[adventurerId] as Adventurer;
if (adventurer.canLevelUp()) {
adventurer.levelUp();
}
}
}
}
},
data: () => ({
guild: new Guild(1, 500),
lastQuestGot: {
S: null as null|number,
A: null as null|number,
B: null as null|number,
C: null as null|number,
D: null as null|number,
E: null as null|number,
F: null as null|number,
},
lastRecruitHandled: null as null|number,
adventurers: {
} as { [key: string]: Adventurer },
quests: {
F: {
"1": new Quest("1", QuestRank.F, "Frog Frenzy", "Kill 10 demon frogs.", 30, 1, 25),
"2": new Quest("2", QuestRank.F, "Rats!", "Get rid of the rats in someone's basement.", 21, 1, 15),
"3": new Quest("3", QuestRank.F, "Herb gethering", "Colect medicinal herbs.", 25, 1, 19),
"4": new Quest("4", QuestRank.F, "Big pile of rubble", "Tavern collapsed. Again. Help clean up the debris.", 27, 1, 10),
} as { [key: string]: Quest },
E: {
"1": new Quest("1", QuestRank.E, "Gnoblin subjegation", "Kill 3 gnoblins.", 500, 2, 30),
"2": new Quest("2", QuestRank.E, "Phantom menace", "Exorcise ghosts out of someone's apartment.", 510, 2, 28),
"3": new Quest("3", QuestRank.E, "Scratchy in peril", "Get Scratchy the cat from the tree safe to the ground.", 550, 2, 32),
} as { [key: string]: Quest },
D: {
"1": new Quest("1", QuestRank.D, "Caravan escort", "Escort a merchant caravan.", 2000, 3, 47),
"2": new Quest("2", QuestRank.D, "Rare ore", "Obtain laudanium ore for town's blacksmith.", 2200, 3, 54),
"3": new Quest("3", QuestRank.D, "Demonic pests!", "Clear the fields from cabbage imps.", 2149, 3, 49),
} as { [key: string]: Quest },
C: {
"1": new Quest("1", QuestRank.C, "Scratchy, the butcher", "Scratchy turned evil and is terrorizing its victims. Put a stop to it!", 7500, 5, 150),
"2": new Quest("2", QuestRank.C, "Hobgnoblin subjegation", "Gnoblins evolved and are back for vengance.", 7600, 5, 150),
"3": new Quest("3", QuestRank.C, "Holy", "Gnoblins summoned their machine god and it started going haywire on everything around it. Destroy it!", 7777, 5, 49),
} as { [key: string]: Quest },
B: {
"1": new Quest("1", QuestRank.B, "Undead horde", "Due to the spillage of necromancy potion at nearby graveyard we now have an undead army on our doorstep.", 25000, 8, 200),
"2": new Quest("2", QuestRank.B, "Runaway prisoner", "During the last prison guard strike a prisoner managed to escape. Bring them back to their cell.", 26000, 8, 210),
"3": new Quest("3", QuestRank.B, "The aristocrats", "Royalty wants an escort for one of their carriages.", 25000, 8, 200),
} as { [key: string]: Quest },
A: {
"1": new Quest("1", QuestRank.A, "Ogre king", "Ogres have chosen a new king through democratic vote. They all voted for the strongest ogre.", 100000, 12, 200),
"2": new Quest("2", QuestRank.A, "Devilish dungeon", "New dungeon was discovered. It needs to be mapped and explored so lower rank adventurers can enter.", 100000, 12, 210),
"3": new Quest("3", QuestRank.A, "Eater of Worlds", "A giant worm emerged from the ground and appears to be consuming the ground itself.", 100000, 12, 200),
} as { [key: string]: Quest },
S: {
"1": new Quest("1", QuestRank.S, "The Demon King", "Demon King has awoken and is a threat to whole existence. Heroes needed.", 1000000, 20, 200),
"2": new Quest("2", QuestRank.S, "Scratchy, Destruction Incarnate", "Scratchy was reborn as a machine of pure destruction and needs to be stopped.", 1000000, 20, 210),
"3": new Quest("3", QuestRank.S, "Jiggly Jungle", "A jungle south began rapidly expanding and experts think arson is our only option.", 1000000, 20, 200),
} as { [key: string]: Quest },
} as { [key: string]: { [key: string]: Quest } },
missives: {
F: {} as { [key: string]: Quest },
E: {} as { [key: string]: Quest },
D: {} as { [key: string]: Quest },
C: {} as { [key: string]: Quest },
B: {} as { [key: string]: Quest },
A: {} as { [key: string]: Quest },
S: {} as { [key: string]: Quest },
} as { [key: string]: { [key: string]: Quest } },
}),
methods: {
async updateMissives() {
for (const missiveRank in this.missives) {
const rank = getFromString(missiveRank as QuestRank);
for (const missiveId in this.missives[rank.toString() as QuestRank]) {
const missive = this.missives[rank.toString()][missiveId];
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
continue;
}
for (const adventurerId in missive.adventurers) {
const adventurer = missive.adventurers[adventurerId];
const attack = adventurer.attackPerLevel * adventurer.level;
missive.progressPoints = Math.min(missive.progressPoints + attack, missive.maxProgress);
}
}
}
},
finalizeQuest(missive: Quest) {
missive.progressPoints = 0;
this.guild.gold += missive.goldReward;
for (const adventurerId in missive.adventurers) {
const adventurer = missive.adventurers[adventurerId];
adventurer.exp += (missive.expReward / missive.adventurers.length);
adventurer.busy = false;
}
missive.adventurers = [];
delete this.missives[missive.rank.toString() as QuestRank][missive.id];
},
getRandomQuest(rank: QuestRank): Quest | null {
const keys = Object.keys(this.quests[rank]);
if (keys.length <= 0) return null;
const questsForRank = this.quests[rank] as { [key: string]: Quest };
const randomId = keys.length * Math.random() << 0;
const randomIdString = keys[randomId] as string;
return questsForRank[randomIdString];
},
createMissive(questToAdd: Quest, rank: QuestRank) {
const quest = JSON.parse(JSON.stringify(questToAdd));
const newId = Math.random().toString(16).slice(2).toString();
quest.id = newId;
this.missives[rank][newId] = quest;
},
saveGame() {
console.debug("Saving game...");
window.localStorage.setItem("savedGame", JSON.stringify({
guild: this.guild,
adventurers: this.adventurers,
missives: this.missives,
lastQuestGot: this.lastQuestGot,
lastRecruitAction: this.lastRecruitHandled,
}));
},
loadGame() {
const rawData = window.localStorage.getItem("savedGame");
if (!rawData) return;
const saveData = JSON.parse(rawData);
this.lastQuestGot = saveData.lastQuestGot;
this.guild = new Guild(saveData.guild.level, saveData.guild.gold);
const adventurers = {} as { [key: string]: Adventurer };
for (const id in saveData.adventurers) {
const data = saveData.adventurers[id];
const adventurer = new Adventurer(data.id, data.name, data.portrait, data.attackPerLevel, data.defensePerLevel, data.level);
adventurer.busy = data.busy;
adventurers[data.id] = adventurer;
}
this.adventurers = adventurers;
const missives = {} as { [key: string]: { [key: string]: Quest } };
for (const id in saveData.missives) {
const missiveRank = {} as { [key: string]: Quest }
for (const questId in saveData.missives[id]) {
const data = saveData.missives[id][questId];
const quest = new Quest(questId, getFromString(data.rank), data.title, data.text, data.maxProgress, data.expReward, data.goldReward);
quest.progressPoints = data.progressPoints;
if (data.adventurers.length > 0) {
quest.adventurers.push(this.adventurers[data.adventurers[0].id])
}
missiveRank[questId] = quest;
}
missives[id] = missiveRank;
}
this.missives = missives;
this.lastRecruitHandled = saveData.lastRecruitAction;
},
resetSave() {
if (!confirm("You are about to wipe your save file. Are you sure?")) return;
window.localStorage.removeItem("savedGame");
window.location.reload();
}
},
mounted() {
this.loadGame();
setInterval(() => {
this.saveGame();
}, 30*1000)
setInterval(() => {
this.updateMissives();
const now = Number(new Date());
for (const id in this.lastQuestGot) {
const lastTime = this.lastQuestGot[getFromString(id as QuestRank)];
if (lastTime === null) this.lastQuestGot[getFromString(id as QuestRank)] = 0;
}
if (Number(now) - Number(this.lastQuestGot.F) >= 12 * 1000) {
this.lastQuestGot.F = now;
const keys = Object.keys(this.missives[QuestRank.F]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.F);
if (quest !== null) {
this.createMissive(quest, QuestRank.F);
}
}
if (this.guild.level < 2) return;
if (Number(now) - Number(this.lastQuestGot.E) >= 20 * 1000) {
this.lastQuestGot.E = now;
const keys = Object.keys(this.missives[QuestRank.E]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.E);
if (quest !== null) {
this.createMissive(quest, QuestRank.E);
}
}
if (this.guild.level < 3) return;
if (Number(now) - Number(this.lastQuestGot.D) >= 50 * 1000) {
this.lastQuestGot.D = now;
const keys = Object.keys(this.missives[QuestRank.D]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.D);
if (quest !== null) {
this.createMissive(quest, QuestRank.D);
}
}
if (this.guild.level < 4) return;
if (Number(now) - Number(this.lastQuestGot.C) >= 2 * 60 * 1000) {
this.lastQuestGot.C = now;
const keys = Object.keys(this.missives[QuestRank.C]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.C);
if (quest !== null) {
this.createMissive(quest, QuestRank.C);
}
}
if (this.guild.level < 5) return;
if (Number(now) - Number(this.lastQuestGot.B) >= 2 * 60 * 1000) {
this.lastQuestGot.B = now;
const keys = Object.keys(this.missives[QuestRank.B]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.B);
if (quest !== null) {
this.createMissive(quest, QuestRank.B);
}
}
if (this.guild.level < 6) return;
if (Number(now) - Number(this.lastQuestGot.A) >= 5 * 60 * 1000) {
this.lastQuestGot.A = now;
const keys = Object.keys(this.missives[QuestRank.A]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.A);
if (quest !== null) {
this.createMissive(quest, QuestRank.A);
}
}
if (this.guild.level < 6) return;
if (Number(now) - Number(this.lastQuestGot.S) >= 30 * 60 * 1000) {
this.lastQuestGot.S = now;
const keys = Object.keys(this.missives[QuestRank.S]);
if (keys.length >= 5) return;
const quest = this.getRandomQuest(QuestRank.S);
if (quest !== null) {
this.createMissive(quest, QuestRank.S);
}
}
}, 1000);
}
})
</script>
<style lang="scss" scoped>
header {
line-height: 1;
max-height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
nav {
width: max-content;
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
padding: 2rem;
background-size: 200px;
background-blend-mode: darken;
background-color: rgba(0, 0, 0, 0.65);
a {
font-size: 2rem;
color: #fff;
}
&.router-link-exact-active {
}
}
</style>
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
export class Adventurer {
id: string;
name: string;
portrait: string;
level: number;
exp: number;
attackPerLevel: number;
defensePerLevel: number;
busy: boolean;
constructor(id: string, name: string, portrait: string, attackPerLevel: number, defensePerLevel: number, level: number = 1) {
this.id = id;
this.name = name;
this.portrait = portrait;
this.attackPerLevel = attackPerLevel;
this.defensePerLevel = defensePerLevel;
this.level = level;
this.exp = 0;
this.busy = false;
}
levelUp(): void {
this.exp = 0;
this.level += 1;
}
canLevelUp(): boolean {
const requirement = this.level * 3;
return this.exp >= requirement;
}
}
+39
View File
@@ -0,0 +1,39 @@
export class Guild {
gold: number;
level: number;
displayUpgradeCost: number|string;
constructor(level: number, gold: number) {
this.gold = gold;
this.level = level;
this.displayUpgradeCost = this.getUpgradeCost() ?? "Max level";
}
upgrade(): void {
const cost = this.getUpgradeCost();
if (cost === null) return;
if (this.gold < cost) return;
this.gold -= cost;
this.level += 1;
if (this.level >= 7) {
this.displayUpgradeCost = "Max level";
} else {
const newCost = this.getUpgradeCost();
if (newCost === null) return;
this.displayUpgradeCost = newCost;
}
}
getUpgradeCost(): number|null {
return upgradeCosts[this.level] ?? null;
}
}
const upgradeCosts = {
"1": 1000,
"2": 2500,
"3": 5000,
"4": 10000,
"5": 25000,
"6": 50000,
} as {[index:string]: number}
+27
View File
@@ -0,0 +1,27 @@
import type {Adventurer} from "@/classes/Adventurer";
import type {QuestRank} from "@/classes/QuestRank";
export class Quest {
id: string;
rank: QuestRank;
title: string;
text: string;
adventurers: Array<Adventurer>;
progressPoints: number;
maxProgress: number;
expReward: number;
goldReward: number;
constructor(id: string, rank: QuestRank, title: string, text: string, maxProgress: number, expReward: number, goldReward: number) {
this.id = id;
this.rank = rank;
this.title = title;
this.text = text;
this.maxProgress = maxProgress;
this.expReward = expReward;
this.goldReward = goldReward;
this.progressPoints = 0;
this.adventurers = [];
}
}
+13
View File
@@ -0,0 +1,13 @@
export enum QuestRank {
S = "S",
A = "A",
B = "B",
C = "C",
D = "D",
E = "E",
F = "F",
}
export function getFromString(string: keyof typeof QuestRank): QuestRank {
return QuestRank[string];
}
+54
View File
@@ -0,0 +1,54 @@
<template>
<div class="slots">
<button class="slot" v-for="adventurer in adventurers" :key="adventurer.id">
<AdventurerMiniComponent
:adventurer="currentAdventurer"
:all-adventurers="adventurers"
/>
</button>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import AdventurerMiniComponent from "@/components/AdventurerMiniComponent.vue";
import type {Adventurer} from "@/classes/Adventurer";
export default defineComponent({
name: "AdventurerList",
components: {AdventurerMiniComponent},
data: () => ({
currentAdventurer: null as Adventurer|null
}),
props: {
adventurers: {
type: Array<Adventurer>
},
},
methods: {
print(a: string) {
console.log(a);
}
}
})
</script>
<style lang="scss" scoped>
.slots {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
.slot {
padding: 0;
width: 5rem;
height: 5rem;
border: 2px solid #000;
background-color: rgba(0,0,0, 0.2);
cursor: pointer;
border-radius: 0.2rem;
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<AdventurerTile
v-if="adventurer"
:adventurer="adventurer"
@click="() => {
$emit('freeAdventurer', adventurer.id)
}"
/>
<article
class="select"
v-else
@click="() => {
if (Object.keys(allAdventurers).length <= 0) return;
selection = !selection;
}"
>
<span>+</span>
</article>
<div class="selection" v-if="selection">
<button
class="slot"
v-for="adventurer in allAdventurers"
:key="adventurer.id"
:class="{busy: adventurer.busy}"
@click="() => {
if (adventurer.busy) return;
$emit('hireAdventurer', adventurer.id);
selection = false;
}"
>
<AdventurerTile
:adventurer="adventurer"
/>
</button>
</div>
</template>
<script lang="ts">
import {defineComponent, type PropType} from "vue";
import type {Adventurer} from "@/classes/Adventurer";
import AdventurerTile from "@/components/AdventurerTile.vue";
export default defineComponent({
name: "AdventurerMiniComponent",
components: {AdventurerTile},
emits: [ "freeAdventurer", "hireAdventurer" ],
data() {
return {
selection: false,
}
},
props: {
adventurer: {
type: Object as PropType<Adventurer>,
},
allAdventurers: {
type: Object as PropType<{[key: string]: Adventurer}>,
},
},
methods: {
print(a:string) {
console.log(a);
},
}
})
</script>
<style lang="scss" scoped>
.selection {
position: absolute;
bottom: 0;
left: 50%;
width: max-content;
max-width: 16rem;
transform: translateX(-50%) translateY(104%);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem;
background-color: rgba(0,0,0, 0.2);
z-index: 2;
.slot {
width: 5rem;
height: 5rem;
cursor: pointer;
padding: 0;
border-color: #000;
background-color: transparent;
background-blend-mode: darken;
transition: background-color 0.25s linear, filter 0.25s linear;
&.busy {
filter: grayscale(1);
background-color: rgba(0,0,0, 0.6);
&:hover {
cursor: not-allowed;
border-color: #000;
}
}
&:hover {
border-color: #fff;
}
img {
width: 100%;
height: 100%;
}
}
}
.select {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 4.5rem;
}
</style>
+50
View File
@@ -0,0 +1,50 @@
<template>
<article
class="adventurer"
:title="adventurer.name + (adventurer.busy ? ' (busy)' : '')"
>
<img :src="adventurer.portrait" :alt="adventurer.name" draggable="false">
<div class="level">{{adventurer.level}}</div>
</article>
</template>
<script lang="ts">
import type {Adventurer} from "@/classes/Adventurer";
import {defineComponent, type PropType } from "vue";
export default defineComponent({
name: "AdventurerTile",
props: {
adventurer: {
type: Object as PropType<Adventurer>,
}
}
});
</script>
<style lang="scss" scoped>
.adventurer {
width: 100%;
height: 100%;
overflow: clip;
font-size: 5rem;
line-height: 1;
color: rgba(0,0,0, 0.75);
position: relative;
.level {
position: absolute;
top: 0;
left: 0;
font-size: 1rem;
background-color: rgba(0,0,0, 0.75);
border-bottom-right-radius: 0.2rem;
padding: 0.1rem;
color: #fff;
}
img {
width: 100%;
height: 100%;
}
}
</style>
+11
View File
@@ -0,0 +1,11 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(router)
app.mount('#app')
+25
View File
@@ -0,0 +1,25 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'guild',
component: HomeView
},
{
path: '/quests',
name: 'quests',
component: () => import('../views/QuestView.vue')
},
{
path: '/adventurers',
name: 'adventurers',
component: () => import('../views/AdventurerView.vue')
}
]
})
export default router
+210
View File
@@ -0,0 +1,210 @@
<template>
<div class="adventurer-section">
<section class="recruit">
<h1>Applying adventurers</h1>
<div class="adventurers">
<div v-if="currentlyForHire">
<adventurer-tile class="hire-tile" :adventurer="currentlyForHire"/>
<div class="decision">
<span
title="Hire"
@click="hireAdventurer(currentlyForHire)"
>
</span>
<span
title="Dismiss"
:class="{disabled: Object.keys(adventurers).length <= 0}"
@click="dismissAdventurer()"
>
</span>
</div>
</div>
<div v-else>
<span>Noone applied as of now. Check back later!</span>
</div>
</div>
</section>
<section class="collection">
<h1>Recruited adventurers</h1>
<div class="adventurers">
<div class="adventurer-tile" v-for="adventurer in adventurers" :key="adventurer.id">
<AdventurerTile class="entry" :adventurer="adventurer" />
<b>{{ adventurer.name }}</b>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import type {PropType} from "vue";
import {defineComponent} from "vue";
import AdventurerTile from "@/components/AdventurerTile.vue";
import {Adventurer} from "@/classes/Adventurer";
export default defineComponent({
name: "RecruitView",
components: {AdventurerTile},
data() {
return {
currentlyForHire: null as Adventurer|null,
adventurersForHire: [
new Adventurer("rincewind-diskworld", "Rincewind", "/img/adventurers/rincewind.png", 2, 2),
new Adventurer("fran-sword-isekai", "Fran", "/img/adventurers/fran.png", 3, 1.5),
new Adventurer("kazuma-konosuba", "Kazuma", "/img/adventurers/kazuma.png", 2, 2),
new Adventurer("rein-beast-tamer", "Rein", "/img/adventurers/rein.png", 2, 2),
new Adventurer("momon-overlord", "Momon", "/img/adventurers/momon.png", 2, 2),
] as Array<Adventurer>,
}
},
props: {
adventurers: {
type: Object as PropType<{ [key: string]: Adventurer }>,
default() {
return {};
},
},
lastRecruitTime: {
type: Number as PropType<null|number>,
default() {
return null;
}
},
},
methods: {
getRandomAdventurer(): Adventurer|null {
if (this.adventurersForHire.length <= 0) return null;
const randomId = this.adventurersForHire.length * Math.random() << 0;
return this.adventurersForHire[randomId];
},
getNewAdventurerForHire(): void {
const adventurer = this.getRandomAdventurer();
if (adventurer === null) {
this.currentlyForHire = null;
return;
}
if (this.adventurers[adventurer.id] != null) {
this.currentlyForHire = null;
return;
}
this.currentlyForHire = adventurer;
window.localStorage.setItem("currentlyForHire", adventurer.id);
},
hireAdventurer(adventurer: Adventurer): void {
this.adventurers[adventurer.id] = adventurer;
this.currentlyForHire = null;
window.localStorage.removeItem("currentlyForHire");
this.$emit("recruitActionTaken", adventurer);
},
dismissAdventurer() {
this.currentlyForHire = null;
this.$emit("recruitActionTaken", null);
window.localStorage.removeItem("currentlyForHire");
}
},
mounted() {
if (Object.keys(this.adventurers).length <= 0) {
this.currentlyForHire = this.adventurersForHire[0];
window.localStorage.setItem("currentlyForHire", this.adventurersForHire[0].id);
return;
}
const savedCurrentForHire = window.localStorage.getItem("currentlyForHire");
if (savedCurrentForHire !== null) {
for (const id in this.adventurersForHire) {
const adventurer = this.adventurersForHire[id];
if (adventurer.id === savedCurrentForHire) {
this.currentlyForHire = adventurer;
return;
}
}
}
const now = Number(new Date());
if (now - this.lastRecruitTime >= 1000 * 60 * 30 && this.currentlyForHire === null) {
this.getNewAdventurerForHire();
}
},
emits: ["recruitActionTaken"]
});
</script>
<style lang="scss" scoped>
.adventurer-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
section {
max-width: 1280px;
width: 100%;
text-align: center;
padding-block: 1rem;
}
h1 {
font-size: 2rem;
font-weight: bold;
margin: 0;
}
.adventurers {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
.adventurer-tile {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.25rem;
font-size: 1.1rem;
.entry {
height: 7rem;
width: 7rem;
}
b {
line-height: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
}
}
.decision {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-size: 2rem;
gap: 1rem;
span {
cursor: pointer;
&:hover {
color: #fff;
}
&.disabled {
color: rgba(0,0,0, 0.5)
}
}
}
.hire-tile {
width: 8rem;
height: 8rem;
}
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<main>
<section class="title">
<h1>Guild Master</h1>
<h3>Adventurer's guild management game</h3>
<small>v{{version}}</small>
</section>
<section class="coffer">
<p>Coffer: {{guild.gold}} gold</p>
</section>
<section class="upgrade">
<p>Guild level: {{ guild.level }}</p>
<button :disabled="guild.gold < 1000" @click="guild.upgrade()">
<span>Upgrade guild level</span><br>
<span>({{ guild.displayUpgradeCost }})</span>
</button>
</section>
<section class="upgrade">
<span class="wipe-save" @click="$emit('wipeSave')">Wipe your save data</span>
</section>
</main>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import type {PropType} from "vue";
import type {Guild} from "@/classes/Guild";
import {version} from "../../package.json"
export default defineComponent({
name: "GuildView",
data() {
return {
version: version,
}
},
props: {
guild: {
type: Object as PropType<Guild>,
},
}
});
</script>
<style lang="scss">
.title {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2.5rem;
text-align: center;
gap: 0.5rem;
h1 {
font-size: 4rem;
line-height: 0.75;
margin: 0;
}
h3 {
margin: 0;
line-height: 0.9;
}
small {
font-size: 0.9rem;
font-weight: bold;
line-height: 0.25;
}
}
.coffer {
text-align: center;
p {
font-size: 1.5rem;
font-weight: bold;
}
}
.upgrade {
text-align: center;
.wipe-save {
display: inline-flex;
font-weight: bold;
margin-block: 1rem;
color: #d52121;
cursor: pointer;
}
p {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
}
</style>
+384
View File
@@ -0,0 +1,384 @@
<template>
<div class="guild" v-if="guild.level >= 7 && Object.keys(quests.S).length > 0">
<h1>Rank S Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.S"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild" v-if="guild.level >= 6 && Object.keys(quests.A).length > 0">
<h1>Rank A Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.A"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild" v-if="guild.level >= 5 && Object.keys(quests.B).length > 0">
<h1>Rank B Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.B"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild" v-if="guild.level >= 4 && Object.keys(quests.C).length > 0">
<h1>Rank C Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.C"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild" v-if="guild.level >= 3 && Object.keys(quests.D).length > 0">
<h1>Rank D Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.D"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild" v-if="guild.level >= 2 && Object.keys(quests.E).length > 0">
<h1>Rank E Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.E"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
<div class="guild">
<h1>Rank F Quests</h1>
<section class="missives">
<div
class="missive"
:class="{done: missive.maxProgress <= missive.progressPoints}"
v-for="missive in quests.F"
:key="missive.id"
@click="() => {
if (missive.progressPoints < missive.maxProgress) return;
$emit('finalizeQuest', missive)
}"
>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<progress :max="missive.maxProgress" :value="missive.progressPoints"></progress>
</div>
</section>
</div>
</template>
<script lang="ts">
import {defineComponent, type PropType} from "vue";
import AdventurerComponent from "@/components/AdventurerMiniComponent.vue";
import type {Adventurer} from "@/classes/Adventurer";
import type {Quest} from "@/classes/Quest";
import type {Guild} from "@/classes/Guild";
export default defineComponent({
name: "QuestView",
components: {AdventurerComponent},
props: {
guild: {
type: Object as PropType<Guild>,
},
adventurers: {
type: Object as PropType<{ [key: string]: Adventurer }>,
default() {
return {};
},
},
quests: {
type: Object as PropType<{ [key: string]: Quest }>,
default() {
return {};
},
},
lastRecruitTime: {
type: Number as PropType<null|number>,
default() {
return null;
}
},
},
data() {
return {};
},
emits: [ 'finalizeQuest', 'wipeSave', 'recruitActionTaken'],
})
</script>
<style lang="scss" scoped>
.guild {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
padding-bottom: 0.5rem;
h1 {
font-size: 3rem;
}
.missives {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: calc(100% - 2rem);
justify-content: center;
align-items: center;
padding-inline: 1rem;
gap: 1rem;
.missive {
width: min(100%, 14rem);
text-align: center;
border: 2px solid #000;
padding: 0.5rem;
position: relative;
background-color: rgba(255, 255, 255, 0.2);
&.done {
cursor: pointer;
&::after {
position: absolute;
top: 0;
right: 0;
content: "";
font-size: 5rem;
color: green;
transform: translate(45%, -40%);
}
}
.slots {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
.slot {
padding: 0;
width: 5rem;
height: 5rem;
border: 2px solid #000;
background-color: rgba(0, 0, 0, 0.2);
cursor: pointer;
border-radius: 0.2rem;
position: relative;
}
}
}
}
}
</style>
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
+15
View File
@@ -0,0 +1,15 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
base: "/GuildMaster/"
})