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" }

Choose a reason for hiding this comment

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

medium

The dest property in this route configuration appears to be incorrect for a Vercel deployment. With @vercel/static-build and distDir: "web", the content of the web directory is served from the project root. This route will rewrite an incoming request like / to /web/, causing Vercel to look for web/index.html inside the output directory, which is not the intended structure. The destination should likely not include the /web prefix to serve files correctly.

    { "src": "/(.*)", "dest": "/$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 no-op (echo 'No tests specified'). However, the pull request description mentions npm --prefix web test as a testing step. Please either implement tests or update the PR description and script 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",
Comment on lines +16 to +17

Choose a reason for hiding this comment

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

high

There is a version mismatch between the React dependencies and their corresponding type definitions. You are using React version 18, but the types are for version 19. This can lead to incorrect type checking, build errors, or unexpected behavior. Please align the versions of the type definitions with the library versions to ensure type safety.

    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",

"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.

medium

Using as any bypasses TypeScript's type safety and should be avoided. You can use a more specific type assertion here to maintain type safety while still satisfying the compiler.

    setOptions((prev) => ({ ...prev, [key]: e.target.value as typeof prev[key] }));

};

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 checked={options.includeFarmland !== false} can be confusing because it evaluates to true if options.includeFarmland is either true or undefined. A more explicit and common pattern is to use a double negation (!!) to convert the value to a boolean. With !!, undefined correctly becomes false. This makes the intent clearer and the code less prone to bugs. This feedback applies to the other checkboxes in this component 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' };
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[]> = {
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