Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"builds": [
{ "src": "web/package.json", "use": "@vercel/static-build", "config": { "distDir": "web" } }
],
"routes": [
{ "src": "/(.*)", "dest": "/web/$1" }
]
}
2 changes: 2 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
20 changes: 20 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Village Generator</title>
<script type="importmap">
{
"imports": {
"react": "https://cdn.skypack.dev/react",
"react-dom": "https://cdn.skypack.dev/react-dom",
"simple-wfc": "https://cdn.skypack.dev/simple-wfc"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./dist/index.js"></script>
</body>
</html>
114 changes: 114 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "village-generator",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "npm run build -- --watch",
"build": "tsc",
"test": "echo 'No tests specified'"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test script is currently a placeholder that echoes a message. The pull request description mentions running tests with npm --prefix web test. It would be good to either implement some basic tests or update the description to reflect the current state.

},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"typescript": "^5.4.0"
}
}
99 changes: 99 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { VillagePane } from './components/VillagePane';
import {
generateWfcGrid,
transformGridToLayout,
VillageLayout,
VillageOptions,
} from './services/villageGenerationService';

const defaultOptions: VillageOptions = {
type: 'farming',
size: 'small',
includeFarmland: true,
includeMarket: true,
includeWalls: true,
includeWells: true,
};

export const App: React.FC = () => {
const [options, setOptions] = useState<VillageOptions>(defaultOptions);
const [layout, setLayout] = useState<VillageLayout>();

const handleCheckbox = (key: keyof VillageOptions) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
setOptions((prev) => ({ ...prev, [key]: e.target.checked }));
};

const handleSelect = (
key: 'type' | 'size'
) => (e: React.ChangeEvent<HTMLSelectElement>) => {
setOptions((prev) => ({ ...prev, [key]: e.target.value as any }));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using as any bypasses TypeScript's type checking, which can hide bugs. It's better to use a more specific type assertion to maintain type safety.

    setOptions((prev) => ({ ...prev, [key]: e.target.value as VillageOptions['type'] | VillageOptions['size'] }));

};

const handleGenerate = async () => {
const seed = Date.now().toString();
const grid = await generateWfcGrid(seed, options);
const l = transformGridToLayout(grid, options);
setLayout(l);
};

return (
<div>
<div style={{ marginBottom: '1rem' }}>
<label>
Type:
<select value={options.type} onChange={handleSelect('type')}>
<option value="farming">farming</option>
<option value="fishing">fishing</option>
<option value="fortified">fortified</option>
</select>
</label>
<label style={{ marginLeft: '1rem' }}>
Size:
<select value={options.size} onChange={handleSelect('size')}>
<option value="small">small</option>
<option value="medium">medium</option>
</select>
</label>
<label style={{ marginLeft: '1rem' }}>
<input
type="checkbox"
checked={options.includeFarmland !== false}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The expression options.includeFarmland !== false is a bit verbose. Since options.includeFarmland will hold a boolean value after initialization, you can simplify this to just checked={options.includeFarmland}. This simplification can be applied to the other similar checkboxes as well.

            checked={!!options.includeFarmland}

onChange={handleCheckbox('includeFarmland')}
/>
Farmland
</label>
<label style={{ marginLeft: '1rem' }}>
<input
type="checkbox"
checked={options.includeMarket !== false}
onChange={handleCheckbox('includeMarket')}
/>
Market
</label>
<label style={{ marginLeft: '1rem' }}>
<input
type="checkbox"
checked={options.includeWalls !== false}
onChange={handleCheckbox('includeWalls')}
/>
Walls
</label>
<label style={{ marginLeft: '1rem' }}>
<input
type="checkbox"
checked={options.includeWells !== false}
onChange={handleCheckbox('includeWells')}
/>
Wells
</label>
</div>
<button onClick={handleGenerate}>Generate Village</button>
<div style={{ marginTop: '1rem' }}>
{layout && <VillagePane layout={layout} />}
</div>
</div>
);
};
22 changes: 22 additions & 0 deletions web/src/components/SubmapPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { useSubmapProceduralData } from '../hooks/useSubmapProceduralData';
import { VillagePane } from './VillagePane';
import { VillageOptions } from '../services/villageGenerationService';

interface Props {
currentWorldBiomeId: string;
}

export const SubmapPane: React.FC<Props> = ({ currentWorldBiomeId }) => {
const options: VillageOptions = { type: 'farming', size: 'small' };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The VillageOptions are hardcoded here. This limits the reusability of the component. It would be better to pass these options in as props to make the component more flexible.

const { villageLayout } = useSubmapProceduralData(currentWorldBiomeId, options);

if (villageLayout) {
const handleEnterBuilding = (id: string, type: string) => {
console.log('ENTER_BUILDING', id, type);
};
return <VillagePane layout={villageLayout} onEnterBuilding={handleEnterBuilding} />;
}

return <div>Grid-based map not implemented.</div>;
};
48 changes: 48 additions & 0 deletions web/src/components/VillagePane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { FC } from 'react';
import { VillageLayout } from '../services/villageGenerationService';

interface Props {
layout: VillageLayout;
onEnterBuilding?: (id: string, type: string) => void;
}

export const VillagePane: FC<Props> = ({ layout, onEnterBuilding }) => {
const fillForType: Record<string, string> = {
house: '#cfa',
farmland: '#deb887',
market: '#f5a',
well: '#ccc',
};
return (
<svg width="400" height="400" viewBox="0 0 40 40" style={{ border: '1px solid #ccc' }}>
{layout.roads.map((road) => (
<polyline
key={road.id}
points={road.pathPoints.map((p) => `${p.x},${p.y}`).join(' ')}
stroke="sienna"
fill="none"
strokeWidth={0.2}
/>
))}
{layout.buildings.map((b) => (
<polygon
key={b.id}
points={b.polygon.map((p) => `${p.x},${p.y}`).join(' ')}
fill={fillForType[b.type] || '#cfa'}
stroke="#333"
onClick={() => onEnterBuilding?.(b.id, b.type)}
style={{ cursor: 'pointer' }}
/>
))}
{layout.walls.map((w) => (
<polyline
key={w.id}
points={w.pathPoints.map((p) => `${p.x},${p.y}`).join(' ')}
stroke="black"
fill="none"
strokeWidth={0.5}
/>
))}
</svg>
);
};
39 changes: 39 additions & 0 deletions web/src/config/wfcRulesets/village.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export interface WfcTile {
id: string;
}

export const villageTiles: WfcTile[] = [
{ id: 'grass' },
{ id: 'dirt' },
{ id: 'road_center' },
{ id: 'road_edge' },
{ id: 'building_wall_n' },
{ id: 'building_wall_s' },
{ id: 'building_door' },
{ id: 'building_roof_edge' },
{ id: 'building_roof_center' },
{ id: 'town_wall' },
{ id: 'gate' },
{ id: 'tower_base' },
{ id: 'farmland' },
{ id: 'market_stall' },
{ id: 'well' }
];

export const adjacencyRules: Record<string, string[]> = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve type safety and prevent typos, consider defining a string literal union type for the tile IDs from villageTiles. This type can then be used to strongly type adjacencyRules, ensuring that all keys and values are valid tile IDs.

For example:

export type VillageTileId =
  | 'grass'
  | 'dirt'
  | 'road_center'
  // ... etc.

export const villageTiles: { id: VillageTileId }[] = [
  // ...
];

export const adjacencyRules: Record<VillageTileId, VillageTileId[]> = {
  // ...
};

grass: ['grass', 'dirt', 'road_edge', 'farmland'],
dirt: ['grass', 'dirt', 'road_edge'],
road_center: ['road_center', 'road_edge', 'gate'],
road_edge: ['road_center', 'road_edge', 'building_door', 'grass', 'dirt'],
building_wall_n: ['building_roof_edge', 'building_wall_n', 'building_door'],
building_wall_s: ['building_roof_edge', 'building_wall_s', 'building_door'],
building_door: ['road_edge', 'road_center'],
building_roof_edge: ['building_roof_center', 'building_wall_n', 'building_wall_s'],
building_roof_center: ['building_roof_center', 'building_roof_edge'],
town_wall: ['town_wall', 'gate', 'tower_base'],
gate: ['road_center', 'town_wall'],
tower_base: ['town_wall'],
farmland: ['farmland', 'grass', 'road_edge'],
market_stall: ['road_edge', 'road_center'],
well: ['road_center', 'road_edge']
};
Loading