diff --git a/apps/website/src/lib/components/CurrencyDisplay.svelte b/apps/website/src/lib/components/CurrencyDisplay.svelte new file mode 100644 index 0000000..b42b2ef --- /dev/null +++ b/apps/website/src/lib/components/CurrencyDisplay.svelte @@ -0,0 +1,19 @@ + + +
+ {currency.name} + {amount} +
diff --git a/apps/website/src/lib/components/farming/FarmDisplay.svelte b/apps/website/src/lib/components/farming/FarmDisplay.svelte new file mode 100644 index 0000000..d5c9715 --- /dev/null +++ b/apps/website/src/lib/components/farming/FarmDisplay.svelte @@ -0,0 +1,114 @@ + + +
+
+
+
+
+ + +
+ + + + {#each plants as plant (plant.id)} + selectSeed(plant.id)} + class="cursor-pointer {selectedSeed === plant.id ? 'bg-base-200' : ''}" + > + + + + + {/each} + +
+
+ {plant.name} + {plant.name} +
+
{currency.getBalance(plant.id)} + +
+
+
+
+ + +
+ +
+ {#each farming.plots as plot, i (i)} + + {/each} +
+
+
+
+
diff --git a/apps/website/src/lib/components/farming/FarmPlot.svelte b/apps/website/src/lib/components/farming/FarmPlot.svelte new file mode 100644 index 0000000..46bf2db --- /dev/null +++ b/apps/website/src/lib/components/farming/FarmPlot.svelte @@ -0,0 +1,32 @@ + + +
+ {#if plant} +
+ {plant.name} + + {#if !farming.isReady(index)} + + {/if} +
+ {/if} +
+ + diff --git a/apps/website/src/lib/demo/content.ts b/apps/website/src/lib/demo/content.ts index 8e56ed6..0e2af29 100644 --- a/apps/website/src/lib/demo/content.ts +++ b/apps/website/src/lib/demo/content.ts @@ -7,15 +7,154 @@ import type { PlantDetail } from '$lib/demo/model/PlantDetail'; // First we declare our fully static content export const plants = [ - { id: '/plant/sunflower', name: 'Sunflower', growthTime: 1, moneyReward: 10 }, - { id: '/plant/cauliflower', name: 'Cauliflower', growthTime: 1.5, moneyReward: 20 }, + { + id: '/plant/cabbage', + name: 'Cabbage', + growthTime: 10, + moneyReward: 10, + seedCost: 6, + stages: [ + '/plants/cabbage/cabbage_00.png', + '/plants/cabbage/cabbage_01.png', + '/plants/cabbage/cabbage_02.png', + '/plants/cabbage/cabbage_03.png', + '/plants/cabbage/cabbage_04.png', + '/plants/cabbage/cabbage_05.png', + ], + }, + { + id: '/plant/potato', + name: 'Potato', + growthTime: 20, + moneyReward: 20, + seedCost: 12, + stages: [ + '/plants/potato/potato_00.png', + '/plants/potato/potato_01.png', + '/plants/potato/potato_02.png', + '/plants/potato/potato_03.png', + '/plants/potato/potato_04.png', + '/plants/potato/potato_05.png', + ], + }, + { + id: '/plant/carrot', + name: 'Carrot', + growthTime: 30, + moneyReward: 40, + seedCost: 20, + stages: [ + '/plants/carrot/carrot_00.png', + '/plants/carrot/carrot_01.png', + '/plants/carrot/carrot_02.png', + '/plants/carrot/carrot_03.png', + '/plants/carrot/carrot_04.png', + '/plants/carrot/carrot_05.png', + ], + }, + { + id: '/plant/beetroot', + name: 'Beetroot', + growthTime: 40, + moneyReward: 50, + seedCost: 40, + stages: [ + '/plants/beetroot/beetroot_00.png', + '/plants/beetroot/beetroot_01.png', + '/plants/beetroot/beetroot_02.png', + '/plants/beetroot/beetroot_03.png', + '/plants/beetroot/beetroot_04.png', + '/plants/beetroot/beetroot_05.png', + ], + }, + { + id: '/plant/radish', + name: 'Radish', + growthTime: 50, + moneyReward: 20, + seedCost: 75, + stages: [ + '/plants/radish/radish_00.png', + '/plants/radish/radish_01.png', + '/plants/radish/radish_02.png', + '/plants/radish/radish_03.png', + '/plants/radish/radish_04.png', + '/plants/radish/radish_05.png', + ], + }, + { + id: '/plant/cauliflower', + name: 'Cauliflower', + growthTime: 30, + moneyReward: 20, + seedCost: 120, + stages: [ + '/plants/cauliflower/cauliflower_00.png', + '/plants/cauliflower/cauliflower_01.png', + '/plants/cauliflower/cauliflower_02.png', + '/plants/cauliflower/cauliflower_03.png', + '/plants/cauliflower/cauliflower_04.png', + '/plants/cauliflower/cauliflower_05.png', + ], + }, + { + id: '/plant/pumpkin', + name: 'Pumpkin', + growthTime: 30, + seedCost: 200, + moneyReward: 20, + stages: [ + '/plants/pumpkin/pumpkin_00.png', + '/plants/pumpkin/pumpkin_01.png', + '/plants/pumpkin/pumpkin_02.png', + '/plants/pumpkin/pumpkin_03.png', + '/plants/pumpkin/pumpkin_04.png', + '/plants/pumpkin/pumpkin_05.png', + ], + }, + { + id: '/plant/sunflower', + name: 'Sunflower', + growthTime: 10, + moneyReward: 10, + seedCost: 1000, + stages: [ + '/plants/sunflower/sunflower_00.png', + '/plants/sunflower/sunflower_01.png', + '/plants/sunflower/sunflower_02.png', + '/plants/sunflower/sunflower_03.png', + '/plants/sunflower/sunflower_04.png', + '/plants/sunflower/sunflower_05.png', + ], + }, ] as const satisfies PlantDetail[]; +export type PlantId = (typeof plants)[number]['id']; + +export const getPlant = (id: PlantId): PlantDetail => { + // TODO(@Isha): Make into dictionary lookup + const plant = plants.find((p) => p.id === id); + if (!plant) { + throw new Error(`Plant with id '${id}' not found.`); + } + return plant; +}; export const currencies: CurrencyDetail[] = [ - { id: '/currency/money', name: 'Money', icon: '/icon/coin' }, - { id: '/currency/gems', name: 'Gems', icon: '/icon/gem-blue' }, + { id: '/currency/money', name: 'Money', icon: '/icons/egg.png' }, + { id: '/currency/compost', name: 'Compost', icon: '/icon/compost' }, ]; +export type CurrencyId = (typeof currencies)[number]['id']; + +export const getCurrency = (id: CurrencyId): CurrencyDetail => { + // TODO(@Isha): Make into dictionary lookup + const currency = currencies.find((c) => c.id === id); + if (!currency) { + throw new Error(`Currency with id '${id}' not found.`); + } + return currency; +}; + export const statistics: StatisticDetail[] = [ { id: '/statistic/total-money', type: 'scalar' }, { id: '/statistic/plants-planted', type: 'map' }, diff --git a/apps/website/src/lib/demo/demo.svelte.ts b/apps/website/src/lib/demo/demo.svelte.ts index 91d60e8..30d5cf3 100644 --- a/apps/website/src/lib/demo/demo.svelte.ts +++ b/apps/website/src/lib/demo/demo.svelte.ts @@ -1,20 +1,20 @@ import { AchievementPlugin, AlwaysTrueCondition, - type ExtractOutput, CouponPlugin, createAchievementState, createCouponState, createCurrencyState, createStatisticState, CurrencyPlugin, + type ExtractCondition, type ExtractInput, + type ExtractOutput, LudiekEngine, LudiekGame, - type ExtractCondition, StatisticPlugin, } from '@123ishatest/ludiek'; -import { Farming } from '$lib/demo/features/Farming'; +import { Farming } from '$lib/demo/features/Farming.svelte'; import { achievements, currencies, plants, statistics } from '$lib/demo/content'; // Define plugins with reactive state @@ -43,17 +43,19 @@ export type Condition = ExtractCondition; export type PlantId = (typeof plants)[number]['id']; // Create your game -const farming = new Farming(plants); +const farmingFeature = new Farming(plants); export const game = new LudiekGame(engine, { - features: [farming], + features: [farmingFeature], saveKey: '@123ishatest/ludiek-demo', tickDuration: 0.1, saveInterval: 30, }); engine.plugins.currency.loadContent(currencies); +engine.plugins.currency.loadContent(plants); engine.plugins.statistic.loadContent(statistics); engine.plugins.achievement.loadContent(achievements); export const { currency, statistic, achievement } = engine.plugins; +export const { farming } = game.features; diff --git a/apps/website/src/lib/demo/features/Farming.svelte.ts b/apps/website/src/lib/demo/features/Farming.svelte.ts new file mode 100644 index 0000000..ceab068 --- /dev/null +++ b/apps/website/src/lib/demo/features/Farming.svelte.ts @@ -0,0 +1,128 @@ +import { LudiekFeature } from '@123ishatest/ludiek'; +import type { EnginePlugins } from '$lib/demo/demo.svelte'; +import type { PlantDetail } from '$lib/demo/model/PlantDetail'; +import { emptyPlot, type FarmingState, type FarmPlotState } from '$lib/demo/features/FarmingState'; +import { getPlant, type PlantId } from '$lib/demo/content'; +import { SowSeedController } from '$lib/demo/features/SowSeedController'; +import { SowAllController } from '$lib/demo/features/SowAllController'; +import { ReapAllController } from '$lib/demo/features/ReapAllController'; + +export class Farming extends LudiekFeature { + public readonly name: string = 'farming'; + public readonly config = { + controllers: [new SowSeedController(this), new SowAllController(this), new ReapAllController(this)], + }; + + public readonly FARM_PLOTS = 25; + + protected _state: FarmingState = $state({ + plots: [], + }); + + public readonly plants: PlantDetail[]; + + constructor(plants: PlantDetail[]) { + super(); + this.plants = plants; + + for (let i = 0; i < this.FARM_PLOTS; i++) { + this._state.plots.push(emptyPlot()); + } + } + + update(delta: number) { + this.growAllPlants(delta); + } + + private growAllPlants(amount: number): void { + this._state.plots.forEach((plot) => { + if (!plot.plant) { + return; + } + plot.progress += amount; + }); + } + + public sow(index: number, id: PlantId): void { + if (!this.isEmpty(index)) { + return; + } + const plant = getPlant(id); + if ( + !this._plugins.currency.payCurrency({ + id: plant.id, + amount: 1, + }) + ) { + return; + } + + this._plugins.statistic.incrementMapStatistic('/statistic/plants-planted', id); + this._state.plots[index] = { + plant: id, + progress: 0, + }; + } + + public reap(index: number): void { + if (!this.isReady(index)) { + return; + } + const plot = this.getPlot(index); + const plant = getPlant(plot.plant as PlantId); + + this._plugins.currency.gainCurrency({ id: '/currency/money', amount: plant.moneyReward }); + this.clear(index); + } + + public getGrowthStage(index: number): string { + if (this.isEmpty(index)) { + return ''; + } + const progress = this.getProgress(index); + const plot = this.getPlot(index); + const plant = getPlant(plot.plant as PlantId); + + const stage = Math.floor(progress * (plant.stages.length - 1)); + return plant.stages[stage]; + } + + public getProgress(index: number): number { + if (this.isEmpty(index)) { + return 0; + } + const plot = this.getPlot(index); + const plant = getPlant(plot.plant as PlantId); + return Math.min(1, plot.progress / plant.growthTime); + } + + public clear(index: number): void { + this._state.plots[index] = emptyPlot(); + } + + public getPlant(id: PlantId): PlantDetail { + return this.plants.find((plant) => plant.id === id) as PlantDetail; + } + + public isEmpty(index: number): boolean { + return this.getPlot(index).plant == null; + } + + public isReady(index: number): boolean { + const plot = this.getPlot(index); + if (!plot.plant) { + return false; + } + + const plant = getPlant(plot.plant); + return plot.progress >= plant.growthTime; + } + + public getPlot(index: number): FarmPlotState { + return this._state.plots[index]; + } + + public get plots(): FarmPlotState[] { + return this._state.plots; + } +} diff --git a/apps/website/src/lib/demo/features/Farming.ts b/apps/website/src/lib/demo/features/Farming.ts deleted file mode 100644 index 4a4e302..0000000 --- a/apps/website/src/lib/demo/features/Farming.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LudiekFeature } from '@123ishatest/ludiek'; -import type { EnginePlugins, PlantId } from '$lib/demo/demo.svelte'; - -import type { PlantDetail } from '$lib/demo/model/PlantDetail'; -import { SowSeedController } from '$lib/demo/features/SowPlantController'; - -export class Farming extends LudiekFeature { - readonly name = 'farming'; - protected _state = {}; - - public readonly config = { - controllers: [new SowSeedController(this)], - }; - - public readonly plants: PlantDetail[]; - - constructor(plants: PlantDetail[]) { - super(); - this.plants = plants; - } - - public sow(id: PlantId): void { - const plant = this.getPlant(id); - this._plugins.currency.gainCurrency({ id: '/currency/money', amount: plant.moneyReward }); - this._plugins.statistic.incrementMapStatistic('/statistic/plants-planted', plant.id); - } - - public getPlant(id: PlantId): PlantDetail { - return this.plants.find((plant) => plant.id === id) as PlantDetail; - } -} diff --git a/apps/website/src/lib/demo/features/FarmingState.ts b/apps/website/src/lib/demo/features/FarmingState.ts new file mode 100644 index 0000000..f7d3ce6 --- /dev/null +++ b/apps/website/src/lib/demo/features/FarmingState.ts @@ -0,0 +1,17 @@ +import type { PlantId } from '$lib/demo/content'; + +export interface FarmPlotState { + plant: PlantId | null; + progress: number; +} + +export const emptyPlot = (): FarmPlotState => { + return { + plant: null, + progress: 0, + }; +}; + +export interface FarmingState { + plots: FarmPlotState[]; +} diff --git a/apps/website/src/lib/demo/features/ReapAllController.ts b/apps/website/src/lib/demo/features/ReapAllController.ts new file mode 100644 index 0000000..1755327 --- /dev/null +++ b/apps/website/src/lib/demo/features/ReapAllController.ts @@ -0,0 +1,23 @@ +import type { BaseRequestShape, LudiekController } from '@123ishatest/ludiek'; +import type { Farming } from '$lib/demo/features/Farming.svelte'; + +export interface ReapAllRequest extends BaseRequestShape { + type: '/farming/reap-all'; +} + +export class ReapAllController implements LudiekController { + readonly type: string = '/farming/reap-all'; + + private readonly _farming: Farming; + + constructor(farming: Farming) { + this._farming = farming; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolve(request: ReapAllRequest): void { + for (let i = 0; i < this._farming.FARM_PLOTS; i++) { + this._farming.reap(i); + } + } +} diff --git a/apps/website/src/lib/demo/features/SowAllController.ts b/apps/website/src/lib/demo/features/SowAllController.ts new file mode 100644 index 0000000..2fe0c91 --- /dev/null +++ b/apps/website/src/lib/demo/features/SowAllController.ts @@ -0,0 +1,24 @@ +import type { BaseRequestShape, LudiekController } from '@123ishatest/ludiek'; +import type { PlantId } from '$lib/demo/demo.svelte'; +import type { Farming } from '$lib/demo/features/Farming.svelte'; + +export interface SowAllRequest extends BaseRequestShape { + type: '/farming/sow-all'; + plant: PlantId; +} + +export class SowAllController implements LudiekController { + readonly type: string = '/farming/sow-all'; + + private readonly _farming: Farming; + + constructor(farming: Farming) { + this._farming = farming; + } + + resolve(request: SowAllRequest): void { + for (let i = 0; i < this._farming.FARM_PLOTS; i++) { + this._farming.sow(i, request.plant); + } + } +} diff --git a/apps/website/src/lib/demo/features/SowPlantController.ts b/apps/website/src/lib/demo/features/SowSeedController.ts similarity index 79% rename from apps/website/src/lib/demo/features/SowPlantController.ts rename to apps/website/src/lib/demo/features/SowSeedController.ts index 5f8e626..2569576 100644 --- a/apps/website/src/lib/demo/features/SowPlantController.ts +++ b/apps/website/src/lib/demo/features/SowSeedController.ts @@ -1,9 +1,10 @@ import type { BaseRequestShape, LudiekController } from '@123ishatest/ludiek'; -import type { Farming } from '$lib/demo/features/Farming'; import type { PlantId } from '$lib/demo/demo.svelte'; +import type { Farming } from '$lib/demo/features/Farming.svelte'; export interface SowSeedRequest extends BaseRequestShape { type: '/farming/sow-seed'; + plot: number; plant: PlantId; } @@ -17,6 +18,6 @@ export class SowSeedController implements LudiekController { } resolve(request: SowSeedRequest): void { - this._farming.sow(request.plant); + this._farming.sow(request.plot, request.plant); } } diff --git a/apps/website/src/lib/demo/model/PlantDetail.ts b/apps/website/src/lib/demo/model/PlantDetail.ts index 84f7d5c..c1e3e83 100644 --- a/apps/website/src/lib/demo/model/PlantDetail.ts +++ b/apps/website/src/lib/demo/model/PlantDetail.ts @@ -2,5 +2,7 @@ export interface PlantDetail { id: string; name: string; growthTime: number; + seedCost: number; moneyReward: number; + stages: string[]; } diff --git a/apps/website/src/routes/+layout.svelte b/apps/website/src/routes/+layout.svelte index 22b272c..97cffb2 100644 --- a/apps/website/src/routes/+layout.svelte +++ b/apps/website/src/routes/+layout.svelte @@ -7,7 +7,7 @@
-
+
{@render children()}
diff --git a/apps/website/src/routes/demo/+page.svelte b/apps/website/src/routes/demo/+page.svelte index 6406619..c40fef1 100644 --- a/apps/website/src/routes/demo/+page.svelte +++ b/apps/website/src/routes/demo/+page.svelte @@ -1,9 +1,10 @@