Add quests that give resources as reward.

This commit is contained in:
Adrian 2024-11-07 17:15:41 +01:00
parent 1f312c072b
commit 10bd9121a2
16 changed files with 242 additions and 16 deletions

View File

@ -37,9 +37,6 @@ h1 {
} }
#app { #app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center; text-align: center;
} }

View File

@ -40,7 +40,6 @@
.outside-map { .outside-map {
display: grid; display: grid;
height: 100%; height: 100%;
margin-top: 0.8em;
position: relative; position: relative;
} }

View File

@ -43,7 +43,6 @@
.village-map { .village-map {
display: grid; display: grid;
height: 100%; height: 100%;
margin-top: 0.8em;
position: relative; position: relative;
} }

View File

@ -17,7 +17,9 @@
<style> <style>
.worldmap { .worldmap {
display: flex; display: flex;
justify-content: space-between; justify-content: space-around;
flex-wrap: wrap;
gap: 1em;
} }
.region { .region {

View File

@ -1,6 +1,6 @@
import buildings from "./data/buildings"; import buildings from "./data/buildings";
import { Hex } from "./hexgrid"; import { Hex } from "./hexgrid";
import type { BuildingType, BuildingSource } from "./types"; import type { BuildingSource, BuildingType } from "./types";
let uid = 0; let uid = 0;

View File

@ -13,6 +13,7 @@
import Resources from "./Resources.svelte"; import Resources from "./Resources.svelte";
import Units from "./Units.svelte"; import Units from "./Units.svelte";
import Victory from "./Victory.svelte"; import Victory from "./Victory.svelte";
import Quests from "./Quests.svelte";
onMount(() => { onMount(() => {
@ -36,10 +37,13 @@
<section class="hud"> <section class="hud">
<header> <header>
<Units />
<Resources />
<Navigation { setTab } /> <Navigation { setTab } />
<Resources />
</header> </header>
<div class="side">
<Units />
<Quests />
</div>
<div class="board"> <div class="board">
{ #if $gameTab === 'village' } { #if $gameTab === 'village' }
<Village /> <Village />
@ -57,9 +61,19 @@
</section> </section>
<style> <style>
header { .hud {
display: grid;
grid-template-columns: 20vw 1fr 20vw;
grid-template-rows: 20vh 1fr;
height: 100vh;
width: 100vw;
}
.hud header {
display: flex; display: flex;
justify-content: space-around; justify-content: center;
flex-direction: column;
grid-area: 1 / span 3;
} }
.overlay { .overlay {

50
src/hud/Quests.svelte Normal file
View File

@ -0,0 +1,50 @@
<script lang="ts">
import { fly } from "svelte/transition";
import moves from "../moves";
import village from "../village";
import Reward from "./Reward.svelte";
import { flip } from "svelte/animate";
$: isQuestStarted = $village.quests.some(q => q.started);
function startQuest(id: number) {
moves.startQuest(id);
}
</script>
<section>
{ #each $village.quests as quest (quest.id) }
<div
class="quest"
in:fly={{ duration: 200, delay: 100, x: -500 }}
out:fly={{ duration: 200, x: 500 }}
animate:flip={{ duration: 100 }}
>
<p>
<Reward reward={ quest.reward } />
</p>
<p>
<img src="/img/icons/time.png" alt="Duration">
{ #if quest.started }
{ Math.ceil((quest.remainingTime || 0) / 1000) }
{ :else }
{ quest.duration }
<button
on:click={ () => startQuest(quest.id) }
disabled={ isQuestStarted }
>
Start quest
</button>
{ /if }
</p>
</div>
{ /each }
</section>
<style>
.quest {
text-align: left;
}
</style>

View File

@ -38,8 +38,8 @@
<style> <style>
.resources { .resources {
display: flex; display: flex;
justify-content: space-around; justify-content: center;
gap: 1em; gap: 2em;
} }
.resources div { .resources div {

53
src/hud/Reward.svelte Normal file
View File

@ -0,0 +1,53 @@
<script lang="ts">
import type { ResourcesType } from "../types";
export let reward: ResourcesType;
</script>
<div class="reward">
{ #if reward.wood > 0 }
<div>
<img src="/img/icons/wood.png" alt="Wood" />
{ reward.wood }
</div>
{ /if }
{ #if reward.stone > 0 }
<div>
<img src="/img/icons/stone.png" alt="Stone" />
{ reward.stone }
</div>
{ /if }
{ #if reward.iron > 0 }
<div>
<img src="/img/icons/iron.png" alt="Iron" />
{ reward.iron }
</div>
{ /if }
{ #if reward.food > 0 }
<div>
<img src="/img/icons/food.png" alt="Food" />
{ reward.food }
</div>
{ /if }
{ #if reward.culture > 0 }
<div>
<img src="/img/icons/culture.png" alt="Culture" />
{ reward.culture }
</div>
{ /if }
</div>
<style>
.reward {
align-items: center;
display: flex;
gap: 1em;
justify-content: center;
}
.reward div {
display: flex;
align-items: center;
gap: 0.2em;
}
</style>

View File

@ -2,9 +2,10 @@ import { produce } from 'immer';
import village, { type VillageState } from '../village'; import village, { type VillageState } from '../village';
import build from './build'; import build from './build';
import upgradeBuilding from './upgradeBuilding';
import recruitUnits from './recruitUnits';
import pillage from './pillage'; import pillage from './pillage';
import recruitUnits from './recruitUnits';
import startQuest from './startQuest';
import upgradeBuilding from './upgradeBuilding';
// Encapsulates a move function into a store update, where the data is made // Encapsulates a move function into a store update, where the data is made
@ -34,4 +35,5 @@ export default {
upgradeBuilding: makeMove(upgradeBuilding), upgradeBuilding: makeMove(upgradeBuilding),
pillage: makeMove(pillage), pillage: makeMove(pillage),
recruitUnits: makeMove(recruitUnits), recruitUnits: makeMove(recruitUnits),
startQuest: makeMove(startQuest),
}; };

13
src/moves/startQuest.ts Normal file
View File

@ -0,0 +1,13 @@
import type { VillageState } from "../village";
export default function startQuest(V: VillageState, questId: number) {
const quest = V.quests.find(q => q.id === questId);
if (!quest) {
return false;
}
quest.started = true;
quest.remainingTime = quest.duration * 1000;
return true;
}

34
src/quests.ts Normal file
View File

@ -0,0 +1,34 @@
import type { QuestType, ResourcesType } from "./types";
import { getEmptyResources, random } from "./utils";
let uid = 0;
export function createQuest(level: number): QuestType {
const reward = getEmptyResources();
const adjustedLevel = level * 2 + 5;
const duration = random(adjustedLevel - 2, adjustedLevel + 2);
Object.keys(reward).forEach(r => {
const resource = r as keyof ResourcesType;
reward[resource] = random(
duration * 5 - level * 10,
duration * 5 + level * 10,
);
if (resource === 'food') {
reward[resource] = Math.round(reward[resource] / 3);
}
else if (resource === 'culture') {
reward[resource] = Math.round(reward[resource] / 20);
}
});
return {
id: uid++,
duration,
reward,
level,
started: false,
};
}

View File

@ -95,3 +95,18 @@ export interface BourgadeType extends BaseRegionType {
export type RegionType = OasisType | BourgadeType; export type RegionType = OasisType | BourgadeType;
export interface HeroType {
id: number;
}
export interface QuestType {
id: number;
duration: number;
reward: ResourcesType;
level: number;
started: boolean;
remainingTime?: number;
}

View File

@ -5,6 +5,7 @@ import { resolveMission } from './missions';
import type { ProductionType } from './types'; import type { ProductionType } from './types';
import { getProduction, getStorage, shuffle } from './utils'; import { getProduction, getStorage, shuffle } from './utils';
import village, { type VillageState } from "./village"; import village, { type VillageState } from "./village";
import { createQuest } from './quests';
let lastFrame: number; let lastFrame: number;
@ -46,6 +47,26 @@ export default function update(timestamp: number) {
} }
}); });
// Advance quests.
V.quests.filter(q => q.started).forEach(quest => {
if (!quest.remainingTime) {
return;
}
quest.remainingTime -= delta;
if (quest.remainingTime <= 0) {
V.resources.wood += quest.reward.wood;
V.resources.stone += quest.reward.stone;
V.resources.iron += quest.reward.iron;
V.resources.food += quest.reward.food;
V.resources.culture += quest.reward.culture;
// Replace the finished quest with a new one.
V.quests = V.quests.filter(q => q.id !== quest.id);
V.quests.push(createQuest(quest.level + 1));
}
});
// Make all buildings and units produce and consume. // Make all buildings and units produce and consume.
const productionPerMinute = getProduction(V); const productionPerMinute = getProduction(V);
const storage = getStorage(V); const storage = getStorage(V);

View File

@ -130,6 +130,21 @@ export function shuffle<T>(array: Array<T>): Array<T> {
} }
/**
* Return a random integer in the range [ min, max ] (min and max can be returned).
* @param min integer
* @param max integer
* @returns integer
*/
export function random(min: number, max?: number) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1.0));
}
export function getBuildingUpgradeCost(_V: VillageState, building: BuildingType): CostType { export function getBuildingUpgradeCost(_V: VillageState, building: BuildingType): CostType {
const level = building.level + 1; const level = building.level + 1;
return building.cost(level); return building.cost(level);

View File

@ -3,8 +3,9 @@ import { writable } from "svelte/store";
import { createBuilding } from "./create"; import { createBuilding } from "./create";
import worldmap from "./data/worldmap"; import worldmap from "./data/worldmap";
import { getTilesAtDistance, Hex } from "./hexgrid"; import { getTilesAtDistance, Hex } from "./hexgrid";
import type { BuildingType, RegionType, ResourcesType } from "./types"; import type { BuildingType, QuestType, RegionType, ResourcesType } from "./types";
import { getKeysAsNumbers, shuffle } from "./utils"; import { getKeysAsNumbers, shuffle } from "./utils";
import { createQuest } from "./quests";
type Board = { type Board = {
@ -23,6 +24,7 @@ export interface VillageState {
villageTiles: Board; villageTiles: Board;
outsideTiles: Board; outsideTiles: Board;
worldmap: RegionType[]; worldmap: RegionType[];
quests: QuestType[];
victory: boolean; victory: boolean;
} }
@ -78,6 +80,15 @@ function getInitialWorldmap(): RegionType[] {
} }
function getInitialQuests(): QuestType[] {
return [
createQuest(1),
createQuest(1),
createQuest(1),
];
}
function getInitialState() { function getInitialState() {
const state: VillageState = { const state: VillageState = {
buildings: [], buildings: [],
@ -92,6 +103,7 @@ function getInitialState() {
villageTiles: getInitialVillageBoard(), villageTiles: getInitialVillageBoard(),
outsideTiles: getInitialOutsideBoard(), outsideTiles: getInitialOutsideBoard(),
worldmap: getInitialWorldmap(), worldmap: getInitialWorldmap(),
quests: getInitialQuests(),
victory: false, victory: false,
}; };