Compare commits
4 Commits
933796bb4a
...
8bed89b6dc
Author | SHA1 | Date | |
---|---|---|---|
8bed89b6dc | |||
0caa40bdca | |||
04b7ab7339 | |||
83adff2603 |
BIN
public/img/buildings/bakery.png
Normal file
BIN
public/img/buildings/bakery.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
BIN
public/img/buildings/blacksmith.png
Normal file
BIN
public/img/buildings/blacksmith.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
BIN
public/img/buildings/sawmill.png
Normal file
BIN
public/img/buildings/sawmill.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 143 KiB |
BIN
public/img/buildings/stonecutter.png
Normal file
BIN
public/img/buildings/stonecutter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
@ -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>
|
|
138
src/board/OasisRegionPanel.svelte
Normal file
138
src/board/OasisRegionPanel.svelte
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<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 clear() {
|
||||||
|
if (!region) return;
|
||||||
|
moves.clear(region.id, numberOfUnits);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.cleared }
|
||||||
|
<p>Region cleared</p>
|
||||||
|
{ :else }
|
||||||
|
<p>Defensive strength: { region.strength }</p>
|
||||||
|
{ /if }
|
||||||
|
{ #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>
|
||||||
|
{ #if region.state.mission.type === 'pillage' }
|
||||||
|
<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>
|
||||||
|
{ /if }
|
||||||
|
</div>
|
||||||
|
{ :else if !region.state.cleared }
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
name="units"
|
||||||
|
min="0"
|
||||||
|
max={ maximumUnits }
|
||||||
|
bind:value={ numberOfUnits }
|
||||||
|
/>
|
||||||
|
{ numberOfUnits }
|
||||||
|
<button on:click={ setMaxUnits }>↑</button>
|
||||||
|
<button on:click={ clear } disabled={ numberOfUnits === 0 }>Clear</button>
|
||||||
|
</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>
|
@ -1,14 +1,33 @@
|
|||||||
<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 }
|
||||||
|
class:cleared={ region.type === WORLDMAP_TYPES.OASIS && region.state.cleared }
|
||||||
|
>
|
||||||
|
{ #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 +35,39 @@
|
|||||||
|
|
||||||
<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: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region.oasis.cleared {
|
||||||
|
background-color: cyan;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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;
|
||||||
|
@ -24,6 +24,11 @@ function getUnitBuildingCost(level: number, initial: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getMultiplierBuildingCost(level: number, initial: number) {
|
||||||
|
return initial * Math.round( (level + level * level) / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
type: 'townhall',
|
type: 'townhall',
|
||||||
@ -353,6 +358,114 @@ export default [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'sawmill',
|
||||||
|
name: 'Sawmill',
|
||||||
|
maxLevel: 5,
|
||||||
|
unique: true,
|
||||||
|
requiredTownhallLevel: 7,
|
||||||
|
cost: (level: number) => {
|
||||||
|
return {
|
||||||
|
wood: getMultiplierBuildingCost(level, 440),
|
||||||
|
stone: getMultiplierBuildingCost(level, 610),
|
||||||
|
iron: getMultiplierBuildingCost(level, 870),
|
||||||
|
food: getMultiplierBuildingCost(level, 700),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
timeToBuild: (level: number) => getStandardTimeToBuild(level) * 3,
|
||||||
|
behavior: {
|
||||||
|
production: (V: VillageState, self: BuildingType) => {
|
||||||
|
const resourceProd = V.buildings
|
||||||
|
.filter(b => b.type === 'woodcutter')
|
||||||
|
.map(f => f.behavior.production?.(V, f).wood)
|
||||||
|
.reduce((acc, f) => acc + f, 0);
|
||||||
|
const prod = getEmptyResources();
|
||||||
|
prod.wood = resourceProd * self.level * 0.05;
|
||||||
|
return prod;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'stonecutter',
|
||||||
|
name: 'Stonecutter',
|
||||||
|
maxLevel: 5,
|
||||||
|
unique: true,
|
||||||
|
requiredTownhallLevel: 7,
|
||||||
|
cost: (level: number) => {
|
||||||
|
return {
|
||||||
|
wood: getMultiplierBuildingCost(level, 870),
|
||||||
|
stone: getMultiplierBuildingCost(level, 440),
|
||||||
|
iron: getMultiplierBuildingCost(level, 610),
|
||||||
|
food: getMultiplierBuildingCost(level, 700),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
timeToBuild: (level: number) => getStandardTimeToBuild(level) * 3,
|
||||||
|
behavior: {
|
||||||
|
production: (V: VillageState, self: BuildingType) => {
|
||||||
|
const resourceProd = V.buildings
|
||||||
|
.filter(b => b.type === 'pit')
|
||||||
|
.map(f => f.behavior.production?.(V, f).stone)
|
||||||
|
.reduce((acc, f) => acc + f, 0);
|
||||||
|
const prod = getEmptyResources();
|
||||||
|
prod.stone = resourceProd * self.level * 0.05;
|
||||||
|
return prod;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'blacksmith',
|
||||||
|
name: 'Blacksmith',
|
||||||
|
maxLevel: 5,
|
||||||
|
unique: true,
|
||||||
|
requiredTownhallLevel: 7,
|
||||||
|
cost: (level: number) => {
|
||||||
|
return {
|
||||||
|
wood: getMultiplierBuildingCost(level, 610),
|
||||||
|
stone: getMultiplierBuildingCost(level, 870),
|
||||||
|
iron: getMultiplierBuildingCost(level, 440),
|
||||||
|
food: getMultiplierBuildingCost(level, 700),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
timeToBuild: (level: number) => getStandardTimeToBuild(level) * 3,
|
||||||
|
behavior: {
|
||||||
|
production: (V: VillageState, self: BuildingType) => {
|
||||||
|
const resourceProd = V.buildings
|
||||||
|
.filter(b => b.type === 'mine')
|
||||||
|
.map(f => f.behavior.production?.(V, f).iron)
|
||||||
|
.reduce((acc, f) => acc + f, 0);
|
||||||
|
const prod = getEmptyResources();
|
||||||
|
prod.iron = resourceProd * self.level * 0.05;
|
||||||
|
return prod;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bakery',
|
||||||
|
name: 'Bakery',
|
||||||
|
maxLevel: 5,
|
||||||
|
unique: true,
|
||||||
|
requiredTownhallLevel: 7,
|
||||||
|
cost: (level: number) => {
|
||||||
|
return {
|
||||||
|
wood: getMultiplierBuildingCost(level, 670),
|
||||||
|
stone: getMultiplierBuildingCost(level, 740),
|
||||||
|
iron: getMultiplierBuildingCost(level, 670),
|
||||||
|
food: getMultiplierBuildingCost(level, 520),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
timeToBuild: (level: number) => getStandardTimeToBuild(level) * 3,
|
||||||
|
behavior: {
|
||||||
|
production: (V: VillageState, self: BuildingType) => {
|
||||||
|
const fields = V.buildings.filter(b => b.type === 'field');
|
||||||
|
const fieldFoodProd = fields
|
||||||
|
.map(f => f.behavior.production?.(V, f).food)
|
||||||
|
.reduce((acc, f) => acc + f, 0);
|
||||||
|
const prod = getEmptyResources();
|
||||||
|
prod.food = fieldFoodProd * self.level * 0.05;
|
||||||
|
return prod;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'wonder',
|
type: 'wonder',
|
||||||
name: 'World Wonder',
|
name: 'World Wonder',
|
||||||
|
@ -2,21 +2,81 @@ export default [
|
|||||||
{
|
{
|
||||||
type: 'oasis',
|
type: 'oasis',
|
||||||
resource: 'food',
|
resource: 'food',
|
||||||
distance: 1,
|
distance: [1, 2],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'oasis',
|
type: 'oasis',
|
||||||
resource: 'wood',
|
resource: 'wood',
|
||||||
distance: 1,
|
distance: [1, 2],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'oasis',
|
type: 'oasis',
|
||||||
resource: 'stone',
|
resource: 'stone',
|
||||||
distance: 1,
|
distance: [1, 2],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'oasis',
|
type: 'oasis',
|
||||||
resource: 'iron',
|
resource: 'iron',
|
||||||
distance: 1,
|
distance: [1, 2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'food',
|
||||||
|
distance: [2, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'wood',
|
||||||
|
distance: [2, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'stone',
|
||||||
|
distance: [2, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'iron',
|
||||||
|
distance: [2, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'food',
|
||||||
|
distance: [3, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'wood',
|
||||||
|
distance: [3, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'stone',
|
||||||
|
distance: [3, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'iron',
|
||||||
|
distance: [3, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'food',
|
||||||
|
distance: [4, 5],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'wood',
|
||||||
|
distance: [4, 5],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'stone',
|
||||||
|
distance: [4, 5],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oasis',
|
||||||
|
resource: 'iron',
|
||||||
|
distance: [4, 5],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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,16 +13,21 @@ 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;
|
||||||
|
case 'clear':
|
||||||
|
if (region.type === WORLDMAP_TYPES.OASIS) {
|
||||||
|
resolveClearOasis(V, region);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown mission type: "${ mission.type }"`);
|
throw new Error(`Unknown mission type: "${ mission.type }"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -28,6 +35,36 @@ export function resolveMission(V: VillageState, region: RegionType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function resolveClearOasis(V: VillageState, region: OasisType) {
|
||||||
|
const mission = region.state.mission;
|
||||||
|
if (!mission) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = mission.unitCount / region.strength;
|
||||||
|
const lostRatio = 1 / (ratio * ratio);
|
||||||
|
const lostUnits = Math.min(
|
||||||
|
Math.round(mission.unitCount * lostRatio),
|
||||||
|
mission.unitCount
|
||||||
|
);
|
||||||
|
|
||||||
|
mission.unitCount -= lostUnits;
|
||||||
|
V.units.soldier -= lostUnits;
|
||||||
|
|
||||||
|
if (ratio >= 1) {
|
||||||
|
region.state.cleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = getUnitSource('soldier');
|
||||||
|
const maxResources = region.distance * region.distance * 100;
|
||||||
|
|
||||||
|
V.resources[region.resource] += Math.min(
|
||||||
|
mission.unitCount * unit.behavior.caryingCapacity,
|
||||||
|
maxResources
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function resolvePillageOasis(V: VillageState, region: OasisType) {
|
function resolvePillageOasis(V: VillageState, region: OasisType) {
|
||||||
const mission = region.state.mission;
|
const mission = region.state.mission;
|
||||||
if (!mission) {
|
if (!mission) {
|
||||||
@ -35,6 +72,10 @@ function resolvePillageOasis(V: VillageState, region: OasisType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unit = getUnitSource('soldier');
|
const unit = getUnitSource('soldier');
|
||||||
|
const maxResources = region.distance * region.distance * 100;
|
||||||
|
|
||||||
V.resources[region.resource] += mission.unitCount * unit.behavior.caryingCapacity;
|
V.resources[region.resource] += Math.min(
|
||||||
|
mission.unitCount * unit.behavior.caryingCapacity,
|
||||||
|
maxResources
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
28
src/moves/clear.ts
Normal file
28
src/moves/clear.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { WORLDMAP_TYPES } from "../types";
|
||||||
|
import { assert, getRemainingUnitCount } from "../utils";
|
||||||
|
import type { VillageState } from "../village";
|
||||||
|
|
||||||
|
|
||||||
|
export default function clear(
|
||||||
|
V: VillageState, regionIndex: number, soldiersCount: number
|
||||||
|
) {
|
||||||
|
if (soldiersCount > getRemainingUnitCount(V, 'soldier')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const region = V.worldmap[regionIndex];
|
||||||
|
assert(region.type === WORLDMAP_TYPES.OASIS);
|
||||||
|
|
||||||
|
if (region.state.mission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.state.mission = {
|
||||||
|
type: 'clear',
|
||||||
|
unitType: 'soldier',
|
||||||
|
unitCount: soldiersCount,
|
||||||
|
remainingTime: region.distance * 10 * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
@ -2,6 +2,7 @@ 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 clear from './clear';
|
||||||
import pillage from './pillage';
|
import pillage from './pillage';
|
||||||
import recruitUnits from './recruitUnits';
|
import recruitUnits from './recruitUnits';
|
||||||
import startQuest from './startQuest';
|
import startQuest from './startQuest';
|
||||||
@ -33,6 +34,7 @@ export function makeMove(move: (...args: any[]) => boolean) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
build: makeMove(build),
|
build: makeMove(build),
|
||||||
|
clear: makeMove(clear),
|
||||||
upgradeBuilding: makeMove(upgradeBuilding),
|
upgradeBuilding: makeMove(upgradeBuilding),
|
||||||
pillage: makeMove(pillage),
|
pillage: makeMove(pillage),
|
||||||
recruitUnits: makeMove(recruitUnits),
|
recruitUnits: makeMove(recruitUnits),
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { getRemainingUnitCount } from "../utils";
|
import { WORLDMAP_TYPES } from "../types";
|
||||||
|
import { assert, getRemainingUnitCount } from "../utils";
|
||||||
import type { VillageState } from "../village";
|
import type { VillageState } from "../village";
|
||||||
|
|
||||||
|
|
||||||
export default function pillage(V: VillageState, regionIndex: number, soldiersCount: number, repeat: boolean) {
|
export default function pillage(
|
||||||
|
V: VillageState, regionIndex: number, soldiersCount: number, repeat: boolean
|
||||||
|
) {
|
||||||
if (soldiersCount > getRemainingUnitCount(V, 'soldier')) {
|
if (soldiersCount > getRemainingUnitCount(V, 'soldier')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
4
src/stores/showOasisPanel.ts
Normal file
4
src/stores/showOasisPanel.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
|
||||||
|
export default writable<number | null>(null);
|
45
src/types.ts
45
src/types.ts
@ -1,6 +1,12 @@
|
|||||||
import type { Hex } from "./hexgrid";
|
import type { Hex } from "./hexgrid";
|
||||||
|
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type GameTab = 'village' | 'resources' | 'world';
|
export type GameTab = 'village' | 'resources' | 'world';
|
||||||
|
|
||||||
|
|
||||||
@ -74,35 +80,50 @@ export interface UnitType {
|
|||||||
|
|
||||||
|
|
||||||
export interface MissionType {
|
export interface MissionType {
|
||||||
type: string;
|
type: 'pillage' | 'clear';
|
||||||
unitType: string;
|
unitType: string;
|
||||||
unitCount: number;
|
unitCount: number;
|
||||||
remainingTime: number;
|
remainingTime: number;
|
||||||
repeat: boolean;
|
repeat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export enum WORLDMAP_TYPES {
|
||||||
|
EMPTY,
|
||||||
|
OASIS,
|
||||||
|
BOURGADE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface BaseRegionType {
|
interface BaseRegionType {
|
||||||
|
id: number;
|
||||||
|
type: WORLDMAP_TYPES;
|
||||||
distance: number;
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface EmptyRegionType extends BaseRegionType {
|
||||||
|
type: WORLDMAP_TYPES.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface OasisType extends BaseRegionType {
|
||||||
|
type: WORLDMAP_TYPES.OASIS;
|
||||||
|
resource: keyof CostType;
|
||||||
|
strength: number;
|
||||||
state: {
|
state: {
|
||||||
index: number;
|
cleared: boolean;
|
||||||
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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
50
src/utils.ts
50
src/utils.ts
@ -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 Point, 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,41 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function distanceBetweenCells(a: Point, b: Point) {
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(b.x - a.x, 2)
|
||||||
|
+ Math.pow(b.y - a.y, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function indexToPoint(index: number): Point {
|
||||||
|
return {
|
||||||
|
x: index % WORLD_MAP_WIDTH,
|
||||||
|
y: Math.floor(index / WORLD_MAP_WIDTH),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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 { distanceBetweenCells, getAdjacentWorldmapCells, getKeysAsNumbers, indexToPoint, shuffle } from "./utils";
|
||||||
|
import { WORLD_MAP_HEIGHT, WORLD_MAP_WIDTH } from "./constants";
|
||||||
|
|
||||||
|
|
||||||
type Board = {
|
type Board = {
|
||||||
@ -68,15 +69,39 @@ function getInitialOutsideBoard() {
|
|||||||
|
|
||||||
|
|
||||||
function getInitialWorldmap(): RegionType[] {
|
function getInitialWorldmap(): RegionType[] {
|
||||||
return worldmap.map((r, index) => {
|
const board: RegionType[] = [];
|
||||||
const region = r as RegionType;
|
const centerIndex = Math.floor((WORLD_MAP_WIDTH * WORLD_MAP_HEIGHT) / 2);
|
||||||
return {
|
const centerPoint = indexToPoint(centerIndex);
|
||||||
...region,
|
|
||||||
|
for (let i = 0; i < WORLD_MAP_WIDTH * WORLD_MAP_HEIGHT; i++) {
|
||||||
|
board[i] = {
|
||||||
|
id: i,
|
||||||
|
type: WORLDMAP_TYPES.EMPTY,
|
||||||
|
distance: distanceBetweenCells(indexToPoint(i), centerPoint),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
board[centerIndex].type = WORLDMAP_TYPES.BOURGADE;
|
||||||
|
|
||||||
|
worldmap.forEach(region => {
|
||||||
|
const candidates = board.filter(c =>
|
||||||
|
c.type === WORLDMAP_TYPES.EMPTY
|
||||||
|
&& c.distance >= region.distance[0]
|
||||||
|
&& c.distance < region.distance[1]
|
||||||
|
);
|
||||||
|
const cell = shuffle(candidates)[0];
|
||||||
|
board[cell.id] = {
|
||||||
|
type: WORLDMAP_TYPES.OASIS,
|
||||||
|
id: cell.id,
|
||||||
|
resource: region.resource as keyof CostType,
|
||||||
|
distance: cell.distance,
|
||||||
|
strength: Math.round(cell.distance * cell.distance * 50),
|
||||||
state: {
|
state: {
|
||||||
index,
|
cleared: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -117,7 +142,7 @@ function getInitialState() {
|
|||||||
state.buildings.push(townhall);
|
state.buildings.push(townhall);
|
||||||
|
|
||||||
// Create all the resource buildings.
|
// Create all the resource buildings.
|
||||||
const resourceBuildingTypes: Array<string> = shuffle([
|
const resourceBuildingTypes = shuffle([
|
||||||
'woodcutter', 'woodcutter', 'woodcutter', 'woodcutter',
|
'woodcutter', 'woodcutter', 'woodcutter', 'woodcutter',
|
||||||
'mine', 'mine', 'mine', 'mine',
|
'mine', 'mine', 'mine', 'mine',
|
||||||
'pit', 'pit', 'pit', 'pit',
|
'pit', 'pit', 'pit', 'pit',
|
||||||
|
Loading…
Reference in New Issue
Block a user