Compare commits

..

18 Commits

17 changed files with 982 additions and 665 deletions
+20 -8
View File
@@ -1,18 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guild Master - Adventurer's guild management game</title> <title>Guild Master - Adventurer's guild management game</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=EB+Garamond&display=swap" rel="stylesheet">
<meta name="description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="twitter:title" content="Guild Master - Adventurer's guild management game"/>
<meta property="twitter:image" content="https://guildmaster.yht.one/img/compass_rose.png"/>
<meta property="twitter:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:title" content="Guild Master - Adventurer's guild management game"/>
<meta property="og:url" content="https://guildmaster.yht.one/"/>
<meta property="og:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:image" content="https://guildmaster.yht.one/img/compass_rose.png"/>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<noscript> <noscript>
This is a javascript game. You need to enable javascript for it to work. This is a javascript game. You need to enable javascript for it to work.
</noscript> </noscript>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
+1
View File
@@ -0,0 +1 @@
Looking for artists for assets for this game! Need backgrounds, icons and character portraits! Join discord for more info!
+537 -571
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "adventurers-guild", "name": "adventurers-guild",
"version": "0.7.0", "version": "0.9.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -11,19 +11,19 @@
}, },
"dependencies": { "dependencies": {
"@vueuse/components": "^9.13.0", "@vueuse/components": "^9.13.0",
"sass": "^1.59.3", "sass": "^1.63.4",
"vue": "^3.2.45", "vue": "^3.3.4",
"vue-router": "^4.1.6" "vue-router": "^4.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.12", "@types/node": "^18.16.18",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.2.3",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.4.0",
"eslint": "^8.36.0", "eslint": "^8.43.0",
"eslint-plugin-vue": "^9.9.0", "eslint-plugin-vue": "^9.15.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"typescript": "~4.7.4", "typescript": "~5.1.3",
"vite": "^4.0.0", "vite": "4.3.9",
"vue-tsc": "^1.0.12" "vue-tsc": "^1.8.1"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

+159 -31
View File
@@ -1,13 +1,47 @@
<script setup lang="ts"> <script setup lang="ts">
import {RouterLink, RouterView} from 'vue-router'</script> import {RouterLink, RouterView} from 'vue-router'
import {version} from "../package.json"
</script>
<template> <template>
<section class="loading-screen" :class="{disabled: !loading}">
<div class="title panel note-paper">
<h1>Guild Master</h1>
<h3>Adventurer's guild management game</h3>
<small>v{{ version }}</small>
<div class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<h3>Loading assets...</h3>
</div>
</section>
<header> <header>
<nav> <nav>
<RouterLink :to="{name: 'guild'}">Guild</RouterLink> <RouterLink
<RouterLink :to="{name: 'quests'}">Quests</RouterLink> :to="{name: 'guild'}"
<RouterLink :to="{name: 'adventurers'}">Adventurers</RouterLink> >
<RouterLink :to="{name: 'technical'}"><img class="icon" src="/img/icons/cog.svg" alt="Technical"></RouterLink> Guild
</RouterLink>
<RouterLink
:to="{name: 'quests'}"
>
Quests
</RouterLink>
<RouterLink
:to="{name: 'adventurers'}"
>
Adventurers
</RouterLink>
<RouterLink
data-tooltip="Technical information"
data-tooltip-position="bottom"
:to="{name: 'technical'}"
>
<img class="icon" src="/img/icons/cog.svg" alt="Technical">
</RouterLink>
</nav> </nav>
</header> </header>
<RouterView <RouterView
@@ -15,8 +49,8 @@ import {RouterLink, RouterView} from 'vue-router'</script>
:adventurers="adventurers" :adventurers="adventurers"
:quests="missives" :quests="missives"
:adventurerForHire="adventurerForHire" :adventurerForHire="adventurerForHire"
:news="news"
@finalizeQuest="finalizeQuest($event)" @finalizeQuest="finalizeQuest($event)"
@wipeSave="resetSave()"
@recruitActionTaken="recruitAction($event)" @recruitActionTaken="recruitAction($event)"
/> />
</template> </template>
@@ -45,9 +79,12 @@ import AutoFinishQuestsUpgrade from "@/classes/guildUpgrades/AutoFinishQuestsUpg
export default defineComponent({ export default defineComponent({
name: "GuildView", name: "GuildView",
data: () => ({ data: () => ({
loading: true as boolean,
screenWakeLock: null as null | WakeLockSentinel,
guild: new Guild(1, 500), guild: new Guild(1, 500),
gameTickTask: null as null | number, gameTickTask: null as null | number,
gameSaveTask: null as null | number, gameSaveTask: null as null | number,
news: "" as string,
lastQuestGot: { lastQuestGot: {
S: null as null | number, S: null as null | number,
A: null as null | number, A: null as null | number,
@@ -179,13 +216,13 @@ export default defineComponent({
const data = saveData.adventurers[id]; const data = saveData.adventurers[id];
try { try {
const adventurer = new Adventurer( const adventurer = new Adventurer(
data.id, data.id,
data.name, data.name,
data.portrait, data.portrait,
data.attackExponent ?? 1.1, data.attackExponent ?? 1.1,
data.level ?? 1, data.level ?? 1,
data.exp ?? 0, data.exp ?? 0,
data.prestige ?? 0, data.prestige ?? 0,
); );
adventurer.busy = data.busy; adventurer.busy = data.busy;
adventurers[data.id] = adventurer; adventurers[data.id] = adventurer;
@@ -224,20 +261,48 @@ export default defineComponent({
} }
} }
}, },
resetSave() { async updateNews() {
if (!confirm("You are about to wipe your save file. Are you sure?")) return; const result = await fetch("https://raw.githubusercontent.com/YouHaveTrouble/GuildMaster/master/news.txt").catch(() => {
window.localStorage.removeItem("savedGame"); return null;
window.location.reload(); });
if (result === null) return;
this.news = await result.text();
} }
}, },
async mounted() { async mounted() {
this.quests = await loadAvailableQuests();
this.adventurersDatabase = await loadAdventurersForHire();
this.loadGame();
setInterval(async () => {
if (this.screenWakeLock) return;
try {
this.screenWakeLock = await navigator.wakeLock.request("screen");
console.debug("Screen wake lock acquired");
} catch (e) {}
}, 1000);
console.debug("Loading game data")
const promises = await Promise.all([
loadAvailableQuests(),
loadAdventurersForHire(),
]);
this.updateNews().then(() => {
setInterval(() => {
this.updateNews();
}, 1000 * 60 * 60);
});
this.quests = promises[0] as { [key: string]: { [key: string]: Quest } };
this.adventurersDatabase = promises[1] as Array<Adventurer>;
console.debug("Game data loaded!")
this.loadGame();
this.adventurersDatabase = removeAlreadyHiredAdventurers(this.adventurersDatabase, this.adventurers); this.adventurersDatabase = removeAlreadyHiredAdventurers(this.adventurersDatabase, this.adventurers);
this.gameSaveTask = setInterval(() => { // Wait a second to make sure at least most images load in behind the loader
setTimeout(() => {
this.loading = false;
}, 1000);
this.gameSaveTask = Number(setInterval(() => {
saveGame(new GameData({ saveGame(new GameData({
adventurers: this.adventurers, adventurers: this.adventurers,
guild: this.guild, guild: this.guild,
@@ -246,9 +311,10 @@ export default defineComponent({
lastRecruitAction: this.lastRecruitHandled, lastRecruitAction: this.lastRecruitHandled,
adventurerForHireId: this.adventurerForHire?.id ?? null, adventurerForHireId: this.adventurerForHire?.id ?? null,
})); }));
}, 10 * 1000) }, 10 * 1000));
this.gameTickTask = setInterval(() => {
this.gameTickTask = Number(setInterval(() => {
this.updateMissives(); this.updateMissives();
const now = Number(new Date()); const now = Number(new Date());
@@ -336,7 +402,7 @@ export default defineComponent({
} }
} }
}, 250); }, 250));
}, },
beforeUnmount() { beforeUnmount() {
@@ -365,22 +431,24 @@ nav {
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
padding: 2rem; padding-inline: 2rem;
background-size: 200px; padding-bottom: 2rem;
background-blend-mode: darken; padding-top: 0.5rem;
background-color: rgba(0, 0, 0, 0.65); background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
background-image: url("/img/background/panels/wall_panel_empty.png");
filter: drop-shadow(0 0 0.5rem rgba(0,0,0, 0.25));
.icon { .icon {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
fill: white;
filter: invert(1);
transform: translateY(0.35rem); transform: translateY(0.35rem);
} }
a { a {
font-size: 2rem; font-size: 2rem;
color: #fff; color: #000;
text-decoration: none; text-decoration: none;
&.router-link-active { &.router-link-active {
@@ -390,4 +458,64 @@ nav {
} }
.loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 5;
user-select: none;
background-size: 25rem;
background-color: rgba(87, 50, 20, 0.45);
background-image: url("/img/background/wood/cut_wood_background.png");
background-blend-mode: darken;
background-repeat: repeat;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 0.25s linear;
&.disabled {
opacity: 0;
pointer-events: none;
}
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #000 transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
</style> </style>
+84
View File
@@ -82,3 +82,87 @@ body {
filter: brightness(1.2); filter: brightness(1.2);
} }
} }
.title {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-block: 2.5rem;
text-align: center;
width: 100%;
max-width: 45rem;
gap: 0.5rem;
h1 {
font-size: 4rem;
line-height: 0.75;
white-space: pre-wrap;
margin: 0;
}
h3 {
margin: 0;
line-height: 0.9;
}
small {
font-size: 0.9rem;
font-weight: bold;
line-height: 0.25;
}
}
[data-tooltip] {
position: relative;
&:after {
pointer-events: none;
transition: opacity 0.25s ease-in-out;
opacity: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: max-content;
min-height: 2rem;
background-color: rgba(0,0,0, 0.5);
color: #fff;
font-size: 1rem;
padding-block: 0.25rem;
padding-inline: 0.5rem;
white-space: break-spaces;
}
&[data-tooltip-position="bottom"]:after {
content: attr(data-tooltip);
bottom: calc(-100% - 0.5rem);
left: 50%;
transform: translateX(-50%);
max-width: 200%;
}
&[data-tooltip-position="top"]:after {
content: attr(data-tooltip);
top: calc(-100% - 0.5rem);
left: 50%;
transform: translateX(-50%);
max-width: 200%;
}
&[data-tooltip-position="left"]:after {
content: attr(data-tooltip);
top: 50%;
right: calc(100% + 0.5rem);
transform: translateY(-50%);
max-width: 20rem;
}
&[data-tooltip-position="right"]:after {
content: attr(data-tooltip);
top: 50%;
left: calc(100% + 0.5rem);
transform: translateY(-50%);
max-width: 20rem;
}
&:hover:after {
opacity: 100%;
}
}
+3 -1
View File
@@ -70,7 +70,9 @@ export default defineComponent({
}, },
methods: { methods: {
closeSelect() { closeSelect() {
this.selection = false; setTimeout(() => {
this.selection = false;
}, 0);
} }
} }
}) })
@@ -53,7 +53,6 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
this.getReleases(1); this.getReleases(1);
} }
}); });
</script> </script>
@@ -0,0 +1,146 @@
<template>
<div class="save-manager panel pinned-paper">
<dialog ref="importDialog" >
<div class="import-dialog">
<textarea
v-model="importSaveText"
placeholder="Paste your save data here"
></textarea>
<div style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.5rem;">
<button type="button" class="button metal" @click="importSave()" :disabled="importSaveText === ''">
Import
</button>
<button class="close" @click="closeSaveImport"></button>
</div>
</div>
</dialog>
<div class="nail top-left">
<img src="/img/quests/overlays/nail.png" alt="" draggable="false"/>
</div>
<div class="nail top-right">
<img src="/img/quests/overlays/nail.png" alt="" draggable="false"/>
</div>
<h1>Save file</h1>
<div class="buttons">
<button class="button metal" @click="exportSave()">
Export
</button>
<button class="button metal" @click="openSaveImport()">
Import
</button>
<button class="button metal" @click="wipeSave()">
<span class="red">Wipe</span>
</button>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
export default defineComponent({
name: "ChangelogComponent",
data: () => ({
importSaveText: "",
}),
methods: {
openSaveImport() {
const dialog = this.$refs.importDialog as HTMLDialogElement;
dialog.showModal();
},
closeSaveImport() {
const dialog = this.$refs.importDialog as HTMLDialogElement;
dialog.close();
},
exportSave() {
const save = window.localStorage.getItem("savedGame");
if (!save) {
alert("No save file found!");
return;
}
navigator.clipboard.writeText(btoa(save));
setTimeout(() => alert("Save data copied to clipboard!"), 100);
},
importSave() {
const saveBase64 = this.importSaveText;
try {
const save = atob(saveBase64);
JSON.parse(save);
window.localStorage.setItem("savedGame", save);
window.location.reload();
} catch (e) {
alert("Invalid save file!");
return;
}
},
wipeSave() {
if (!confirm("You are about to wipe your save file. Are you sure?")) return;
window.localStorage.removeItem("savedGame");
window.location.reload();
},
}
});
</script>
<style scoped lang="scss">
.save-manager {
padding-block: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
max-width: 45rem;
width: 100%;
h1 {
margin-block: 0;
font-size: 2rem;
}
dialog {
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
.red {
color: #d52121;
}
}
.import-dialog {
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: url("/img/background/panels/plaster.png");
background-size: 100% 100%;
gap: 1rem;
position: relative;
textarea {
width: 90%;
height: 10rem;
resize: none;
border: 1px solid var(--color-metal);
border-radius: 0.25rem;
padding: 0.5rem;
font-family: monospace;
}
.close {
color: #ab0707;
font-size: 2rem;
border: none;
background: transparent;
&:hover {
color: #d52121;
cursor: pointer;
}
}
}
}
</style>
+1 -1
View File
@@ -2,7 +2,7 @@ import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import './assets/main.scss' import '@/assets/main.scss'
const app = createApp(App) const app = createApp(App)
+4 -5
View File
@@ -1,5 +1,4 @@
import {createRouter, createWebHashHistory} from 'vue-router' import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
@@ -7,22 +6,22 @@ const router = createRouter({
{ {
path: '/', path: '/',
name: 'guild', name: 'guild',
component: HomeView component: () => import('@/views/HomeView.vue')
}, },
{ {
path: '/quests', path: '/quests',
name: 'quests', name: 'quests',
component: () => import('../views/QuestView.vue') component: () => import('@/views/QuestView.vue')
}, },
{ {
path: '/adventurers', path: '/adventurers',
name: 'adventurers', name: 'adventurers',
component: () => import('../views/AdventurerView.vue') component: () => import('@/views/AdventurerView.vue')
}, },
{ {
path: '/technical', path: '/technical',
name: 'technical', name: 'technical',
component: () => import('../views/TechnicalView.vue') component: () => import('@/views/TechnicalView.vue')
} }
] ]
}) })
+9 -31
View File
@@ -4,6 +4,7 @@
<h1>Guild Master</h1> <h1>Guild Master</h1>
<h3>Adventurer's guild management game</h3> <h3>Adventurer's guild management game</h3>
<small>v{{ version }}</small> <small>v{{ version }}</small>
<p class="news">{{ news }}</p>
</section> </section>
<section class="upgrades panel pinned-paper"> <section class="upgrades panel pinned-paper">
<div class="nail top-left"> <div class="nail top-left">
@@ -29,9 +30,6 @@
<section class="upgrade"> <section class="upgrade">
<UpgradesList :guild="guild"/> <UpgradesList :guild="guild"/>
</section> </section>
<section class="upgrade">
<span class="wipe-save" @click="$emit('wipeSave')">Wipe your save data</span>
</section>
</section> </section>
</main> </main>
@@ -56,6 +54,10 @@ export default defineComponent({
} }
}, },
props: { props: {
news: {
type: String,
default: "",
},
guild: { guild: {
type: Object as PropType<Guild>, type: Object as PropType<Guild>,
default: () => new Guild(1, 0) as Guild, default: () => new Guild(1, 0) as Guild,
@@ -77,37 +79,13 @@ main {
.upgrades { .upgrades {
max-width: 45rem; max-width: 45rem;
width: 100%; width: 100%;
padding-bottom: 1rem;
} }
} }
.title { .news {
display: flex; max-width: 75%;
flex-direction: column; color: #ab0707;
justify-content: center;
align-items: center;
padding-block: 2.5rem;
text-align: center;
width: 100%;
max-width: 45rem;
gap: 0.5rem;
h1 {
font-size: 4rem;
line-height: 0.75;
white-space: pre-wrap;
margin: 0;
}
h3 {
margin: 0;
line-height: 0.9;
}
small {
font-size: 0.9rem;
font-weight: bold;
line-height: 0.25;
}
} }
.coffer { .coffer {
+4 -2
View File
@@ -19,6 +19,7 @@
</a> </a>
</div> </div>
</div> </div>
<SaveManagerComponent/>
<ChangelogComponent/> <ChangelogComponent/>
</section> </section>
@@ -26,11 +27,12 @@
<script lang="ts"> <script lang="ts">
import {defineComponent} from "vue"; import {defineComponent} from "vue";
import ChangelogComponent from "@/components/ChangelogComponent.vue"; import ChangelogComponent from "@/components/technical/ChangelogComponent.vue";
import SaveManagerComponent from "@/components/technical/SaveManagerComponent.vue";
export default defineComponent({ export default defineComponent({
name: "TechnicalView", name: "TechnicalView",
components: {ChangelogComponent}, components: {SaveManagerComponent, ChangelogComponent},
}) })
</script> </script>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"extends": "@vue/tsconfig/tsconfig.node.json", "extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",