Skip to content

Commit

Permalink
feat: added ability to upgrade buildings from building upgrade indica…
Browse files Browse the repository at this point in the history
…tor, npc villages now get stronger further away from the start
  • Loading branch information
jurerotar committed Oct 10, 2024
1 parent a0596b3 commit 5d8da56
Show file tree
Hide file tree
Showing 19 changed files with 375 additions and 120 deletions.
23 changes: 22 additions & 1 deletion app/(game)/(map)/components/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
Tile as TileType,
} from 'app/interfaces/models/game/tile';
import type { Tribe } from 'app/interfaces/models/game/tribe';
import type { VillageSize } from 'app/interfaces/models/game/village';
import clsx from 'clsx';
import type React from 'react';
import { memo } from 'react';
Expand All @@ -40,6 +41,7 @@ type OccupiedTileWithFactionAndTribe = OccupiedOccupiableTileType & {
faction: PlayerFaction;
reputationLevel: ReputationLevel;
tribe: Tribe;
population: number;
};

type TroopMovementsProps = {
Expand Down Expand Up @@ -102,6 +104,22 @@ const CellIcons: React.FC<CellIconsProps> = ({ tile, mapFilters }) => {
);
};

const populationToVillageSizeMap = new Map<number, VillageSize>([
[500, 'xl'],
[250, 'md'],
[100, 'sm'],
]);

const getVillageSize = (population: number): VillageSize => {
for (const [key, size] of populationToVillageSizeMap) {
if (population >= key) {
return size;
}
}

return 'xs';
};

type CellProps = GridChildComponentProps<CellBaseProps>;

const dynamicCellClasses = (tile: TileType | OccupiedTileWithFactionAndTribe): string => {
Expand All @@ -114,7 +132,10 @@ const dynamicCellClasses = (tile: TileType | OccupiedTileWithFactionAndTribe): s
}

if (isOccupiedOccupiableCell) {
const { tribe, villageSize } = tile as OccupiedTileWithFactionAndTribe;
const { tribe, population } = tile as OccupiedTileWithFactionAndTribe;

const villageSize = getVillageSize(population);

return clsx(cellStyles['occupied-tile'], cellStyles[`occupied-tile-${tribe}`], cellStyles[`occupied-tile-${tribe}-${villageSize}`]);
}

Expand Down
18 changes: 17 additions & 1 deletion app/(game)/(map)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { MapProvider, useMapOptions } from 'app/(game)/(map)/providers/map-conte
import { useMap } from 'app/(game)/hooks/use-map';
import { usePlayers } from 'app/(game)/hooks/use-players';
import { useReputations } from 'app/(game)/hooks/use-reputations';
import { useVillages } from 'app/(game)/hooks/use-villages';
import { calculatePopulationFromBuildingFields } from 'app/(game)/utils/building';
import { isOccupiedOccupiableTile } from 'app/(game)/utils/guards/map-guards';
import { Tooltip } from 'app/components/tooltip';
import { useDialog } from 'app/hooks/use-dialog';
import type { Point } from 'app/interfaces/models/common';
import type { OccupiedOccupiableTile, Tile as TileType } from 'app/interfaces/models/game/tile';
import type { Village } from 'app/interfaces/models/game/village';
import { useViewport } from 'app/providers/viewport-context';
import type React from 'react';
import { useLayoutEffect } from 'react';
Expand All @@ -33,6 +36,8 @@ const MapPage: React.FC = () => {
const { getPlayerByPlayerId } = usePlayers();
const { getReputationByFaction } = useReputations();
const [searchParams] = useSearchParams();
const { villages } = useVillages();

const startingX = Number.parseInt(searchParams.get('x') ?? '0');
const startingY = Number.parseInt(searchParams.get('y') ?? '0');

Expand All @@ -54,24 +59,35 @@ const MapPage: React.FC = () => {
// Fun fact, using any kind of hooks in rendered tiles absolutely hammers performance.
// We need to get all tile information in here and pass it down as props
const tilesWithFactions = useMemo(() => {
const villageCoordinatesToVillagesMap = new Map<string, Village>(
villages.map((village) => {
return [`${village.coordinates.x}-${village.coordinates.y}`, village];
}),
);

return map.map((tile: TileType) => {
const isOccupiedOccupiableCell = isOccupiedOccupiableTile(tile);

if (isOccupiedOccupiableCell) {
const { faction, tribe } = getPlayerByPlayerId((tile as OccupiedOccupiableTile).ownedBy);
const reputationLevel = getReputationByFaction(faction)?.reputationLevel;
const { x, y } = tile.coordinates;
const { buildingFields, buildingFieldsPresets } = villageCoordinatesToVillagesMap.get(`${x}-${y}`)!;

const population = calculatePopulationFromBuildingFields(buildingFields!, buildingFieldsPresets);

return {
...tile,
faction,
reputationLevel,
tribe,
population,
};
}

return tile;
});
}, [map, getReputationByFaction, getPlayerByPlayerId]);
}, [map, getReputationByFaction, getPlayerByPlayerId, villages]);

const fixedGridData = useMemo(() => {
return {
Expand Down
24 changes: 17 additions & 7 deletions app/(game)/components/building-upgrade-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { useDeveloperMode } from 'app/(game)/hooks/use-developer-mode';
import { useEvents } from 'app/(game)/hooks/use-events';
import { useCurrentResources } from 'app/(game)/providers/current-resources-provider';
import { calculatePopulationFromBuildingFields, getBuildingDataForLevel } from 'app/(game)/utils/building';
import useLongPress from 'app/hooks/events/use-long-press';
import type { BuildingField } from 'app/interfaces/models/game/village';
import { useViewport } from 'app/providers/viewport-context';
import clsx from 'clsx';
import type React from 'react';
import { useState } from 'react';
Expand All @@ -30,12 +32,23 @@ export const BuildingUpgradeIndicator: React.FC<BuildingUpgradeIndicatorProps> =
const { wood, clay, iron, wheat } = useCurrentResources();
const { canAddAdditionalBuildingToQueue, currentVillageBuildingEvents } = useEvents();
const { isDeveloperModeActive } = useDeveloperMode();
const { isWiderThanMd } = useViewport();

const population = calculatePopulationFromBuildingFields(buildingFields, buildingFieldsPresets);
const { buildingId, level } = buildingFields.find(({ id }) => buildingFieldId === id)!;
const { isMaxLevel, nextLevelResourceCost, nextLevelCropConsumption } = getBuildingDataForLevel(buildingId, level);
const { upgradeBuilding } = useBuildingActions(buildingId, buildingFieldId);

const onUpgradeButtonClick = (event: React.MouseEvent | React.TouchEvent) => {
upgradeBuilding();
event.stopPropagation();
event.preventDefault();
};

const longPressEvent = useLongPress((event) => {
onUpgradeButtonClick(event);
});

const [shouldShowUpgradeButton, setShouldShowUpgradeButton] = useState<boolean>(false);

const variant = ((): BorderIndicatorBorderVariant => {
Expand Down Expand Up @@ -89,19 +102,16 @@ export const BuildingUpgradeIndicator: React.FC<BuildingUpgradeIndicatorProps> =
return 'white';
})();

const onUpgradeButtonClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
upgradeBuilding();
event.stopPropagation();
event.preventDefault();
};

// TODO: Transitions needs to added here, the icon currently just pops in

return (
<button
className={clsx(canUpgrade && 'hover:scale-125', 'rounded-full cursor-pointer transition-transform duration-300')}
type="button"
onClick={onUpgradeButtonClick}
{...(isWiderThanMd && {
onClick: onUpgradeButtonClick,
})}
{...(!isWiderThanMd && { ...longPressEvent })}
disabled={!canUpgrade}
>
<BorderIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '@vitest/web-worker';
import { QueryClient, hydrate } from '@tanstack/react-query';
import type { PersistedClient } from '@tanstack/react-query-persist-client';
import { achievementsCacheKey } from 'app/(game)/hooks/use-achievements';
import { currentServerCacheKey } from 'app/(game)/hooks/use-current-server';
import { effectsCacheKey } from 'app/(game)/hooks/use-effects';
import { eventsCacheKey } from 'app/(game)/hooks/use-events';
import { mapCacheKey } from 'app/(game)/hooks/use-map';
Expand All @@ -17,12 +18,14 @@ import { reputationsCacheKey } from 'app/(game)/hooks/use-reputations';
import { troopsCacheKey } from 'app/(game)/hooks/use-troops';
import { unitResearchCacheKey } from 'app/(game)/hooks/use-unit-research';
import { villagesCacheKey } from 'app/(game)/hooks/use-villages';
import { getVillageSize } from 'app/factories/utils/common';
import type { GameEvent } from 'app/interfaces/models/events/game-event';
import type { Achievement } from 'app/interfaces/models/game/achievement';
import type { Effect } from 'app/interfaces/models/game/effect';
import type { Player } from 'app/interfaces/models/game/player';
import type { Quest } from 'app/interfaces/models/game/quest';
import type { Reputation } from 'app/interfaces/models/game/reputation';
import type { Server } from 'app/interfaces/models/game/server';
import type { Troop } from 'app/interfaces/models/game/troop';
import type { UnitResearch } from 'app/interfaces/models/game/unit-research';
import type { Village } from 'app/interfaces/models/game/village';
Expand Down Expand Up @@ -135,48 +138,60 @@ describe('Server initialization', () => {
});

test('No oasis should be occupied by villages of size "xs"', () => {
const server = queryClient.getQueryData<Server>([currentServerCacheKey])!;
const tiles = queryClient.getQueryData<Tile[]>([mapCacheKey])!;
const occupiedOasisTiles = tiles.filter(isOccupiedOasisTile);
const occupiedOccupiableTiles = tiles.filter(isOccupiedOccupiableTile);

const extraSmallVillageTileIds = occupiedOccupiableTiles.filter(({ villageSize }) => villageSize === 'xs').map(({ id }) => id);
const extraSmallVillageTileIds = occupiedOccupiableTiles
.filter(({ coordinates }) => getVillageSize(server.configuration.mapSize, coordinates) === 'sm')
.map(({ id }) => id);
const occupiedOasisVillageIds = occupiedOasisTiles.map(({ villageId }) => villageId);

const listOfOccurrences = extraSmallVillageTileIds.map((id) => occupiedOasisVillageIds.filter((villageId) => villageId === id));
expect(listOfOccurrences.every((occurrence) => occurrence.length === 0)).toBe(true);
});

// We're counting how many times occupying tile id appears in list of occupied oasis ids
test('No more than 1 oasis per village should be occupied by villages of size "sm"', () => {
test('No oasis should be occupied by villages of size "sm"', () => {
const server = queryClient.getQueryData<Server>([currentServerCacheKey])!;
const tiles = queryClient.getQueryData<Tile[]>([mapCacheKey])!;
const occupiedOasisTiles = tiles.filter(isOccupiedOasisTile);
const occupiedOccupiableTiles = tiles.filter(isOccupiedOccupiableTile);

const smallVillageTileIds = occupiedOccupiableTiles.filter(({ villageSize }) => villageSize === 'sm').map(({ id }) => id);
const smallVillageTileIds = occupiedOccupiableTiles
.filter(({ coordinates }) => getVillageSize(server.configuration.mapSize, coordinates) === 'sm')
.map(({ id }) => id);
const occupiedOasisVillageIds = occupiedOasisTiles.map(({ villageId }) => villageId);

const listOfOccurrences = smallVillageTileIds.map((id) => occupiedOasisVillageIds.filter((villageId) => villageId === id));
expect(listOfOccurrences.every((occurrence) => occurrence.length <= 1)).toBe(true);
});

test('No more than 2 oasis per village should be occupied by villages of size "md"', () => {
const server = queryClient.getQueryData<Server>([currentServerCacheKey])!;
const tiles = queryClient.getQueryData<Tile[]>([mapCacheKey])!;
const occupiedOasisTiles = tiles.filter(isOccupiedOasisTile);
const occupiedOccupiableTiles = tiles.filter(isOccupiedOccupiableTile);

const mediumVillageTileIds = occupiedOccupiableTiles.filter(({ villageSize }) => villageSize === 'md').map(({ id }) => id);
const mediumVillageTileIds = occupiedOccupiableTiles
.filter(({ coordinates }) => getVillageSize(server.configuration.mapSize, coordinates) === 'md')
.map(({ id }) => id);
const occupiedOasisVillageIds = occupiedOasisTiles.map(({ villageId }) => villageId);

const listOfOccurrences = mediumVillageTileIds.map((id) => occupiedOasisVillageIds.filter((villageId) => villageId === id));
expect(listOfOccurrences.every((occurrence) => occurrence.length <= 2)).toBe(true);
});

test('No more than 3 oasis per village should be occupied by villages of size "lg"', () => {
const server = queryClient.getQueryData<Server>([currentServerCacheKey])!;
const tiles = queryClient.getQueryData<Tile[]>([mapCacheKey])!;
const occupiedOasisTiles = tiles.filter(isOccupiedOasisTile);
const occupiedOccupiableTiles = tiles.filter(isOccupiedOccupiableTile);

const largeVillageTileIds = occupiedOccupiableTiles.filter(({ villageSize }) => villageSize === 'md').map(({ id }) => id);
const largeVillageTileIds = occupiedOccupiableTiles
.filter(({ coordinates }) => getVillageSize(server.configuration.mapSize, coordinates) === 'md')
.map(({ id }) => id);
const occupiedOasisVillageIds = occupiedOasisTiles.map(({ villageId }) => villageId);

const listOfOccurrences = largeVillageTileIds.map((id) => occupiedOasisVillageIds.filter((villageId) => villageId === id));
Expand Down
1 change: 1 addition & 0 deletions app/(public)/components/create-server-modal-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const initializeServer = async ({ server }: OnSubmitArgs) => {
// Non-dependant factories can run in sync
const [{ villages }, { troops }, effects, hero, mapFilters, unitResearch, unitImprovement] = await Promise.all([
workerFactory<GenerateVillageWorkerPayload, GenerateVillageWorkerReturn>(GenerateVillagesWorker, {
server,
occupiedOccupiableTiles,
players,
}),
Expand Down
2 changes: 2 additions & 0 deletions app/(public)/workers/generate-villages-worker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { generateVillages } from 'app/factories/village-factory';
import type { Player } from 'app/interfaces/models/game/player';
import type { Server } from 'app/interfaces/models/game/server';
import type { OccupiedOccupiableTile } from 'app/interfaces/models/game/tile';
import type { Village } from 'app/interfaces/models/game/village';

export type GenerateVillageWorkerPayload = {
server: Server;
occupiedOccupiableTiles: OccupiedOccupiableTile[];
players: Player[];
};
Expand Down
21 changes: 21 additions & 0 deletions app/assets/npc-village-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,44 @@ import {
lgVillageResourceFieldsPreset,
mdVillageResourceFieldsPreset,
smVillageResourceFieldsPreset,
xlVillageResourceFieldsPreset,
xsVillageResourceFieldsPreset,
xxlVillageResourceFieldsPreset,
xxsVillageResourceFieldsPreset,
xxxlVillageResourceFieldsPreset,
xxxxlVillageResourceFieldsPreset,
} from 'app/factories/presets/resource-building-fields-presets';
import {
lgVillageBuildingFieldsPreset,
mdVillageBuildingFieldsPreset,
smVillageBuildingFieldsPreset,
xlVillageBuildingFieldsPreset,
xsVillageBuildingFieldsPreset,
xxlVillageBuildingFieldsPreset,
xxsVillageBuildingFieldsPreset,
xxxlVillageBuildingFieldsPreset,
xxxxlVillageBuildingFieldsPreset,
} from 'app/factories/presets/village-building-fields-presets';
import type { BuildingField, VillagePresetId } from 'app/interfaces/models/game/village';

export const presetIdToPresetMap = new Map<VillagePresetId, BuildingField[]>([
['resources-xxs', xxsVillageResourceFieldsPreset],
['resources-xs', xsVillageResourceFieldsPreset],
['resources-sm', smVillageResourceFieldsPreset],
['resources-md', mdVillageResourceFieldsPreset],
['resources-lg', lgVillageResourceFieldsPreset],
['resources-xl', xlVillageResourceFieldsPreset],
['resources-2xl', xxlVillageResourceFieldsPreset],
['resources-3xl', xxxlVillageResourceFieldsPreset],
['resources-4xl', xxxxlVillageResourceFieldsPreset],
['village-xxs', xxsVillageBuildingFieldsPreset],
['village-xxs', xsVillageBuildingFieldsPreset],
['village-xs', xsVillageBuildingFieldsPreset],
['village-sm', smVillageBuildingFieldsPreset],
['village-md', mdVillageBuildingFieldsPreset],
['village-lg', lgVillageBuildingFieldsPreset],
['village-xl', xlVillageBuildingFieldsPreset],
['village-2xl', xxlVillageBuildingFieldsPreset],
['village-3xl', xxxlVillageBuildingFieldsPreset],
['village-4xl', xxxxlVillageBuildingFieldsPreset],
]);
Loading

0 comments on commit 5d8da56

Please sign in to comment.