-
Notifications
You must be signed in to change notification settings - Fork 2
feat: scaffold react village generator #2
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'" | ||
|
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. |
||
| }, | ||
| "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
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. 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. |
||
| "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' }; | ||
| 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[]> = { | ||
| 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
destproperty in this route configuration appears to be incorrect for a Vercel deployment. With@vercel/static-buildanddistDir: "web", the content of thewebdirectory is served from the project root. This route will rewrite an incoming request like/to/web/, causing Vercel to look forweb/index.htmlinside the output directory, which is not the intended structure. The destination should likely not include the/webprefix to serve files correctly.