Compare commits

..

No commits in common. "cb1f9480c33c6ecacdf0411781284ffad759d7e4" and "3b38d16868177bcf54483b1db0ab00aff3513840" have entirely different histories.

12 changed files with 45 additions and 205 deletions

View File

@ -1,12 +1,7 @@
import type { Resource } from "./types";
export const CULTURE_TO_WIN = 20000; export const CULTURE_TO_WIN = 20000;
export const WORLD_MAP_HEIGHT = 9; export const WORLD_MAP_HEIGHT = 9;
export const WORLD_MAP_WIDTH = 9; export const WORLD_MAP_WIDTH = 9;
export const RESOURCES: Resource[] = ['wood', 'stone', 'iron', 'food', 'culture'];
// Debug values. // Debug values.
export const TOWN_HALL_START_LEVEL = 1; export const TOWN_HALL_START_LEVEL = 1;
export const RESOURCE_BUILDINGS_START_LEVEL = 1; export const RESOURCE_BUILDINGS_START_LEVEL = 1;

View File

@ -1,8 +1,8 @@
import buildings from "./data/buildings"; import buildings from "./data/buildings";
import { NAMES } from "./data/heroes"; import { NAMES } from "./data/heroes";
import { Hex } from "./hexgrid"; import { Hex } from "./hexgrid";
import type { BuildingSource, BuildingType, HeroType, QuestType, Resource } from "./types"; import type { BuildingSource, BuildingType, HeroType, QuestType, ResourcesType } from "./types";
import { random, shuffle } from "./utils"; import { getEmptyResources, random, shuffle } from "./utils";
let uid = 0; let uid = 0;
@ -37,18 +37,25 @@ export function createBuilding(buildingType: string): BuildingType {
} }
export function createQuest(resource: Resource, level: number): QuestType { export function createQuest(level: number): QuestType {
const adjustedLevel = Math.ceil((level * level + 3) / 3); const reward = getEmptyResources();
const duration = Math.max(1, random(adjustedLevel - 2, adjustedLevel + 2)); const adjustedLevel = level * level + 3;
const reward = Math.max(10, random( const duration = random(adjustedLevel - 2, adjustedLevel + 2);
Math.round((duration * 5 - level * 10) * (1 + level / 3)), Object.keys(reward).forEach(r => {
Math.round((duration * 5 + level * 10) * (1 + level / 3)), const resource = r as keyof ResourcesType;
)); reward[resource] = random(
Math.round((duration * 5 - level * 10) * (1 + level / 3)),
Math.round((duration * 5 + level * 10) * (1 + level / 3)),
);
if (resource === 'culture') {
reward[resource] = Math.round(reward[resource] / 20);
}
});
return { return {
id: uid++, id: uid++,
duration, duration,
resource,
reward, reward,
level, level,
started: false, started: false,
@ -60,8 +67,5 @@ export function createHero(): HeroType {
return { return {
id: uid++, id: uid++,
name: shuffle(NAMES)[0], name: shuffle(NAMES)[0],
health: 100.0,
level: 1,
experience: 0,
}; };
} }

View File

@ -58,7 +58,7 @@ export default [
}, },
description: { description: {
short: '', short: '',
long: 'Reduces the construction time of buildings by 2,5% per level, and unlocks new buildings.' long: 'Increases the construction time of buildings by 2,5% per level, and unlocks new buildings.'
}, },
}, },
{ {

View File

@ -1,41 +0,0 @@
<script lang="ts">
import heroes from "../modules/heroes";
import type { HeroType } from "../types";
export let hero: HeroType;
</script>
<div class="hero">
<span class="level">{ hero.level }</span>
<p class="name">{ hero.name }</p>
<p>Exp: { hero.experience } / { heroes.getExperienceNeeded(hero) }</p>
<div class="health" style="--health-percent: { Math.floor(hero.health) }%;"></div>
</div>
<style>
.hero {
margin: 1em auto;
width: 80%;
}
.health {
background-color: hsl(0, 86%, 33%);
border: 1px solid hsl(61, 83%, 14%);
width: 100%;
height: 1em;
position: relative;
}
.health::after {
content: '';
background-color: hsl(120, 41%, 53%);
width: 100%;
height: 1em;
position: absolute;
top: 0;
left: 0;
scale: var(--health-percent) 1;
transform-origin: left;
transition: scale 400ms ease-in-out;
}
</style>

View File

@ -3,8 +3,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import moves from "../moves"; import moves from "../moves";
import type { Resource, ResourcesType } from "../types"; import { getPrettyTime } from "../utils";
import { getEmptyResources, getPrettyTime } from "../utils";
import village from "../village"; import village from "../village";
import Reward from "./Reward.svelte"; import Reward from "./Reward.svelte";
@ -14,12 +13,6 @@
function startQuest(id: number, heroId: number) { function startQuest(id: number, heroId: number) {
moves.startQuest(id, heroId); moves.startQuest(id, heroId);
} }
function getRewardAsResources(resource: Resource, reward: number): ResourcesType {
const output = getEmptyResources();
output[resource] = reward;
return output;
}
</script> </script>
<section> <section>
@ -30,19 +23,15 @@
out:fly={{ duration: 200, x: 500 }} out:fly={{ duration: 200, x: 500 }}
animate:flip={{ duration: 100 }} animate:flip={{ duration: 100 }}
> >
<p class="reward">
<span>
<img src="./img/icons/time.png" alt="Duration">
{ #if quest.started }
{ getPrettyTime(quest.remainingTime || 0) }
{ :else }
{ quest.duration }
{ /if }
</span>
<Reward reward={ getRewardAsResources(quest.resource, quest.reward) } />
</p>
{ #if !quest.started }
<p> <p>
<Reward reward={ quest.reward } />
</p>
<p>
<img src="./img/icons/time.png" alt="Duration">
{ #if quest.started }
{ getPrettyTime(quest.remainingTime || 0) }
{ :else }
{ quest.duration }
{ #each availableHeroes as hero } { #each availableHeroes as hero }
<button <button
on:click={ () => startQuest(quest.id, hero.id) } on:click={ () => startQuest(quest.id, hero.id) }
@ -52,29 +41,14 @@
{ :else } { :else }
<span>No heroes available</span> <span>No heroes available</span>
{ /each } { /each }
{ /if }
</p> </p>
{ /if }
</div> </div>
{ /each } { /each }
</section> </section>
<style> <style>
.quest { .quest {
border: 1px solid hsl(0, 0%, 20%);
border-radius: 1em;
margin-bottom: 0.5em;
padding: 0 1em;
text-align: left; text-align: left;
} }
.quest .reward {
display: flex;
justify-content: space-between;
}
.quest .reward span {
display: flex;
align-items: center;
gap: 0.5em;
}
</style> </style>

View File

@ -2,7 +2,6 @@
import units from "../data/units"; import units from "../data/units";
import { getRemainingUnitCount } from "../utils"; import { getRemainingUnitCount } from "../utils";
import village from "../village"; import village from "../village";
import Hero from "./Hero.svelte";
$: currentUnits = Object.entries($village.units).map(([type, count]) => { $: currentUnits = Object.entries($village.units).map(([type, count]) => {
const unit = units.find(u => u.type === type); const unit = units.find(u => u.type === type);
@ -19,7 +18,7 @@
<section> <section>
{ #each $village.heroes as hero } { #each $village.heroes as hero }
<Hero { hero } /> <p>{ hero.name }</p>
{ /each } { /each }
{ #each currentUnits as unit } { #each currentUnits as unit }
<p>{ unit.name }: { getRemainingUnitCount($village, unit.type) } / { unit.count }</p> <p>{ unit.name }: { getRemainingUnitCount($village, unit.type) } / { unit.count }</p>

View File

@ -1,79 +0,0 @@
import type { HeroType } from "../types";
import type { VillageState } from "../village";
const LEVELS = [
0,
10,
25,
50,
100,
250,
500,
1000,
2500,
5000,
10000,
];
export function getHero(V: VillageState, heroId: number) {
const hero = V.heroes.find(h => h.id === heroId);
if (!hero) {
throw new Error(`Cannot find hero with id "${heroId}"`);
}
return hero;
}
export function getMaxHealth(hero: HeroType): number {
return 100.0 + (hero.level - 1) * 5.0;
}
export function hurt(hero: HeroType, damage: number) {
hero.health -= damage;
if (hero.health <= 0) {
hero.health = 0.0;
// Do something to show that the hero is dead.
}
}
export function heal(hero: HeroType, health: number) {
hero.health -= health;
const maxHealth = getMaxHealth(hero);
if (hero.health > maxHealth) {
hero.health = maxHealth;
}
}
export function gainExperience(hero: HeroType, xp: number) {
hero.experience += xp;
if (hero.experience >= LEVELS[hero.level]) {
hero.experience -= LEVELS[hero.level];
hero.level++;
hero.health = getMaxHealth(hero);
}
}
export function getExperienceNeeded(hero: HeroType) {
return LEVELS[hero.level];
}
export default {
getHero,
getExperienceNeeded,
getMaxHealth,
heal,
hurt,
gainExperience,
};

View File

@ -1,6 +1,5 @@
import type { VillageState } from "../village"; import type { VillageState } from "../village";
export default function startQuest(V: VillageState, questId: number, heroId: number) { export default function startQuest(V: VillageState, questId: number, heroId: number) {
const quest = V.quests.find(q => q.id === questId); const quest = V.quests.find(q => q.id === questId);
if (!quest) { if (!quest) {

View File

@ -26,9 +26,6 @@ export interface ResourcesType extends CostType {
} }
export type Resource = keyof ResourcesType;
export interface BuildingSource { export interface BuildingSource {
name: string; name: string;
type: string; type: string;
@ -136,17 +133,13 @@ export type RegionType = EmptyRegionType | OasisType | BourgadeType;
export interface HeroType { export interface HeroType {
id: number; id: number;
name: string; name: string;
level: number;
health: number;
experience: number;
} }
export interface QuestType { export interface QuestType {
id: number; id: number;
duration: number; duration: number;
resource: keyof ResourcesType reward: ResourcesType;
reward: number;
level: number; level: number;
started: boolean; started: boolean;
hero?: number; hero?: number;

View File

@ -6,7 +6,6 @@ import { resolveMission } from './missions';
import type { ProductionType } from './types'; import type { ProductionType } from './types';
import { getProduction, getRegionsWithMissions, getStorage, shuffle } from './utils'; import { getProduction, getRegionsWithMissions, getStorage, shuffle } from './utils';
import village, { type VillageState } from "./village"; import village, { type VillageState } from "./village";
import heroes from './modules/heroes';
let lastFrame: number; let lastFrame: number;
@ -57,15 +56,15 @@ export default function update(timestamp: number) {
quest.remainingTime -= delta; quest.remainingTime -= delta;
if (quest.remainingTime <= 0) { if (quest.remainingTime <= 0) {
V.resources[quest.resource] += quest.reward; V.resources.wood += quest.reward.wood;
V.resources.stone += quest.reward.stone;
const hero = heroes.getHero(V, quest.hero as number); V.resources.iron += quest.reward.iron;
heroes.hurt(hero, 10.0); V.resources.food += quest.reward.food;
heroes.gainExperience(hero, 2); V.resources.culture += quest.reward.culture;
// Replace the finished quest with a new one. // Replace the finished quest with a new one.
const index = V.quests.findIndex(q => q.id === quest.id); V.quests = V.quests.filter(q => q.id !== quest.id);
V.quests[index] = createQuest(quest.resource, quest.level + 1); V.quests.push(createQuest(quest.level + 1));
} }
}); });
@ -121,11 +120,6 @@ export default function update(timestamp: number) {
} }
}); });
// Heal heroes.
V.heroes.forEach(h => {
heroes.heal(h, delta / 5000.0)
});
// Check if the game is won. // Check if the game is won.
if (V.resources.culture >= CULTURE_TO_WIN) { if (V.resources.culture >= CULTURE_TO_WIN) {
V.victory = true; V.victory = true;

View File

@ -1,6 +1,6 @@
import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants"; import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
import units from "./data/units"; import units from "./data/units";
import { WORLDMAP_TYPES, type BuildingType, type CostType, type HeroType, type OasisType, type Point, type ProductionType, type ResourcesType } from "./types"; import { WORLDMAP_TYPES, type BuildingType, type CostType, type OasisType, type Point, type ProductionType, type ResourcesType } from "./types";
import type { VillageState } from "./village"; import type { VillageState } from "./village";

View File

@ -1,11 +1,11 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { RESOURCE_BUILDINGS_START_LEVEL, RESOURCES, TOWN_HALL_START_LEVEL, WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
import { createBuilding, createHero, createQuest } from "./create"; import { createBuilding, createHero, createQuest } from "./create";
import worldmap from "./data/worldmap"; import worldmap from "./data/worldmap";
import { getTilesAtDistance, Hex } from "./hexgrid"; import { getTilesAtDistance, Hex } from "./hexgrid";
import { WORLDMAP_TYPES, type BuildingType, type CostType, type HeroType, type QuestType, type RegionType, type ResourcesType } from "./types"; import { WORLDMAP_TYPES, type BuildingType, type CostType, type HeroType, type QuestType, type RegionType, type ResourcesType } from "./types";
import { distanceBetweenCells, getKeysAsNumbers, indexToPoint, shuffle } from "./utils"; import { distanceBetweenCells, getAdjacentWorldmapCells, getKeysAsNumbers, indexToPoint, shuffle } from "./utils";
import { RESOURCE_BUILDINGS_START_LEVEL, TOWN_HALL_START_LEVEL, WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
type Board = { type Board = {
@ -106,9 +106,11 @@ function getInitialWorldmap(): RegionType[] {
function getInitialQuests(): QuestType[] { function getInitialQuests(): QuestType[] {
return RESOURCES.map(r => { return [
return createQuest(r, 1); createQuest(1),
}); createQuest(1),
createQuest(1),
];
} }