Transform the world map into an actual map with regions on it.

This commit is contained in:
Adrian 2024-11-15 10:59:05 +01:00
parent 83adff2603
commit 04b7ab7339
14 changed files with 266 additions and 117 deletions

View File

@ -1,74 +0,0 @@
<script lang="ts">
import moves from "../moves";
import type { OasisType } from "../types";
import { getRemainingUnitCount } from "../utils";
import village from "../village";
export let region: OasisType;
let numberOfUnits = 0;
let repeatMission = false;
$: maximumUnits = getRemainingUnitCount($village, 'soldier');
$: if (numberOfUnits > maximumUnits) {
numberOfUnits = maximumUnits || 0;
}
function setMaxUnits() {
numberOfUnits = maximumUnits;
}
function pillage() {
moves.pillage(region.state.index, numberOfUnits, repeatMission);
}
function toggleMissionRepeat() {
moves.toggleMissionRepeat(region.state.index);
}
</script>
<div>
<h2>
<img src="/img/icons/{ region.resource }.png" alt="{ region.resource }">
Oasis
(→ { region.distance })
</h2>
{ #if region.state.mission }
<div>
<p>{ region.state.mission.unitCount } soldiers are on a mission here.</p>
<p>Remaining: { Math.ceil(region.state.mission.remainingTime / 1000) }</p>
<label>
<input type="checkbox" checked={ region.state.mission.repeat } on:change={ toggleMissionRepeat }>
<span title="Send the same troops again when they come back">Repeat</span>
</label>
</div>
{ :else }
<div>
<input
type="range"
name="units"
min="0"
max={ maximumUnits }
bind:value={ numberOfUnits }
/>
{ numberOfUnits }
<button on:click={ setMaxUnits }>↑</button>
<button on:click={ pillage } disabled={ numberOfUnits === 0 }>Pillage</button>
<label>
<input type="checkbox" bind:value={ repeatMission }>
<span title="Send the same troops again when they come back">Repeat</span>
</label>
</div>
{ /if }
</div>
<style>
h2 {
align-items: center;
display: flex;
gap: 0.4em;
}
</style>

View File

@ -0,0 +1,112 @@
<script lang="ts">
import moves from "../moves";
import showOasisPanel from "../stores/showOasisPanel";
import type { OasisType } from "../types";
import { getRemainingUnitCount } from "../utils";
import village from "../village";
$: region = ($showOasisPanel !== null) ? $village.worldmap[$showOasisPanel] as OasisType : null;
let numberOfUnits = 0;
let repeatMission = false;
$: maximumUnits = getRemainingUnitCount($village, 'soldier');
$: if (numberOfUnits > maximumUnits) {
numberOfUnits = maximumUnits || 0;
}
function setMaxUnits() {
numberOfUnits = maximumUnits;
}
function pillage() {
if (!region) return;
moves.pillage(region.id, numberOfUnits, repeatMission);
}
function toggleMissionRepeat() {
if (!region) return;
moves.toggleMissionRepeat(region.id);
}
function close() {
showOasisPanel.set(null);
}
</script>
{ #if region !== null }
<section>
<div class="oasis-panel">
<header>
<h1>
<img src="/img/icons/{ region.resource }.png" alt="{ region.resource }">
Oasis
</h1>
<span class="close">
<button on:click={ close }>X</button>
</span>
</header>
{ #if region.state.mission }
<div>
<p>{ region.state.mission.unitCount } soldiers are on a mission here.</p>
<p>Remaining: { Math.ceil(region.state.mission.remainingTime / 1000) }</p>
<label>
<input type="checkbox" checked={ region.state.mission.repeat } on:change={ toggleMissionRepeat }>
<span title="Send the same troops again when they come back">Repeat</span>
</label>
</div>
{ :else }
<div>
<input
type="range"
name="units"
min="0"
max={ maximumUnits }
bind:value={ numberOfUnits }
/>
{ numberOfUnits }
<button on:click={ setMaxUnits }>↑</button>
<button on:click={ pillage } disabled={ numberOfUnits === 0 }>Pillage</button>
<label>
<input type="checkbox" bind:value={ repeatMission }>
<span title="Send the same troops again when they come back">Repeat</span>
</label>
</div>
{ /if }
</div>
</section>
{ /if }
<style>
section {
background-color: hsl(0, 0%, 10%, 0.8);
display: grid;
place-items: center;
height: 100vh;
left: 0;
position: absolute;
top: 0;
width: 100vw;
}
.oasis-panel {
background-color: hsl(0, 0%, 20%);
border: 0.2em solid grey;
border-radius: .4em;
width: 80%;
height: 60%;
}
.oasis-panel header {
position: relative;
}
.oasis-panel header .close {
position: absolute;
right: 1em;
top: 0;
}
</style>

View File

@ -1,14 +1,32 @@
<script lang="ts"> <script lang="ts">
import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "../constants";
import gameTab from "../stores/gameTab";
import showOasisPanel from "../stores/showOasisPanel";
import { WORLDMAP_TYPES } from "../types";
import village from "../village"; import village from "../village";
import OasisRegion from "./OasisRegion.svelte";
</script> </script>
<section class="worldmap"> <section
class="worldmap"
style="--map-height: {WORLD_MAP_HEIGHT}; --map-width: {WORLD_MAP_WIDTH};"
>
{ #each $village.worldmap as region } { #each $village.worldmap as region }
<div class="region"> <div
{ #if region.type === 'oasis' } class="region"
<OasisRegion { region } /> class:empty={ region.type === WORLDMAP_TYPES.EMPTY }
class:bourgade={ region.type === WORLDMAP_TYPES.BOURGADE }
class:oasis={ region.type === WORLDMAP_TYPES.OASIS }
>
{ #if region.type === WORLDMAP_TYPES.BOURGADE }
<button class="invisible" on:click={ () => gameTab.set('resources') }>
<img src="/img/icons/field.svg" alt="">
</button>
{ :else if region.type === WORLDMAP_TYPES.OASIS }
<button class="invisible" on:click={ () => showOasisPanel.set(region.id) }>
<img src="/img/icons/{region.resource}.png" alt="">
</button>
{ :else }
{ region.id }
{ /if } { /if }
</div> </div>
{ /each } { /each }
@ -16,15 +34,35 @@
<style> <style>
.worldmap { .worldmap {
display: flex; aspect-ratio: 1;
justify-content: space-around; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(var(--map-width), 1fr);
gap: 1em; grid-template-rows: repeat(var(--map-height), 1fr);
height: 80vh;
margin: auto;
} }
.region { .region {
border: 1px solid white; border: 1px solid white;
padding: 1em; padding: 0;
width: 40%; margin: 0;
}
.region > button {
cursor: pointer;
height: 100%;
width: 100%;
}
.region.bourgade {
background-color: black;
}
.region.empty {
background-color: grey;
}
.region.oasis {
background-color: cyan;
} }
</style> </style>

View File

@ -1 +1,3 @@
export const CULTURE_TO_WIN = 20000; export const CULTURE_TO_WIN = 20000;
export const WORLD_MAP_HEIGHT = 9;
export const WORLD_MAP_WIDTH = 9;

View File

@ -6,11 +6,11 @@
import BuildingRecruitment from "./BuildingRecruitment.svelte"; import BuildingRecruitment from "./BuildingRecruitment.svelte";
import Cost from "./Cost.svelte"; import Cost from "./Cost.svelte";
function close() { function close() {
showBuildingPanel.set(null); showBuildingPanel.set(null);
} }
function upgrade() { function upgrade() {
if ($showBuildingPanel === null) { if ($showBuildingPanel === null) {
return; return;

View File

@ -14,6 +14,7 @@
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"; import Quests from "./Quests.svelte";
import OasisRegionPanel from "../board/OasisRegionPanel.svelte";
onMount(() => { onMount(() => {
@ -57,6 +58,7 @@
<section class="overlay"> <section class="overlay">
<BuildingCreator /> <BuildingCreator />
<BuildingPanel /> <BuildingPanel />
<OasisRegionPanel />
<Victory /> <Victory />
</section> </section>

View File

@ -1,9 +1,11 @@
import type { OasisType, RegionType } from "./types"; import { WORLDMAP_TYPES, type OasisType, type RegionType } from "./types";
import { getUnitSource } from "./utils"; import { assert, getUnitSource } from "./utils";
import type { VillageState } from "./village"; import type { VillageState } from "./village";
export function resolveMission(V: VillageState, region: RegionType) { export function resolveMission(V: VillageState, region: RegionType) {
assert(region.type === WORLDMAP_TYPES.OASIS);
const mission = region.state.mission; const mission = region.state.mission;
if (!mission) { if (!mission) {
return; return;
@ -11,7 +13,7 @@ export function resolveMission(V: VillageState, region: RegionType) {
switch (mission.type) { switch (mission.type) {
case 'pillage': case 'pillage':
if (region.type === 'oasis') { if (region.type === WORLDMAP_TYPES.OASIS) {
resolvePillageOasis(V, region); resolvePillageOasis(V, region);
} }
break; break;
@ -20,7 +22,7 @@ export function resolveMission(V: VillageState, region: RegionType) {
} }
if (mission.repeat) { if (mission.repeat) {
mission.remainingTime = region.distance * 10 * 1000; mission.remainingTime = 1 * 10 * 1000;
} }
else { else {
delete region.state.mission; delete region.state.mission;

View File

@ -1,4 +1,5 @@
import { getRemainingUnitCount } from "../utils"; import { WORLDMAP_TYPES } from "../types";
import { assert, getRemainingUnitCount } from "../utils";
import type { VillageState } from "../village"; import type { VillageState } from "../village";
@ -9,6 +10,8 @@ export default function pillage(V: VillageState, regionIndex: number, soldiersCo
const region = V.worldmap[regionIndex]; const region = V.worldmap[regionIndex];
assert(region.type === WORLDMAP_TYPES.OASIS);
if (region.state.mission) { if (region.state.mission) {
return false; return false;
} }
@ -17,7 +20,7 @@ export default function pillage(V: VillageState, regionIndex: number, soldiersCo
type: 'pillage', type: 'pillage',
unitType: 'soldier', unitType: 'soldier',
unitCount: soldiersCount, unitCount: soldiersCount,
remainingTime: region.distance * 10 * 1000, remainingTime: 1 * 10 * 1000,
repeat, repeat,
}; };

View File

@ -1,9 +1,13 @@
import { WORLDMAP_TYPES } from "../types";
import { assert } from "../utils";
import type { VillageState } from "../village"; import type { VillageState } from "../village";
export default function toggleMissionRepeat(V: VillageState, regionIndex: number) { export default function toggleMissionRepeat(V: VillageState, regionIndex: number) {
const region = V.worldmap[regionIndex]; const region = V.worldmap[regionIndex];
assert(region.type === WORLDMAP_TYPES.OASIS);
if (!region.state.mission) { if (!region.state.mission) {
return false; return false;
} }

View File

@ -0,0 +1,4 @@
import { writable } from "svelte/store";
export default writable<number | null>(null);

View File

@ -81,28 +81,40 @@ export interface MissionType {
repeat: boolean; repeat: boolean;
} }
export enum WORLDMAP_TYPES {
EMPTY,
OASIS,
BOURGADE,
}
interface BaseRegionType { interface BaseRegionType {
distance: number; id: number;
type: WORLDMAP_TYPES;
}
export interface EmptyRegionType extends BaseRegionType {
type: WORLDMAP_TYPES.EMPTY;
}
export interface OasisType extends BaseRegionType {
type: WORLDMAP_TYPES.OASIS;
resource: keyof CostType;
state: { state: {
index: number;
mission?: MissionType; mission?: MissionType;
}; };
} }
export interface OasisType extends BaseRegionType {
type: 'oasis';
resource: keyof CostType;
}
export interface BourgadeType extends BaseRegionType { export interface BourgadeType extends BaseRegionType {
type: 'bourgade'; type: WORLDMAP_TYPES.BOURGADE;
distance: number;
} }
export type RegionType = OasisType | BourgadeType; export type RegionType = EmptyRegionType | OasisType | BourgadeType;
export interface HeroType { export interface HeroType {

View File

@ -4,7 +4,7 @@ import { CULTURE_TO_WIN } from './constants';
import { createQuest } from './create'; import { createQuest } from './create';
import { resolveMission } from './missions'; import { resolveMission } from './missions';
import type { ProductionType } from './types'; import type { ProductionType } from './types';
import { getProduction, getStorage, shuffle } from './utils'; import { getProduction, getRegionsWithMissions, getStorage, shuffle } from './utils';
import village, { type VillageState } from "./village"; import village, { type VillageState } from "./village";
@ -36,7 +36,7 @@ export default function update(timestamp: number) {
}); });
// Advance missions. // Advance missions.
V.worldmap.forEach(region => { getRegionsWithMissions(V).forEach(region => {
if (!region.state.mission) { if (!region.state.mission) {
return; return;
} }

View File

@ -1,5 +1,6 @@
import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
import units from "./data/units"; import units from "./data/units";
import type { BuildingType, CostType, MissionType, ProductionType, ResourcesType } from "./types"; import { WORLDMAP_TYPES, type BuildingType, type CostType, type OasisType, type ProductionType, type ResourcesType } from "./types";
import type { VillageState } from "./village"; import type { VillageState } from "./village";
@ -88,9 +89,16 @@ export function getUnitSource(unitType: string) {
} }
export function getRegionsWithMissions(V: VillageState): OasisType[] {
return V.worldmap.filter(r => r.type === WORLDMAP_TYPES.OASIS);
}
export function getRemainingUnitCount(V: VillageState, unitType: string) { export function getRemainingUnitCount(V: VillageState, unitType: string) {
let total = V.units[unitType] || 0; let total = V.units[unitType] || 0;
const missions = V.worldmap.filter(r => r.state.mission?.unitType === unitType).map(r => r.state.mission); const missions = getRegionsWithMissions(V)
.filter(r => r.state.mission?.unitType === unitType)
.map(r => r.state.mission);
missions.forEach(m => total -= m?.unitCount || 0); missions.forEach(m => total -= m?.unitCount || 0);
return total; return total;
} }
@ -170,3 +178,25 @@ export function getTownhall(V: VillageState): BuildingType {
} }
return townhall; return townhall;
} }
export function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
export function getAdjacentWorldmapCells(cellIndex: number) {
const cells = [
cellIndex - WORLD_MAP_WIDTH - 1,
cellIndex - WORLD_MAP_WIDTH,
cellIndex - WORLD_MAP_WIDTH + 1,
cellIndex - 1,
cellIndex + 1,
cellIndex + WORLD_MAP_WIDTH - 1,
cellIndex + WORLD_MAP_WIDTH,
cellIndex + WORLD_MAP_WIDTH + 1,
];
return cells.filter(c => c >= 0 && c < WORLD_MAP_WIDTH * WORLD_MAP_HEIGHT);
}

View File

@ -3,8 +3,9 @@ import { writable } from "svelte/store";
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 type { BuildingType, HeroType, QuestType, RegionType, ResourcesType } from "./types"; import { WORLDMAP_TYPES, type BuildingType, type CostType, type HeroType, type QuestType, type RegionType, type ResourcesType } from "./types";
import { getKeysAsNumbers, shuffle } from "./utils"; import { getAdjacentWorldmapCells, getKeysAsNumbers, shuffle } from "./utils";
import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
type Board = { type Board = {
@ -68,15 +69,28 @@ function getInitialOutsideBoard() {
function getInitialWorldmap(): RegionType[] { function getInitialWorldmap(): RegionType[] {
return worldmap.map((r, index) => { const board: RegionType[] = []
const region = r as RegionType; for (let i = 0; i < WORLD_MAP_WIDTH * WORLD_MAP_HEIGHT; i++) {
return { board[i] = {
...region, id: i,
state: { type: WORLDMAP_TYPES.EMPTY,
index, };
}, }
const centerIndex = Math.floor((WORLD_MAP_WIDTH * WORLD_MAP_HEIGHT) / 2);
board[centerIndex].type = WORLDMAP_TYPES.BOURGADE;
const adj = shuffle(getAdjacentWorldmapCells(centerIndex));
worldmap.forEach(c => {
const cellIndex = adj.pop() || 0;
board[cellIndex] = {
type: WORLDMAP_TYPES.OASIS,
id: cellIndex,
resource: c.resource as keyof CostType,
state: {},
}; };
}); });
return board;
} }