Compare commits
2 Commits
3b38d16868
...
cb1f9480c3
Author | SHA1 | Date | |
---|---|---|---|
cb1f9480c3 | |||
f7f86db510 |
@ -1,7 +1,12 @@
|
|||||||
|
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;
|
||||||
|
@ -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, ResourcesType } from "./types";
|
import type { BuildingSource, BuildingType, HeroType, QuestType, Resource } from "./types";
|
||||||
import { getEmptyResources, random, shuffle } from "./utils";
|
import { random, shuffle } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
let uid = 0;
|
let uid = 0;
|
||||||
@ -37,25 +37,18 @@ export function createBuilding(buildingType: string): BuildingType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function createQuest(level: number): QuestType {
|
export function createQuest(resource: Resource, level: number): QuestType {
|
||||||
const reward = getEmptyResources();
|
const adjustedLevel = Math.ceil((level * level + 3) / 3);
|
||||||
const adjustedLevel = level * level + 3;
|
const duration = Math.max(1, random(adjustedLevel - 2, adjustedLevel + 2));
|
||||||
const duration = random(adjustedLevel - 2, adjustedLevel + 2);
|
const reward = Math.max(10, random(
|
||||||
Object.keys(reward).forEach(r => {
|
Math.round((duration * 5 - level * 10) * (1 + level / 3)),
|
||||||
const resource = r as keyof ResourcesType;
|
Math.round((duration * 5 + level * 10) * (1 + level / 3)),
|
||||||
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,
|
||||||
@ -67,5 +60,8 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default [
|
|||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
short: '',
|
short: '',
|
||||||
long: 'Increases the construction time of buildings by 2,5% per level, and unlocks new buildings.'
|
long: 'Reduces the construction time of buildings by 2,5% per level, and unlocks new buildings.'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
41
src/hud/Hero.svelte
Normal file
41
src/hud/Hero.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
@ -3,7 +3,8 @@
|
|||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
|
|
||||||
import moves from "../moves";
|
import moves from "../moves";
|
||||||
import { getPrettyTime } from "../utils";
|
import type { Resource, ResourcesType } from "../types";
|
||||||
|
import { getEmptyResources, getPrettyTime } from "../utils";
|
||||||
import village from "../village";
|
import village from "../village";
|
||||||
import Reward from "./Reward.svelte";
|
import Reward from "./Reward.svelte";
|
||||||
|
|
||||||
@ -13,6 +14,12 @@
|
|||||||
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>
|
||||||
@ -23,15 +30,19 @@
|
|||||||
out:fly={{ duration: 200, x: 500 }}
|
out:fly={{ duration: 200, x: 500 }}
|
||||||
animate:flip={{ duration: 100 }}
|
animate:flip={{ duration: 100 }}
|
||||||
>
|
>
|
||||||
<p>
|
<p class="reward">
|
||||||
<Reward reward={ quest.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>
|
</p>
|
||||||
|
{ #if !quest.started }
|
||||||
<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) }
|
||||||
@ -41,14 +52,29 @@
|
|||||||
{ :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>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
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);
|
||||||
@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
{ #each $village.heroes as hero }
|
{ #each $village.heroes as hero }
|
||||||
<p>{ hero.name }</p>
|
<Hero { hero } />
|
||||||
{ /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>
|
||||||
|
79
src/modules/heroes.ts
Normal file
79
src/modules/heroes.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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,
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
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) {
|
||||||
|
@ -26,6 +26,9 @@ export interface ResourcesType extends CostType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Resource = keyof ResourcesType;
|
||||||
|
|
||||||
|
|
||||||
export interface BuildingSource {
|
export interface BuildingSource {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@ -133,13 +136,17 @@ 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;
|
||||||
reward: ResourcesType;
|
resource: keyof ResourcesType
|
||||||
|
reward: number;
|
||||||
level: number;
|
level: number;
|
||||||
started: boolean;
|
started: boolean;
|
||||||
hero?: number;
|
hero?: number;
|
||||||
|
@ -6,6 +6,7 @@ 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;
|
||||||
@ -56,15 +57,15 @@ export default function update(timestamp: number) {
|
|||||||
|
|
||||||
quest.remainingTime -= delta;
|
quest.remainingTime -= delta;
|
||||||
if (quest.remainingTime <= 0) {
|
if (quest.remainingTime <= 0) {
|
||||||
V.resources.wood += quest.reward.wood;
|
V.resources[quest.resource] += quest.reward;
|
||||||
V.resources.stone += quest.reward.stone;
|
|
||||||
V.resources.iron += quest.reward.iron;
|
const hero = heroes.getHero(V, quest.hero as number);
|
||||||
V.resources.food += quest.reward.food;
|
heroes.hurt(hero, 10.0);
|
||||||
V.resources.culture += quest.reward.culture;
|
heroes.gainExperience(hero, 2);
|
||||||
|
|
||||||
// Replace the finished quest with a new one.
|
// Replace the finished quest with a new one.
|
||||||
V.quests = V.quests.filter(q => q.id !== quest.id);
|
const index = V.quests.findIndex(q => q.id === quest.id);
|
||||||
V.quests.push(createQuest(quest.level + 1));
|
V.quests[index] = createQuest(quest.resource, quest.level + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,6 +121,11 @@ 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;
|
||||||
|
@ -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 OasisType, type Point, type ProductionType, type ResourcesType } from "./types";
|
import { WORLDMAP_TYPES, type BuildingType, type CostType, type HeroType, type OasisType, type Point, type ProductionType, type ResourcesType } from "./types";
|
||||||
import type { VillageState } from "./village";
|
import type { VillageState } from "./village";
|
||||||
|
|
||||||
|
|
||||||
|
@ -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, getAdjacentWorldmapCells, getKeysAsNumbers, indexToPoint, shuffle } from "./utils";
|
import { distanceBetweenCells, 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,11 +106,9 @@ function getInitialWorldmap(): RegionType[] {
|
|||||||
|
|
||||||
|
|
||||||
function getInitialQuests(): QuestType[] {
|
function getInitialQuests(): QuestType[] {
|
||||||
return [
|
return RESOURCES.map(r => {
|
||||||
createQuest(1),
|
return createQuest(r, 1);
|
||||||
createQuest(1),
|
});
|
||||||
createQuest(1),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user