mirror of
https://github.com/YouHaveTrouble/GuildMaster.git
synced 2026-05-12 06:26:59 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d625fa7eee | |||
| 463fc90c9a | |||
| 7da3e91af2 | |||
| 03c16126da | |||
| d221e8f8f7 | |||
| 28e9df8251 | |||
| 2ce333fd63 | |||
| c0b51e8362 | |||
| 6cc8304018 | |||
| 49baa613bd | |||
| ae65ff9d51 | |||
| 4de7ac97d1 | |||
| 6e9dc1ada1 | |||
| 3368755a31 | |||
| a821094513 | |||
| 2cb9221da1 | |||
| 85ed1224c0 | |||
| 0d87376270 | |||
| ae89704380 | |||
| 972b9251c3 | |||
| 2f10f940d8 | |||
| d962c85629 | |||
| e71326d89b | |||
| 20567be96d | |||
| 3c79074c4c | |||
| 571dee6cc9 |
@@ -0,0 +1,23 @@
|
|||||||
|
# Game Design Document
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
Guild Master is a game simulating being a fantasy guild master. The player will be able to recruit adventurers,
|
||||||
|
send them on quests, and manage the guild's resources.
|
||||||
|
|
||||||
|
## 2. Gameplay
|
||||||
|
|
||||||
|
Player will recruit adventurers and assign then to quests. Adventurers will have different statistics and a passive
|
||||||
|
ability that will make each character unique. Player will have to manage guild's resources to complete more and more
|
||||||
|
resource intensive quests and assignments.
|
||||||
|
|
||||||
|
## 3. Mechanics
|
||||||
|
Menus. Lots of menus. Possibly with fancy animations.
|
||||||
|
|
||||||
|
## 4. Characters
|
||||||
|
Set amount of available adventurers. Each with their own inventory and passive ability. Items in the inventory will
|
||||||
|
boost specific statistics of the character. They will be scaled based on level and the xp curve will be exponential.
|
||||||
|
|
||||||
|
## 5. Quests
|
||||||
|
There will always be a minimum set amount of quests available. Adventurers will have to be assigned to a quest that is
|
||||||
|
within their level range. Quests will come in different difficulties within the level range.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* This file is used to import the character portraits to base64 from raw assets
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const characterData = require('./rawAssets/data/adventurers.json');
|
||||||
|
|
||||||
|
for (const character of characterData) {
|
||||||
|
try {
|
||||||
|
const base64 = base64_encode(`./rawAssets/img/portraits/${character.id}.png`);
|
||||||
|
character.portrait = "data:image/png;base64,"+base64;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error: Didn't find portrait for ${character.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('./public/data/adventurers.json', JSON.stringify(characterData, null, 2), "utf-8");
|
||||||
|
|
||||||
|
function base64_encode(file) {
|
||||||
|
return fs.readFileSync(file, "base64");
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<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" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="preload" href="/img/promo/chicken_mage.jpg" as="image">
|
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
|
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:title" content="Guild Master - Adventurer's guild management game"/>
|
||||||
|
|||||||
Generated
+1535
-8022
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -1,31 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "adventurers-guild",
|
"name": "adventurers-guild",
|
||||||
"version": "0.14.0",
|
"version": "0.15.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev-public": "vite --host",
|
"dev-public": "vite --host",
|
||||||
"build": "run-p build-only && cp -r CNAME dist/CNAME",
|
"build": "run-p type-check build-only && cp -r CNAME dist/CNAME",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --noEmit"
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"gen-character-data": "node characterDataGenerator.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/components": "^11.3.0",
|
"@vueuse/components": "^9.13.0",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.66.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.19.64",
|
"@types/node": "^18.17.6",
|
||||||
"@vitejs/plugin-vue": "^5.2.0",
|
"@vitejs/plugin-vue": "^4.3.1",
|
||||||
"@vue/tsconfig": "^0.6.0",
|
"@vue/tsconfig": "^0.4.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-vue": "^9.31.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.1.6",
|
||||||
"vite": "5.4.11",
|
"vite": "4.4.9",
|
||||||
"vite-plugin-pwa": "^0.21.0",
|
"vue-tsc": "^1.8.3"
|
||||||
"vue-tsc": "^2.1.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+70
-79
@@ -1,14 +1,75 @@
|
|||||||
{
|
{
|
||||||
"levelRequirement": {
|
|
||||||
"F": 1,
|
|
||||||
"E": 5,
|
|
||||||
"D": 10,
|
|
||||||
"C": 25,
|
|
||||||
"B": 50,
|
|
||||||
"A": 100,
|
|
||||||
"S": 250
|
|
||||||
},
|
|
||||||
"ranks": {
|
"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": [
|
"F": [
|
||||||
{
|
{
|
||||||
"title": "Frog Frenzy",
|
"title": "Frog Frenzy",
|
||||||
@@ -27,76 +88,6 @@
|
|||||||
"text": "Tavern collapsed. Again. Help clean up the debris."
|
"text": "Tavern collapsed. Again. Help clean up the debris."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"C": [
|
|
||||||
{
|
|
||||||
"title": "Scratchy, the butcher",
|
|
||||||
"text": "Scratchy turned evil and is terrorizing its victims. Put a stop to it!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Hobgnoblin subjegation",
|
|
||||||
"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!"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"S": [
|
"S": [
|
||||||
{
|
{
|
||||||
"title": "The Demon King",
|
"title": "The Demon King",
|
||||||
|
|||||||
@@ -1,82 +1,102 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "aldek",
|
"id": "aldek",
|
||||||
"name": "Aldek"
|
"name": "Aldek",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "aria",
|
"id": "aria",
|
||||||
"name": "Aria"
|
"name": "Aria",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "burnett",
|
"id": "burnett",
|
||||||
"name": "Burnett"
|
"name": "Burnett",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "charlotte",
|
"id": "charlotte",
|
||||||
"name": "Charlotte"
|
"name": "Charlotte",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ella",
|
"id": "ella",
|
||||||
"name": "Ella"
|
"name": "Ella",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "elyza",
|
"id": "elyza",
|
||||||
"name": "Elyza"
|
"name": "Elyza",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "emille",
|
"id": "emille",
|
||||||
"name": "Emille"
|
"name": "Emille",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "garret",
|
"id": "garret",
|
||||||
"name": "Garret"
|
"name": "Garret",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gryza",
|
"id": "gryza",
|
||||||
"name": "Gryza"
|
"name": "Gryza",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lestat",
|
"id": "lestat",
|
||||||
"name": "Lestat"
|
"name": "Lestat",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lydia",
|
"id": "lydia",
|
||||||
"name": "Lydia"
|
"name": "Lydia",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "noor",
|
"id": "noor",
|
||||||
"name": "Noor"
|
"name": "Noor",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "noron",
|
"id": "noron",
|
||||||
"name": "Noron"
|
"name": "Noron",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "oola",
|
"id": "oola",
|
||||||
"name": "Oola"
|
"name": "Oola",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "owen",
|
"id": "owen",
|
||||||
"name": "Owen"
|
"name": "Owen",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ryslette",
|
"id": "ryslette",
|
||||||
"name": "Ryslette"
|
"name": "Ryslette",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sally",
|
"id": "sally",
|
||||||
"name": "Sally"
|
"name": "Sally",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tovu",
|
"id": "tovu",
|
||||||
"name": "Tovu"
|
"name": "Tovu",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "wrydio",
|
"id": "wrydio",
|
||||||
"name": "Wrydio"
|
"name": "Wrydio",
|
||||||
|
"attackExponent": 1.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "xarya",
|
"id": "xarya",
|
||||||
"name": "Xarya"
|
"name": "Xarya",
|
||||||
|
"attackExponent": 1.1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+401
-66
File diff suppressed because one or more lines are too long
+146
@@ -0,0 +1,146 @@
|
|||||||
|
import {Guild} from "@/classes/Guild";
|
||||||
|
import {Adventurer} from "@/classes/Adventurer";
|
||||||
|
import {Quest} from "@/classes/Quest";
|
||||||
|
import {getFromString, QuestRank} from "@/classes/QuestRank";
|
||||||
|
|
||||||
|
export class GameData {
|
||||||
|
guild: Guild;
|
||||||
|
adventurers: { [key: string]: Adventurer };
|
||||||
|
missives: Array<Quest>;
|
||||||
|
lastQuestGot: { [key: string]: null | number };
|
||||||
|
adventurersForHire: {[key: string]: Adventurer};
|
||||||
|
nextRecruitArrival: Date;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
data: any,
|
||||||
|
) {
|
||||||
|
this.guild = data.guild ?? new Guild(1, 0);
|
||||||
|
this.adventurers = data.adventurers ?? {};
|
||||||
|
this.missives = data.missives ?? [] as Array<Quest>;
|
||||||
|
this.lastQuestGot = data.lastQuestGot ?? {};
|
||||||
|
this.adventurersForHire = data.adventurersForHire ?? {};
|
||||||
|
this.nextRecruitArrival = data.nextRecruitArrival ? new Date(data.nextRecruitArrival) : new Date();
|
||||||
|
if (isNaN(this.nextRecruitArrival.getTime())) {
|
||||||
|
this.nextRecruitArrival = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the game to local storage
|
||||||
|
*/
|
||||||
|
export function saveGame(
|
||||||
|
data: GameData
|
||||||
|
): void {
|
||||||
|
console.debug("Saving game...");
|
||||||
|
|
||||||
|
const adventurers = {} as { [key: string]: any };
|
||||||
|
for (const adventurerId in data.adventurers) {
|
||||||
|
const adventurer: {[key: string]: any} = JSON.parse(JSON.stringify(data.adventurers[adventurerId]));
|
||||||
|
delete adventurer.portrait;
|
||||||
|
adventurers[adventurerId] = adventurer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adventurersForHire = {} as { [key: string]: any };
|
||||||
|
for (const adventurerId in data.adventurersForHire) {
|
||||||
|
const adventurer: {[key: string]: any} = JSON.parse(JSON.stringify(data.adventurersForHire[adventurerId]));
|
||||||
|
delete adventurer.portrait;
|
||||||
|
adventurersForHire[adventurerId] = adventurer;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem("savedGame", JSON.stringify({
|
||||||
|
guild: data.guild,
|
||||||
|
adventurers: adventurers,
|
||||||
|
missives: data.missives,
|
||||||
|
lastQuestGot: data.lastQuestGot,
|
||||||
|
adventurersForHire: adventurersForHire,
|
||||||
|
nextRecruitArrival: data.nextRecruitArrival.getTime(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadGame(): GameData | null {
|
||||||
|
const savedGame = window.localStorage.getItem("savedGame");
|
||||||
|
if (!savedGame) return null;
|
||||||
|
const parsedGame = JSON.parse(savedGame);
|
||||||
|
console.debug("Loading game...");
|
||||||
|
return new GameData(parsedGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAvailableQuests(): Promise<{ [key: string]: { [key: string]: Quest } }> {
|
||||||
|
const quests = {
|
||||||
|
S: {} as { [key: string]: Quest },
|
||||||
|
A: {} as { [key: string]: Quest },
|
||||||
|
B: {} as { [key: string]: Quest },
|
||||||
|
C: {} as { [key: string]: Quest },
|
||||||
|
D: {} as { [key: string]: Quest },
|
||||||
|
E: {} as { [key: string]: Quest },
|
||||||
|
F: {} as { [key: string]: Quest },
|
||||||
|
} as { [key: string]: { [key: string]: Quest } };
|
||||||
|
|
||||||
|
const questsResponse = await fetch(`data/quests.json`);
|
||||||
|
|
||||||
|
if (questsResponse.status !== 200) {
|
||||||
|
console.error("Failed to load quests");
|
||||||
|
alert("Failed to load quests. Please try refreshing the page.");
|
||||||
|
return quests;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questsData = await questsResponse.json();
|
||||||
|
|
||||||
|
for (const rank of Object.keys(questsData.ranks)) {
|
||||||
|
const questRank = getFromString(rank as keyof typeof QuestRank);
|
||||||
|
if (!questRank) {
|
||||||
|
console.error(`Invalid quest rank: ${rank}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const questRankData = questsData.ranks[questRank];
|
||||||
|
|
||||||
|
for (const quest of questRankData) {
|
||||||
|
const id = quest.id;
|
||||||
|
quests[questRank][id] = new Quest(
|
||||||
|
id,
|
||||||
|
questRank,
|
||||||
|
quest.title,
|
||||||
|
quest.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return quests;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAdventurersForHire(): Promise<{[key: string]: Adventurer}> {
|
||||||
|
const response = await fetch("data/adventurers.json");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error("Failed to load adventurers");
|
||||||
|
alert("Failed to load adventurers. Please try refreshing the page.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const adventurerData = await response.json();
|
||||||
|
|
||||||
|
const adventurers: {[key: string]: Adventurer} = {};
|
||||||
|
for (const adventurer of adventurerData) {
|
||||||
|
const loadedAdventurer = new Adventurer(
|
||||||
|
adventurer.id,
|
||||||
|
adventurer.name,
|
||||||
|
adventurer.portrait,
|
||||||
|
adventurer.attackExponent,
|
||||||
|
adventurer.level,
|
||||||
|
adventurer.exp,
|
||||||
|
)
|
||||||
|
adventurers[loadedAdventurer.id] = loadedAdventurer;
|
||||||
|
}
|
||||||
|
return adventurers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAlreadyHiredAdventurers(
|
||||||
|
adventurers: { [key: string]: Adventurer },
|
||||||
|
adventurersHired: { [key: string]: Adventurer }
|
||||||
|
): { [key: string]: Adventurer } {
|
||||||
|
const adventurersForHire: { [key: string]: Adventurer } = {};
|
||||||
|
for (const adventurer of Object.values(adventurers)) {
|
||||||
|
if (adventurersHired[adventurer.id]) continue;
|
||||||
|
adventurersForHire[adventurer.id] = adventurer;
|
||||||
|
}
|
||||||
|
return adventurersForHire;
|
||||||
|
}
|
||||||
+161
-66
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
|
|||||||
|
export class Adventurer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
portrait: string;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
attackExponent: number;
|
||||||
|
prestige: number;
|
||||||
|
busy: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
portrait: string,
|
||||||
|
attackExponent: number,
|
||||||
|
level: number = 1,
|
||||||
|
exp: number = 0,
|
||||||
|
prestige: number = 0
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.portrait = portrait;
|
||||||
|
this.attackExponent = attackExponent;
|
||||||
|
this.level = level;
|
||||||
|
this.exp = exp;
|
||||||
|
this.prestige = prestige;
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
levelUp(): void {
|
||||||
|
this.exp = 0;
|
||||||
|
this.level += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
prestigeUp(): void {
|
||||||
|
this.level = 1;
|
||||||
|
this.exp = 0;
|
||||||
|
this.prestige += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
canLevelUp(): boolean {
|
||||||
|
if (this.level >= this.getMaxLevel()) return false;
|
||||||
|
return this.exp >= this.getNextLevelExpRequirement();
|
||||||
|
}
|
||||||
|
|
||||||
|
canPrestigeUp(): boolean {
|
||||||
|
if (this.busy) return false;
|
||||||
|
if (this.level < getMaxLevelForPrestige(this.prestige)) return false;
|
||||||
|
return this.prestige < 5
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextLevelExpRequirement(): number {
|
||||||
|
return Math.max(1, Math.floor((3 * Math.pow(1.2, this.level - 1)) * Math.pow(1.025, this.level - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the percentage of exp to the next level
|
||||||
|
*/
|
||||||
|
getExpPercentage(): number {
|
||||||
|
return (this.exp / this.getNextLevelExpRequirement()) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
addExp(exp: number): void {
|
||||||
|
if (this.isMaxLevel()) return;
|
||||||
|
this.exp += exp;
|
||||||
|
if (this.canLevelUp()) {
|
||||||
|
this.levelUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttack(): number {
|
||||||
|
const scalingFactor = Math.pow(1.05, this.level - 1);
|
||||||
|
return (2 * scalingFactor) * Math.pow(this.attackExponent, this.level - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDPS(): number {
|
||||||
|
return this.getAttack() * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxLevel(): number {
|
||||||
|
return getMaxLevelForPrestige(this.prestige);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaxLevel(): boolean {
|
||||||
|
return this.level >= this.getMaxLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxLevelForPrestige(prestige: number): number {
|
||||||
|
return 25 + (prestige * 5);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
import AdventurerCapacityUpgrade from "@/classes/guildUpgrades/AdventurerCapacityUpgrade";
|
||||||
|
import {formatGold} from "@/classes/NumberMagic";
|
||||||
|
import QuestExpUpgrade from "@/classes/guildUpgrades/QuestExpUpgrade";
|
||||||
|
import QuestGoldUpgrade from "@/classes/guildUpgrades/QuestGoldUpgrade";
|
||||||
|
import AutoFinishQuestsUpgrade from "@/classes/guildUpgrades/AutoFinishQuestsUpgrade";
|
||||||
|
import RecruitmentCapacityUpgrade from "@/classes/guildUpgrades/RecruitmentCapacityUpgrade";
|
||||||
|
|
||||||
|
const MAX_LEVEL: number = 8;
|
||||||
|
|
||||||
|
export class Guild {
|
||||||
|
gold: number;
|
||||||
|
level: number;
|
||||||
|
displayUpgradeCost: number|string;
|
||||||
|
upgradeCost: number|null = null;
|
||||||
|
adventurerCapacity: AdventurerCapacityUpgrade;
|
||||||
|
expModifier: QuestExpUpgrade;
|
||||||
|
goldModifier: QuestGoldUpgrade;
|
||||||
|
autoFinishQuestsUpgrade: AutoFinishQuestsUpgrade;
|
||||||
|
recruitmentCapacity: RecruitmentCapacityUpgrade;
|
||||||
|
|
||||||
|
constructor(level: number, gold: number, upgrades: {[index:string]: GuildUpgrade} = {}) {
|
||||||
|
this.gold = gold;
|
||||||
|
this.level = level;
|
||||||
|
const rawDisplayUpgradeCost = this.getUpgradeCost();
|
||||||
|
this.displayUpgradeCost = rawDisplayUpgradeCost ? formatGold(rawDisplayUpgradeCost) : "Max level";
|
||||||
|
this.upgradeCost = this.getUpgradeCost();
|
||||||
|
|
||||||
|
this.adventurerCapacity = upgrades.adventurerCapacity as AdventurerCapacityUpgrade ?? new AdventurerCapacityUpgrade();
|
||||||
|
this.expModifier = upgrades.expModifier as QuestExpUpgrade ?? new QuestExpUpgrade();
|
||||||
|
this.goldModifier = upgrades.goldModifier as QuestGoldUpgrade ?? new QuestGoldUpgrade();
|
||||||
|
this.autoFinishQuestsUpgrade = upgrades.autoFinishQuestsUpgrade as AutoFinishQuestsUpgrade ?? new AutoFinishQuestsUpgrade();
|
||||||
|
this.recruitmentCapacity = upgrades.recruitmentCapacity as RecruitmentCapacityUpgrade ?? new RecruitmentCapacityUpgrade();
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
const cost = this.getUpgradeCost();
|
||||||
|
if (cost === null) return;
|
||||||
|
if (this.gold < cost) return;
|
||||||
|
this.gold -= cost;
|
||||||
|
this.level += 1;
|
||||||
|
if (this.level >= MAX_LEVEL) {
|
||||||
|
this.displayUpgradeCost = "Max level";
|
||||||
|
this.upgradeCost = null;
|
||||||
|
} else {
|
||||||
|
const newCost = this.getUpgradeCost();
|
||||||
|
if (newCost === null) return;
|
||||||
|
this.displayUpgradeCost = formatGold(newCost);
|
||||||
|
this.upgradeCost = newCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpgradeCost(): number|null {
|
||||||
|
return upgradeCosts[this.level] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaxLevel(): boolean {
|
||||||
|
return this.level >= MAX_LEVEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgradeCosts = {
|
||||||
|
"1": 1000,
|
||||||
|
"2": 2500,
|
||||||
|
"3": 5000,
|
||||||
|
"4": 10000,
|
||||||
|
"5": 25000,
|
||||||
|
"6": 100000,
|
||||||
|
"7": 750000,
|
||||||
|
} as {[index:string]: number}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class GuildUpgrade {
|
||||||
|
|
||||||
|
level: number = 1;
|
||||||
|
nextLevelCost: number | null = null;
|
||||||
|
guildLevelRequirement: number = 1;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default interface MaxLevellable {
|
||||||
|
|
||||||
|
maxLevel: number;
|
||||||
|
|
||||||
|
isMaxLevel(): boolean;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const goldFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
maximumFractionDigits: 3,
|
||||||
|
// @ts-ignore - typescript doesn't know about this option for some godforsaken reason
|
||||||
|
notation: "compact",
|
||||||
|
useGrouping: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const damageFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
// @ts-ignore - typescript doesn't know about this option for some godforsaken reason
|
||||||
|
notation: "compact",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatGold(number: number | null): string {
|
||||||
|
if (number === null) return "";
|
||||||
|
return goldFormatter.format(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDamage(number: number): string {
|
||||||
|
return damageFormatter.format(number);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type {Adventurer} from "@/classes/Adventurer";
|
||||||
|
import {QuestRank} from "@/classes/QuestRank";
|
||||||
|
|
||||||
|
export class Quest {
|
||||||
|
id: string;
|
||||||
|
rank: QuestRank;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
adventurers: Array<Adventurer>;
|
||||||
|
maxAdventurers: number;
|
||||||
|
progressPoints: number;
|
||||||
|
maxProgress: number;
|
||||||
|
expReward: number;
|
||||||
|
goldReward: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
rank: QuestRank,
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
maxProgress: number = 1,
|
||||||
|
expReward: number = 0,
|
||||||
|
goldReward: number = 0,
|
||||||
|
maxAdventurers: number = 1
|
||||||
|
) {
|
||||||
|
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 = [];
|
||||||
|
this.maxAdventurers = maxAdventurers;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPercentProgress(): number {
|
||||||
|
return Math.round(this.progressPoints / this.maxProgress * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate rewards for a quest and return it
|
||||||
|
* @param quest
|
||||||
|
* @param expModifier - multiplification modifier for the exp reward
|
||||||
|
* @param goldModifier - multiplification modifier for the gold reward
|
||||||
|
*/
|
||||||
|
export function getQuestWithRewards(quest: Quest, expModifier: number = 1, goldModifier: number = 1) {
|
||||||
|
|
||||||
|
let maxProgress = 1;
|
||||||
|
|
||||||
|
switch (quest.rank) {
|
||||||
|
case QuestRank.S:
|
||||||
|
// at level 30 adventurers have ~6513 dps, this will take 30 seconds on level 30
|
||||||
|
maxProgress = 195390;
|
||||||
|
break;
|
||||||
|
case QuestRank.A:
|
||||||
|
// at level 25 adventurers have ~2051 dps, this will take 15 seconds on level 25
|
||||||
|
maxProgress = 30770;
|
||||||
|
break;
|
||||||
|
case QuestRank.B:
|
||||||
|
// at level 20 adventurers have ~645 dps, this will take 15 seconds on level 20
|
||||||
|
maxProgress = 9690;
|
||||||
|
break;
|
||||||
|
case QuestRank.C:
|
||||||
|
// at level 15 adventurers have ~203 dps, this will take 15 seconds on level 15
|
||||||
|
maxProgress = 3045;
|
||||||
|
break;
|
||||||
|
case QuestRank.D:
|
||||||
|
// at level 10 adventurers have ~64 dps, this will take 15 seconds on level 10
|
||||||
|
maxProgress = 960;
|
||||||
|
break;
|
||||||
|
case QuestRank.E:
|
||||||
|
// at level 5 adventurers have ~20 dps, this will take 15 seconds on level 5
|
||||||
|
maxProgress = 300;
|
||||||
|
break;
|
||||||
|
case QuestRank.F:
|
||||||
|
// at level 1 adventurers have ~8 dps, this will take 15 seconds on level 1
|
||||||
|
maxProgress = 120;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let goldReward = Math.floor(maxProgress/6 * goldModifier);
|
||||||
|
let expReward = Math.floor((Math.floor(maxProgress/120) - maxProgress/1000) * expModifier);
|
||||||
|
|
||||||
|
// add some randomness to the rewards
|
||||||
|
goldReward = Math.floor(randomNumberBetween(goldReward * 0.95, goldReward * 1.1));
|
||||||
|
expReward = Math.max(1, Math.floor(randomNumberBetween(expReward * 0.95, expReward * 1.2)));
|
||||||
|
|
||||||
|
return new Quest(quest.id, quest.rank, quest.title, quest.text, maxProgress, expReward, goldReward);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomNumberBetween(min: number, max: number) {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type {Adventurer} from "@/classes/Adventurer";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random adventurer from the pool
|
||||||
|
* @param adventurerPool
|
||||||
|
* @param exceptions
|
||||||
|
* @returns {Adventurer|null} null if the pool is empty
|
||||||
|
*/
|
||||||
|
export function getNewAdventurerForHire(adventurerPool: Array<Adventurer>, exceptions: Array<Adventurer> = []): Adventurer|null {
|
||||||
|
if (adventurerPool.length <= 0) return null;
|
||||||
|
const pool = Array.from(adventurerPool);
|
||||||
|
const exceptionSet = new Set(exceptions);
|
||||||
|
pool.filter((adventurer) => !exceptionSet.has(adventurer));
|
||||||
|
const randomId = adventurerPool.length * Math.random() << 0;
|
||||||
|
return adventurerPool[randomId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
|
||||||
|
export default class AdventurerCapacityUpgrade extends GuildUpgrade {
|
||||||
|
constructor(level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.level = level;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
this.guildLevelRequirement = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
this.level += 1;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostForLevel(level: number): number {
|
||||||
|
if (level === 1) return 1500;
|
||||||
|
return Math.floor(1500 * (level * 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of adventurers the guild can have
|
||||||
|
*/
|
||||||
|
getAdventurerCapacity(): number {
|
||||||
|
return 1 + this.level ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
import type MaxLevellable from "@/classes/MaxLevellable";
|
||||||
|
import {QuestRank} from "@/classes/QuestRank";
|
||||||
|
|
||||||
|
export default class AutoFinishQuestsUpgrade extends GuildUpgrade implements MaxLevellable {
|
||||||
|
|
||||||
|
maxLevel: number;
|
||||||
|
|
||||||
|
constructor(level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.level = level;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
this.guildLevelRequirement = 7;
|
||||||
|
this.maxLevel = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
this.level += 1;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostForLevel(level: number): number {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return 25000;
|
||||||
|
case 2:
|
||||||
|
return 50000;
|
||||||
|
case 3:
|
||||||
|
return 75000;
|
||||||
|
case 4:
|
||||||
|
return 150000;
|
||||||
|
case 5:
|
||||||
|
return 275000;
|
||||||
|
case 6:
|
||||||
|
return 750000;
|
||||||
|
case 7:
|
||||||
|
return 1500000;
|
||||||
|
case 8:
|
||||||
|
return 2500000;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaxLevel(): boolean {
|
||||||
|
return this.level >= this.maxLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRanksToAutoFinishQuestsIn(): Array<QuestRank> {
|
||||||
|
switch (this.level) {
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
case 2:
|
||||||
|
return [QuestRank.F];
|
||||||
|
case 3:
|
||||||
|
return [QuestRank.F, QuestRank.E];
|
||||||
|
case 4:
|
||||||
|
return [QuestRank.F, QuestRank.E, QuestRank.D];
|
||||||
|
case 5:
|
||||||
|
return [QuestRank.F, QuestRank.E, QuestRank.D, QuestRank.C];
|
||||||
|
case 6:
|
||||||
|
return [QuestRank.F, QuestRank.E, QuestRank.D, QuestRank.C, QuestRank.B];
|
||||||
|
case 7:
|
||||||
|
return [QuestRank.F, QuestRank.E, QuestRank.D, QuestRank.C, QuestRank.B, QuestRank.A];
|
||||||
|
case 8:
|
||||||
|
return [QuestRank.F, QuestRank.E, QuestRank.D, QuestRank.C, QuestRank.B, QuestRank.A, QuestRank.S];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
|
||||||
|
export default class QuestExpUpgrade extends GuildUpgrade {
|
||||||
|
constructor(level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.level = level;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
this.guildLevelRequirement = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
this.level += 1;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostForLevel(level: number): number {
|
||||||
|
if (level === 1) return 1000000;
|
||||||
|
return Math.floor(1000000 * (level * 1.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifier(): number {
|
||||||
|
return 1 + (this.level * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
|
||||||
|
export default class QuestGoldUpgrade extends GuildUpgrade {
|
||||||
|
constructor(level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.level = level;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
this.guildLevelRequirement = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
this.level += 1;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostForLevel(level: number): number {
|
||||||
|
if (level === 1) return 1000000;
|
||||||
|
return Math.floor(1000000 * (level * 1.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifier(): number {
|
||||||
|
return 1 + (this.level * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {GuildUpgrade} from "@/classes/GuildUpgrade";
|
||||||
|
import type MaxLevellable from "@/classes/MaxLevellable";
|
||||||
|
|
||||||
|
export default class AdventurerCapacityUpgrade extends GuildUpgrade implements MaxLevellable {
|
||||||
|
|
||||||
|
maxLevel: number;
|
||||||
|
|
||||||
|
constructor(level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.level = level;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
this.guildLevelRequirement = 3;
|
||||||
|
this.maxLevel = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade(): void {
|
||||||
|
this.level += 1;
|
||||||
|
this.nextLevelCost = this.getCostForLevel(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostForLevel(level: number): number {
|
||||||
|
if (level === 1) return 1500;
|
||||||
|
return Math.floor(1500 * (level * 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of adventurers the guild can have
|
||||||
|
*/
|
||||||
|
getRecruitmentCapacity(): number {
|
||||||
|
return this.level ;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaxLevel(): boolean {
|
||||||
|
return this.level >= this.maxLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
|
|||||||
|
<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, type PropType} 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: Object as PropType<Array<Adventurer>>,
|
||||||
|
default() {
|
||||||
|
return [] as Array<Adventurer>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { vOnClickOutside } from '@vueuse/components'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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" v-on-click-outside="closeSelect">
|
||||||
|
<span>Choose adventurer</span>
|
||||||
|
<div class="list">
|
||||||
|
<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>
|
||||||
|
</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|null|any>,
|
||||||
|
default() {
|
||||||
|
return null as Adventurer|null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allAdventurers: {
|
||||||
|
type: Object as PropType<Array<Adventurer>>,
|
||||||
|
default() {
|
||||||
|
return [] as Array<Adventurer>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeSelect() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.selection = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: rgba(0,0,0, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 2;
|
||||||
|
cursor: default;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
color: #000;
|
||||||
|
span {
|
||||||
|
transform: translateY(-0.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<template>
|
||||||
|
<section class="recruit panel pinned-paper">
|
||||||
|
<h1>Applying adventurers
|
||||||
|
{{ `(${Object.keys(adventurersForHire).length}/${guild.recruitmentCapacity.getRecruitmentCapacity()})` }}</h1>
|
||||||
|
<div class="adventurers">
|
||||||
|
<div class="adventurer-tile" v-for="adventurerForHire in currentlyForHire" :key="adventurerForHire.id">
|
||||||
|
<adventurer-tile
|
||||||
|
class="hire-tile"
|
||||||
|
:adventurer="adventurerForHire"
|
||||||
|
@click="previewAdventurer(adventurerForHire)"
|
||||||
|
/>
|
||||||
|
<div class="decision">
|
||||||
|
<span
|
||||||
|
title="Hire"
|
||||||
|
@click="hireAdventurer(adventurerForHire)"
|
||||||
|
:class="{disabled: !canRecruitMore}"
|
||||||
|
>
|
||||||
|
✔
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:title="Object.keys(adventurersForHire).length > 0 ? 'Dismiss' : ''"
|
||||||
|
:class="{disabled: Object.keys(adventurersForHire).length <= 0}"
|
||||||
|
@click="dismissAdventurer(adventurerForHire)"
|
||||||
|
>
|
||||||
|
✗
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="Object.keys(adventurersForHire).length == 0">
|
||||||
|
<span>No one applied as of now. Check back later!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="button metal find-recruit"
|
||||||
|
:disabled="recruitSlotsFilled || guild.gold < newRecruitCost"
|
||||||
|
@click="findNewRecruit()"
|
||||||
|
>Find a recruit now {{(`(${formatGold(newRecruitCost)}) gold`)}}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {defineComponent, type PropType} from "vue";
|
||||||
|
import AdventurerTile from "@/components/AdventurerTile.vue";
|
||||||
|
import type {Guild} from "@/classes/Guild";
|
||||||
|
import type {Adventurer} from "@/classes/Adventurer";
|
||||||
|
import {formatGold} from "@/classes/NumberMagic";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "RecruitView",
|
||||||
|
components: {AdventurerTile},
|
||||||
|
computed: {
|
||||||
|
currentlyForHire(): Array<Adventurer> {
|
||||||
|
return Object.values(this.adventurersForHire);
|
||||||
|
},
|
||||||
|
canRecruitMore() {
|
||||||
|
return Object.keys(this.adventurers).length < this.guild.adventurerCapacity.getAdventurerCapacity();
|
||||||
|
},
|
||||||
|
newRecruitCost(): number {
|
||||||
|
const guildLevel = this.guild.level;
|
||||||
|
return Math.max(500, 500 * Math.pow(2.2, guildLevel - 1));
|
||||||
|
},
|
||||||
|
recruitSlotsFilled(): boolean {
|
||||||
|
return Object.keys(this.adventurersForHire).length >= this.guild.recruitmentCapacity.getRecruitmentCapacity();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatGold,
|
||||||
|
hireAdventurer(adventurer: Adventurer): void {
|
||||||
|
if (!this.canRecruitMore) return;
|
||||||
|
this.$emit("hireAdventurer", adventurer);
|
||||||
|
},
|
||||||
|
dismissAdventurer(adventurer: Adventurer) {
|
||||||
|
if (Object.keys(this.adventurersForHire).length <= 0) return;
|
||||||
|
this.$emit("dismissAdventurer", adventurer);
|
||||||
|
},
|
||||||
|
previewAdventurer(adventurer: Adventurer): void {
|
||||||
|
this.$emit("previewAdventurer", adventurer);
|
||||||
|
},
|
||||||
|
findNewRecruit(): void {
|
||||||
|
if (this.recruitSlotsFilled) return;
|
||||||
|
this.$emit("findNewRecruit");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
guild: {
|
||||||
|
type: Object as PropType<Guild>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurersForHire: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurers: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["dismissAdventurer", "hireAdventurer", "previewAdventurer", "findNewRecruit"],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-recruit {
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adventurers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 1rem;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
overflow-x: scroll;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 12rem;
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-snap-type: none;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adventurer-tile {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-block: 1rem;
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
min-width: 100%;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 4rem;
|
||||||
|
content: "⇠";
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 4rem;
|
||||||
|
content: "⇢";
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
min-width: auto;
|
||||||
|
scroll-snap-align: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hire-tile {
|
||||||
|
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;
|
||||||
|
line-height: 1;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<article
|
||||||
|
class="adventurer"
|
||||||
|
:title="adventurer.name + (adventurer.busy ? ' (busy)' : '')"
|
||||||
|
>
|
||||||
|
<img :src="adventurer.portrait" :alt="adventurer.name" draggable="false">
|
||||||
|
<div class="level" :title="adventurer.isMaxLevel() ? 'Max level' : ''">{{ adventurer.level }}<span
|
||||||
|
v-if="adventurer.isMaxLevel()">⇪</span></div>
|
||||||
|
<div class="exp"></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>,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
expProgress: "0%",
|
||||||
|
}),
|
||||||
|
watch: {
|
||||||
|
adventurer: {
|
||||||
|
deep: true,
|
||||||
|
handler: function (adventurer: Adventurer) {
|
||||||
|
this.expProgress = adventurer.getExpPercentage() + "%";
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.adventurer === undefined) return;
|
||||||
|
this.expProgress = this.adventurer.getExpPercentage() + "%";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.adventurer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: clip;
|
||||||
|
font-size: 5rem;
|
||||||
|
line-height: 1;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
position: relative;
|
||||||
|
background: rgb(2,0,36);
|
||||||
|
background: radial-gradient(circle, rgba(2,0,36,1) 0%, rgb(69, 69, 84) 57%, rgb(85, 112, 117) 100%);
|
||||||
|
|
||||||
|
.level {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 1rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
border-bottom-right-radius: 0.2rem;
|
||||||
|
padding: 0.1rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: v-bind(expProgress);
|
||||||
|
height: 3.5%;
|
||||||
|
background-color: rgba(203, 33, 213, 0.75);
|
||||||
|
transition: width 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="missives-wrapper">
|
||||||
|
<h1 v-if="label !== undefined">{{ label }}</h1>
|
||||||
|
<section class="missives">
|
||||||
|
<QuestMissive
|
||||||
|
v-for="(missive, key) in quests"
|
||||||
|
:key="key"
|
||||||
|
:adventurers="adventurers"
|
||||||
|
:missive="missive"
|
||||||
|
@click="finalizeQuest(missive)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from 'vue'
|
||||||
|
import QuestMissive from "@/components/QuestMissive.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "QuestGroup",
|
||||||
|
components: {QuestMissive},
|
||||||
|
props: {
|
||||||
|
quests: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurers: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
finalizeQuest: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missives-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missives {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-block: 0.5rem;
|
||||||
|
padding-inline: 5rem;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: 800px) {
|
||||||
|
.missives-wrapper {
|
||||||
|
padding-inline: 1rem;
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missives {
|
||||||
|
display: grid;
|
||||||
|
padding-inline: 0;
|
||||||
|
max-width: 1200px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<template>
|
||||||
|
<article
|
||||||
|
class="missive"
|
||||||
|
:class="{done: missive.maxProgress <= missive.progressPoints}"
|
||||||
|
>
|
||||||
|
<div class="parchment">
|
||||||
|
<Parchment/>
|
||||||
|
</div>
|
||||||
|
<div class="stain" v-if="stain">
|
||||||
|
<WaterStain/>
|
||||||
|
</div>
|
||||||
|
<div class="drink-stain" v-if="drinkStain.exists">
|
||||||
|
<DrinkStain/>
|
||||||
|
</div>
|
||||||
|
<div class="rank">{{missive.rank}}</div>
|
||||||
|
<h2>{{ missive.title }}</h2>
|
||||||
|
<p>{{ missive.text }}</p>
|
||||||
|
<div class="slots">
|
||||||
|
<button class="slot">
|
||||||
|
<AdventurerComponent
|
||||||
|
:adventurer="missive.adventurers[0]"
|
||||||
|
:all-adventurers="notBusyAdventurers"
|
||||||
|
@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>
|
||||||
|
<div class="progressWrap">
|
||||||
|
<span class="progress"></span>
|
||||||
|
<span class="percentage">{{ `${progressPercentage.toFixed(2)}%` }}</span>
|
||||||
|
</div>
|
||||||
|
<h3>Rewards</h3>
|
||||||
|
<div class="rewards">
|
||||||
|
<span>Gold: <b>{{ missive.goldReward }}</b></span>
|
||||||
|
<span>Exp: <b>{{ missive.expReward }}</b></span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type {Quest} from "@/classes/Quest";
|
||||||
|
import AdventurerComponent from "@/components/AdventurerMiniComponent.vue";
|
||||||
|
import type {Adventurer} from "@/classes/Adventurer";
|
||||||
|
import {defineComponent, type PropType} from "vue";
|
||||||
|
import DrinkStain from "@/components/misc/DrinkStain.vue";
|
||||||
|
import WaterStain from "@/components/misc/WaterStain.vue";
|
||||||
|
import Parchment from "@/components/misc/Parchment.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "QuestMissive",
|
||||||
|
components: {Parchment, WaterStain, DrinkStain, AdventurerComponent},
|
||||||
|
computed: {
|
||||||
|
progressPercentageValue(): string {
|
||||||
|
return `${this.missive.progressPoints / this.missive.maxProgress * 100}%`;
|
||||||
|
},
|
||||||
|
notBusyAdventurers(): Adventurer[] {
|
||||||
|
return Object.values(this.adventurers).filter(adventurer => !adventurer.busy);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
missive: {
|
||||||
|
type: Object as PropType<Quest | any>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurers: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
default() {
|
||||||
|
return {} as { [key: string]: Adventurer };
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
progressPercentage: 0,
|
||||||
|
stain: false,
|
||||||
|
drinkStain: {
|
||||||
|
exists: false,
|
||||||
|
offsetX: "0%",
|
||||||
|
offsetY: "0%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateProgress() {
|
||||||
|
if (this.missive === undefined) return;
|
||||||
|
this.progressPercentage = this.missive.progressPoints / this.missive.maxProgress * 100;
|
||||||
|
},
|
||||||
|
randomNumber(min: number, max: number) {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateProgress();
|
||||||
|
this.stain = Math.random() < 0.35;
|
||||||
|
this.drinkStain.exists = Math.random() < 0.15;
|
||||||
|
if (this.drinkStain.exists) {
|
||||||
|
this.drinkStain.offsetX = `${this.randomNumber(-1, 1) * 100}%`;
|
||||||
|
this.drinkStain.offsetY = `${this.randomNumber(-1, 1) * 100}%`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"missive.progressPoints": {
|
||||||
|
handler() {
|
||||||
|
this.updateProgress();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.missive {
|
||||||
|
width: 14rem;
|
||||||
|
min-width: 14rem;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.parchment {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -5;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressWrap {
|
||||||
|
width: 80%;
|
||||||
|
border: 1px solid #000;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
position: relative;
|
||||||
|
height: 1.25rem;
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
width: v-bind(progressPercentageValue);
|
||||||
|
background-color: rgba(0, 128, 0, 0.65);
|
||||||
|
transition: width 250ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.5rem;
|
||||||
|
left: 0.25rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ab0707;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stain {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: contain;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: -4;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: grayscale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drink-stain {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.45;
|
||||||
|
z-index: -1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 45%;
|
||||||
|
height: 35%;
|
||||||
|
filter: grayscale(0.8);
|
||||||
|
transform: translate(v-bind("drinkStain.offsetX"), v-bind("drinkStain.offsetY"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<section class="upgrades">
|
||||||
|
<h2>Upgrades</h2>
|
||||||
|
<div class="upgrade">
|
||||||
|
<span>Adventurer capacity (level {{ guild.adventurerCapacity.level }})</span>
|
||||||
|
<small>Increases the maximum amount of recruited adventurers</small>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
v-if="guild.adventurerCapacity.nextLevelCost"
|
||||||
|
:disabled="guild.gold < guild.adventurerCapacity.nextLevelCost"
|
||||||
|
@click="upgradeAdventurerCapacity()"
|
||||||
|
>
|
||||||
|
Upgrade ({{ formatGold(guild.adventurerCapacity.nextLevelCost) }} gold)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="upgrade">
|
||||||
|
<span>Recruitment capacity (level {{ guild.recruitmentCapacity.level }})</span>
|
||||||
|
<small>Increases the maximum amount of adventurers that await recruitment</small>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
v-if="guild.recruitmentCapacity.nextLevelCost"
|
||||||
|
:disabled="guild.gold < guild.recruitmentCapacity.nextLevelCost || guild.recruitmentCapacity.isMaxLevel()"
|
||||||
|
@click="upgradeRecruitmentCapacity()"
|
||||||
|
>
|
||||||
|
<span v-if="!guild.recruitmentCapacity.isMaxLevel()">Upgrade ({{ formatGold(guild.recruitmentCapacity.nextLevelCost) }} gold)</span>
|
||||||
|
<span v-else>Max level</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="upgrade" v-if="guild.level >= guild.autoFinishQuestsUpgrade.guildLevelRequirement">
|
||||||
|
<span>Auto-finish quests (level {{ guild.autoFinishQuestsUpgrade.level - 1 }})</span>
|
||||||
|
<small>Automatically finish quests when they are completed.</small>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
v-if="guild.autoFinishQuestsUpgrade.nextLevelCost"
|
||||||
|
:disabled="guild.gold < guild.autoFinishQuestsUpgrade.nextLevelCost || guild.autoFinishQuestsUpgrade.isMaxLevel()"
|
||||||
|
@click="upgradeAutoFinishQuests()"
|
||||||
|
>
|
||||||
|
<span v-if="!guild.autoFinishQuestsUpgrade.isMaxLevel()">Upgrade ({{ formatGold(guild.autoFinishQuestsUpgrade.nextLevelCost) }} gold)</span>
|
||||||
|
<span v-else>Max level</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="upgrade" v-if="guild.level >= guild.expModifier.guildLevelRequirement">
|
||||||
|
<span>Quest exp modifier (level {{ guild.expModifier.level - 1 }})</span>
|
||||||
|
<small>Increases exp from newly offered quests by 10% per level</small>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
v-if="guild.expModifier.nextLevelCost"
|
||||||
|
:disabled="guild.gold < guild.expModifier.nextLevelCost"
|
||||||
|
@click="upgradeQuestExpModifier()"
|
||||||
|
>
|
||||||
|
Upgrade ({{ formatGold(guild.expModifier.nextLevelCost) }} gold)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="upgrade" v-if="guild.level >= guild.goldModifier.guildLevelRequirement">
|
||||||
|
<span>Quest gold modifier (level {{ guild.goldModifier.level - 1 }})</span>
|
||||||
|
<small>Increases gold from newly offered quests by 10% per level</small>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
v-if="guild.goldModifier.nextLevelCost"
|
||||||
|
:disabled="guild.gold < guild.goldModifier.nextLevelCost"
|
||||||
|
@click="upgradeQuestGoldModifier()"
|
||||||
|
>
|
||||||
|
Upgrade ({{ formatGold(guild.goldModifier.nextLevelCost) }} gold)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Guild} from "@/classes/Guild";
|
||||||
|
import {defineComponent, type PropType} from "vue";
|
||||||
|
import {formatGold} from "@/classes/NumberMagic";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "UpgradesList",
|
||||||
|
props: {
|
||||||
|
guild: {
|
||||||
|
type: Object as PropType<Guild>,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatGold,
|
||||||
|
upgradeAdventurerCapacity(): void {
|
||||||
|
if (!this.guild.adventurerCapacity) return;
|
||||||
|
if (!this.guild.adventurerCapacity.nextLevelCost) return;
|
||||||
|
if (this.guild.gold < this.guild.adventurerCapacity.nextLevelCost) return;
|
||||||
|
this.guild.gold -= this.guild.adventurerCapacity.nextLevelCost;
|
||||||
|
this.guild.adventurerCapacity.upgrade();
|
||||||
|
},
|
||||||
|
upgradeRecruitmentCapacity(): void {
|
||||||
|
if (!this.guild.recruitmentCapacity) return;
|
||||||
|
if (this.guild.recruitmentCapacity.isMaxLevel()) return;
|
||||||
|
if (!this.guild.recruitmentCapacity.nextLevelCost) return;
|
||||||
|
if (this.guild.gold < this.guild.recruitmentCapacity.nextLevelCost) return;
|
||||||
|
this.guild.gold -= this.guild.recruitmentCapacity.nextLevelCost;
|
||||||
|
this.guild.recruitmentCapacity.upgrade();
|
||||||
|
},
|
||||||
|
upgradeAutoFinishQuests(): void {
|
||||||
|
if (!this.guild.autoFinishQuestsUpgrade) return;
|
||||||
|
if (this.guild.autoFinishQuestsUpgrade.isMaxLevel()) return;
|
||||||
|
if (!this.guild.autoFinishQuestsUpgrade.nextLevelCost) return;
|
||||||
|
if (this.guild.gold < this.guild.autoFinishQuestsUpgrade.nextLevelCost) return;
|
||||||
|
this.guild.gold -= this.guild.autoFinishQuestsUpgrade.nextLevelCost;
|
||||||
|
this.guild.autoFinishQuestsUpgrade.upgrade();
|
||||||
|
},
|
||||||
|
upgradeQuestExpModifier(): void {
|
||||||
|
if (!this.guild.expModifier) return;
|
||||||
|
if (!this.guild.expModifier.nextLevelCost) return;
|
||||||
|
if (this.guild.gold < this.guild.expModifier.nextLevelCost) return;
|
||||||
|
this.guild.gold -= this.guild.expModifier.nextLevelCost;
|
||||||
|
this.guild.expModifier.upgrade();
|
||||||
|
},
|
||||||
|
upgradeQuestGoldModifier(): void {
|
||||||
|
if (!this.guild.goldModifier) return;
|
||||||
|
if (!this.guild.goldModifier.nextLevelCost) return;
|
||||||
|
if (this.guild.gold < this.guild.goldModifier.nextLevelCost) return;
|
||||||
|
this.guild.gold -= this.guild.goldModifier.nextLevelCost;
|
||||||
|
this.guild.goldModifier.upgrade();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.upgrades {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin: 2rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: min(25rem, 100%);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
small {
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "DiscordLogo",
|
||||||
|
props: {
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: "2rem",
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "2rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<title>Discord</title>
|
||||||
|
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
svg {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "GithubLogo",
|
||||||
|
props: {
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: "2rem",
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "2rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect width="24" height="24" fill="none"/>
|
||||||
|
<path d="M12,2A10,10,0,0,0,8.84,21.5c.5.08.66-.23.66-.5V19.31C6.73,19.91,6.14,18,6.14,18A2.69,2.69,0,0,0,5,16.5c-.91-.62.07-.6.07-.6a2.1,2.1,0,0,1,1.53,1,2.15,2.15,0,0,0,2.91.83,2.16,2.16,0,0,1,.63-1.34C8,16.17,5.62,15.31,5.62,11.5a3.87,3.87,0,0,1,1-2.71,3.58,3.58,0,0,1,.1-2.64s.84-.27,2.75,1a9.63,9.63,0,0,1,5,0c1.91-1.29,2.75-1,2.75-1a3.58,3.58,0,0,1,.1,2.64,3.87,3.87,0,0,1,1,2.71c0,3.82-2.34,4.66-4.57,4.91a2.39,2.39,0,0,1,.69,1.85V21c0,.27.16.59.67.5A10,10,0,0,0,12,2Z"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
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
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="changelog panel pinned-paper">
|
||||||
|
<div class="nail top-left">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<div class="nail top-right">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<h1>Changelog</h1>
|
||||||
|
<div class="changelog-list">
|
||||||
|
<div class="changelog-entry" v-for="release in releases">
|
||||||
|
<hr>
|
||||||
|
<h2><span>Version {{ release.name }}</span><small class="date">{{ timeFormat.format(release.createdAt) }}</small></h2>
|
||||||
|
<pre>{{ release.body }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from "vue";
|
||||||
|
import Nail from "@/components/misc/Nail.vue";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ChangelogComponent",
|
||||||
|
components: {Nail},
|
||||||
|
data: () => ({
|
||||||
|
timeFormat: Intl.DateTimeFormat(Intl.DateTimeFormat().resolvedOptions().locale, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
}),
|
||||||
|
releases: [] as Array<{body: string, name: string, createdAt: Date}>,
|
||||||
|
lastPage: 1,
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
async getReleases(page: number): Promise<void> {
|
||||||
|
const result = await fetch("https://api.github.com/repos/YouHaveTrouble/GuildMaster/releases?per_page=10")
|
||||||
|
.catch((e) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) return;
|
||||||
|
const json = await result.json();
|
||||||
|
|
||||||
|
for (const release of json) {
|
||||||
|
const version = {} as any;
|
||||||
|
version.body = release.body.trim();
|
||||||
|
version.name = release.name;
|
||||||
|
version.createdAt = new Date(release.published_at);
|
||||||
|
if (release.body.length === 0) continue;
|
||||||
|
this.releases.push(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.getReleases(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.changelog {
|
||||||
|
padding-block: 3rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 45rem;
|
||||||
|
min-height: 30rem;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media(min-width: 800px) {
|
||||||
|
max-height: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-entry {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin: 0;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: rgba(0,0,0, 0.6);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: 'EB Garamond', serif;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
|
|||||||
import type StatHolder from "@/models/StatHolder.ts";
|
|
||||||
import AdventurerInventory from "@/models/AdventurerInventory.ts";
|
|
||||||
import type AdventurerIdentity from "@/models/AdventurerIdentity.ts";
|
|
||||||
|
|
||||||
export default class Adventurer implements StatHolder {
|
|
||||||
|
|
||||||
identity: AdventurerIdentity;
|
|
||||||
experience: number = 0;
|
|
||||||
basePower: number = 0;
|
|
||||||
baseDefense: number = 0;
|
|
||||||
inventory: AdventurerInventory;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
identity: AdventurerIdentity,
|
|
||||||
experience: number = 0,
|
|
||||||
basePower: number = 0,
|
|
||||||
baseDefense: number = 0,
|
|
||||||
inventory: AdventurerInventory = new AdventurerInventory()
|
|
||||||
) {
|
|
||||||
this.identity = identity;
|
|
||||||
this.experience = experience;
|
|
||||||
this.basePower = basePower;
|
|
||||||
this.baseDefense = baseDefense;
|
|
||||||
this.inventory = inventory;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPower(): number {
|
|
||||||
let power = this.basePower;
|
|
||||||
power += this.inventory.getPower();
|
|
||||||
return power;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefense(): number {
|
|
||||||
let defense = this.baseDefense;
|
|
||||||
defense += this.inventory.getDefense();
|
|
||||||
return defense;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export default class AdventurerIdentity {
|
|
||||||
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
portrait: string;
|
|
||||||
|
|
||||||
constructor(id: string, name: string, portrait: string) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.portrait = portrait;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type Item from "@/models/Item.ts";
|
|
||||||
import type StatHolder from "@/models/StatHolder.ts";
|
|
||||||
|
|
||||||
export default class AdventurerInventory implements StatHolder {
|
|
||||||
|
|
||||||
helmetId = 0;
|
|
||||||
armorId = 1;
|
|
||||||
bootsId = 2;
|
|
||||||
weaponId = 3;
|
|
||||||
|
|
||||||
items: Array<Item|null> = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
helmet: Item | null = null,
|
|
||||||
armor: Item | null = null,
|
|
||||||
boots: Item | null = null,
|
|
||||||
weapon: Item | null = null
|
|
||||||
) {
|
|
||||||
this.items[this.helmetId] = helmet;
|
|
||||||
this.items[this.armorId] = armor;
|
|
||||||
this.items[this.bootsId] = boots;
|
|
||||||
this.items[this.weaponId] = weapon;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHelmet(): Item | null {
|
|
||||||
return this.items[this.helmetId];
|
|
||||||
}
|
|
||||||
|
|
||||||
getArmor(): Item | null {
|
|
||||||
return this.items[this.armorId];
|
|
||||||
}
|
|
||||||
|
|
||||||
getBoots(): Item | null {
|
|
||||||
return this.items[this.bootsId];
|
|
||||||
}
|
|
||||||
|
|
||||||
getWeapon(): Item | null {
|
|
||||||
return this.items[this.weaponId];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefense(): number {
|
|
||||||
let defense = 0;
|
|
||||||
for (let item of this.items) {
|
|
||||||
if (item !== null) {
|
|
||||||
defense += item.getDefense();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defense
|
|
||||||
}
|
|
||||||
|
|
||||||
getPower(): number {
|
|
||||||
let power = 0;
|
|
||||||
for (let item of this.items) {
|
|
||||||
if (item !== null) {
|
|
||||||
power += item.getPower();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return power;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type StatHolder from "@/models/StatHolder.ts";
|
|
||||||
import type {ItemType} from "@/models/ItemType.ts";
|
|
||||||
|
|
||||||
export default class Item implements StatHolder {
|
|
||||||
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
power: number;
|
|
||||||
defense: number;
|
|
||||||
type: ItemType;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
power: number,
|
|
||||||
defense: number,
|
|
||||||
type: ItemType
|
|
||||||
) {
|
|
||||||
this.name = name;
|
|
||||||
this.description = description;
|
|
||||||
this.power = power;
|
|
||||||
this.defense = defense;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefense(): number {
|
|
||||||
return this.defense;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPower(): number {
|
|
||||||
return this.power;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export enum ItemType {
|
|
||||||
|
|
||||||
HELMET = 'helmet',
|
|
||||||
ARMOR = 'armor',
|
|
||||||
BOOTS = 'boots',
|
|
||||||
WEAPON = 'weapon',
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import Adventurer from "@/models/Adventurer.ts";
|
|
||||||
import type AdventurerIdentity from "@/models/AdventurerIdentity.ts";
|
|
||||||
import AdventurerInventory from "@/models/AdventurerInventory.ts";
|
|
||||||
|
|
||||||
export default class SaveData {
|
|
||||||
|
|
||||||
adventurers: Array<Adventurer> = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
data: {[key:string]: unknown} = {},
|
|
||||||
adventurerIdentities: {[key:string]: AdventurerIdentity} = {}
|
|
||||||
) {
|
|
||||||
if (Array.isArray(data?.adventurers)) {
|
|
||||||
for (const adventurerData of data.adventurers) {
|
|
||||||
const identity = adventurerIdentities[adventurerData?.identity?.id];
|
|
||||||
if (!identity) {
|
|
||||||
console.error("Adventurer identity not found for adventurer data", adventurerData);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.adventurers.push(new Adventurer(
|
|
||||||
identity,
|
|
||||||
adventurerData?.experience ?? 0,
|
|
||||||
adventurerData?.basePower ?? 0,
|
|
||||||
adventurerData?.baseDefense ?? 0,
|
|
||||||
new AdventurerInventory(
|
|
||||||
adventurerData?.inventory?.helmet ?? null,
|
|
||||||
adventurerData?.inventory?.armor ?? null,
|
|
||||||
adventurerData?.inventory?.boots ?? null,
|
|
||||||
adventurerData?.inventory?.weapon ?? null,
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default interface StatHolder {
|
|
||||||
|
|
||||||
getPower(): number;
|
|
||||||
getDefense(): number;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||||
|
|
||||||
import HomeView from '@/views/HomeView.vue';
|
import HomeView from '@/views/HomeView.vue';
|
||||||
|
import QuestView from "@/views/QuestView.vue";
|
||||||
|
import AdventurerView from "@/views/AdventurerView.vue";
|
||||||
|
import TechnicalView from "@/views/TechnicalView.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
@@ -10,6 +13,21 @@ const router = createRouter({
|
|||||||
name: 'guild',
|
name: 'guild',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/quests',
|
||||||
|
name: 'quests',
|
||||||
|
component: QuestView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/adventurers',
|
||||||
|
name: 'adventurers',
|
||||||
|
component: AdventurerView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/technical',
|
||||||
|
name: 'technical',
|
||||||
|
component: TechnicalView,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div class="adventurer-section">
|
||||||
|
<AdventurerDetails
|
||||||
|
:adventurer="selectedAdventurer"
|
||||||
|
v-if="selectedAdventurer !== null"
|
||||||
|
@closeButtonClicked="selectedAdventurer = null"
|
||||||
|
:show-prestige-button="adventurers[selectedAdventurer?.id] !== undefined"
|
||||||
|
/>
|
||||||
|
<AdventurerRecruitment
|
||||||
|
:guild="guild"
|
||||||
|
:adventurers-for-hire="adventurersForHire"
|
||||||
|
:adventurers="adventurers"
|
||||||
|
@hireAdventurer="$emit('hireAdventurer', $event)"
|
||||||
|
@dismissAdventurer="$emit('dismissAdventurer', $event)"
|
||||||
|
@previewAdventurer="selectedAdventurer = $event"
|
||||||
|
@findNewRecruit="$emit('findNewRecruit')"
|
||||||
|
/>
|
||||||
|
<section class="collection panel pinned-paper">
|
||||||
|
<h1>
|
||||||
|
Recruited adventurers ({{ Object.keys(adventurers).length }} /
|
||||||
|
{{ guild.adventurerCapacity.getAdventurerCapacity() }})
|
||||||
|
</h1>
|
||||||
|
<small>Click an adventurer to see details about them</small>
|
||||||
|
<div class="adventurers">
|
||||||
|
<div
|
||||||
|
class="adventurer-tile"
|
||||||
|
v-for="adventurer in adventurers"
|
||||||
|
:key="adventurer.id"
|
||||||
|
@click="selectedAdventurer = adventurer"
|
||||||
|
>
|
||||||
|
<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 type {Adventurer} from "@/classes/Adventurer";
|
||||||
|
import type {Guild} from "@/classes/Guild";
|
||||||
|
import AdventurerDetails from "@/components/AdventurerDetails.vue";
|
||||||
|
import AdventurerRecruitment from "@/components/AdventurerRecruitment.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "AdventurerView",
|
||||||
|
components: {AdventurerDetails, AdventurerTile, AdventurerRecruitment},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
selectedAdventurer: null as Adventurer | null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
guild: {
|
||||||
|
type: Object as PropType<Guild>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurers: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurersForHire: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["hireAdventurer", "dismissAdventurer", "findNewRecruit"],
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.adventurer-section {
|
||||||
|
padding-block: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection {
|
||||||
|
small {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hire-tile {
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+82
-2
@@ -6,18 +6,49 @@
|
|||||||
<small>v{{ version }}</small>
|
<small>v{{ version }}</small>
|
||||||
<p class="news">{{ news }}</p>
|
<p class="news">{{ news }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="upgrades panel pinned-paper">
|
||||||
|
<div class="nail top-left">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<div class="nail top-right">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<section class="coffer">
|
||||||
|
<p>Coffer: {{ formatGold(guild.gold) }} gold</p>
|
||||||
|
</section>
|
||||||
|
<section class="upgrade">
|
||||||
|
<p>Guild level: {{ guild.level }}</p>
|
||||||
|
<button
|
||||||
|
class="button metal"
|
||||||
|
:disabled="guild.upgradeCost ? guild.gold < guild.upgradeCost : true"
|
||||||
|
@click="guild.upgrade()"
|
||||||
|
>
|
||||||
|
<span>Upgrade guild level </span>
|
||||||
|
<span>({{ guild.displayUpgradeCost }})</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<section class="upgrade">
|
||||||
|
<UpgradesList :guild="guild"/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from "vue";
|
import {defineComponent} from "vue";
|
||||||
|
import type {PropType} from "vue";
|
||||||
|
import {Guild} from "@/classes/Guild";
|
||||||
|
|
||||||
import {version} from "../../package.json"
|
import {version} from "../../package.json"
|
||||||
|
import UpgradesList from "@/components/UpgradesList.vue";
|
||||||
|
import {formatGold} from "@/classes/NumberMagic";
|
||||||
|
import Nail from "@/components/misc/Nail.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "GuildView",
|
name: "GuildView",
|
||||||
methods: {},
|
methods: {formatGold},
|
||||||
components: {},
|
components: {Nail, UpgradesList},
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
version: version,
|
version: version,
|
||||||
@@ -28,11 +59,60 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
guild: {
|
||||||
|
type: Object as PropType<Guild>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-block: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.upgrades {
|
||||||
|
max-width: 45rem;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.news {
|
||||||
|
max-width: 75%;
|
||||||
|
color: #ab0707;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<QuestGroup
|
||||||
|
:adventurers="adventurers"
|
||||||
|
:quests="quests.filter(quest => quest.progressPoints < quest.maxProgress)"
|
||||||
|
:finalizeQuest="finalizeQuest"
|
||||||
|
label="Quests"
|
||||||
|
v-show="quests.filter(quest => quest.progressPoints < quest.maxProgress).length > 0"
|
||||||
|
/>
|
||||||
|
<QuestGroup
|
||||||
|
:finalize-quest="finalizeQuest"
|
||||||
|
:adventurers="adventurers"
|
||||||
|
:quests="quests.filter(quest => quest.progressPoints >= quest.maxProgress)"
|
||||||
|
label="Completed Quests"
|
||||||
|
v-show="quests.filter(quest => quest.progressPoints >= quest.maxProgress).length > 0"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</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 {Guild} from "@/classes/Guild";
|
||||||
|
import QuestMissive from "@/components/QuestMissive.vue";
|
||||||
|
import QuestGroup from "@/components/QuestGroup.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "QuestView",
|
||||||
|
components: {QuestGroup, QuestMissive, AdventurerComponent},
|
||||||
|
props: {
|
||||||
|
guild: {
|
||||||
|
type: Object as PropType<Guild>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
adventurers: {
|
||||||
|
type: Object as PropType<{ [key: string]: Adventurer }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
quests: {
|
||||||
|
type: Object as PropType<Array<Quest>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
lastRecruitTime: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['finalizeQuest', 'wipeSave', 'recruitActionTaken'],
|
||||||
|
methods: {
|
||||||
|
// This is a workaround for vue not reporting Quest as Quest in v-for
|
||||||
|
finalizeQuest(quest: any | Quest): void {
|
||||||
|
if (quest.progressPoints < quest.maxProgress) return;
|
||||||
|
this.$emit('finalizeQuest', quest)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: 800px) {
|
||||||
|
section {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<section class="technical-view">
|
||||||
|
<div class="socials panel pinned-paper">
|
||||||
|
<div class="nail top-left small">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<div class="nail top-right small">
|
||||||
|
<Nail/>
|
||||||
|
</div>
|
||||||
|
<h1>Socials</h1>
|
||||||
|
<div class="links">
|
||||||
|
<a class="link" href="https://discord.gg/j8KK5dGBps">
|
||||||
|
<DiscordLogo/>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
<a class="link" href="https://github.com/YouHaveTrouble/GuildMaster">
|
||||||
|
<GithubLogo/>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SaveManagerComponent/>
|
||||||
|
<ChangelogComponent/>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from "vue";
|
||||||
|
import ChangelogComponent from "@/components/technical/ChangelogComponent.vue";
|
||||||
|
import SaveManagerComponent from "@/components/technical/SaveManagerComponent.vue";
|
||||||
|
import Nail from "@/components/misc/Nail.vue";
|
||||||
|
import DiscordLogo from "@/components/misc/DiscordLogo.vue";
|
||||||
|
import GithubLogo from "@/components/misc/GithubLogo.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "TechnicalView",
|
||||||
|
components: {
|
||||||
|
DiscordLogo,
|
||||||
|
GithubLogo,
|
||||||
|
Nail,
|
||||||
|
SaveManagerComponent,
|
||||||
|
ChangelogComponent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.technical-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-block: 1rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socials {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.05rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+1
-12
@@ -2,21 +2,10 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import {VitePWA} from "vite-plugin-pwa";
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [vue()],
|
||||||
vue(),
|
|
||||||
VitePWA({
|
|
||||||
registerType: 'prompt',
|
|
||||||
injectRegister: 'script',
|
|
||||||
filename: 'service-worker.js',
|
|
||||||
workbox: {
|
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,jpeg,svg,json}'],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|||||||
Reference in New Issue
Block a user