Compare commits

...

23 Commits

Author SHA1 Message Date
YouHaveTrouble 5aa8ef1d0e update depends 2024-11-24 16:08:30 +01:00
YouHaveTrouble bfd30c935b centralized save data 2024-11-22 22:53:11 +01:00
YouHaveTrouble dd92009cba restore a little bit of loading data logic 2024-11-21 20:57:02 +01:00
YouHaveTrouble 7ab0bfbf67 add adventurer identities 2024-11-21 20:56:40 +01:00
YouHaveTrouble b956152edb normalize some styles 2024-11-21 20:56:20 +01:00
YouHaveTrouble dc9cc5c74c remove attack exponent 2024-11-21 20:54:57 +01:00
YouHaveTrouble 06278ca11e move quests to a single file 2024-11-21 20:54:23 +01:00
YouHaveTrouble 53865f1349 framework for adventurers 2024-11-20 21:29:54 +01:00
YouHaveTrouble 98cd9d829f clean the heck up samurai, we have a game to rewrite 2024-11-19 20:13:49 +01:00
YouHaveTrouble c26c425c6d update news 2024-08-23 23:01:07 +02:00
YouHaveTrouble 423444a139 configure vite plugin for pwa handling 2023-09-24 23:56:10 +02:00
YouHaveTrouble b49a30bb6e actually bearable mobile view for quests board 2023-09-23 15:29:47 +02:00
YouHaveTrouble 2a38461a47 move icon svgs to components, so they can load along everything else 2023-09-16 15:58:54 +02:00
YouHaveTrouble 52c340be26 bump version 2023-09-16 15:39:23 +02:00
YouHaveTrouble f2c5304643 don't load paths dynamically to allow offline play 2023-09-16 15:26:36 +02:00
YouHaveTrouble b6aefde9da add / to the path 2023-09-16 14:29:29 +02:00
YouHaveTrouble f31a0013a9 Merge pull request #11 from xYundy/fix-quest-button-on-ios
Fix select button color for iOS Safari
2023-09-15 12:04:09 +02:00
Wojciech 'xYundy' Zięciak e4af877358 Add missing empty line at EOF 2023-09-15 12:03:48 +02:00
YouHaveTrouble d162771749 Merge pull request #10 from xYundy/public-testing
Add script which allow to test app on external devices
2023-09-15 12:03:15 +02:00
YouHaveTrouble f53644119a Merge pull request #9 from xYundy/pwa-mobile-icon
Add PWA icons and manifest
2023-09-15 12:02:21 +02:00
Wojciech 'xYundy' Zięciak 34999a27b7 add PWA icons and manifest 2023-09-15 11:59:11 +02:00
Wojciech 'xYundy' Zięciak 6e23f9a7b4 Fix select button color for iOS Safar 2023-09-15 11:55:27 +02:00
Wojciech 'xYundy' Zięciak d852f6fd96 Add script which allow to test app on ext. devices 2023-09-15 11:47:43 +02:00
67 changed files with 8663 additions and 4606 deletions
-21
View File
@@ -1,21 +0,0 @@
/**
* 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");
}
+4 -3
View File
@@ -8,18 +8,19 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond&display=swap" rel="stylesheet">
<link rel="manifest" href="/manifest.json" />
<link rel="preload" href="/img/promo/chicken_mage.jpg" as="image">
<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/icon.png"/>
<meta property="twitter:image" content="https://guildmaster.yht.one/img/app-icons/icon.png"/>
<meta property="twitter:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:title" content="Guild Master - Adventurer's guild management game"/>
<meta property="og:url" content="https://guildmaster.yht.one/"/>
<meta property="og:description"
content="Guild Master is a browser game where you manage your own adventurer's guild!"/>
<meta property="og:image" content="https://guildmaster.yht.one/img/icon.png"/>
<meta property="og:image" content="https://guildmaster.yht.one/img/app-icons/icon.png"/>
</head>
<body>
<div id="app"></div>
-1
View File
@@ -1 +0,0 @@
Looking for artists for assets for this game! Need backgrounds and icons! Join discord for more info!
+8100 -1611
View File
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -1,30 +1,31 @@
{
"name": "adventurers-guild",
"version": "0.12.0",
"version": "0.14.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only && cp -r CNAME dist/CNAME",
"dev-public": "vite --host",
"build": "run-p build-only && cp -r CNAME dist/CNAME",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"gen-character-data": "node characterDataGenerator.js"
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@vueuse/components": "^9.13.0",
"sass": "^1.66.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"@vueuse/components": "^11.3.0",
"sass": "^1.81.0",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@types/node": "^18.17.6",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"@types/node": "^18.19.64",
"@vitejs/plugin-vue": "^5.2.0",
"@vue/tsconfig": "^0.6.0",
"eslint": "^9.15.0",
"eslint-plugin-vue": "^9.31.0",
"npm-run-all": "^4.1.5",
"typescript": "~5.1.6",
"vite": "4.4.9",
"vue-tsc": "^1.8.3"
"typescript": "~5.7.2",
"vite": "5.4.11",
"vite-plugin-pwa": "^0.21.0",
"vue-tsc": "^2.1.10"
}
}
File diff suppressed because one or more lines are too long
+115
View File
@@ -0,0 +1,115 @@
{
"levelRequirement": {
"F": 1,
"E": 5,
"D": 10,
"C": 25,
"B": 50,
"A": 100,
"S": 250
},
"ranks": {
"F": [
{
"title": "Frog Frenzy",
"text": "Kill 10 demon frogs."
},
{
"title": "Rats!",
"text": "Get rid of the rats from someone's basement."
},
{
"title": "Herb gathering",
"text": "Collect medicinal herbs."
},
{
"title": "Big pile of rubble",
"text": "Tavern collapsed. Again. Help clean up the debris."
}
],
"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": [
{
"title": "The Demon King",
"text": "Demon King has awoken and is a threat to whole existence. Heroes needed."
},
{
"title": "Scratchy, Destruction Incarnate",
"text": "Scratchy was reborn as a machine of pure destruction and needs to be stopped."
},
{
"title": "Jiggly Jungle",
"text": "A jungle south began rapidly expanding and experts think arson is our only option."
}
]
}
}
-14
View File
@@ -1,14 +0,0 @@
[
{
"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."
}
]
-14
View File
@@ -1,14 +0,0 @@
[
{
"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."
}
]
-14
View File
@@ -1,14 +0,0 @@
[
{
"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!"
}
]
-14
View File
@@ -1,14 +0,0 @@
[
{
"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."
}
]
-14
View File
@@ -1,14 +0,0 @@
[
{
"title": "Gnoblin subjegation",
"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."
}
]
-18
View File
@@ -1,18 +0,0 @@
[
{
"title": "Frog Frenzy",
"text": "Kill 10 demon frogs."
},
{
"title": "Rats!",
"text": "Get rid of the rats from someone's basement."
},
{
"title": "Herb gathering",
"text": "Collect medicinal herbs."
},
{
"title": "Big pile of rubble",
"text": "Tavern collapsed. Again. Help clean up the debris."
}
]
-14
View File
@@ -1,14 +0,0 @@
[
{
"title": "The Demon King",
"text": "Demon King has awoken and is a threat to whole existence. Heroes needed."
},
{
"title": "Scratchy, Destruction Incarnate",
"text": "Scratchy was reborn as a machine of pure destruction and needs to be stopped."
},
{
"title": "Jiggly Jungle",
"text": "A jungle south began rapidly expanding and experts think arson is our only option."
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 304 KiB

-1
View File
@@ -1 +0,0 @@
<svg 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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" 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>

Before

Width:  |  Height:  |  Size: 660 B

+59
View File
@@ -0,0 +1,59 @@
{
"name": "Guild Master - Adventurer's guild management game",
"short_name": "Guild Master",
"theme_color": "#3C2114",
"background_color": "#d9c8b3",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "img/app-icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "img/app-icons/icon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}
+20 -40
View File
@@ -1,102 +1,82 @@
[
{
"id": "aldek",
"name": "Aldek",
"attackExponent": 1.1
"name": "Aldek"
},
{
"id": "aria",
"name": "Aria",
"attackExponent": 1.1
"name": "Aria"
},
{
"id": "burnett",
"name": "Burnett",
"attackExponent": 1.1
"name": "Burnett"
},
{
"id": "charlotte",
"name": "Charlotte",
"attackExponent": 1.1
"name": "Charlotte"
},
{
"id": "ella",
"name": "Ella",
"attackExponent": 1.1
"name": "Ella"
},
{
"id": "elyza",
"name": "Elyza",
"attackExponent": 1.1
"name": "Elyza"
},
{
"id": "emille",
"name": "Emille",
"attackExponent": 1.1
"name": "Emille"
},
{
"id": "garret",
"name": "Garret",
"attackExponent": 1.1
"name": "Garret"
},
{
"id": "gryza",
"name": "Gryza",
"attackExponent": 1.1
"name": "Gryza"
},
{
"id": "lestat",
"name": "Lestat",
"attackExponent": 1.1
"name": "Lestat"
},
{
"id": "lydia",
"name": "Lydia",
"attackExponent": 1.1
"name": "Lydia"
},
{
"id": "noor",
"name": "Noor",
"attackExponent": 1.1
"name": "Noor"
},
{
"id": "noron",
"name": "Noron",
"attackExponent": 1.1
"name": "Noron"
},
{
"id": "oola",
"name": "Oola",
"attackExponent": 1.1
"name": "Oola"
},
{
"id": "owen",
"name": "Owen",
"attackExponent": 1.1
"name": "Owen"
},
{
"id": "ryslette",
"name": "Ryslette",
"attackExponent": 1.1
"name": "Ryslette"
},
{
"id": "sally",
"name": "Sally",
"attackExponent": 1.1
"name": "Sally"
},
{
"id": "tovu",
"name": "Tovu",
"attackExponent": 1.1
"name": "Tovu"
},
{
"id": "wrydio",
"name": "Wrydio",
"attackExponent": 1.1
"name": "Wrydio"
},
{
"id": "xarya",
"name": "Xarya",
"attackExponent": 1.1
"name": "Xarya"
}
]
+66 -369
View File
File diff suppressed because one or more lines are too long
-131
View File
@@ -1,131 +0,0 @@
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: { [key: string]: { [key: string]: Quest } };
lastQuestGot: { [key: string]: null | number };
lastRecruitAction: null | number;
adventurerForHireId: string | null;
constructor(
data: any,
) {
this.guild = data.guild ?? new Guild(1, 0);
this.adventurers = data.adventurers ?? {} as { [key: string]: Adventurer };
this.missives = data.missives ?? {} as { [key: string]: { [key: string]: Quest } };
this.lastQuestGot = data.lastQuestGot ?? {} as { [key: string]: null | number };
this.lastRecruitAction = data.lastRecruitAction ?? null;
this.adventurerForHireId = data.adventurerForHireId ?? null;
}
}
/**
* 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;
}
window.localStorage.setItem("savedGame", JSON.stringify({
guild: data.guild,
adventurers: adventurers,
missives: data.missives,
lastQuestGot: data.lastQuestGot,
lastRecruitAction: data.lastRecruitAction,
adventurerForHireId: data.adventurerForHireId,
}));
}
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 } };
for (const rank in quests) {
const response = await fetch(`data/quests/Rank${rank}.json`);
if (response.status !== 200) {
console.error("Failed to load quests");
alert("Failed to load quests. Please try refreshing the page.");
return quests;
}
const questData = await response.json();
let id = 0;
for (const quest of questData) {
id++;
quests[rank.toString()][id] = new Quest(
id.toString(),
getFromString(rank as QuestRank),
quest.title,
quest.text,
1,
0,
0
);
}
}
return quests;
}
export async function loadAdventurersForHire(): Promise<Array<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: Array<Adventurer> = [];
for (const adventurer of adventurerData) {
adventurers.push(new Adventurer(
adventurer.id,
adventurer.name,
adventurer.portrait,
adventurer.attackExponent,
adventurer.level,
adventurer.exp,
));
}
return adventurers;
}
export function removeAlreadyHiredAdventurers(
adventurers: Array<Adventurer>,
adventurersHired: { [key: string]: Adventurer }
): Array<Adventurer> {
const adventurersForHire: Array<Adventurer> = [];
for (const adventurer of adventurers) {
if (adventurersHired[adventurer.id]) continue;
adventurersForHire.push(adventurer);
}
return adventurersForHire;
}
+66 -156
View File
File diff suppressed because one or more lines are too long
-92
View File
@@ -1,92 +0,0 @@
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);
}
-68
View File
@@ -1,68 +0,0 @@
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";
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;
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();
}
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}
-7
View File
@@ -1,7 +0,0 @@
export class GuildUpgrade {
level: number = 1;
nextLevelCost: number | null = null;
guildLevelRequirement: number = 1;
}
-7
View File
@@ -1,7 +0,0 @@
export default interface MaxLevellable {
maxLevel: number;
isMaxLevel(): boolean;
}
-21
View File
@@ -1,21 +0,0 @@
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);
}
-86
View File
@@ -1,86 +0,0 @@
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>;
progressPoints: number;
maxProgress: number;
expReward: number;
goldReward: number;
constructor(id: string, rank: QuestRank, title: string, text: string, maxProgress: number, expReward: number, goldReward: number) {
this.id = id;
this.rank = rank;
this.title = title;
this.text = text;
this.maxProgress = maxProgress;
this.expReward = expReward;
this.goldReward = goldReward;
this.progressPoints = 0;
this.adventurers = [];
}
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;
}
-13
View File
@@ -1,13 +0,0 @@
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];
}
-13
View File
@@ -1,13 +0,0 @@
import type {Adventurer} from "@/classes/Adventurer";
/**
* Get a random adventurer from the pool
* @param adventurerPool
* @returns {Adventurer|null} null if the pool is empty
*/
export function getNewAdventurerForHire(adventurerPool: Array<Adventurer>): Adventurer|null {
if (adventurerPool.length <= 0) return null;
const randomId = adventurerPool.length * Math.random() << 0;
return adventurerPool[randomId];
}
@@ -1,27 +0,0 @@
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 ;
}
}
@@ -1,70 +0,0 @@
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];
}
}
}
@@ -1,24 +0,0 @@
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);
}
}
@@ -1,24 +0,0 @@
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);
}
}
File diff suppressed because one or more lines are too long
-53
View File
@@ -1,53 +0,0 @@
<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<{ [key: string]: Adventurer }>,
default() {
return {} as { [key: string]: 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>
-135
View File
@@ -1,135 +0,0 @@
<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">
<button
class="slot"
v-for="adventurer in allAdventurers"
:key="adventurer.id"
:class="{busy: adventurer.busy}"
@click="() => {
if (adventurer.busy) return;
$emit('hireAdventurer', adventurer.id);
selection = false;
}"
>
<AdventurerTile
:adventurer="adventurer"
/>
</button>
</div>
</template>
<script lang="ts">
import {defineComponent, type PropType} from "vue";
import type {Adventurer} from "@/classes/Adventurer";
import AdventurerTile from "@/components/AdventurerTile.vue";
export default defineComponent({
name: "AdventurerMiniComponent",
components: {AdventurerTile},
emits: [ "freeAdventurer", "hireAdventurer" ],
data: () => {
return {
selection: false,
}
},
props: {
adventurer: {
type: Object as PropType<Adventurer|null|any>,
default() {
return null as Adventurer|null;
},
},
allAdventurers: {
type: Object as PropType<{[key: string]: Adventurer}>,
default() {
return {} as {[key: string]: Adventurer};
},
},
},
methods: {
closeSelect() {
setTimeout(() => {
this.selection = false;
}, 0);
}
}
})
</script>
<style lang="scss" scoped>
.selection {
position: absolute;
bottom: 0;
left: 50%;
width: max-content;
max-width: 17rem;
transform: translateX(-50%) translateY(104%);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem;
background-color: rgba(0,0,0, 0.2);
z-index: 2;
cursor: default;
max-height: 12rem;
overflow-y: auto;
.slot {
width: 5rem;
height: 5rem;
cursor: pointer;
padding: 0;
border-color: #000;
background-color: transparent;
background-blend-mode: darken;
transition: background-color 0.25s linear, filter 0.25s linear;
&.busy {
filter: grayscale(1);
background-color: rgba(0,0,0, 0.6);
&:hover {
cursor: not-allowed;
border-color: #000;
}
}
&:hover {
border-color: #fff;
}
img {
width: 100%;
height: 100%;
}
}
}
.select {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 4.5rem;
}
</style>
-83
View File
@@ -1,83 +0,0 @@
<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;
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>
-260
View File
@@ -1,260 +0,0 @@
<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>
<h2>{{ missive.title }}</h2>
<p>{{ missive.text }}</p>
<div class="slots">
<button class="slot">
<AdventurerComponent
:adventurer="missive.adventurers[0]"
:all-adventurers="adventurers"
@hire-adventurer="(id) => {
adventurers[id].busy = true;
missive.adventurers[0] = adventurers[id];
}"
@free-adventurer="(id) => {
if (missive.progressPoints >= missive.maxProgress) return;
adventurers[id].busy = false;
missive.adventurers.splice(0, 1);
if (missive.adventurers.length <= 0) {
missive.progressPoints = 0;
}
}"
/>
</button>
</div>
<div class="progressWrap">
<span class="progress"></span>
<span class="percentage">{{ progressPercentage }}</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},
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;
const progress = (this.missive.progressPoints / this.missive.maxProgress * 100).toFixed(2);
this.progressPercentage = `${progress}%`;
},
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: {
handler() {
this.updateProgress();
},
deep: true,
}
}
});
</script>
<style lang="scss" scoped>
.missive {
width: min(100%, 14rem);
text-align: center;
border: 2px solid #000;
padding: 0.5rem;
position: relative;
.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(progressPercentage);
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%;
}
}
.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;
position: relative;
}
}
.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>
-134
View File
@@ -1,134 +0,0 @@
<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" 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();
},
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>
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
File diff suppressed because one or more lines are too long
@@ -1,128 +0,0 @@
<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;
max-height: 30rem;
width: 100%;
overflow-y: auto;
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%;
overflow-y: auto;
}
.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
+39
View File
@@ -0,0 +1,39 @@
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;
}
}
+13
View File
@@ -0,0 +1,13 @@
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;
}
}
+61
View File
@@ -0,0 +1,61 @@
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;
}
}
+34
View File
@@ -0,0 +1,34 @@
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;
}
}
+8
View File
@@ -0,0 +1,8 @@
export enum ItemType {
HELMET = 'helmet',
ARMOR = 'armor',
BOOTS = 'boots',
WEAPON = 'weapon',
}
+36
View File
@@ -0,0 +1,36 @@
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,
)
));
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
export default interface StatHolder {
getPower(): number;
getDefense(): number;
}
+3 -16
View File
@@ -1,28 +1,15 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '@/views/HomeView.vue';
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'guild',
component: () => import('@/views/HomeView.vue')
component: HomeView,
},
{
path: '/quests',
name: 'quests',
component: () => import('@/views/QuestView.vue')
},
{
path: '/adventurers',
name: 'adventurers',
component: () => import('@/views/AdventurerView.vue')
},
{
path: '/technical',
name: 'technical',
component: () => import('@/views/TechnicalView.vue')
}
]
})
-202
View File
@@ -1,202 +0,0 @@
<template>
<div class="adventurer-section">
<AdventurerDetails
:adventurer="selectedAdventurer"
v-if="selectedAdventurer !== null"
@closeButtonClicked="selectedAdventurer = null"
/>
<section class="recruit panel pinned-paper">
<h1>Applying adventurers</h1>
<div class="adventurers">
<div v-if="adventurerForHire">
<adventurer-tile class="hire-tile" :adventurer="adventurerForHire"/>
<div class="decision">
<span
title="Hire"
@click="hireAdventurer()"
:class="{disabled: Object.keys(adventurers).length >= guild.adventurerCapacity.getAdventurerCapacity()}"
>
</span>
<span
:title="Object.keys(adventurers).length > 0 ? 'Dismiss' : ''"
:class="{disabled: Object.keys(adventurers).length <= 0}"
@click="dismissAdventurer()"
>
</span>
</div>
</div>
<div v-else>
<span>Noone applied as of now. Check back later!</span>
</div>
</div>
</section>
<section class="collection 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";
export default defineComponent({
name: "RecruitView",
components: {AdventurerDetails, AdventurerTile},
data: () => {
return {
currentlyForHire: null as Adventurer | null,
adventurersForHire: [] as Array<Adventurer>,
selectedAdventurer: null as Adventurer | null,
}
},
props: {
guild: {
type: Object as PropType<Guild>,
required: true,
},
adventurers: {
type: Object as PropType<{ [key: string]: Adventurer }>,
required: true,
},
adventurerForHire: {
type: Object as PropType<Adventurer | null>,
default() {
return null;
}
},
},
methods: {
hireAdventurer(): void {
if (Object.keys(this.adventurers).length >= this.guild.adventurerCapacity.getAdventurerCapacity()) return;
this.$emit("recruitActionTaken", this.adventurerForHire);
},
dismissAdventurer() {
if (Object.keys(this.adventurers).length <= 0) return;
this.$emit("recruitActionTaken", null);
}
},
async mounted() {
},
emits: ["recruitActionTaken"]
});
</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>
+2 -82
View File
@@ -6,49 +6,18 @@
<small>v{{ version }}</small>
<p class="news">{{ news }}</p>
</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>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import type {PropType} from "vue";
import {Guild} from "@/classes/Guild";
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({
name: "GuildView",
methods: {formatGold},
components: {Nail, UpgradesList},
methods: {},
components: {},
data: () => {
return {
version: version,
@@ -59,60 +28,11 @@ export default defineComponent({
type: String,
default: "",
},
guild: {
type: Object as PropType<Guild>,
required: true,
},
}
});
</script>
<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>
-157
View File
@@ -1,157 +0,0 @@
<template>
<section>
<div class="guild" v-if="guild.level >= 7 && Object.keys(quests.S).length > 0">
<h1>Rank S Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.S"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
<div class="guild" v-if="guild.level >= 6 && Object.keys(quests.A).length > 0">
<h1>Rank A Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.A"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
<div class="guild" v-if="guild.level >= 5 && Object.keys(quests.B).length > 0">
<h1>Rank B Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.B"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
<div class="guild" v-if="guild.level >= 4 && Object.keys(quests.C).length > 0">
<h1>Rank C Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.C"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
<div class="guild" v-if="guild.level >= 3 && Object.keys(quests.D).length > 0">
<h1>Rank D Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.D"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest((missive))"
/>
</section>
</div>
<div class="guild" v-if="guild.level >= 2 && Object.keys(quests.E).length > 0">
<h1>Rank E Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.E"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
<div class="guild" v-if="Object.keys(quests.F).length > 0">
<h1>Rank F Quests</h1>
<section class="missives">
<QuestMissive
v-for="(missive, key, index) in quests.F"
:key="key"
:adventurers="adventurers"
:missive="missive"
@click="finalizeQuest(missive)"
/>
</section>
</div>
</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";
export default defineComponent({
name: "QuestView",
components: {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<{ [key: string]: 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>
.guild {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
padding-bottom: 0.5rem;
h1 {
font-size: 3rem;
text-align: center;
}
.missives {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: calc(100% - 2rem);
justify-content: center;
align-items: stretch;
padding-inline: 1rem;
gap: 1rem;
}
}
</style>
-91
View File
@@ -1,91 +0,0 @@
<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">
<img class="icon" src="/img/icons/discord.svg" alt="Discord"/>
Discord
</a>
<a class="link" href="https://github.com/YouHaveTrouble/GuildMaster">
<img class="icon" src="/img/icons/github.svg" alt="GitHub"/>
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";
export default defineComponent({
name: "TechnicalView",
components: {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>
+12 -1
View File
@@ -2,10 +2,21 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {VitePWA} from "vite-plugin-pwa";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
plugins: [
vue(),
VitePWA({
registerType: 'prompt',
injectRegister: 'script',
filename: 'service-worker.js',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,jpeg,svg,json}'],
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))