-
Notifications
You must be signed in to change notification settings - Fork 2
feat: scaffold react village generator #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| node_modules/ | ||
| dist/ |
| 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> |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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'" | ||
| }, | ||
| "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" | ||
| } | ||
| } | ||
| 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 })); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }; | ||
|
|
||
| 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} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The expression 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> | ||
| ); | ||
| }; | ||
| 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' }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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>; | ||
| }; | ||
| 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> | ||
| ); | ||
| }; |
| 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[]> = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve type safety and prevent typos, consider defining a string literal union type for the tile IDs from 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'] | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
testscript is currently a placeholder that echoes a message. The pull request description mentions running tests withnpm --prefix web test. It would be good to either implement some basic tests or update the description to reflect the current state.