├── .npmrc ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── env.d.ts ├── public └── favicon.ico ├── src ├── assets │ ├── models │ │ ├── Island │ │ │ └── Island.bin │ │ ├── _textures │ │ │ └── ID_19.png │ │ ├── BuildArea │ │ │ ├── BuildArea.bin │ │ │ └── BuildArea.gltf │ │ └── Buildings │ │ │ ├── Buildings.bin │ │ │ └── Buildings.gltf │ ├── images │ │ ├── Buildings │ │ │ ├── HomeA.png │ │ │ ├── Well.png │ │ │ ├── Market.png │ │ │ ├── Tavern.png │ │ │ ├── Windmill.png │ │ │ ├── Blacksmith.png │ │ │ └── Lumbermill.png │ │ └── day-night-cycle-disk.png │ ├── logo.svg │ └── main.css ├── types │ └── TresJsClickEvent.ts ├── App.vue ├── composables │ ├── useSelectedBuildingAreaStore.ts │ ├── useDebugStore.ts │ ├── useGameStateStore.ts │ ├── useBuildingAreasStore.ts │ ├── useSunPositionStore.ts │ ├── useGameTimeStore.ts │ └── useCalculatedLightsStore.ts ├── components │ ├── ui │ │ ├── ProgressBar.vue │ │ ├── misc │ │ │ └── resources │ │ │ │ └── ResourceEntry.vue │ │ ├── layout │ │ │ └── game │ │ │ │ ├── ui │ │ │ │ ├── bottom │ │ │ │ │ ├── LegalFooter.vue │ │ │ │ │ ├── CurrentResources.vue │ │ │ │ │ └── SelectedBuildingArea.vue │ │ │ │ └── top │ │ │ │ │ ├── TimeSpeedControlButton.vue │ │ │ │ │ └── TimeDisplayWithControl.vue │ │ │ │ ├── debug │ │ │ │ ├── ToggleVisibilityButton.vue │ │ │ │ └── DebugMenu.vue │ │ │ │ ├── GameCanvas.vue │ │ │ │ └── sidebar │ │ │ │ ├── SelectedBuildingInConstruction.vue │ │ │ │ ├── SelectedBuildingUpgrading.vue │ │ │ │ ├── SelectedEmptyBuildingArea.vue │ │ │ │ ├── BuildingRow.vue │ │ │ │ └── SelectedBuildingDetails.vue │ │ └── Resources.vue │ ├── VisualHelper.vue │ ├── DistanceFog.vue │ ├── models │ │ ├── BuildArea.vue │ │ ├── Island.vue │ │ ├── buildings │ │ │ ├── Well.vue │ │ │ ├── HomeA.vue │ │ │ ├── Market.vue │ │ │ ├── Tavern.vue │ │ │ ├── Blacksmith.vue │ │ │ ├── Lumbermill.vue │ │ │ └── Windmill.vue │ │ ├── Ocean.vue │ │ └── SkyBoxWithOceanFloor.vue │ ├── buildings │ │ ├── behaviors │ │ │ ├── ConstructingBehavior.vue │ │ │ ├── UpgradingBehavior.vue │ │ │ └── ProducingBehavior.vue │ │ └── BuildingArea.vue │ ├── Lights.vue │ ├── GameEngine.vue │ └── CameraAndControls.vue ├── main.ts ├── views │ ├── HomeView.vue │ └── GameView.vue ├── game-logic │ ├── buildings │ │ ├── index.ts │ │ ├── Well.ts │ │ ├── HomeA.ts │ │ ├── Market.ts │ │ ├── Tavern.ts │ │ ├── Windmill.ts │ │ ├── Lumbermill.ts │ │ └── Blacksmith.ts │ ├── level-progression │ │ ├── linear-progression.ts │ │ ├── calculating-progression.ts │ │ ├── base.ts │ │ └── fixed-progression.ts │ ├── types.ts │ └── resources.ts ├── router │ └── index.ts └── utils │ ├── gltfLoader.ts │ ├── duration.ts │ ├── threeHelper.ts │ └── WaterShader.ts ├── tsconfig.json ├── vercel.json ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── vite.config.ts ├── eslint.config.js ├── LICENSE.md ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: toddeTV 2 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/models/Island/Island.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/models/Island/Island.bin -------------------------------------------------------------------------------- /src/assets/images/Buildings/HomeA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/HomeA.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Well.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Well.png -------------------------------------------------------------------------------- /src/assets/models/_textures/ID_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/models/_textures/ID_19.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Market.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Market.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Tavern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Tavern.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Windmill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Windmill.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Blacksmith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Blacksmith.png -------------------------------------------------------------------------------- /src/assets/images/Buildings/Lumbermill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/Buildings/Lumbermill.png -------------------------------------------------------------------------------- /src/assets/images/day-night-cycle-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/images/day-night-cycle-disk.png -------------------------------------------------------------------------------- /src/assets/models/BuildArea/BuildArea.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/models/BuildArea/BuildArea.bin -------------------------------------------------------------------------------- /src/assets/models/Buildings/Buildings.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddeTV/zlig/HEAD/src/assets/models/Buildings/Buildings.bin -------------------------------------------------------------------------------- /src/types/TresJsClickEvent.ts: -------------------------------------------------------------------------------- 1 | import type { DomEvent, ThreeEvent } from '@tresjs/core' 2 | 3 | export type TresJsClickEvent = ThreeEvent & Event 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.node.json" 5 | }, 6 | { 7 | "path": "./tsconfig.app.json" 8 | } 9 | ], 10 | "files": [ 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import '@nuxt/ui'; 3 | 4 | @plugin "@iconify/tailwind4" { 5 | prefix: 'icon'; 6 | } 7 | 8 | html, 9 | body { 10 | @apply h-screen; 11 | } 12 | 13 | body { 14 | @apply flex flex-col; 15 | } 16 | 17 | #app { 18 | @apply flex flex-col h-full; 19 | } 20 | -------------------------------------------------------------------------------- /src/composables/useSelectedBuildingAreaStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import type { BuildingAreaId } from '@/game-logic/types.js' 4 | 5 | export const useSelectedBuildingAreaStore = defineStore('selected-building-area', () => { 6 | const id = ref(null) 7 | 8 | return { 9 | id, 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "pnpm run build", 3 | "devCommand": "pnpm run dev", 4 | "framework": "vue", 5 | "git": { 6 | "deploymentEnabled": false 7 | }, 8 | "installCommand": "pnpm install --frozen-lockfile", 9 | "outputDirectory": "dist", 10 | "rewrites": [ 11 | { 12 | "destination": "/", 13 | "source": "/(.*)" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ui/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | zlig 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router/index.js' 2 | // eslint-disable-next-line import/extensions 3 | import ui from '@nuxt/ui/vue-plugin' 4 | import { createPinia } from 'pinia' 5 | import { createApp } from 'vue' 6 | import App from './App.vue' 7 | import './assets/main.css' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(createPinia()) 12 | app.use(router) 13 | app.use(ui) 14 | 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "types": [ 9 | "node" 10 | ], 11 | "noEmit": true 12 | }, 13 | "include": [ 14 | "vite.config.*", 15 | "tools/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/components/ui/misc/resources/ResourceEntry.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /src/components/VisualHelper.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /src/composables/useDebugStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useDebugStore = defineStore('debug', () => { 5 | const showVisualHelper = ref(false) 6 | const showLightHelper = ref(false) 7 | const showCameraHelper = ref(false) 8 | const showFog = ref(true) 9 | const showWaterWireframe = ref(false) 10 | 11 | return { 12 | showCameraHelper, 13 | showFog, 14 | showLightHelper, 15 | showVisualHelper, 16 | showWaterWireframe, 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | } 12 | }, 13 | "include": [ 14 | "env.d.ts", 15 | "src/**/*", 16 | "src/**/*.vue", 17 | "auto-imports.d.ts", 18 | "components.d.ts" 19 | ], 20 | "exclude": [ 21 | "src/**/__tests__/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | pnpm-debug.log* 6 | lerna-debug.log* 7 | 8 | node_modules 9 | .DS_Store 10 | dist 11 | dist-ssr 12 | coverage 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | *.tsbuildinfo 24 | 25 | # Auto-generated type declarations from NuxtUI 26 | auto-imports.d.ts 27 | components.d.ts 28 | 29 | # Generated glTF model files (from dependency `@todde.tv/gltf-type-toolkit`) 30 | *.gltf.d.ts 31 | *.gltf.js 32 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/ui/bottom/LegalFooter.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/ui/bottom/CurrentResources.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/debug/ToggleVisibilityButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/ui/top/TimeSpeedControlButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/game-logic/buildings/index.ts: -------------------------------------------------------------------------------- 1 | import Blacksmith from '@/game-logic/buildings/Blacksmith.js' 2 | import HomeA from '@/game-logic/buildings/HomeA.js' 3 | import Lumbermill from '@/game-logic/buildings/Lumbermill.js' 4 | import Market from '@/game-logic/buildings/Market.js' 5 | import Tavern from '@/game-logic/buildings/Tavern.js' 6 | import Well from '@/game-logic/buildings/Well.js' 7 | import Windmill from '@/game-logic/buildings/Windmill.js' 8 | 9 | export const buildingTypes = { 10 | Blacksmith, 11 | HomeA, 12 | Lumbermill, 13 | Market, 14 | Tavern, 15 | Well, 16 | Windmill, 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ui/Resources.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.goto-alias", 4 | "antfu.iconify", 5 | "bradlc.vscode-tailwindcss", 6 | "cesium.gltf-vscode", 7 | "dbaeumer.vscode-eslint", 8 | "eamodio.gitlens", 9 | "GitHub.copilot", 10 | "GitHub.vscode-pull-request-github", 11 | "GitHub.vscode-github-actions", 12 | "Gruntfuggly.todo-tree", 13 | "naumovs.color-highlight", 14 | "Orta.vscode-twoslash-queries", 15 | "richie5um2.vscode-sort-json", 16 | "streetsidesoftware.code-spell-checker", 17 | "Vue.volar", 18 | "yoavbls.pretty-ts-errors", 19 | "yzhang.markdown-all-in-one" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import HomeView from '@/views/HomeView.vue' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | component: HomeView, 9 | name: 'home', 10 | path: '/', 11 | }, 12 | // { 13 | // path: '/about', 14 | // name: 'about', 15 | // component: () => import('../views/AboutView.vue') 16 | // } 17 | { 18 | component: async () => import('../views/GameView.vue'), 19 | name: 'Game', 20 | path: '/game', 21 | }, 22 | ], 23 | }) 24 | 25 | export default router 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | workflow_dispatch: {} 14 | 15 | jobs: 16 | ci: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - run: corepack enable 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22 27 | 28 | - name: Enable pnpm 29 | run: | 30 | corepack enable 31 | corepack prepare pnpm@v9.15.4 --activate 32 | 33 | - name: 📦 Install dependencies 34 | run: pnpm install --frozen-lockfile 35 | 36 | - name: 🧪 Test lint 37 | run: pnpm run test:lint 38 | 39 | - name: 🎇 Test TS types 40 | run: pnpm run test:types 41 | -------------------------------------------------------------------------------- /src/composables/useGameStateStore.ts: -------------------------------------------------------------------------------- 1 | import { ResourceRecord } from '@/game-logic/resources.js' 2 | import Big from 'big.js' 3 | import { defineStore } from 'pinia' 4 | import { ref } from 'vue' 5 | import type { BuildingAreaId, BuildingInstance } from '@/game-logic/types.js' 6 | 7 | export const useGameStateStore = defineStore('game-state', () => { 8 | return { 9 | buildings: buildingsState(), 10 | resources: availableResources(), 11 | } 12 | }) 13 | 14 | function buildingsState() { 15 | const buildings = ref>({ 16 | // TODO: Load from the saved state. 17 | }) 18 | 19 | return buildings 20 | } 21 | 22 | function availableResources() { 23 | const availableResourceRecord = ref(new ResourceRecord({ 24 | // TODO: Load from the saved state. 25 | gold: new Big('100'), 26 | })) 27 | 28 | return availableResourceRecord 29 | } 30 | -------------------------------------------------------------------------------- /src/composables/useBuildingAreasStore.ts: -------------------------------------------------------------------------------- 1 | import { getNode, IslandScene } from '@/assets/models/Island/Island.gltf.js' 2 | import { defineStore } from 'pinia' 3 | import { readonly, ref } from 'vue' 4 | import type { BuildingAreaId } from '@/game-logic/types.js' 5 | import type { Euler, Vector3 } from 'three' 6 | 7 | export const useBuildingAreasStore = defineStore('building-areas', () => { 8 | const BuildingAreas = ref<{ 9 | id: BuildingAreaId 10 | position: Vector3 11 | rotation: Euler 12 | }[]>([]) 13 | 14 | async function init() { 15 | (await getNode(IslandScene)).traverse((obj) => { 16 | if (obj.userData.isBuildArea && obj.name) { 17 | BuildingAreas.value.push({ 18 | id: obj.name, 19 | position: obj.position, 20 | rotation: obj.rotation, 21 | }) 22 | } 23 | }) 24 | } 25 | 26 | return { 27 | areas: readonly(BuildingAreas), 28 | init, 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/DistanceFog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /src/components/models/BuildArea.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 34 | 35 | 37 | -------------------------------------------------------------------------------- /src/components/models/Island.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/composables/useSunPositionStore.ts: -------------------------------------------------------------------------------- 1 | import { useGameTimeStore } from '@/composables/useGameTimeStore.js' 2 | import { defineStore, storeToRefs } from 'pinia' 3 | import { Vector3 } from 'three' 4 | import { mapLinear } from 'three/src/math/MathUtils.js' 5 | import { computed } from 'vue' 6 | 7 | const a = 50 // Semi-major axis along the X-axis 8 | const b = 25 // Semi-minor axis along the Y-axis 9 | 10 | export const useSunPositionStore = defineStore('sun-position', () => { 11 | const { currentTime } = storeToRefs(useGameTimeStore()) 12 | 13 | // Calculate the angle (here you scale the time to a full cycle in a virtual "day") 14 | const theta = computed(() => { 15 | return (currentTime.value.getTime() / 1000 * 2 * Math.PI) / (24 * 3600) 16 | }) 17 | 18 | // Calculate the light's position on the ellipse 19 | const x = computed(() => a * Math.cos(theta.value)) 20 | const y = computed(() => b * Math.sin(theta.value)) 21 | 22 | const sunPosition = computed(() => new Vector3(x.value, y.value, 100)) 23 | 24 | // Normalize the height between values 25 | const normalizedSunY = computed(() => mapLinear(y.value, -b, b, -1.0, 1.0)) 26 | 27 | return { 28 | normalizedSunY, 29 | sunPosition, 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | // eslint-disable-next-line import/extensions 3 | import ui from '@nuxt/ui/vite' 4 | // eslint-disable-next-line import/extensions 5 | import gltf from '@todde.tv/gltf-type-toolkit/vite' 6 | import { templateCompilerOptions } from '@tresjs/core' 7 | import vue from '@vitejs/plugin-vue' 8 | import { defineConfig } from 'vite' 9 | import vueDevTools from 'vite-plugin-vue-devtools' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [ 14 | vue({ // from `vue` 15 | ...templateCompilerOptions, 16 | }), 17 | vueDevTools(), // from `vite-plugin-vue-devtools` 18 | gltf({ // from `@todde.tv/gltf-type-toolkit` 19 | customGltfLoaderModule: '@/utils/gltfLoader.ts', 20 | verbose: true, 21 | }), 22 | ui({ // from `@nuxt/ui` (including `unplugin-vue-components` & `tailwindcss`) 23 | colorMode: false, 24 | components: { // from `unplugin-vue-components` 25 | }, 26 | ui: { 27 | colors: { 28 | // neutral: 'slate', 29 | // primary: 'green', 30 | }, 31 | }, 32 | }), 33 | ], 34 | resolve: { 35 | alias: { 36 | '@': fileURLToPath(new URL('./src', import.meta.url)), 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/models/buildings/Well.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Well.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Well.png' 2 | import Well from '@/components/models/buildings/Well.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A place where you can get water. It is very deep.', 12 | id: 'Well', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('4')), 16 | initial: Duration.fromHours(new Big('10')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('20') }), 20 | initial: new ResourceRecord({ gold: new Big('30') }), 21 | }, 22 | getModel: () => markRaw(Well), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.2') }), 25 | initial: resourcesPerHour({ gold: new Big('0.55') }), 26 | }, 27 | maxLevel: 10, 28 | }), 29 | maxInstances: 3, 30 | name: 'Well', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/components/models/buildings/HomeA.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/components/models/buildings/Market.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/components/models/buildings/Tavern.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/game-logic/buildings/HomeA.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/HomeA.png' 2 | import HomeA from '@/components/models/buildings/HomeA.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A house for your citizens to live in.', 12 | id: 'HomeA', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('1.5')), 16 | initial: Duration.fromHours(new Big('5')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('25') }), 20 | initial: new ResourceRecord({ gold: new Big('20') }), 21 | }, 22 | getModel: () => markRaw(HomeA), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.05') }), 25 | initial: resourcesPerHour({ gold: new Big('0.2') }), 26 | }, 27 | maxLevel: 4, 28 | }), 29 | maxInstances: undefined, 30 | name: 'Home A', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Market.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Market.png' 2 | import Market from '@/components/models/buildings/Market.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A place where you can buy and sell goods.', 12 | id: 'Market', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('1')), 16 | initial: Duration.fromHours(new Big('4')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('15') }), 20 | initial: new ResourceRecord({ gold: new Big('50') }), 21 | }, 22 | getModel: () => markRaw(Market), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.15') }), 25 | initial: resourcesPerHour({ gold: new Big('0.35') }), 26 | }, 27 | maxLevel: 7, 28 | }), 29 | maxInstances: 1, 30 | name: 'Market', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/components/models/buildings/Blacksmith.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/components/models/buildings/Lumbermill.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 45 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Tavern.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Tavern.png' 2 | import Tavern from '@/components/models/buildings/Tavern.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A place where you can drink and eat. And sometimes even sleep.', 12 | id: 'Tavern', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('2.5')), 16 | initial: Duration.fromHours(new Big('10')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('20') }), 20 | initial: new ResourceRecord({ gold: new Big('60') }), 21 | }, 22 | getModel: () => markRaw(Tavern), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.15') }), 25 | initial: resourcesPerHour({ gold: new Big('0.6') }), 26 | }, 27 | maxLevel: 3, 28 | }), 29 | maxInstances: 1, 30 | name: 'Tavern', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Windmill.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Windmill.png' 2 | import Windmill from '@/components/models/buildings/Windmill.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A innovative conception which extracts gold from the wind by rotating sharp blades 🤯', 12 | id: 'Windmill', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('48')), 16 | initial: Duration.fromHours(new Big('24')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('123') }), 20 | initial: new ResourceRecord({ gold: new Big('1000') }), 21 | }, 22 | getModel: () => markRaw(Windmill), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.3') }), 25 | initial: resourcesPerHour({ gold: new Big('1.5') }), 26 | }, 27 | }), 28 | maxInstances: 2, 29 | name: 'Windmill', 30 | previewImgSrc, 31 | } 32 | 33 | export default building 34 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Lumbermill.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Lumbermill.png' 2 | import Lumbermill from '@/components/models/buildings/Lumbermill.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A place where you can cut down trees and produce wood.', 12 | id: 'Lumbermill', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('1.5')), 16 | initial: Duration.fromHours(new Big('12')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('20') }), 20 | initial: new ResourceRecord({ gold: new Big('90') }), 21 | }, 22 | getModel: () => markRaw(Lumbermill), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.15') }), 25 | initial: resourcesPerHour({ gold: new Big('0.8') }), 26 | }, 27 | maxLevel: 10, 28 | }), 29 | maxInstances: 3, 30 | name: 'Lumbermill', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/GameCanvas.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 34 | 35 | 37 | -------------------------------------------------------------------------------- /src/game-logic/buildings/Blacksmith.ts: -------------------------------------------------------------------------------- 1 | import previewImgSrc from '@/assets/images/Buildings/Blacksmith.png' 2 | import Blacksmith from '@/components/models/buildings/Blacksmith.vue' 3 | import { LinearLevelProgression } from '@/game-logic/level-progression/linear-progression.js' 4 | import { ResourceRecord, resourcesPerHour } from '@/game-logic/resources.js' 5 | import { Duration } from '@/utils/duration.js' 6 | import Big from 'big.js' 7 | import { markRaw } from 'vue' 8 | import type { BuildingType } from '@/game-logic/types.js' 9 | 10 | const building: BuildingType = { 11 | description: 'A place where you can forge your weapons and armor - and repair your broken household stuff.', 12 | id: 'Blacksmith', 13 | levelProgression: new LinearLevelProgression({ 14 | buildingDuration: { 15 | additionalPerLevel: Duration.fromHours(new Big('4')), 16 | initial: Duration.fromHours(new Big('15')), 17 | }, 18 | costs: { 19 | additionalPerLevel: new ResourceRecord({ gold: new Big('60') }), 20 | initial: new ResourceRecord({ gold: new Big('60') }), 21 | }, 22 | getModel: () => markRaw(Blacksmith), 23 | income: { 24 | additionalPerLevel: resourcesPerHour({ gold: new Big('0.35') }), 25 | initial: resourcesPerHour({ gold: new Big('0.7') }), 26 | }, 27 | maxLevel: 5, 28 | }), 29 | maxInstances: 2, 30 | name: 'Blacksmith', 31 | previewImgSrc, 32 | } 33 | 34 | export default building 35 | -------------------------------------------------------------------------------- /src/components/buildings/behaviors/ConstructingBehavior.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | 44 | 45 | 47 | -------------------------------------------------------------------------------- /src/components/buildings/behaviors/UpgradingBehavior.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /src/views/GameView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /src/components/models/Ocean.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 52 | 53 | 55 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/sidebar/SelectedBuildingInConstruction.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | 48 | 50 | -------------------------------------------------------------------------------- /src/game-logic/level-progression/linear-progression.ts: -------------------------------------------------------------------------------- 1 | import { CalculatingLevelProgression } from '@/game-logic/level-progression/calculating-progression.js' 2 | import { type ResourceRecord, ResourcesPerMillisecond } from '@/game-logic/resources.js' 3 | import type { BuildingModel } from '@/game-logic/types.js' 4 | import type { Duration } from '@/utils/duration.js' 5 | 6 | /** 7 | * This type of building level progression calculates all values based on an initial value and additional values per 8 | * level. 9 | */ 10 | export class LinearLevelProgression extends CalculatingLevelProgression { 11 | constructor({ buildingDuration: buildingMilliseconds, costs, getModel, income, maxLevel }: { 12 | costs: { 13 | initial: ResourceRecord 14 | additionalPerLevel: ResourceRecord 15 | } 16 | buildingDuration: { 17 | initial: Duration 18 | additionalPerLevel: Duration 19 | } 20 | income: { 21 | initial: ResourcesPerMillisecond 22 | additionalPerLevel: ResourcesPerMillisecond 23 | } 24 | getModel: (level: number) => BuildingModel 25 | maxLevel?: number 26 | }) { 27 | super({ 28 | costs(level) { 29 | if (level === 1) { 30 | return costs.initial 31 | } 32 | 33 | return costs.additionalPerLevel.times(level - 1) 34 | }, 35 | 36 | duration(level) { 37 | if (level === 1) { 38 | return buildingMilliseconds.initial 39 | } 40 | 41 | return buildingMilliseconds.additionalPerLevel.times(level - 1) 42 | }, 43 | 44 | income(level) { 45 | return new ResourcesPerMillisecond(income.initial.plus(income.additionalPerLevel.times(level - 1))) 46 | }, 47 | 48 | maxLevel, 49 | 50 | model: getModel, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import perfectionist from 'eslint-plugin-perfectionist' 3 | 4 | export default antfu( 5 | { 6 | formatters: { 7 | css: 'prettier', 8 | html: 'prettier', 9 | markdown: 'prettier', 10 | }, 11 | 12 | jsonc: true, 13 | 14 | stylistic: { 15 | indent: 2, 16 | quotes: 'single', 17 | }, 18 | 19 | typescript: true, 20 | 21 | vue: true, 22 | 23 | yaml: true, 24 | }, 25 | { 26 | files: [ 27 | '**/*.html', 28 | '**/*.js', 29 | '**/*.json', 30 | '**/*.md', 31 | '**/*.toml', 32 | '**/*.ts', 33 | '**/*.vue', 34 | '**/*.xml', 35 | '**/*.yaml', 36 | '**/*.yaml', 37 | '**/*.yml', 38 | ], 39 | 40 | ignores: [ 41 | ], 42 | 43 | plugins: { 44 | perfectionist, 45 | }, 46 | 47 | rules: { 48 | 'antfu/consistent-chaining': [ 49 | 'off', 50 | ], 51 | 'import/extensions': [ // ensure consistent file extensions in import declarations 52 | 'error', 53 | 'always', 54 | { 55 | gltf: 'always', 56 | js: 'always', 57 | ts: 'never', 58 | vue: 'always', 59 | }, 60 | ], 61 | 'jsonc/sort-keys': [ 62 | 'error', 63 | ], 64 | 'perfectionist/sort-objects': [ 65 | 'error', 66 | { 67 | order: 'asc', 68 | type: 'natural', 69 | }, 70 | ], 71 | 'vue/attributes-order': [ 72 | 'error', 73 | { 74 | alphabetical: true, 75 | }, 76 | ], 77 | 'vue/max-attributes-per-line': [ 78 | 'error', 79 | { 80 | multiline: 1, 81 | singleline: 3, 82 | }, 83 | ], 84 | }, 85 | }, 86 | ) 87 | -------------------------------------------------------------------------------- /src/components/models/buildings/Windmill.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 54 | 55 | 57 | -------------------------------------------------------------------------------- /src/game-logic/level-progression/calculating-progression.ts: -------------------------------------------------------------------------------- 1 | import { LevelProgression } from '@/game-logic/level-progression/base.js' 2 | import type { ResourceRecord, ResourcesPerMillisecond } from '@/game-logic/resources.js' 3 | import type { BuildingModel } from '@/game-logic/types.js' 4 | import type { Duration } from '@/utils/duration.js' 5 | 6 | /** 7 | * This type of building level progression accepts arbitrary functions to perform calculations and serves as an 8 | * alternative to traditional sub-classing. 9 | */ 10 | export class CalculatingLevelProgression extends LevelProgression { 11 | constructor({ costs, duration, income, maxLevel, model }: { 12 | costs: typeof LevelProgression.prototype['doGetBaseCostsForLevel'] 13 | duration: typeof LevelProgression.prototype['doGetBaseBuildingDurationForLevel'] 14 | income: typeof LevelProgression.prototype['doGetBaseIncomeForLevel'] 15 | model: typeof LevelProgression.prototype['doGetModelForLevel'] 16 | maxLevel?: number 17 | }) { 18 | super(maxLevel) 19 | 20 | this.doGetBaseCostsForLevel = costs 21 | this.doGetBaseBuildingDurationForLevel = duration 22 | this.doGetBaseIncomeForLevel = income 23 | this.doGetModelForLevel = model 24 | } 25 | 26 | protected doGetBaseCostsForLevel(_level: number): ResourceRecord { 27 | throw new Error('Method should be replaced by the constructor!') 28 | } 29 | 30 | protected doGetBaseBuildingDurationForLevel(_level: number): Duration { 31 | throw new Error('Method should be replaced by the constructor!') 32 | } 33 | 34 | protected doGetBaseIncomeForLevel(_level: number): ResourcesPerMillisecond { 35 | throw new Error('Method should be replaced by the constructor!') 36 | } 37 | 38 | protected doGetModelForLevel(_level: number): BuildingModel { 39 | throw new Error('Method should be replaced by the constructor!') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/gltfLoader.ts: -------------------------------------------------------------------------------- 1 | import { useLogger } from '@tresjs/core' 2 | import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js' 3 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' 4 | import decoderUrl from 'three/examples/jsm/libs/draco/draco_decoder.js?url' 5 | import wasmUrl from 'three/examples/jsm/libs/draco/draco_decoder.wasm?url' 6 | import wasmWrapperUrl from 'three/examples/jsm/libs/draco/draco_wasm_wrapper.js?url' 7 | 8 | /* 9 | The DRACOLoader uses extra files for decoding the model files. These files are loaded at runtime based on some 10 | settings and browser capabilities. For this, the original loader accepts a base URL and appends the file names to it. 11 | While this works it is rather cumbersome to include the files that are supplied by the three.js project without 12 | manual build steps. 13 | To ease the process we take advantage of vite/rollup to allow bundling files and return the url to the asset. In the 14 | following subclass we override the known file names to paths that resolve to the assets after the build step. 15 | */ 16 | 17 | class BundledDRACOLoader extends DRACOLoader { 18 | _loadLibrary(url: string, responseType: string) { 19 | const { logError } = useLogger() 20 | 21 | if (url === 'draco_decoder.js') { 22 | url = decoderUrl 23 | } 24 | else if (url === 'draco_wasm_wrapper.js') { 25 | url = wasmWrapperUrl 26 | } 27 | else if (url === 'draco_decoder.wasm') { 28 | url = wasmUrl 29 | } 30 | else { 31 | logError(`[BundledDRACOLoader] Unknown DRACO file: ${url}`) 32 | } 33 | 34 | // @ts-expect-error This does not exist in the typings //TODO fix later 35 | return super._loadLibrary(url, responseType) 36 | } 37 | } 38 | 39 | const dracoLoader = new BundledDRACOLoader().preload() 40 | 41 | const gltfLoader = new GLTFLoader().setDRACOLoader(dracoLoader) 42 | 43 | export default gltfLoader 44 | -------------------------------------------------------------------------------- /src/utils/duration.ts: -------------------------------------------------------------------------------- 1 | import Big from 'big.js' 2 | 3 | export class Duration { 4 | private constructor(readonly milliseconds: Big) {} 5 | 6 | static fromDates(start: Date, end: Date): Duration { 7 | return new this(new Big(end.getTime() - start.getTime())) 8 | } 9 | 10 | static fromSeconds(seconds: number | Big): Duration { 11 | return new this((typeof seconds === 'number' ? new Big(seconds) : seconds).times(1000)) 12 | } 13 | 14 | static fromMinutes(minutes: Big): Duration { 15 | return new this(minutes.times(1000 * 60)) 16 | } 17 | 18 | static fromHours(hours: Big): Duration { 19 | return new this(hours.times(1000 * 60 * 60)) 20 | } 21 | 22 | static fromDays(days: Big): Duration { 23 | return new this(days.times(1000 * 60 * 60 * 24)) 24 | } 25 | 26 | plus(other: this): Duration { 27 | return new Duration(this.milliseconds.plus(other.milliseconds)) 28 | } 29 | 30 | minus(other: this): Duration { 31 | return new Duration(this.milliseconds.minus(other.milliseconds)) 32 | } 33 | 34 | times(other: number): Duration { 35 | return new Duration(this.milliseconds.times(other)) 36 | } 37 | 38 | format(): string { 39 | const milliseconds = this.milliseconds.mod(1000) 40 | 41 | const seconds_ = this.milliseconds.minus(milliseconds).div(1000) 42 | const seconds = seconds_.mod(60) 43 | 44 | const minutes_ = seconds_.minus(seconds).div(60) 45 | const minutes = minutes_.mod(60) 46 | 47 | const hours_ = minutes_.minus(minutes).div(60) 48 | const hours = hours_.mod(24) 49 | 50 | const days = hours_.minus(hours).div(24) 51 | 52 | const parts = [ 53 | [days, 'd'], 54 | [hours, 'h'], 55 | [minutes, 'm'], 56 | ] as const 57 | 58 | const idxOfFirstPartToPrint = parts.findIndex(([val]) => val.gt(0)) 59 | 60 | const partsToPrint = parts.slice(idxOfFirstPartToPrint) 61 | 62 | return partsToPrint.map(([val, suffix]) => `${val.toNumber().toLocaleString()}${suffix}`).join(' ') 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/sidebar/SelectedBuildingUpgrading.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 61 | 62 | 64 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/sidebar/SelectedEmptyBuildingArea.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 58 | 59 | 61 | -------------------------------------------------------------------------------- /src/components/buildings/behaviors/ProducingBehavior.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /src/composables/useGameTimeStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { mapLinear } from 'three/src/math/MathUtils.js' 3 | import { computed, onUnmounted, readonly, ref } from 'vue' 4 | 5 | // One second in real time are 15 minutes in game time. 6 | export const GAME_TIME_FACTOR_REGULAR = 60 * 15 7 | 8 | // One second in real time is one hour in game time. 9 | export const GAME_TIME_FACTOR_FAST = 60 * 60 10 | 11 | // One second in real time are three hours in game time. 12 | export const GAME_TIME_FACTOR_FASTER = 60 * 60 * 3 13 | 14 | export const useGameTimeStore = defineStore('game-time', () => { 15 | const startTime = new Date(0) 16 | // TODO: Find hour that starts the day. 17 | startTime.setHours(8) 18 | 19 | const currentMilliseconds = ref(startTime.getTime()) 20 | const currentTime = computed(() => new Date(currentMilliseconds.value)) 21 | 22 | const currentFactor = ref(GAME_TIME_FACTOR_REGULAR) 23 | 24 | const listeners = new Set() 25 | 26 | function tick(deltaSeconds: number) { 27 | if (currentFactor.value > 0) { // no updates when paused 28 | const gameTimeSecondsPassed = deltaSeconds * currentFactor.value 29 | 30 | currentMilliseconds.value += gameTimeSecondsPassed * 1000 31 | 32 | const divisor = mapLinear(gameTimeSecondsPassed, 0, 100, 1000, 4000) 33 | const ambientAnimationDelta = gameTimeSecondsPassed / divisor 34 | 35 | const args: OnTickFnArgs = { 36 | ambientAnimationDelta, 37 | deltaGameSeconds: gameTimeSecondsPassed, 38 | gameTime: currentTime.value, 39 | } 40 | 41 | listeners.forEach(cb => cb(args)) 42 | } 43 | } 44 | 45 | function onTick(callback: OnTickFn) { 46 | listeners.add(callback) 47 | 48 | onUnmounted(() => { 49 | listeners.delete(callback) 50 | }) 51 | } 52 | 53 | return { 54 | currentFactor, 55 | currentTime: readonly(currentTime), 56 | onTick, 57 | tick, 58 | } 59 | }) 60 | 61 | interface OnTickFnArgs { 62 | deltaGameSeconds: number 63 | gameTime: Date 64 | ambientAnimationDelta: number 65 | } 66 | 67 | type OnTickFn = (args: OnTickFnArgs) => void 68 | -------------------------------------------------------------------------------- /src/game-logic/level-progression/base.ts: -------------------------------------------------------------------------------- 1 | import type { ResourceRecord, ResourcesPerMillisecond } from '@/game-logic/resources.js' 2 | import type { BuildingModel } from '@/game-logic/types.js' 3 | import type { Duration } from '@/utils/duration.js' 4 | 5 | export abstract class LevelProgression { 6 | constructor( 7 | /** 8 | * Limits the maximum level of the building. `undefined` means the building can be upgraded endlessly. 9 | */ 10 | readonly maxLevel: number | undefined, 11 | ) {} 12 | 13 | /** 14 | * @returns The costs to build/upgrade the building without taking into account any modifiers. 15 | */ 16 | getBaseCostsForLevel(level: number) { 17 | this.validateLevel(level) 18 | 19 | return this.doGetBaseCostsForLevel(level) 20 | } 21 | 22 | /** 23 | * @returns The duration it takes to build/upgrade the building without taking into account any modifiers. 24 | */ 25 | getBaseBuildingDurationForLevel(level: number) { 26 | this.validateLevel(level) 27 | 28 | return this.doGetBaseBuildingDurationForLevel(level) 29 | } 30 | 31 | /** 32 | * @returns The currency the player receives each millisecond without taking into account any modifiers. 33 | */ 34 | getBaseIncomeForLevel(level: number) { 35 | this.validateLevel(level) 36 | 37 | return this.doGetBaseIncomeForLevel(level) 38 | } 39 | 40 | /** 41 | * Defines the appearance of the building on this level. 42 | */ 43 | getModelForLevel(level: number) { 44 | this.validateLevel(level) 45 | 46 | return this.doGetModelForLevel(level) 47 | } 48 | 49 | protected abstract doGetBaseCostsForLevel(level: number): ResourceRecord 50 | protected abstract doGetBaseBuildingDurationForLevel(level: number): Duration 51 | protected abstract doGetBaseIncomeForLevel(level: number): ResourcesPerMillisecond 52 | protected abstract doGetModelForLevel(level: number): BuildingModel 53 | 54 | private validateLevel(level: number) { 55 | if (level < 1) { 56 | throw new Error('Each building needs at least one level') 57 | } 58 | else if (this.maxLevel && level > this.maxLevel) { 59 | throw new Error('This building does not have that much levels') 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/ui/bottom/SelectedBuildingArea.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | 51 | 53 | -------------------------------------------------------------------------------- /src/game-logic/level-progression/fixed-progression.ts: -------------------------------------------------------------------------------- 1 | import { LevelProgression } from '@/game-logic/level-progression/base.js' 2 | import type { ResourceRecord, ResourcesPerMillisecond } from '@/game-logic/resources.js' 3 | import type { BuildingModel } from '@/game-logic/types.js' 4 | import type { Duration } from '@/utils/duration.js' 5 | 6 | /** 7 | * This type of building level progression has fixed values defined for each level. Only those defined levels are 8 | * available in the game. 9 | */ 10 | export class FixedLevelProgression extends LevelProgression { 11 | constructor(private levels: [FirstLevelFixedProgression, ...LaterLevelsFixedProgression[]]) { 12 | // The first entry with index 0 is for level 1. 13 | super(levels.length) 14 | } 15 | 16 | protected doGetBaseCostsForLevel(level: number) { 17 | return this.levels[level - 1].baseCosts 18 | } 19 | 20 | protected doGetBaseBuildingDurationForLevel(level: number) { 21 | return this.levels[level - 1].baseBuildingDuration 22 | } 23 | 24 | protected doGetBaseIncomeForLevel(level: number) { 25 | return this.levels[level - 1].baseIncome 26 | } 27 | 28 | protected doGetModelForLevel(level: number) { 29 | // Try to find a model from the requested level downwards. 30 | for (;level > 1; level--) { 31 | const model = this.levels[level]?.model 32 | if (model !== undefined) { 33 | return model 34 | } 35 | } 36 | 37 | // As a fallback use the base model 38 | return this.levels[0].model 39 | } 40 | } 41 | 42 | type BaseLevelFixedProgression = Readonly<{ 43 | /** 44 | * The costs to build/upgrade the building without taking into account any modifiers. 45 | */ 46 | baseCosts: ResourceRecord 47 | 48 | /** 49 | * The duration it takes to build/upgrade the building without taking into account any modifiers. 50 | */ 51 | baseBuildingDuration: Duration 52 | 53 | /** 54 | * The currency the player receives each game time millisecond without taking into account any modifiers. 55 | */ 56 | baseIncome: ResourcesPerMillisecond 57 | }> 58 | 59 | type FirstLevelFixedProgression = BaseLevelFixedProgression & Readonly<{ 60 | /** 61 | * Defines the appearance of the building on the first level. 62 | */ 63 | model: BuildingModel 64 | }> 65 | 66 | type LaterLevelsFixedProgression = BaseLevelFixedProgression & Readonly<{ 67 | /** 68 | * Defines the appearance of the building on this level. If not given defaults to the appearance of the previous 69 | * level. 70 | */ 71 | model?: BuildingModel 72 | }> 73 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[vue]": { 3 | "editor.defaultFormatter": "Vue.volar" 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.organizeImports": "never" 8 | }, 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | "editor.formatOnSave": false, 11 | "editor.linkedEditing": true, 12 | "editor.quickSuggestions": { 13 | "strings": "on" 14 | }, 15 | "editor.rulers": [120], 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "format/*", "severity": "off" }, 19 | { "rule": "*-indent", "severity": "off" }, 20 | { "rule": "*-spacing", "severity": "off" }, 21 | { "rule": "*-spaces", "severity": "off" }, 22 | { "rule": "*-order", "severity": "off" }, 23 | { "rule": "*-dangle", "severity": "off" }, 24 | { "rule": "*-newline", "severity": "off" }, 25 | { "rule": "*quotes", "severity": "off" }, 26 | { "rule": "*semi", "severity": "off" } 27 | ], 28 | "eslint.useFlatConfig": true, 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml", 40 | "toml", 41 | "xml", 42 | "gql", 43 | "graphql", 44 | "astro", 45 | "css", 46 | "less", 47 | "scss", 48 | "pcss", 49 | "postcss" 50 | ], 51 | "files.associations": { 52 | "*.css": "tailwindcss" 53 | }, 54 | "files.autoSave": "onFocusChange", 55 | "javascript.preferences.importModuleSpecifier": "non-relative", 56 | "javascript.preferences.importModuleSpecifierEnding": "js", 57 | "javascript.preferences.renameMatchingJsxTags": true, 58 | "npm.packageManager": "pnpm", 59 | "prettier.enable": false, 60 | "search.exclude": { 61 | "**/.output": true, 62 | "**/.pnp.*": true, 63 | "**/dist": true, 64 | "node_modules": true, 65 | "pnpm-lock.yaml": true 66 | }, 67 | "search.useIgnoreFiles": false, 68 | "tailwindCSS.classAttributes": [ 69 | "class", 70 | "ui" 71 | ], 72 | "tailwindCSS.experimental.classRegex": [ 73 | [ 74 | "ui:\\s*{([^)]*)\\s*}", 75 | "(?:'|\"|`)([^']*)(?:'|\"|`)" 76 | ] 77 | ], 78 | "typescript.enablePromptUseWorkspaceTsdk": true, 79 | "typescript.preferences.importModuleSpecifier": "non-relative", 80 | "typescript.preferences.importModuleSpecifierEnding": "js", 81 | "typescript.preferences.renameMatchingJsxTags": true, 82 | "typescript.tsdk": "node_modules/typescript/lib", 83 | "volar.autoCompleteRefs": true, 84 | "volar.completion.preferredTagNameCase": "pascal" 85 | } 86 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # LICENSE 2 | 3 | This license is based on the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0), with additional terms specific to this project. 4 | 5 | By using this project, you agree to the following terms and conditions. 6 | 7 | ## License Summary 8 | 9 | This project, including original code and models, is licensed under a modified version of the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0). Under this license, you are permitted to: 10 | 11 | - **Share** - copy and redistribute the material in any medium or format 12 | - **Adapt** - remix, transform, and build upon the material 13 | 14 | However, the following conditions must be met: 15 | 16 | 1. **Attribution**: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 17 | 2. **Non-Commercial**: You may not use the material for commercial purposes. 18 | 3. **ShareAlike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. 19 | 20 | The full standard license text can be found here: [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). 21 | 22 | ## Scope of This License 23 | 24 | This license applies only to the original content authored within this project, including the original code and models. Any third-party libraries, assets, 3D models, or other materials utilized within this project are attributed in the README under the "Attribution/ Contribution" section. These assets remain the property of their original creators and are licensed under their respective terms, as indicated in that section. 25 | 26 | ## Additional Licensing Clauses 27 | 28 | The project founder, [Thorsten Seyschab](https://todde.tv), reserves the right to modify the terms of this license or to issue separate licenses for individual use cases. This means that: 29 | 30 | - **Modifications to License Terms**: The project founder may modify or update the terms of this license at any time for future distributions of the project. 31 | - **Alternative Licensing Arrangements**: For specific, individual cases, the project founder may grant alternative licenses or permissions as requested. Interested parties are welcome to reach out to discuss custom licensing needs. Note, however, that custom licensing is not guaranteed. 32 | 33 | Thank you for respecting the terms of this modified license, which allows the community to use, adapt, and share this work in a non-commercial manner while maintaining the integrity of the contributions. 34 | -------------------------------------------------------------------------------- /src/components/Lights.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 64 | 66 | 67 | 69 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/debug/DebugMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 71 | 72 | 74 | -------------------------------------------------------------------------------- /src/composables/useCalculatedLightsStore.ts: -------------------------------------------------------------------------------- 1 | import { useSunPositionStore } from '@/composables/useSunPositionStore.js' 2 | import { defineStore, storeToRefs } from 'pinia' 3 | import { Color } from 'three' 4 | import { lerp, smootherstep, smoothstep } from 'three/src/math/MathUtils.js' 5 | import { computed } from 'vue' 6 | 7 | export const useCalculatedLightsStore = defineStore('calculated-lights', () => { 8 | const intensityFraction = calculateSunFractionAtHorizonForIntensities() 9 | const ambientIntensity = computed(() => lerp(3, 0.8, intensityFraction.value)) 10 | const sunIntensity = computed(() => lerp(0, 1, intensityFraction.value)) 11 | 12 | const lightColors = calculateLightColors() 13 | 14 | return { 15 | ambientIntensity, 16 | lightColors, 17 | sunIntensity, 18 | } 19 | }) 20 | 21 | function calculateSunFractionAtHorizonForIntensities() { 22 | const AT_HORIZON_Y = 0.01 23 | const NEAR_HORIZON_Y = 0.2 24 | 25 | const { normalizedSunY } = storeToRefs(useSunPositionStore()) 26 | 27 | // The smootherstep function returns a fraction that the sun y is between the two constants. 28 | // It also clamps them which is all we want, yay. 29 | return computed(() => smootherstep(normalizedSunY.value, AT_HORIZON_Y, NEAR_HORIZON_Y)) 30 | } 31 | 32 | function calculateLightColors() { 33 | const BELOW_HORIZON_Y = -0.25 34 | const AT_HORIZON_Y = 0.01 35 | const NEAR_HORIZON_Y = 0.25 36 | 37 | const { normalizedSunY } = storeToRefs(useSunPositionStore()) 38 | 39 | return computed(() => { 40 | const colorSunDay = new Color(0xFFF9ED) 41 | const colorAmbientDay = new Color(0xE0F3FF) 42 | const colorSunDown = new Color(0xEB5B00) 43 | // const colorAmbientDown = new Color(0xFFDDB7) 44 | const colorAmbientDown = new Color(0xB6818F) 45 | const colorSkyDown = new Color(0x403538) 46 | const colorAmbientNight = new Color(0x8F91C4) 47 | const colorSkyNight = new Color(0x1E1F39) 48 | 49 | const ambient = colorAmbientDay 50 | const sky = colorAmbientDay 51 | const sun = colorSunDay 52 | 53 | if (normalizedSunY.value <= BELOW_HORIZON_Y) { 54 | ambient.set(colorAmbientNight) 55 | sky.set(colorSkyNight) 56 | } 57 | else if (normalizedSunY.value <= AT_HORIZON_Y) { 58 | const t = smoothstep(normalizedSunY.value, BELOW_HORIZON_Y, AT_HORIZON_Y) 59 | 60 | ambient.lerpColors(colorAmbientNight, colorAmbientDown, t) 61 | sky.lerpColors(colorSkyNight, colorSkyDown, t) 62 | } 63 | else if (normalizedSunY.value <= NEAR_HORIZON_Y) { 64 | const t = smoothstep(normalizedSunY.value, AT_HORIZON_Y, NEAR_HORIZON_Y) 65 | 66 | ambient.lerpColors(colorAmbientDown, colorAmbientDay, t) 67 | sky.lerpColors(colorSkyDown, colorAmbientDay, t) 68 | sun.lerpColors(colorSunDown, colorSunDay, t) 69 | } 70 | 71 | return { ambient, sky, sun } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/game-logic/types.ts: -------------------------------------------------------------------------------- 1 | import type { LevelProgression } from '@/game-logic/level-progression/base.js' 2 | import type { ResourceRecord } from '@/game-logic/resources.js' 3 | import type { Duration } from '@/utils/duration.js' 4 | import type { Component } from 'vue' 5 | 6 | export type BuildingType = Readonly<{ 7 | /** 8 | * An internal identifier. 9 | */ 10 | id: string 11 | 12 | /** 13 | * The display name of the building type. 14 | */ 15 | name: string 16 | 17 | /** 18 | * A description displayed for each building. 19 | */ 20 | description: string 21 | 22 | /** 23 | * Determines the available building levels with costs and benefits. 24 | */ 25 | levelProgression: LevelProgression 26 | 27 | /** 28 | * Determines the maximum allowed buildings to place of this type. 29 | */ 30 | maxInstances: number | undefined 31 | 32 | /** 33 | * The image for the building preview. 34 | */ 35 | previewImgSrc: string 36 | 37 | // TODO: Maybe restrict the available building places by some criteria? 38 | // availableBuildAreas: 'shore' 39 | }> 40 | 41 | // TODO: Make this more type safe. 42 | export type BuildingAreaId = string 43 | 44 | export type BuildingInstance = { 45 | /** 46 | * A reference to the type of the building. 47 | */ 48 | type: BuildingType 49 | } & BuildingState 50 | 51 | export type BuildingState = BuildingStateInConstruction | BuildingStateUpgrading | BuildingStateProducing 52 | 53 | /** 54 | * The state for buildings that are in the process of being built right now. 55 | * 56 | * Next possible states: 'producing' 57 | */ 58 | export type BuildingStateInConstruction = Readonly<{ 59 | state: 'in-construction' 60 | level: 0 61 | 62 | /** 63 | * The duration left when the building will be finished and reach level 1. 64 | */ 65 | durationRemaining: Duration 66 | 67 | /** 68 | * The total duration it takes to construct this building. 69 | */ 70 | initialDuration: Duration 71 | }> 72 | 73 | /** 74 | * The state for buildings that are in the process of being upgraded right now. 75 | * 76 | * Next possible states: 'producing' 77 | */ 78 | export type BuildingStateUpgrading = Readonly<{ 79 | state: 'upgrading' 80 | 81 | /** 82 | * The level of the building before upgrading started. 83 | */ 84 | level: number 85 | 86 | /** 87 | * The duration left when the building will reach the next level. 88 | */ 89 | durationRemaining: Duration 90 | 91 | /** 92 | * The total duration it takes to upgrade this building. 93 | */ 94 | initialDuration: Duration 95 | }> 96 | 97 | /** 98 | * The 'idle' state of buildings. Here they may produce resources based on the time passed. 99 | */ 100 | export type BuildingStateProducing = Readonly<{ 101 | state: 'producing' 102 | 103 | /** 104 | * The current level of the building. 105 | */ 106 | level: number 107 | 108 | /** 109 | * The internal buffer to handle fractional produced resources. 110 | */ 111 | internalBuffer: ResourceRecord 112 | }> 113 | 114 | export type BuildingModel = Component<{ 115 | buildingInstance: BuildingInstance 116 | }> 117 | -------------------------------------------------------------------------------- /src/components/buildings/BuildingArea.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 94 | 95 | 97 | -------------------------------------------------------------------------------- /src/utils/threeHelper.ts: -------------------------------------------------------------------------------- 1 | import { type Group, Mesh, Object3D, type Scene, UniformsUtils, Vector3, type WebGLProgramParametersWithUniforms } from 'three' 2 | 3 | export function getLeafObjects(object: Object3D): Object3D[] { 4 | if (object.children.length === 0) { 5 | return [object] 6 | } 7 | const children = object.children.flatMap(child => getLeafObjects(child)) 8 | return children 9 | } 10 | 11 | export function getAllObjects(object: Object3D): Object3D[] { 12 | if (object.children.length === 0) { 13 | return [object] 14 | } 15 | const children = object.children.flatMap(child => getAllObjects(child)) 16 | return [object, ...children] 17 | } 18 | 19 | export function addShadow( 20 | object: Object3D, 21 | shadowMode: 'both' | 'cast' | 'receive' = 'both', 22 | ) { 23 | // if (object.children.length > 0) { 24 | // object.children.forEach(child => addShadow(child)) 25 | // return 26 | // } 27 | // if (shadowMode === 'both' || shadowMode === 'cast') { 28 | // object.castShadow = true 29 | // } 30 | // if (shadowMode === 'both' || shadowMode === 'receive') { 31 | // object.receiveShadow = true 32 | // } 33 | object.traverse((child) => { 34 | if (child instanceof Object3D) { 35 | if (shadowMode === 'both' || shadowMode === 'cast') { 36 | child.castShadow = true 37 | } 38 | if (shadowMode === 'both' || shadowMode === 'receive') { 39 | child.receiveShadow = true 40 | } 41 | } 42 | }) 43 | } 44 | 45 | export function removeFogDependence( 46 | object: Object3D, 47 | ) { 48 | object.traverse((child) => { 49 | if (child instanceof Mesh) { 50 | child.material.fog = false 51 | } 52 | }) 53 | } 54 | 55 | export function addToGroup( 56 | group: Scene | Group | Object3D, 57 | object: Object3D, 58 | objectEnableFog = false, 59 | ) { 60 | if (objectEnableFog === false) { 61 | removeFogDependence(object) 62 | } 63 | group.add(object) 64 | } 65 | 66 | export function addShadowAndAddToGroup( 67 | group: Scene | Group | Object3D, 68 | object: Object3D, 69 | shadowMode: 'both' | 'cast' | 'receive' = 'both', 70 | objectEnableFog = false, 71 | ) { 72 | addShadow(object, shadowMode) 73 | addToGroup(group, object, objectEnableFog) 74 | } 75 | 76 | export function overrideFogShader( 77 | shader: WebGLProgramParametersWithUniforms, 78 | fogCenter = new Vector3(0, 0, 0), 79 | fogDistanceOffset = 0, 80 | ) { 81 | shader.vertexShader = shader.vertexShader.replace( 82 | `#include `, 83 | `#include 84 | #ifdef USE_FOG 85 | // the center of the fog is set here, but bc it is the world center, we do not have do do anything 86 | uniform vec3 fogCenter; 87 | #endif 88 | `, 89 | ) 90 | shader.vertexShader = shader.vertexShader.replace( 91 | `#include `, 92 | ` 93 | #ifdef USE_FOG 94 | vec3 vertexWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz; 95 | vFogDepth = distance(fogCenter.xz, vertexWorldPosition.xz) + ${fogDistanceOffset.toFixed(1)}; 96 | #endif 97 | 98 | #include `, 99 | ) 100 | shader.vertexShader = shader.vertexShader.replace( 101 | `#include `, 102 | ``, 103 | ) 104 | 105 | const uniforms = ({ 106 | fogCenter: { value: fogCenter }, 107 | }) 108 | 109 | shader.uniforms = UniformsUtils.merge([shader.uniforms, uniforms]) 110 | } 111 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/ui/top/TimeDisplayWithControl.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 102 | 103 | 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@todde.tv/zlig", 3 | "type": "module", 4 | "version": "1.0.0-rc.0", 5 | "packageManager": "pnpm@9.15.4", 6 | "description": "zlig is a Japanese zen-inspired idle browser game showcasing lightweight web technologies like Vue and Three.js to create fully browser-based games.", 7 | "author": "Thorsten Seyschab (https://todde.tv/)", 8 | "contributors": [ 9 | { 10 | "name": "Thorsten Seyschab", 11 | "email": "business@todde.tv", 12 | "url": "https://todde.tv/" 13 | }, 14 | { 15 | "name": "Andreas Fehn", 16 | "url": "https://github.com/fehnomenal" 17 | } 18 | ], 19 | "license": "SEE LICENSE IN LICENSE.md", 20 | "homepage": "https://todde.tv/", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/toddeTV/zlig.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/toddeTV/zlig/issues" 27 | }, 28 | "keywords": [ 29 | "browser-game", 30 | "cc-by-nc-sa-4-0", 31 | "game", 32 | "gltf", 33 | "gltf2", 34 | "idle-game", 35 | "three-js", 36 | "threejs", 37 | "tresjs", 38 | "typescript", 39 | "vite", 40 | "vue3", 41 | "vuejs", 42 | "web", 43 | "website" 44 | ], 45 | "engines": { 46 | "node": "~22", 47 | "pnpm": "~9.15.4" 48 | }, 49 | "scripts": { 50 | "dev": "vite", 51 | "build": "vite build", 52 | "preview": "vite preview", 53 | "test": "run-p \"test:lint\" \"test:types\" --", 54 | "test:types": "vue-tsc --project ./tsconfig.app.json", 55 | "test:lint": "eslint .", 56 | "test:lint:fix": "run-s \"test:lint --fix\"", 57 | "test:lint:print-current-config": "mkdir -p ./dist/_eslintConfig && eslint --print-config file.json > ./dist/_eslintConfig/json.json && eslint --print-config file.vue > ./dist/_eslintConfig/vue.json && eslint --print-config file.js > ./dist/_eslintConfig/js.json && eslint --print-config file.ts > ./dist/_eslintConfig/ts.json && eslint --print-config file.md > ./dist/_eslintConfig/md.json && eslint --print-config file.html > ./dist/_eslintConfig/html.json && eslint --print-config file.yaml > ./dist/_eslintConfig/yaml.json && eslint --print-config file.xml > ./dist/_eslintConfig/xml.json && eslint --print-config file.toml > ./dist/_eslintConfig/toml.json && eslint --print-config file.gql > ./dist/_eslintConfig/gql.json && eslint --print-config file.css > ./dist/_eslintConfig/css.json && eslint --print-config file.astro > ./dist/_eslintConfig/astro.json && eslint --print-config file.svelte > ./dist/_eslintConfig/svelte.json && eslint --print-config file.less > ./dist/_eslintConfig/less.json && eslint --print-config file.scss > ./dist/_eslintConfig/scss.json && eslint --print-config file.pcss > ./dist/_eslintConfig/pcss.json", 58 | "postinstall": "run-s \"generate:gltf-models\"", 59 | "generate:gltf-models": "gltf-codegen" 60 | }, 61 | "dependencies": { 62 | "vue": "~3.5.4" 63 | }, 64 | "devDependencies": { 65 | "@antfu/eslint-config": "~3.5.1", 66 | "@iconify-json/ph": "~1.2.1", 67 | "@iconify/tailwind4": "~1.0.6", 68 | "@nuxt/ui": "~3.0.2", 69 | "@rushstack/eslint-patch": "~1.10.4", 70 | "@tailwindcss/vite": "~4.1.3", 71 | "@todde.tv/gltf-type-toolkit": "~1.1.0", 72 | "@tresjs/cientos": "~4.3.0", 73 | "@tresjs/core": "~4.2.10", 74 | "@tresjs/post-processing": "~2.1.0", 75 | "@tsconfig/node20": "~20.1.4", 76 | "@types/big.js": "~6.2.2", 77 | "@types/node": "~20.16.5", 78 | "@types/three": "~0.168.0", 79 | "@vitejs/plugin-vue": "~5.1.3", 80 | "@vue/eslint-config-typescript": "~13.0.0", 81 | "@vue/tsconfig": "~0.5.1", 82 | "@vueuse/core": "~12.7.0", 83 | "big.js": "~6.2.2", 84 | "eslint": "~9.10.0", 85 | "eslint-plugin-format": "~0.1.2", 86 | "eslint-plugin-perfectionist": "~3.5.0", 87 | "eslint-plugin-vue": "~9.28.0", 88 | "npm-run-all2": "~6.2.2", 89 | "pinia": "~2.2.2", 90 | "postprocessing": "~6.36.7", 91 | "three": "~0.168.0", 92 | "tsx": "~4.19.1", 93 | "typescript": "~5.5.4", 94 | "vite": "~5.4.4", 95 | "vite-plugin-vue-devtools": "~7.4.5", 96 | "vue-router": "~4.4.4", 97 | "vue-tsc": "~2.1.6" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/components/models/SkyBoxWithOceanFloor.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 119 | 120 | 122 | -------------------------------------------------------------------------------- /src/components/GameEngine.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 136 | 137 | 139 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/sidebar/BuildingRow.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 168 | 169 | 171 | -------------------------------------------------------------------------------- /src/game-logic/resources.ts: -------------------------------------------------------------------------------- 1 | import Big from 'big.js' 2 | 3 | /** 4 | * Determines the available resources. 5 | */ 6 | export type Resource = keyof { 7 | // This extracts all keys of the record class that are of the custom big integer type. 8 | [K in keyof ResourceRecord as [ResourceRecord[K]] extends [Big] ? K : never]: 0 9 | } 10 | 11 | export type PlainResources = Record 12 | 13 | const ZERO = new Big('0') 14 | 15 | /** 16 | * A simple holder of all resources. 17 | * 18 | * A new resource can easily be added by adding a new field to this class. It will be automatically available on all 19 | * places. It is only important to give it an initial value of zero. 20 | */ 21 | export class ResourceRecord implements PlainResources { 22 | readonly gold: Big = ZERO 23 | 24 | constructor(init?: Partial) { 25 | for (const [resource, amount] of getPlainResources(init ?? {})) { 26 | this[resource] = amount 27 | } 28 | } 29 | 30 | /** 31 | * Returns the plain resources object. 32 | */ 33 | asPlain(): PlainResources { 34 | return { ...this } 35 | } 36 | 37 | plus(other: Partial): ResourceRecord { 38 | return this.calc(other, (a, b) => a.plus(b)) 39 | } 40 | 41 | minus(other: Partial): ResourceRecord { 42 | return this.calc(other, (a, b) => a.minus(b)) 43 | } 44 | 45 | times(factor: number): ResourceRecord 46 | times(factors: Partial): ResourceRecord 47 | times(factorOrFactors: number | Partial) { 48 | if (typeof factorOrFactors === 'number') { 49 | // This is a shortcut: Pretend to calculate with empty resources but multiplicate each value with the fixed value. 50 | return this.calc({}, a => a.times(factorOrFactors)) 51 | } 52 | 53 | // This is another shortcut: This time actually fill the record and use it for multiplication. 54 | return this.calc(factorOrFactors, (a, b) => a.times(b)) 55 | } 56 | 57 | divided_by(divisor: number): ResourceRecord 58 | divided_by(divisors: Partial): ResourceRecord 59 | divided_by(divisorOrDivisors: number | Partial) { 60 | if (typeof divisorOrDivisors === 'number') { 61 | // This is a shortcut: Pretend to calculate with empty resources but divide each value by the fixed value. 62 | return this.calc({}, a => a.div(divisorOrDivisors)) 63 | } 64 | 65 | // This is another shortcut: This time actually fill the record and use it for division. 66 | return this.calc(divisorOrDivisors, (a, b) => a.div(b)) 67 | } 68 | 69 | gt(other: Partial): boolean { 70 | return this.cmp(other, (a, b) => a.gt(b)) 71 | } 72 | 73 | gte(other: Partial): boolean { 74 | return this.cmp(other, (a, b) => a.gte(b)) 75 | } 76 | 77 | lt(other: Partial): boolean { 78 | return this.cmp(other, (a, b) => a.lt(b)) 79 | } 80 | 81 | lte(other: Partial): boolean { 82 | return this.cmp(other, (a, b) => a.lte(b)) 83 | } 84 | 85 | /** 86 | * @returns A record with only integer values. 87 | */ 88 | round(): ResourceRecord { 89 | // Another shortcut: Pretend to calculate but only use the value of this record. 90 | return this.calc({}, a => a.round(0, Big.roundHalfUp)) 91 | } 92 | 93 | /** 94 | * @returns A record with only integer values (rounded down). 95 | */ 96 | roundDown(): ResourceRecord { 97 | // Another shortcut: Pretend to calculate but only use the value of this record. 98 | return this.calc({}, a => a.round(0, Big.roundDown)) 99 | } 100 | 101 | /** 102 | * This helper method applies the op to each pair of resources from this and the other and collects the result in a 103 | * new resource record. 104 | */ 105 | private calc(other: Partial, op: (a: Big, b: Big) => Big) { 106 | const resources = getPlainResources(this) 107 | const resourcesOther = { ...other } 108 | 109 | for (const [resource, amountThis] of resources) { 110 | const amountOther = resourcesOther[resource] ?? ZERO 111 | 112 | resourcesOther[resource] = op(amountThis, amountOther) 113 | } 114 | 115 | return new ResourceRecord(resourcesOther) 116 | } 117 | 118 | private cmp(other: Partial, cmp: (a: Big, b: Big) => boolean) { 119 | const resources = getPlainResources(this) 120 | 121 | for (const [resource, amountThis] of resources) { 122 | const amountOther = other[resource] 123 | 124 | if (!amountOther || !cmp(amountThis, amountOther)) { 125 | return false 126 | } 127 | } 128 | 129 | return true 130 | } 131 | } 132 | 133 | function getPlainResources(input: Partial) { 134 | // This does not return functions. 135 | return Object.entries(input) as [Resource, Big][] 136 | } 137 | 138 | export class ResourcesPerMillisecond extends ResourceRecord { 139 | perHour() { 140 | return this.times(1000 * 60 * 60) 141 | } 142 | } 143 | 144 | export function resourcesPerSecond(resources: Partial): ResourcesPerMillisecond { 145 | resources = record(resources).divided_by(1000) 146 | 147 | return new ResourcesPerMillisecond(resources) 148 | } 149 | 150 | export function resourcesPerMinute(resources: Partial): ResourcesPerMillisecond { 151 | resources = record(resources).divided_by(1000 * 60) 152 | 153 | return new ResourcesPerMillisecond(resources) 154 | } 155 | 156 | export function resourcesPerHour(resources: Partial): ResourcesPerMillisecond { 157 | resources = record(resources).divided_by(1000 * 60 * 60) 158 | 159 | return new ResourcesPerMillisecond(resources) 160 | } 161 | 162 | export function resourcesPerDay(resources: Partial): ResourcesPerMillisecond { 163 | resources = record(resources).divided_by(1000 * 60 * 60 * 24) 164 | 165 | return new ResourcesPerMillisecond(resources) 166 | } 167 | 168 | function record(resources: Partial) { 169 | if (resources instanceof ResourceRecord) { 170 | return resources 171 | } 172 | return new ResourceRecord(resources) 173 | } 174 | -------------------------------------------------------------------------------- /src/components/ui/layout/game/sidebar/SelectedBuildingDetails.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 188 | 189 | 191 | -------------------------------------------------------------------------------- /src/utils/WaterShader.ts: -------------------------------------------------------------------------------- 1 | import { overrideFogShader } from '@/utils/threeHelper.js' 2 | import { MeshStandardMaterial, UniformsUtils, Vector3 } from 'three' 3 | import type { ColorRepresentation, WebGLProgramParametersWithUniforms } from 'three' 4 | 5 | /** 6 | * Wave displacement code (getWave and wavedx functions) by afl_ext provided under MIT License (https://www.shadertoy.com/view/MdXyzX) 7 | * hash22 function: Copyright (c)2014 David Hoskins; provided under MIT License (https://www.shadertoy.com/view/4djSRW) 8 | * waveTangetialAmplitude: the amount of movement sideways for each point (to break up the repeating wave patterns) 9 | * relativeHeightOffset: waves get offset by relativeHeightOffset * waveAmplitude. The default value of -0.75 puts the peaks of the highest waves roughly at the geometry surface 10 | * 11 | * @param options - Configuration object for the water material 12 | * @param options.fogActive - Whether to be affected by the fog (optional, default: false) 13 | * @param options.fogCenter - The center of the fog (optional, default: Vector3(0, 0, 0)) 14 | * @param options.fogDistanceOffset - The distance offset of the fog (optional, default: 0) 15 | * @param options.relativeHeightOffset - The offset of the waves, moves the wave plane up and down (optional, default: -0.75) 16 | * @param options.waterColor - The color of the water (optional, default: 0x0384C4) 17 | * @param options.waterSwingDirection - The axis to rotate the waves around ('xz' or 'xy') (required) 18 | * @param options.waveAmplitude - The amplitude of the waves (wave height) (optional, default: 1.8) 19 | * @param options.waveSpeed - The speed of the waves (optional, default: 1.0) 20 | * @param options.waveTangentialAmplitude - The amplitude of the tangential waves (side movement amount) (optional, default: 1.0) 21 | * @returns {MeshStandardMaterial} - The water material with attached uniforms 22 | */ 23 | export function getWaterMaterial( 24 | { 25 | fogActive = false, 26 | fogCenter = new Vector3(0, 0, 0), 27 | fogDistanceOffset = 0, 28 | relativeHeightOffset = -0.75, 29 | waterColor = 0x0384C4, 30 | waterSwingDirection = 'xy', 31 | waveAmplitude = 1.8, 32 | waveSpeed = 1.0, 33 | waveTangentialAmplitude = 1.0, 34 | }: 35 | { 36 | fogActive?: boolean 37 | fogCenter?: Vector3 38 | fogDistanceOffset?: number 39 | waterColor?: ColorRepresentation 40 | waveSpeed?: number 41 | waveAmplitude?: number 42 | waveTangentialAmplitude?: number 43 | relativeHeightOffset?: number 44 | waterSwingDirection: 'xz' | 'xy' 45 | }, 46 | ) { 47 | const waterMaterial = new MeshStandardMaterial({ 48 | color: waterColor, 49 | flatShading: true, 50 | metalness: 0.0, 51 | opacity: 0.8, 52 | roughness: 0.0, 53 | shadowSide: 1, // set to Backside; this is a dumb hack to fix the mysterious shadow casting even if shadow casting is disabled 54 | transparent: true, 55 | }) 56 | 57 | const timeUniform = { time: { value: 0 } } 58 | 59 | waterMaterial.onBeforeCompile = function (shader: WebGLProgramParametersWithUniforms) { 60 | if (fogActive) { 61 | overrideFogShader(shader, fogCenter, fogDistanceOffset) 62 | } 63 | 64 | shader.uniforms = UniformsUtils.merge([ 65 | shader.uniforms, 66 | timeUniform, 67 | { 68 | relativeHeightOffset: { value: relativeHeightOffset }, 69 | waveAmplitude: { value: waveAmplitude }, 70 | waveSpeed: { value: waveSpeed }, 71 | waveTangentialAmplitude: { value: waveTangentialAmplitude }, 72 | }, 73 | ]) 74 | 75 | // We need to reassign this because `UniformsUtils.merge` clones each uniform and in this process the connection to 76 | // the original object is lost... 77 | timeUniform.time = shader.uniforms.time 78 | 79 | shader.vertexShader = shader.vertexShader.replace( 80 | `#define STANDARD`, 81 | `#define STANDARD 82 | uniform float time; 83 | uniform float waveSpeed; 84 | uniform float waveAmplitude; 85 | uniform float waveTangentialAmplitude; 86 | uniform float relativeHeightOffset;`, 87 | ) 88 | 89 | shader.vertexShader = shader.vertexShader.replace( 90 | `void main() {`, 91 | `vec2 hash22(vec2 p) 92 | { 93 | vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); 94 | p3 += dot(p3, p3.yzx+33.33); 95 | return fract((p3.xx+p3.yz)*p3.zy); 96 | } 97 | 98 | vec2 wavedx(vec2 position, vec2 direction, float frequency, float timeshift) { 99 | float x = dot(direction, position) * frequency + timeshift; 100 | float wave = exp(sin(x) - 1.0); 101 | float dx = wave * cos(x); 102 | return vec2(wave, -dx); 103 | } 104 | 105 | float getWaves(vec2 position, int iterations, float speed) { 106 | float wavePhaseShift = length(position) * 0.1; // this is to avoid every octave having exactly the same phase everywhere 107 | float iter = 0.0; // this will help generating well distributed wave directions 108 | float frequency = 1.0; // frequency of the wave, this will change every iteration 109 | float timeMultiplier = 2.0; // time multiplier for the wave, this will change every iteration 110 | float weight = 1.0;// weight in final sum for the wave, this will change every iteration 111 | float sumOfValues = 0.0; // will store final sum of values 112 | float sumOfWeights = 0.0; // will store final sum of weights 113 | for(int i=0; i < iterations; i++) { 114 | // generate some wave direction that looks kind of random 115 | vec2 p = vec2(sin(iter), cos(iter)); 116 | 117 | // calculate wave data 118 | vec2 res = wavedx(position, p, frequency, time * speed * timeMultiplier + wavePhaseShift); 119 | 120 | // shift position around according to wave drag and derivative of the wave 121 | position += p * res.y * weight * 0.38; 122 | 123 | // add the results to sums 124 | sumOfValues += res.x * weight; 125 | sumOfWeights += weight; 126 | 127 | // modify next octave 128 | weight = mix(weight, 0.0, 0.2); 129 | frequency *= 1.18; 130 | timeMultiplier *= 1.07; 131 | 132 | // add some kind of random value to make next wave look random too 133 | iter += 1232.399963; 134 | } 135 | // calculate and return 136 | return sumOfValues / sumOfWeights; 137 | } 138 | 139 | void main() {`, 140 | ) 141 | shader.vertexShader = shader.vertexShader.replace( 142 | `#include `, 143 | `#include 144 | transformed.x += getWaves(transformed.${waterSwingDirection}, 2, waveSpeed * 0.3667233576) * waveTangentialAmplitude; 145 | transformed.z += getWaves(transformed.${waterSwingDirection} + vec2(4.0), 2, waveSpeed * 0.72357832) * waveTangentialAmplitude; 146 | transformed.y += getWaves(transformed.${waterSwingDirection}, 12, waveSpeed) * waveAmplitude + relativeHeightOffset * waveAmplitude; 147 | #ifdef USE_ALPHAHASH 148 | vPosition = transformed; 149 | #endif`, 150 | ) 151 | } 152 | 153 | return Object.assign(waterMaterial, { uniforms: timeUniform }) 154 | } 155 | -------------------------------------------------------------------------------- /src/components/CameraAndControls.vue: -------------------------------------------------------------------------------- 1 | 185 | 186 | 219 | 220 | 222 | -------------------------------------------------------------------------------- /src/assets/models/BuildArea/BuildArea.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset":{ 3 | "copyright":"https://github.com/toddeTV/zlig", 4 | "generator":"Khronos glTF Blender I/O v4.2.69", 5 | "version":"2.0" 6 | }, 7 | "extensionsUsed":[ 8 | "KHR_draco_mesh_compression" 9 | ], 10 | "extensionsRequired":[ 11 | "KHR_draco_mesh_compression" 12 | ], 13 | "scene":0, 14 | "scenes":[ 15 | { 16 | "extras":{ 17 | }, 18 | "name":"BuildArea", 19 | "nodes":[ 20 | 1 21 | ] 22 | } 23 | ], 24 | "nodes":[ 25 | { 26 | "mesh":0, 27 | "name":"gate.002" 28 | }, 29 | { 30 | "children":[ 31 | 0 32 | ], 33 | "mesh":1, 34 | "name":"BuildArea" 35 | } 36 | ], 37 | "materials":[ 38 | { 39 | "name":"wood", 40 | "pbrMetallicRoughness":{ 41 | "baseColorFactor":[ 42 | 1, 43 | 0.5568627715110779, 44 | 0.3843137323856354, 45 | 1 46 | ], 47 | "metallicFactor":0 48 | } 49 | }, 50 | { 51 | "name":"stone", 52 | "pbrMetallicRoughness":{ 53 | "baseColorFactor":[ 54 | 0.7215686440467834, 55 | 0.886274516582489, 56 | 0.9098039269447327, 57 | 1 58 | ], 59 | "metallicFactor":0 60 | } 61 | }, 62 | { 63 | "name":"woodDark", 64 | "pbrMetallicRoughness":{ 65 | "baseColorFactor":[ 66 | 0.7686274647712708, 67 | 0.4274509847164154, 68 | 0.29411765933036804, 69 | 1 70 | ], 71 | "metallicFactor":0 72 | } 73 | }, 74 | { 75 | "name":"hexagons_medieval", 76 | "pbrMetallicRoughness":{ 77 | "baseColorTexture":{ 78 | "index":0 79 | }, 80 | "metallicFactor":0, 81 | "roughnessFactor":0.5 82 | } 83 | } 84 | ], 85 | "meshes":[ 86 | { 87 | "name":"Mesh gate.002", 88 | "primitives":[ 89 | { 90 | "attributes":{ 91 | "POSITION":0, 92 | "NORMAL":1, 93 | "TEXCOORD_0":2 94 | }, 95 | "extensions":{ 96 | "KHR_draco_mesh_compression":{ 97 | "bufferView":0, 98 | "attributes":{ 99 | "POSITION":0, 100 | "NORMAL":1, 101 | "TEXCOORD_0":2 102 | } 103 | } 104 | }, 105 | "indices":3, 106 | "material":0, 107 | "mode":4 108 | }, 109 | { 110 | "attributes":{ 111 | "POSITION":4, 112 | "NORMAL":5, 113 | "TEXCOORD_0":6 114 | }, 115 | "extensions":{ 116 | "KHR_draco_mesh_compression":{ 117 | "bufferView":1, 118 | "attributes":{ 119 | "POSITION":0, 120 | "NORMAL":1, 121 | "TEXCOORD_0":2 122 | } 123 | } 124 | }, 125 | "indices":7, 126 | "material":1, 127 | "mode":4 128 | }, 129 | { 130 | "attributes":{ 131 | "POSITION":8, 132 | "NORMAL":9, 133 | "TEXCOORD_0":10 134 | }, 135 | "extensions":{ 136 | "KHR_draco_mesh_compression":{ 137 | "bufferView":2, 138 | "attributes":{ 139 | "POSITION":0, 140 | "NORMAL":1, 141 | "TEXCOORD_0":2 142 | } 143 | } 144 | }, 145 | "indices":11, 146 | "material":2, 147 | "mode":4 148 | } 149 | ] 150 | }, 151 | { 152 | "name":"Mesh fence_gate.002", 153 | "primitives":[ 154 | { 155 | "attributes":{ 156 | "POSITION":12, 157 | "NORMAL":13, 158 | "TEXCOORD_0":14 159 | }, 160 | "extensions":{ 161 | "KHR_draco_mesh_compression":{ 162 | "bufferView":3, 163 | "attributes":{ 164 | "POSITION":0, 165 | "NORMAL":1, 166 | "TEXCOORD_0":2 167 | } 168 | } 169 | }, 170 | "indices":15, 171 | "material":0, 172 | "mode":4 173 | }, 174 | { 175 | "attributes":{ 176 | "POSITION":16, 177 | "NORMAL":17, 178 | "TEXCOORD_0":18 179 | }, 180 | "extensions":{ 181 | "KHR_draco_mesh_compression":{ 182 | "bufferView":4, 183 | "attributes":{ 184 | "POSITION":0, 185 | "NORMAL":1, 186 | "TEXCOORD_0":2 187 | } 188 | } 189 | }, 190 | "indices":19, 191 | "material":2, 192 | "mode":4 193 | }, 194 | { 195 | "attributes":{ 196 | "POSITION":20, 197 | "NORMAL":21, 198 | "TEXCOORD_0":22 199 | }, 200 | "extensions":{ 201 | "KHR_draco_mesh_compression":{ 202 | "bufferView":5, 203 | "attributes":{ 204 | "POSITION":0, 205 | "NORMAL":1, 206 | "TEXCOORD_0":2 207 | } 208 | } 209 | }, 210 | "indices":23, 211 | "material":3, 212 | "mode":4 213 | } 214 | ] 215 | } 216 | ], 217 | "textures":[ 218 | { 219 | "sampler":0, 220 | "source":0 221 | } 222 | ], 223 | "images":[ 224 | { 225 | "mimeType":"image/png", 226 | "name":"ID_19", 227 | "uri":"../_textures/ID_19.png" 228 | } 229 | ], 230 | "accessors":[ 231 | { 232 | "componentType":5126, 233 | "count":370, 234 | "max":[ 235 | 0.37500008940696716, 236 | 0.796381950378418, 237 | 1.2500001192092896 238 | ], 239 | "min":[ 240 | -0.37499985098838806, 241 | 0.11200696229934692, 242 | 1.0750000476837158 243 | ], 244 | "type":"VEC3" 245 | }, 246 | { 247 | "componentType":5126, 248 | "count":370, 249 | "type":"VEC3" 250 | }, 251 | { 252 | "componentType":5126, 253 | "count":370, 254 | "type":"VEC2" 255 | }, 256 | { 257 | "componentType":5123, 258 | "count":834, 259 | "type":"SCALAR" 260 | }, 261 | { 262 | "componentType":5126, 263 | "count":24, 264 | "max":[ 265 | 0.37500008940696716, 266 | 0.6088216304779053, 267 | 1.25 268 | ], 269 | "min":[ 270 | 0.3402988314628601, 271 | 0.2537466287612915, 272 | 1.0750000476837158 273 | ], 274 | "type":"VEC3" 275 | }, 276 | { 277 | "componentType":5126, 278 | "count":24, 279 | "type":"VEC3" 280 | }, 281 | { 282 | "componentType":5126, 283 | "count":24, 284 | "type":"VEC2" 285 | }, 286 | { 287 | "componentType":5123, 288 | "count":36, 289 | "type":"SCALAR" 290 | }, 291 | { 292 | "componentType":5126, 293 | "count":106, 294 | "max":[ 295 | 0.2794337868690491, 296 | 0.7006444931030273, 297 | 1.2500001192092896 298 | ], 299 | "min":[ 300 | -0.2794335186481476, 301 | 0.2075732797384262, 302 | 1.0750000476837158 303 | ], 304 | "type":"VEC3" 305 | }, 306 | { 307 | "componentType":5126, 308 | "count":106, 309 | "type":"VEC3" 310 | }, 311 | { 312 | "componentType":5126, 313 | "count":106, 314 | "type":"VEC2" 315 | }, 316 | { 317 | "componentType":5123, 318 | "count":318, 319 | "type":"SCALAR" 320 | }, 321 | { 322 | "componentType":5126, 323 | "count":568, 324 | "max":[ 325 | 1.2500015497207642, 326 | 0.8625682592391968, 327 | 1.2500001192092896 328 | ], 329 | "min":[ 330 | -1.2500008344650269, 331 | 0, 332 | -1.2500015497207642 333 | ], 334 | "type":"VEC3" 335 | }, 336 | { 337 | "componentType":5126, 338 | "count":568, 339 | "type":"VEC3" 340 | }, 341 | { 342 | "componentType":5126, 343 | "count":568, 344 | "type":"VEC2" 345 | }, 346 | { 347 | "componentType":5123, 348 | "count":1068, 349 | "type":"SCALAR" 350 | }, 351 | { 352 | "componentType":5126, 353 | "count":184, 354 | "max":[ 355 | 1.250001311302185, 356 | 0.8625682592391968, 357 | 1.25 358 | ], 359 | "min":[ 360 | -1.2500008344650269, 361 | 0, 362 | -1.250001311302185 363 | ], 364 | "type":"VEC3" 365 | }, 366 | { 367 | "componentType":5126, 368 | "count":184, 369 | "type":"VEC3" 370 | }, 371 | { 372 | "componentType":5126, 373 | "count":184, 374 | "type":"VEC2" 375 | }, 376 | { 377 | "componentType":5123, 378 | "count":360, 379 | "type":"SCALAR" 380 | }, 381 | { 382 | "componentType":5126, 383 | "count":320, 384 | "max":[ 385 | 0.6300708651542664, 386 | 0.6999936103820801, 387 | 0.5787343978881836 388 | ], 389 | "min":[ 390 | -0.627744197845459, 391 | -1.043081283569336e-06, 392 | -0.5721339583396912 393 | ], 394 | "type":"VEC3" 395 | }, 396 | { 397 | "componentType":5126, 398 | "count":320, 399 | "type":"VEC3" 400 | }, 401 | { 402 | "componentType":5126, 403 | "count":320, 404 | "type":"VEC2" 405 | }, 406 | { 407 | "componentType":5123, 408 | "count":660, 409 | "type":"SCALAR" 410 | } 411 | ], 412 | "bufferViews":[ 413 | { 414 | "buffer":0, 415 | "byteLength":3178, 416 | "byteOffset":0 417 | }, 418 | { 419 | "buffer":0, 420 | "byteLength":343, 421 | "byteOffset":3180 422 | }, 423 | { 424 | "buffer":0, 425 | "byteLength":1054, 426 | "byteOffset":3524 427 | }, 428 | { 429 | "buffer":0, 430 | "byteLength":2298, 431 | "byteOffset":4580 432 | }, 433 | { 434 | "buffer":0, 435 | "byteLength":1175, 436 | "byteOffset":6880 437 | }, 438 | { 439 | "buffer":0, 440 | "byteLength":2387, 441 | "byteOffset":8056 442 | } 443 | ], 444 | "samplers":[ 445 | { 446 | "magFilter":9729, 447 | "minFilter":9987 448 | } 449 | ], 450 | "buffers":[ 451 | { 452 | "byteLength":10444, 453 | "uri":"BuildArea.bin" 454 | } 455 | ] 456 | } 457 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zlig 2 | 3 | ## project overview 4 | 5 | This project represents a japanese style idle game. 6 | 7 | Short project name: `zlig` (stands for `zen-landscape-idle-game`) 8 | 9 | Current deployment on: 10 | 11 | 12 | 13 | - production 14 | - branch `main`: [https://zlig.net/](https://zlig.net/)
15 | (This is a custom domain for [https://zlig.vercel.app/](https://zlig.vercel.app/)) 16 | - development 17 | - branch `dev`: [https://zlig-git-dev-toddetv-projects.vercel.app/](https://zlig-git-dev-toddetv-projects.vercel.app/) 18 | - each PR will get a individual custom URL automatically 19 | 20 | ## Sponsoring 21 | 22 | If you like this project and want to support us, we would be very happy to see you as a sponsor on GitHub ❤️
23 | You can find the `Sponsor` button on the top right of the [GitHub project page](https://github.com/toddeTV/zlig).
24 | Thanks a lot for the support <3 25 | 26 | ## dev 27 | 28 | ### commands 29 | 30 | - cleanup 31 | - `sudo rm -rf build dist .output .data` 32 | - `sudo rm -rf node_modules` 33 | 34 | ### initial setup 35 | 36 | #### VM ID 37 | 38 | Development VM ID from Thorsten for this project: `014`
39 | (Only interesting to him.) 40 | 41 | #### system requirements for developing 42 | 43 | The following softwares are required for development:
44 | (The versions listed were the ones I most recently used for development and testing. So try sticking with them.) 45 | 46 | | software | command for version output | my version at last use | information | 47 | | -------- | ----------------------------------------- | ---------------------- | -------------------- | 48 | | Ubuntu | `lsb_release -a` or `cat /etc/os-release` | 22.04.4 LTS | OS | 49 | | Linux | `uname -r` | 5.15.0-125-generic | Linux Kernel | 50 | | VSCode | `code -v` | 1.93.2 | IDE | 51 | | nvm | `nvm -v` | v0.40.1 | Node Version Manager | 52 | | Node | `node -v` (old `nodejs --version`) | v22.13.0 | NodeJS/ Node.js | 53 | | npm | `npm -v` | v10.9.2 | | 54 | | npx | `npx -v` | v10.9.2 | | 55 | | pnpm | `pnpm -v` | v10.0.0 | | 56 | 57 | In the browser install: 58 | 59 | - [VueJS Devtools](https://devtools.vuejs.org/guide/installation.html) 60 | 61 | #### project setup 62 | 63 | 1. execute a `git pull` 64 | 2. open project in VSCode 65 | 3. If you work with VSCode via remote software: 66 | - `{Ctrl}+{Shift}+{P}` -> `>Preferences: Open Settings (UI)` -> search for `keyboard.dispatch` and set it to `keyCode` 67 | - Restart or reload VSCode. 68 | 4. Install recommended extensions/ plugins: 69 | - Open Extensions menu in VSCode (`{Ctrl}+{Shift}+{X}`) 70 | - type in the search `@recommended` 71 | - install and enable the plugins 72 | - see file `.vscode/extensions.json` for configuring some of the extensions 73 | - Restart or reload VSCode. 74 | 5. In VSCode on the bottom left click your profile image and log in all services (GitHub due to VSCode extensions, ...)
75 | If the browser to VSCode callback fails, wait for the login popup on the bottom right to timeout (ca. 5 minutes) and 76 | then on the upcoming popup question `You have not yet finished authorizing [...] Would you like to try a different way? (local server)` click `Yes` and use this alternative login mechanic.
77 | (When you do not want to wait for the timeout to happen, you can also click the `Cancel` to trigger the dialog faster.) 78 | 6. Install dependencies: `pnpm i` 79 | 7. Happy coding <3 80 | 81 | ### namings 82 | 83 | - we use `build area` and `building area` (not `building lot` nor `building place`). 84 | 85 | ### lint and prettier 86 | 87 | This project uses [antfu/eslint-config](https://github.com/antfu/eslint-config) for eslint most of the files. 88 | The following extend it: 89 | 90 | - [antfu/eslint-plugin-format](https://github.com/antfu/eslint-plugin-format) for using external formatters like 91 | e.g. `prettier` for the file types that eslint cannot handle. 92 | - [azat-io/eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) for 93 | sorting object keys, imports, etc. - with auto-fix. 94 | 95 | Keep in mind that the plugin names are renamed, see 96 | [Plugins Rename](https://github.com/antfu/eslint-config?tab=readme-ov-file#plugins-renaming), e.g.: 97 | 98 | ```diff 99 | -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions 100 | +// eslint-disable-next-line ts/consistent-type-definitions 101 | type foo = { bar: 2 } 102 | ``` 103 | 104 | [Why I don't use Prettier for every file type](https://antfu.me/posts/why-not-prettier) 105 | 106 | ### Design 107 | 108 | #### used icon collections 109 | 110 | This project uses the following icon collections in descending order, try sticking to them and use from top to bottom. 111 | Tipp: Favorite them and use the search over all item collections at once: https://icon-sets.iconify.design/?list=favorite 112 | 113 | | full name | shorthand | license | note | 114 | | ----------------------- | --------- | --------------------------------------------------------------- | -------------- | 115 | | `phosphor` | `ph` | MIT | | 116 | | `Material Design Icons` | `mdi` | Apache 2.0 (commercial use is allowed, no attribution required) | | 117 | | `Material Line Icons` | `line-md` | MIT | animated icons | 118 | 119 | ### glTF export with draco compression 120 | 121 | Use [draco compression](https://github.com/google/draco) in [Blender](https://www.blender.org/) on 122 | [glTF 2](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/README.md) files: 123 | 124 | 1. Create the model in blender 125 | 2. Go to `File` -> `Export` -> `glTF 2.0 (.glb/.gltf)` 126 | 3. In the right side panel 127 | 1. set `Format` to `glTF Seperate (.gltf + .bin + textures)` 128 | 2. set `Remember Export Settings` to `true` 129 | 3. under `Data` set `Compression` to `true` 130 | 4. under `Compression` set the compression level between 0-6 (0=less compression; 6=strongest compression)
131 | I am using `6` most of the time - only when morphing between models, this should be set to `0`. 132 | 133 | ### TresJS & ThreeJS 134 | 135 | #### possible import locations 136 | 137 | Do **not** import from `three` directly but from the direct source instead. Example: 138 | 139 | ```diff 140 | -import { MeshLambertMaterial } from 'three' 141 | +import { MeshLambertMaterial } from 'three/src/materials/MeshLambertMaterial.js' 142 | ``` 143 | 144 | Seems to work for types, but better use the direct import from the source folder (see above): 145 | 146 | ```vue 147 | 150 | ``` 151 | 152 | ```vue 153 | 162 | ``` 163 | 164 | #### import and use `*.gltf` model files 165 | 166 | _Method 1: With direct useGLTF_ 167 | 168 | ```vue 169 | 174 | 175 | 178 | ``` 179 | 180 | - All `*.gltf` models, along with their corresponding `*.bin` and texture files, are located in `/public/`. 181 | The `useGLTF` fetches it over HTTP 182 | - Bc of gltf-files present in the public folder, all gltf files are always bundled in the final app on build, 183 | regardless whether they are used or not. 184 | - no types 185 | - Direct call of `useGLTF`, no extra layers or wrappers 186 | 187 | --- 188 | 189 | _Method 2: With generated helper wrappers and type definitions_ 190 | 191 | ```vue 192 | 197 | 198 | 201 | ``` 202 | 203 | - All `*.gltf` models, along with their corresponding `*.bin` and texture files, are located in `/src/assets/models/`. 204 | - To generate helper wrappers and type definitions, run: 205 | 206 | ```sh 207 | pnpm run generate:gltf-models 208 | ``` 209 | 210 | This script scans all model files in the source folder, deconstructs the GLTF JSON representation, and places 211 | the generated types in `./node_modules/.tmp/model-types/`, ensuring only imported models are included in the 212 | final product. 213 | 214 | The script runs automatically: 215 | 216 | - always a `.gltf` file changes 217 | - before a dev run 218 | - before a build 219 | - after `pnpm i` 220 | 221 | - Nearly type safe GLTF file representations.
222 | Importing models is type-safe, and builds will fail if a model is missing. 223 | - Only the used models are bundled in the final product. 224 | - On runtime: Runs `useGLTF` under the hood. So 100% correct objects and usage, no extra layer.
225 | In dev: Scans the `*.gltf` file on its own, so the generated typing has redundant code and could be different 226 | from what is present on runtime. So be careful when using and test/ double check it!
227 | Example: In `gltfModel.scenes.someScene.traversed.Object` the typing only hints real objects and not each 228 | primitive that is used to build up the objects. But in runtime these primitives are also present in the 229 | `traversed.Object` - but funnily enough not all ... that is the reason I left away all primitives, just to be sure. 230 | 231 | #### Multiple instances of the same model 232 | 233 | For already loaded and parsed models the GLTF loader returns a cached version. So `primitive` uses then the same 234 | model which means the single instance is unmounted and mounted again with other coordinates. 235 | 236 | Solution: clone it 237 | 238 | ```vue 239 | 243 | 244 | 247 | ``` 248 | 249 | #### real time shadows 250 | 251 | Example shadow configuration below. 252 | The so generated shadows currently have heavy artifacts, so we bake the shadows in this project. 253 | But here is an example configuration for shadows: 254 | 255 | ```vue 256 | 269 | 270 | 285 | ``` 286 | 287 | #### postprocessing 288 | 289 | Normally, you could use the packages `@tresjs/post-processing` and `postprocessing` in combination for postprocessing 290 | TresJS/ ThreeJS. Unfortunately, they are currently not compatible with TresJS core >v4, see 291 | [comment on issue #16](https://github.com/toddeTV/zlig/issues/16#issuecomment-2246317999) and 292 | [issue #32](https://github.com/toddeTV/zlig/issues/32). 293 | 294 | ### Docs and helper websites 295 | 296 | - icon browser 297 | - [iconify](https://icon-sets.iconify.design/ph/) (not recommended, but really good) 298 | - [icones](https://icones.js.org/collection/ph) (recommended, but not so good) 299 | - [TailwindCSS cheat sheet](https://nerdcave.com/tailwind-cheat-sheet) 300 | - [TresJS docs](https://docs.tresjs.org/guide/) 301 | 302 | ## prod 303 | 304 | Will use the build command out of `/package.json`.
305 | Building, deploying and hosting is done via [Vercel](https://vercel.com/toddetv-projects/zlig). 306 | 307 | ## Attribution/ Contribution 308 | 309 | Project founder & head of project: 310 | 311 | - [Thorsten Seyschab](https://todde.tv) 312 | 313 | Honorable mentions to people that helped this project: 314 | 315 | - [Andreas Fehn](https://github.com/fehnomenal) helped with the project. Thank you <3 316 | - [Moritz Starke](https://github.com/Myzios) helped with general 3D and Blender questions. Thanks mate <3 317 | - [gundul0](https://github.com/gundul0) helped with the island's design concept and overall concept ideas. Thanks a lot <3 318 | 319 | Respectable mentions to projects that helped this project: 320 | 321 | - \[currently none\] 322 | 323 | Used programs/ softwares, services and dependencies - besides the ones in `./package.json`: 324 | 325 | - [GitHub Copilot](https://github.com/features/copilot) was used in private mode for programming questions. 326 | - [Blender](https://www.blender.org/) was used as software for creating and editing 3D models. 327 | - [ChatGPT](https://chatgpt.com/) used for DALL-E image generation, text based questions and programming code generation. 328 | 329 | Used assets/ materials including images and 3D models: 330 | 331 | - Internally identified with ID `14`: 332 | - Name: `Nature Kit` 333 | - Cost: free 334 | - License: [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) (commercial use allowed, no credits needed) 335 | - Author: [Kenney](https://kenney.nl/) 336 | - Downloaded: 337 | - Date: 2025-03-04 338 | - From: [kenney](https://kenney.nl/assets/nature-kit) 339 | - Internally identified with ID `19`: 340 | - Name: `KayKit - Medieval Hexagon Pack` 341 | - Cost: free 342 | - License: [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) (commercial use allowed, no credits needed) 343 | - Author: [Kay Lousberg](https://kaylousberg.itch.io/) 344 | - Downloaded: 345 | - Date: 2025-03-04 346 | - From: [itch.io](https://kaylousberg.itch.io/kaykit-medieval-hexagon) 347 | 348 | ## License 349 | 350 | Copyright (c) 2024-present, [Thorsten Seyschab](https://todde.tv) 351 | 352 | This project, including original code and models, is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License ([CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)). Under this license, others are allowed to remix, adapt, and build upon this work non-commercially, provided they credit the project founder and license any derivative works under the same terms. 353 | 354 | Please note that this license applies only to the original content authored by the project’s creators. Third-party libraries, assets, 3D models, and other materials utilized in this project are listed under "Attribution/ Contribution" above and remain the property of their original creators, licensed under their respective terms. 355 | 356 | The project founder reserves the right to modify the terms of this license or to offer different licensing arrangements for specific use cases. 357 | 358 | For the full license text, please see the [LICENSE](./LICENSE.md) file. 359 | 360 | ### Need a Different License? 361 | 362 | If you are interested in discussing a different licensing arrangement for individual use cases, please feel free to reach out. Custom licensing may be available, but it is not guaranteed. 363 | -------------------------------------------------------------------------------- /src/assets/models/Buildings/Buildings.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset":{ 3 | "copyright":"https://github.com/toddeTV/zlig", 4 | "generator":"Khronos glTF Blender I/O v4.2.69", 5 | "version":"2.0" 6 | }, 7 | "extensionsUsed":[ 8 | "KHR_draco_mesh_compression" 9 | ], 10 | "extensionsRequired":[ 11 | "KHR_draco_mesh_compression" 12 | ], 13 | "scene":0, 14 | "scenes":[ 15 | { 16 | "name":"Buildings", 17 | "nodes":[ 18 | 2, 19 | 3, 20 | 4, 21 | 5, 22 | 8, 23 | 9, 24 | 10 25 | ] 26 | } 27 | ], 28 | "nodes":[ 29 | { 30 | "mesh":0, 31 | "name":"building_windmill_top_fan_yellow.003", 32 | "translation":[ 33 | -1.9073486328125e-05, 34 | 0.6791393756866455, 35 | 0.8303518295288086 36 | ] 37 | }, 38 | { 39 | "children":[ 40 | 0 41 | ], 42 | "mesh":1, 43 | "name":"building_windmill_top_yellow.003", 44 | "translation":[ 45 | 0, 46 | 1.7134180068969727, 47 | 0 48 | ] 49 | }, 50 | { 51 | "children":[ 52 | 1 53 | ], 54 | "mesh":2, 55 | "name":"building_windmill_yellow.003", 56 | "translation":[ 57 | 3.517876625061035, 58 | 0, 59 | -9.552626609802246 60 | ] 61 | }, 62 | { 63 | "mesh":3, 64 | "name":"building_well_yellow.003", 65 | "translation":[ 66 | -2.7321231365203857, 67 | 0, 68 | -9.552626609802246 69 | ] 70 | }, 71 | { 72 | "mesh":4, 73 | "name":"building_tavern_yellow.003", 74 | "translation":[ 75 | -8.982123374938965, 76 | 0, 77 | -3.302626371383667 78 | ] 79 | }, 80 | { 81 | "mesh":5, 82 | "name":"building_market_yellow.003", 83 | "translation":[ 84 | 9.767876625061035, 85 | 0, 86 | 2.947373628616333 87 | ] 88 | }, 89 | { 90 | "mesh":6, 91 | "name":"building_lumbermill_saw_yellow.003", 92 | "translation":[ 93 | 0.6999893188476562, 94 | 0.7000026106834412, 95 | 0 96 | ] 97 | }, 98 | { 99 | "mesh":7, 100 | "name":"building_lumbermill_top_yellow.003", 101 | "translation":[ 102 | -0.3500211238861084, 103 | 2.625000238418579, 104 | -0.34999847412109375 105 | ] 106 | }, 107 | { 108 | "children":[ 109 | 6, 110 | 7 111 | ], 112 | "mesh":8, 113 | "name":"building_lumbermill_yellow.003", 114 | "translation":[ 115 | 3.517876625061035, 116 | 0, 117 | 2.947373628616333 118 | ] 119 | }, 120 | { 121 | "mesh":9, 122 | "name":"building_home_A_yellow.003", 123 | "translation":[ 124 | -8.982123374938965, 125 | 0, 126 | 2.947373628616333 127 | ] 128 | }, 129 | { 130 | "mesh":10, 131 | "name":"building_blacksmith_yellow.003", 132 | "translation":[ 133 | 3.517876625061035, 134 | 0, 135 | 9.197373390197754 136 | ] 137 | } 138 | ], 139 | "materials":[ 140 | { 141 | "name":"hexagons_medieval", 142 | "pbrMetallicRoughness":{ 143 | "baseColorTexture":{ 144 | "index":0 145 | }, 146 | "metallicFactor":0, 147 | "roughnessFactor":0.5 148 | } 149 | } 150 | ], 151 | "meshes":[ 152 | { 153 | "name":"building_windmill_top_fan_yellow.003", 154 | "primitives":[ 155 | { 156 | "attributes":{ 157 | "POSITION":0, 158 | "NORMAL":1, 159 | "TEXCOORD_0":2 160 | }, 161 | "extensions":{ 162 | "KHR_draco_mesh_compression":{ 163 | "bufferView":0, 164 | "attributes":{ 165 | "POSITION":0, 166 | "NORMAL":1, 167 | "TEXCOORD_0":2 168 | } 169 | } 170 | }, 171 | "indices":3, 172 | "material":0, 173 | "mode":4 174 | } 175 | ] 176 | }, 177 | { 178 | "name":"building_windmill_top_yellow.003", 179 | "primitives":[ 180 | { 181 | "attributes":{ 182 | "POSITION":4, 183 | "NORMAL":5, 184 | "TEXCOORD_0":6 185 | }, 186 | "extensions":{ 187 | "KHR_draco_mesh_compression":{ 188 | "bufferView":1, 189 | "attributes":{ 190 | "POSITION":0, 191 | "NORMAL":1, 192 | "TEXCOORD_0":2 193 | } 194 | } 195 | }, 196 | "indices":7, 197 | "material":0, 198 | "mode":4 199 | } 200 | ] 201 | }, 202 | { 203 | "name":"building_windmill_yellow.003", 204 | "primitives":[ 205 | { 206 | "attributes":{ 207 | "POSITION":8, 208 | "NORMAL":9, 209 | "TEXCOORD_0":10 210 | }, 211 | "extensions":{ 212 | "KHR_draco_mesh_compression":{ 213 | "bufferView":2, 214 | "attributes":{ 215 | "POSITION":0, 216 | "NORMAL":1, 217 | "TEXCOORD_0":2 218 | } 219 | } 220 | }, 221 | "indices":11, 222 | "material":0, 223 | "mode":4 224 | } 225 | ] 226 | }, 227 | { 228 | "name":"building_well_yellow.003", 229 | "primitives":[ 230 | { 231 | "attributes":{ 232 | "POSITION":12, 233 | "NORMAL":13, 234 | "TEXCOORD_0":14 235 | }, 236 | "extensions":{ 237 | "KHR_draco_mesh_compression":{ 238 | "bufferView":3, 239 | "attributes":{ 240 | "POSITION":0, 241 | "NORMAL":1, 242 | "TEXCOORD_0":2 243 | } 244 | } 245 | }, 246 | "indices":15, 247 | "material":0, 248 | "mode":4 249 | } 250 | ] 251 | }, 252 | { 253 | "name":"building_tavern_yellow.003", 254 | "primitives":[ 255 | { 256 | "attributes":{ 257 | "POSITION":16, 258 | "NORMAL":17, 259 | "TEXCOORD_0":18 260 | }, 261 | "extensions":{ 262 | "KHR_draco_mesh_compression":{ 263 | "bufferView":4, 264 | "attributes":{ 265 | "POSITION":0, 266 | "NORMAL":1, 267 | "TEXCOORD_0":2 268 | } 269 | } 270 | }, 271 | "indices":19, 272 | "material":0, 273 | "mode":4 274 | } 275 | ] 276 | }, 277 | { 278 | "name":"building_market_yellow.003", 279 | "primitives":[ 280 | { 281 | "attributes":{ 282 | "POSITION":20, 283 | "NORMAL":21, 284 | "TEXCOORD_0":22 285 | }, 286 | "extensions":{ 287 | "KHR_draco_mesh_compression":{ 288 | "bufferView":5, 289 | "attributes":{ 290 | "POSITION":0, 291 | "NORMAL":1, 292 | "TEXCOORD_0":2 293 | } 294 | } 295 | }, 296 | "indices":23, 297 | "material":0, 298 | "mode":4 299 | } 300 | ] 301 | }, 302 | { 303 | "name":"building_lumbermill_saw_yellow.003", 304 | "primitives":[ 305 | { 306 | "attributes":{ 307 | "POSITION":24, 308 | "NORMAL":25, 309 | "TEXCOORD_0":26 310 | }, 311 | "extensions":{ 312 | "KHR_draco_mesh_compression":{ 313 | "bufferView":6, 314 | "attributes":{ 315 | "POSITION":0, 316 | "NORMAL":1, 317 | "TEXCOORD_0":2 318 | } 319 | } 320 | }, 321 | "indices":27, 322 | "material":0, 323 | "mode":4 324 | } 325 | ] 326 | }, 327 | { 328 | "name":"building_lumbermill_top_yellow.003", 329 | "primitives":[ 330 | { 331 | "attributes":{ 332 | "POSITION":28, 333 | "NORMAL":29, 334 | "TEXCOORD_0":30 335 | }, 336 | "extensions":{ 337 | "KHR_draco_mesh_compression":{ 338 | "bufferView":7, 339 | "attributes":{ 340 | "POSITION":0, 341 | "NORMAL":1, 342 | "TEXCOORD_0":2 343 | } 344 | } 345 | }, 346 | "indices":31, 347 | "material":0, 348 | "mode":4 349 | } 350 | ] 351 | }, 352 | { 353 | "name":"building_lumbermill_yellow.003", 354 | "primitives":[ 355 | { 356 | "attributes":{ 357 | "POSITION":32, 358 | "NORMAL":33, 359 | "TEXCOORD_0":34 360 | }, 361 | "extensions":{ 362 | "KHR_draco_mesh_compression":{ 363 | "bufferView":8, 364 | "attributes":{ 365 | "POSITION":0, 366 | "NORMAL":1, 367 | "TEXCOORD_0":2 368 | } 369 | } 370 | }, 371 | "indices":35, 372 | "material":0, 373 | "mode":4 374 | } 375 | ] 376 | }, 377 | { 378 | "name":"building_home_A_yellow.003", 379 | "primitives":[ 380 | { 381 | "attributes":{ 382 | "POSITION":36, 383 | "NORMAL":37, 384 | "TEXCOORD_0":38 385 | }, 386 | "extensions":{ 387 | "KHR_draco_mesh_compression":{ 388 | "bufferView":9, 389 | "attributes":{ 390 | "POSITION":0, 391 | "NORMAL":1, 392 | "TEXCOORD_0":2 393 | } 394 | } 395 | }, 396 | "indices":39, 397 | "material":0, 398 | "mode":4 399 | } 400 | ] 401 | }, 402 | { 403 | "name":"building_blacksmith_yellow.003", 404 | "primitives":[ 405 | { 406 | "attributes":{ 407 | "POSITION":40, 408 | "NORMAL":41, 409 | "TEXCOORD_0":42 410 | }, 411 | "extensions":{ 412 | "KHR_draco_mesh_compression":{ 413 | "bufferView":10, 414 | "attributes":{ 415 | "POSITION":0, 416 | "NORMAL":1, 417 | "TEXCOORD_0":2 418 | } 419 | } 420 | }, 421 | "indices":43, 422 | "material":0, 423 | "mode":4 424 | } 425 | ] 426 | } 427 | ], 428 | "textures":[ 429 | { 430 | "sampler":0, 431 | "source":0 432 | } 433 | ], 434 | "images":[ 435 | { 436 | "mimeType":"image/png", 437 | "name":"ID_19", 438 | "uri":"../_textures/ID_19.png" 439 | } 440 | ], 441 | "accessors":[ 442 | { 443 | "componentType":5126, 444 | "count":710, 445 | "max":[ 446 | 1.2521045207977295, 447 | 1.2521042823791504, 448 | 0.26360249519348145 449 | ], 450 | "min":[ 451 | -1.2521045207977295, 452 | -1.2521045207977295, 453 | 0 454 | ], 455 | "type":"VEC3" 456 | }, 457 | { 458 | "componentType":5126, 459 | "count":710, 460 | "type":"VEC3" 461 | }, 462 | { 463 | "componentType":5126, 464 | "count":710, 465 | "type":"VEC2" 466 | }, 467 | { 468 | "componentType":5123, 469 | "count":1080, 470 | "type":"SCALAR" 471 | }, 472 | { 473 | "componentType":5126, 474 | "count":698, 475 | "max":[ 476 | 0.8300111293792725, 477 | 1.7344918251037598, 478 | 0.830350399017334 479 | ], 480 | "min":[ 481 | -0.8300111293792725, 482 | 0, 483 | -0.8300110697746277 484 | ], 485 | "type":"VEC3" 486 | }, 487 | { 488 | "componentType":5126, 489 | "count":698, 490 | "type":"VEC3" 491 | }, 492 | { 493 | "componentType":5126, 494 | "count":698, 495 | "type":"VEC2" 496 | }, 497 | { 498 | "componentType":5123, 499 | "count":1539, 500 | "type":"SCALAR" 501 | }, 502 | { 503 | "componentType":5126, 504 | "count":2588, 505 | "max":[ 506 | 1.419677972793579, 507 | 1.7131643295288086, 508 | 1.0374996662139893 509 | ], 510 | "min":[ 511 | -1.3947467803955078, 512 | -3.620029747253284e-05, 513 | -0.9473490118980408 514 | ], 515 | "type":"VEC3" 516 | }, 517 | { 518 | "componentType":5126, 519 | "count":2588, 520 | "type":"VEC3" 521 | }, 522 | { 523 | "componentType":5126, 524 | "count":2588, 525 | "type":"VEC2" 526 | }, 527 | { 528 | "componentType":5123, 529 | "count":5340, 530 | "type":"SCALAR" 531 | }, 532 | { 533 | "componentType":5126, 534 | "count":1253, 535 | "max":[ 536 | 0.842090904712677, 537 | 2.0650272369384766, 538 | 0.9384045004844666 539 | ], 540 | "min":[ 541 | -0.7873932123184204, 542 | -4.028901912533911e-06, 543 | -0.9384045004844666 544 | ], 545 | "type":"VEC3" 546 | }, 547 | { 548 | "componentType":5126, 549 | "count":1253, 550 | "type":"VEC3" 551 | }, 552 | { 553 | "componentType":5126, 554 | "count":1253, 555 | "type":"VEC2" 556 | }, 557 | { 558 | "componentType":5123, 559 | "count":2178, 560 | "type":"SCALAR" 561 | }, 562 | { 563 | "componentType":5126, 564 | "count":4922, 565 | "max":[ 566 | 1.4418730735778809, 567 | 3.491617441177368, 568 | 1.5750017166137695 569 | ], 570 | "min":[ 571 | -1.4874471426010132, 572 | -7.718801498413086e-06, 573 | -1.7557921409606934 574 | ], 575 | "type":"VEC3" 576 | }, 577 | { 578 | "componentType":5126, 579 | "count":4922, 580 | "type":"VEC3" 581 | }, 582 | { 583 | "componentType":5126, 584 | "count":4922, 585 | "type":"VEC2" 586 | }, 587 | { 588 | "componentType":5123, 589 | "count":8976, 590 | "type":"SCALAR" 591 | }, 592 | { 593 | "componentType":5126, 594 | "count":4215, 595 | "max":[ 596 | 2.245000123977661, 597 | 2.440002918243408, 598 | 1.5015063285827637 599 | ], 600 | "min":[ 601 | -2.2534518241882324, 602 | -0.014096181839704514, 603 | -1.7874999046325684 604 | ], 605 | "type":"VEC3" 606 | }, 607 | { 608 | "componentType":5126, 609 | "count":4215, 610 | "type":"VEC3" 611 | }, 612 | { 613 | "componentType":5126, 614 | "count":4215, 615 | "type":"VEC2" 616 | }, 617 | { 618 | "componentType":5123, 619 | "count":9375, 620 | "type":"SCALAR" 621 | }, 622 | { 623 | "componentType":5126, 624 | "count":576, 625 | "max":[ 626 | 0.3500000238418579, 627 | 0.612500011920929, 628 | 0.612500011920929 629 | ], 630 | "min":[ 631 | -0.3500000834465027, 632 | -0.6124998331069946, 633 | -0.6125000715255737 634 | ], 635 | "type":"VEC3" 636 | }, 637 | { 638 | "componentType":5126, 639 | "count":576, 640 | "type":"VEC3" 641 | }, 642 | { 643 | "componentType":5126, 644 | "count":576, 645 | "type":"VEC2" 646 | }, 647 | { 648 | "componentType":5123, 649 | "count":900, 650 | "type":"SCALAR" 651 | }, 652 | { 653 | "componentType":5126, 654 | "count":1615, 655 | "max":[ 656 | 0.9887634515762329, 657 | 1.6358051300048828, 658 | 2.1748993396759033 659 | ], 660 | "min":[ 661 | -0.7875000834465027, 662 | -2.980232238769531e-07, 663 | -0.7875000238418579 664 | ], 665 | "type":"VEC3" 666 | }, 667 | { 668 | "componentType":5126, 669 | "count":1615, 670 | "type":"VEC3" 671 | }, 672 | { 673 | "componentType":5126, 674 | "count":1615, 675 | "type":"VEC2" 676 | }, 677 | { 678 | "componentType":5123, 679 | "count":3450, 680 | "type":"SCALAR" 681 | }, 682 | { 683 | "componentType":5126, 684 | "count":2588, 685 | "max":[ 686 | 1.7020965814590454, 687 | 2.62500262260437, 688 | 1.4194283485412598 689 | ], 690 | "min":[ 691 | -1.7153210639953613, 692 | -0.008758492767810822, 693 | -1.1481832265853882 694 | ], 695 | "type":"VEC3" 696 | }, 697 | { 698 | "componentType":5126, 699 | "count":2588, 700 | "type":"VEC3" 701 | }, 702 | { 703 | "componentType":5126, 704 | "count":2588, 705 | "type":"VEC2" 706 | }, 707 | { 708 | "componentType":5123, 709 | "count":5160, 710 | "type":"SCALAR" 711 | }, 712 | { 713 | "componentType":5126, 714 | "count":1581, 715 | "max":[ 716 | 0.9899497032165527, 717 | 2.325000286102295, 718 | 0.9625002145767212 719 | ], 720 | "min":[ 721 | -0.9899498224258423, 722 | -5.2154067731180476e-08, 723 | -1.1716289520263672 724 | ], 725 | "type":"VEC3" 726 | }, 727 | { 728 | "componentType":5126, 729 | "count":1581, 730 | "type":"VEC3" 731 | }, 732 | { 733 | "componentType":5126, 734 | "count":1581, 735 | "type":"VEC2" 736 | }, 737 | { 738 | "componentType":5123, 739 | "count":3033, 740 | "type":"SCALAR" 741 | }, 742 | { 743 | "componentType":5126, 744 | "count":3517, 745 | "max":[ 746 | 1.643990397453308, 747 | 2.450005531311035, 748 | 1.712876319885254 749 | ], 750 | "min":[ 751 | -1.5750120878219604, 752 | -0.01275584101676941, 753 | -1.3999987840652466 754 | ], 755 | "type":"VEC3" 756 | }, 757 | { 758 | "componentType":5126, 759 | "count":3517, 760 | "type":"VEC3" 761 | }, 762 | { 763 | "componentType":5126, 764 | "count":3517, 765 | "type":"VEC2" 766 | }, 767 | { 768 | "componentType":5123, 769 | "count":7230, 770 | "type":"SCALAR" 771 | } 772 | ], 773 | "bufferViews":[ 774 | { 775 | "buffer":0, 776 | "byteLength":4006, 777 | "byteOffset":0 778 | }, 779 | { 780 | "buffer":0, 781 | "byteLength":4818, 782 | "byteOffset":4008 783 | }, 784 | { 785 | "buffer":0, 786 | "byteLength":19520, 787 | "byteOffset":8828 788 | }, 789 | { 790 | "buffer":0, 791 | "byteLength":8530, 792 | "byteOffset":28348 793 | }, 794 | { 795 | "buffer":0, 796 | "byteLength":32010, 797 | "byteOffset":36880 798 | }, 799 | { 800 | "buffer":0, 801 | "byteLength":28046, 802 | "byteOffset":68892 803 | }, 804 | { 805 | "buffer":0, 806 | "byteLength":3897, 807 | "byteOffset":96940 808 | }, 809 | { 810 | "buffer":0, 811 | "byteLength":10940, 812 | "byteOffset":100840 813 | }, 814 | { 815 | "buffer":0, 816 | "byteLength":17552, 817 | "byteOffset":111780 818 | }, 819 | { 820 | "buffer":0, 821 | "byteLength":9856, 822 | "byteOffset":129332 823 | }, 824 | { 825 | "buffer":0, 826 | "byteLength":25399, 827 | "byteOffset":139188 828 | } 829 | ], 830 | "samplers":[ 831 | { 832 | "magFilter":9729, 833 | "minFilter":9987 834 | } 835 | ], 836 | "buffers":[ 837 | { 838 | "byteLength":164588, 839 | "uri":"Buildings.bin" 840 | } 841 | ] 842 | } 843 | --------------------------------------------------------------------------------