feat(falukant): render houses with 3D models
Some checks failed
Deploy to production / deploy (push) Failing after 1m17s
Some checks failed
Deploy to production / deploy (push) Failing after 1m17s
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Optimiert alle Falukant-Charakter-GLBs mit Draco + Textur-Optimierung.
|
* Optimiert Falukant-Charakter- und Haus-GLBs mit Draco + Textur-Optimierung.
|
||||||
* Ausgabe: *_opt.glb im selben Verzeichnis.
|
* Ausgabe: *_opt.glb im selben Verzeichnis.
|
||||||
* Voraussetzung: npm install (@gltf-transform/cli als Dev-Dep)
|
* Voraussetzung: npm install (@gltf-transform/cli als Dev-Dep)
|
||||||
*
|
*
|
||||||
* Aufruf: npm run optimize-models
|
* Aufruf: npm run optimize-models
|
||||||
* Optional können MODEL_SOURCE_DIR und MODEL_OUTPUT_DIR gesetzt werden, um
|
* Optional können MODEL_SOURCE_DIR und MODEL_OUTPUT_DIR gesetzt werden, um
|
||||||
* Modelle aus dem separaten YourPart3Assets-Repository zu verarbeiten.
|
* genau einen Modellordner aus dem separaten YourPart3Assets-Repository zu
|
||||||
|
* verarbeiten.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -15,24 +16,22 @@ import { fileURLToPath } from 'url';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const defaultSourceDir = path.join(__dirname, '..', 'models-src', '3d', 'falukant', 'characters');
|
const modelsRoot = path.join(__dirname, '..', 'models-src', '3d', 'falukant');
|
||||||
const defaultOutputDir = path.join(__dirname, '..', 'public', 'models', '3d', 'falukant', 'characters');
|
const publicModelsRoot = path.join(__dirname, '..', 'public', 'models', '3d', 'falukant');
|
||||||
const sourceDir = path.resolve(process.env.MODEL_SOURCE_DIR || defaultSourceDir);
|
|
||||||
const outputDir = path.resolve(process.env.MODEL_OUTPUT_DIR || defaultOutputDir);
|
|
||||||
const cli = path.join(__dirname, '..', 'node_modules', '.bin', 'gltf-transform');
|
const cli = path.join(__dirname, '..', 'node_modules', '.bin', 'gltf-transform');
|
||||||
|
|
||||||
function listSourceModels() {
|
function listSourceModels(sourceDir) {
|
||||||
return fs.readdirSync(sourceDir)
|
return fs.readdirSync(sourceDir)
|
||||||
.filter((file) => file.endsWith('.glb'))
|
.filter((file) => file.endsWith('.glb'))
|
||||||
.filter((file) => !file.endsWith('_opt.glb'))
|
.filter((file) => !file.endsWith('_opt.glb'))
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureOutputDir() {
|
function ensureOutputDir(outputDir) {
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStaleOptimizedModels() {
|
function removeStaleOptimizedModels(outputDir) {
|
||||||
if (!fs.existsSync(outputDir)) {
|
if (!fs.existsSync(outputDir)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,43 +42,74 @@ function removeStaleOptimizedModels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Optimize Falukant character GLBs (Draco + texture 1024)\n');
|
function optimizeModelDirectory({ name, sourceDir, outputDir, required = false }) {
|
||||||
|
if (!fs.existsSync(sourceDir)) {
|
||||||
if (!fs.existsSync(sourceDir)) {
|
if (required) {
|
||||||
console.error(`Source directory not found: ${sourceDir}`);
|
console.error(`Source directory not found: ${sourceDir}`);
|
||||||
process.exit(1);
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
ensureOutputDir();
|
}
|
||||||
removeStaleOptimizedModels();
|
|
||||||
|
ensureOutputDir(outputDir);
|
||||||
const models = listSourceModels();
|
removeStaleOptimizedModels(outputDir);
|
||||||
if (models.length === 0) {
|
|
||||||
console.log('No source GLBs found.');
|
const models = listSourceModels(sourceDir);
|
||||||
process.exit(0);
|
if (models.length === 0) {
|
||||||
}
|
console.log(`No ${name} GLBs found.`);
|
||||||
|
return;
|
||||||
for (const f of models) {
|
}
|
||||||
const input = path.join(sourceDir, f);
|
|
||||||
const out = f.replace(/\.glb$/, '_opt.glb');
|
console.log(`Optimize Falukant ${name} GLBs (Draco + texture 1024)\n`);
|
||||||
const output = path.join(outputDir, out);
|
|
||||||
|
for (const f of models) {
|
||||||
try {
|
const input = path.join(sourceDir, f);
|
||||||
const result = spawnSync(
|
const out = f.replace(/\.glb$/, '_opt.glb');
|
||||||
'node',
|
const output = path.join(outputDir, out);
|
||||||
[cli, 'optimize', input, output, '--compress', 'draco', '--texture-size', '1024'],
|
|
||||||
{
|
try {
|
||||||
stdio: 'inherit',
|
const result = spawnSync(
|
||||||
cwd: path.join(__dirname, '..'),
|
'node',
|
||||||
}
|
[cli, 'optimize', input, output, '--compress', 'draco', '--texture-size', '1024'],
|
||||||
);
|
{
|
||||||
if (result.status !== 0) {
|
stdio: 'inherit',
|
||||||
throw new Error(`gltf-transform exited with ${result.status}`);
|
cwd: path.join(__dirname, '..'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`gltf-transform exited with ${result.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed: ${f}`);
|
||||||
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed: ${f}`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nDone. Optimized models written to ${outputDir}`);
|
const customSourceDir = process.env.MODEL_SOURCE_DIR;
|
||||||
|
const customOutputDir = process.env.MODEL_OUTPUT_DIR;
|
||||||
|
if (Boolean(customSourceDir) !== Boolean(customOutputDir)) {
|
||||||
|
console.error('MODEL_SOURCE_DIR and MODEL_OUTPUT_DIR must be set together.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customSourceDir) {
|
||||||
|
optimizeModelDirectory({
|
||||||
|
name: 'custom',
|
||||||
|
sourceDir: path.resolve(customSourceDir),
|
||||||
|
outputDir: path.resolve(customOutputDir),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
optimizeModelDirectory({
|
||||||
|
name: 'character',
|
||||||
|
sourceDir: path.join(modelsRoot, 'characters'),
|
||||||
|
outputDir: path.join(publicModelsRoot, 'characters'),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
optimizeModelDirectory({
|
||||||
|
name: 'house',
|
||||||
|
sourceDir: path.join(modelsRoot, 'houses'),
|
||||||
|
outputDir: path.join(publicModelsRoot, 'houses'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
156
frontend/src/components/falukant/House3D.vue
Normal file
156
frontend/src/components/falukant/House3D.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="container" class="house-3d" role="img" :aria-label="label || 'Haus'">
|
||||||
|
<span v-if="loadFailed" class="house-3d__unavailable">3D-Modell nicht verfügbar</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
const MODEL_BASE_PATH = '/models/3d/falukant/houses';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'House3D',
|
||||||
|
props: {
|
||||||
|
model: { type: String, required: true },
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
scene: null,
|
||||||
|
camera: null,
|
||||||
|
renderer: null,
|
||||||
|
renderedModel: null,
|
||||||
|
resizeObserver: null,
|
||||||
|
loadFailed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
model() {
|
||||||
|
this.loadModel();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.init();
|
||||||
|
await this.loadModel();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.dispose();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const container = this.$refs.container;
|
||||||
|
if (!container || import.meta.env.VITE_DISABLE_3D === 'true') return;
|
||||||
|
|
||||||
|
const runtime = await import('@/utils/threeRuntime.js');
|
||||||
|
this.scene = markRaw(new runtime.Scene());
|
||||||
|
this.scene.background = new runtime.Color(0xf7f1e6);
|
||||||
|
this.camera = markRaw(new runtime.PerspectiveCamera(36, 1, 0.1, 100));
|
||||||
|
this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: false }));
|
||||||
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
container.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
this.scene.add(new runtime.HemisphereLight(0xfff8e8, 0x665b50, 2.2));
|
||||||
|
const keyLight = new runtime.DirectionalLight(0xffffff, 2.8);
|
||||||
|
keyLight.position.set(4, 7, 5);
|
||||||
|
this.scene.add(keyLight);
|
||||||
|
const fillLight = new runtime.DirectionalLight(0xffead0, 1.2);
|
||||||
|
fillLight.position.set(-4, 3, -4);
|
||||||
|
this.scene.add(fillLight);
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => this.render());
|
||||||
|
this.resizeObserver.observe(container);
|
||||||
|
},
|
||||||
|
async loadModel() {
|
||||||
|
if (!this.scene || !this.renderer || !this.model) return;
|
||||||
|
this.loadFailed = false;
|
||||||
|
this.removeModel();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [loaders, modelRuntime] = await Promise.all([
|
||||||
|
import('@/utils/threeLoaders.js'),
|
||||||
|
import('@/utils/threeModelRuntime.js'),
|
||||||
|
]);
|
||||||
|
const dracoLoader = new loaders.DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('/draco/gltf/');
|
||||||
|
const loader = new loaders.GLTFLoader();
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
const gltf = await loader.loadAsync(`${MODEL_BASE_PATH}/${this.model}_opt.glb`);
|
||||||
|
dracoLoader.dispose();
|
||||||
|
|
||||||
|
this.renderedModel = markRaw(gltf.scene);
|
||||||
|
const box = new modelRuntime.Box3().setFromObject(this.renderedModel);
|
||||||
|
const size = box.getSize(new modelRuntime.Vector3());
|
||||||
|
const maxDimension = Math.max(size.x, size.y, size.z);
|
||||||
|
if (!Number.isFinite(maxDimension) || maxDimension <= 0) throw new Error('Empty house model');
|
||||||
|
|
||||||
|
const targetSize = 2.6;
|
||||||
|
const scale = targetSize / maxDimension;
|
||||||
|
this.renderedModel.scale.setScalar(scale);
|
||||||
|
const scaledBox = new modelRuntime.Box3().setFromObject(this.renderedModel);
|
||||||
|
const center = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||||
|
this.renderedModel.position.set(-center.x, -scaledBox.min.y, -center.z);
|
||||||
|
this.scene.add(this.renderedModel);
|
||||||
|
this.render();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unable to load 3D house model '${this.model}'`, error);
|
||||||
|
this.loadFailed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
const container = this.$refs.container;
|
||||||
|
if (!container || !this.renderer || !this.camera || !this.scene) return;
|
||||||
|
const width = Math.max(container.clientWidth, 1);
|
||||||
|
const height = Math.max(container.clientHeight, 1);
|
||||||
|
this.camera.aspect = width / height;
|
||||||
|
this.camera.position.set(0, 1.5, 4.1);
|
||||||
|
this.camera.lookAt(0, 1.05, 0);
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.renderer.setSize(width, height, false);
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
},
|
||||||
|
removeModel() {
|
||||||
|
if (!this.renderedModel) return;
|
||||||
|
this.scene.remove(this.renderedModel);
|
||||||
|
this.renderedModel.traverse((object) => {
|
||||||
|
object.geometry?.dispose();
|
||||||
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||||
|
materials.filter(Boolean).forEach((material) => material.dispose());
|
||||||
|
});
|
||||||
|
this.renderedModel = null;
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
this.resizeObserver?.disconnect();
|
||||||
|
this.removeModel();
|
||||||
|
if (this.renderer) {
|
||||||
|
this.$refs.container?.removeChild(this.renderer.domElement);
|
||||||
|
this.renderer.dispose();
|
||||||
|
}
|
||||||
|
this.scene?.clear();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.house-3d {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: inherit;
|
||||||
|
background: #f7f1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.house-3d__unavailable {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
place-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
frontend/src/utils/falukantHouseModels.js
Normal file
15
frontend/src/utils/falukantHouseModels.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const HOUSE_MODELS = Object.freeze({
|
||||||
|
'Unter der Brücke': 'under_bridge',
|
||||||
|
'Strohhütte': 'straw_hut',
|
||||||
|
Holzhaus: 'wooden_house',
|
||||||
|
Hinterhofzimmer: 'backyard_room',
|
||||||
|
'Kleines Familienhaus': 'family_house',
|
||||||
|
Stadthaus: 'townhouse',
|
||||||
|
Villa: 'villa',
|
||||||
|
Herrenhaus: 'mansion',
|
||||||
|
Schloss: 'castle',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getFalukantHouseModel(houseType) {
|
||||||
|
return HOUSE_MODELS[houseType?.labelTr] || 'under_bridge';
|
||||||
|
}
|
||||||
@@ -19,7 +19,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<div class="existing-house">
|
<div class="existing-house">
|
||||||
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
|
<House3D
|
||||||
|
v-if="houseType?.labelTr"
|
||||||
|
class="house"
|
||||||
|
:model="houseModel(houseType)"
|
||||||
|
:label="$t(`falukant.house.type.${houseType.labelTr}`)"
|
||||||
|
/>
|
||||||
<div class="status-panel surface-card">
|
<div class="status-panel surface-card">
|
||||||
<h3>{{ $t('falukant.house.statusreport') }}</h3>
|
<h3>{{ $t('falukant.house.statusreport') }}</h3>
|
||||||
<div class="status-cards">
|
<div class="status-cards">
|
||||||
@@ -128,8 +133,12 @@
|
|||||||
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
|
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
|
||||||
<div class="houses-list">
|
<div class="houses-list">
|
||||||
<div v-for="house in buyableHouses" :key="house.id" class="house-item">
|
<div v-for="house in buyableHouses" :key="house.id" class="house-item">
|
||||||
<div :style="house.houseType ? houseStyle(house.houseType.position, 114) : {}"
|
<House3D
|
||||||
class="house-preview"></div>
|
v-if="house.houseType?.labelTr"
|
||||||
|
class="house-preview"
|
||||||
|
:model="houseModel(house.houseType)"
|
||||||
|
:label="$t(`falukant.house.type.${house.houseType.labelTr}`)"
|
||||||
|
/>
|
||||||
<div class="house-info">
|
<div class="house-info">
|
||||||
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
|
||||||
<div class="buyable-house-stats">
|
<div class="buyable-house-stats">
|
||||||
@@ -155,13 +164,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||||
|
import House3D from '@/components/falukant/House3D.vue';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import { showError, showSuccess, confirmAction } from '@/utils/feedback.js';
|
import { showError, showSuccess, confirmAction } from '@/utils/feedback.js';
|
||||||
|
import { getFalukantHouseModel } from '@/utils/falukantHouseModels.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HouseView',
|
name: 'HouseView',
|
||||||
components: { StatusBar },
|
components: { StatusBar, House3D },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
userHouse: null,
|
userHouse: null,
|
||||||
@@ -222,17 +233,8 @@ export default {
|
|||||||
if (v >= 1) return this.$t('falukant.conditionBand.catastrophic');
|
if (v >= 1) return this.$t('falukant.conditionBand.catastrophic');
|
||||||
return this.$t('falukant.conditionBand.unknown');
|
return this.$t('falukant.conditionBand.unknown');
|
||||||
},
|
},
|
||||||
houseStyle(position, picSize) {
|
houseModel(houseType) {
|
||||||
const columns = 3;
|
return getFalukantHouseModel(houseType);
|
||||||
const size = picSize;
|
|
||||||
const index = position - 1;
|
|
||||||
const x = (index % columns) * size;
|
|
||||||
const y = Math.floor(index / columns) * size;
|
|
||||||
return {
|
|
||||||
backgroundImage: 'url("/images/falukant/houses.png")',
|
|
||||||
backgroundPosition: `-${x}px -${y}px`,
|
|
||||||
backgroundSize: `${columns * size}px auto`
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
formatPrice(value) {
|
formatPrice(value) {
|
||||||
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
||||||
@@ -456,9 +458,6 @@ h2 {
|
|||||||
.house {
|
.house {
|
||||||
width: 341px;
|
width: 341px;
|
||||||
height: 341px;
|
height: 341px;
|
||||||
background-repeat: no-repeat;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,14 +596,7 @@ h2 {
|
|||||||
.house-preview {
|
.house-preview {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background-repeat: no-repeat;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-size: contain;
|
|
||||||
/* scale image to container */
|
|
||||||
background-position: center;
|
|
||||||
/* center sprite */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.house-info {
|
.house-info {
|
||||||
|
|||||||
@@ -29,7 +29,12 @@
|
|||||||
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
|
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
|
||||||
<div v-if="showPortraitAvatar" :style="getAvatarStyle" class="avatar"></div>
|
<div v-if="showPortraitAvatar" :style="getAvatarStyle" class="avatar"></div>
|
||||||
<div v-if="showCharacterFigure3d" class="house-with-character">
|
<div v-if="showCharacterFigure3d" class="house-with-character">
|
||||||
<div :style="getHouseStyle" class="house"></div>
|
<House3D
|
||||||
|
v-if="falukantUser.userHouse?.houseType"
|
||||||
|
class="house"
|
||||||
|
:model="houseModel"
|
||||||
|
:label="$t(`falukant.house.type.${falukantUser.userHouse.houseType.labelTr}`)"
|
||||||
|
/>
|
||||||
<div class="character-foreground">
|
<div class="character-foreground">
|
||||||
<Character3D
|
<Character3D
|
||||||
:gender="falukantUser.character.gender"
|
:gender="falukantUser.character.gender"
|
||||||
@@ -365,6 +370,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||||
import Character3D from '@/components/Character3D.vue';
|
import Character3D from '@/components/Character3D.vue';
|
||||||
|
import House3D from '@/components/falukant/House3D.vue';
|
||||||
|
import { getFalukantHouseModel } from '@/utils/falukantHouseModels.js';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
@@ -426,6 +433,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Character3D,
|
Character3D,
|
||||||
|
House3D,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -463,25 +471,8 @@ export default {
|
|||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getHouseStyle() {
|
houseModel() {
|
||||||
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
|
return getFalukantHouseModel(this.falukantUser?.userHouse?.houseType);
|
||||||
const imageUrl = '/images/falukant/houses.png';
|
|
||||||
const pos = this.falukantUser.userHouse.houseType.position;
|
|
||||||
const index = pos - 1;
|
|
||||||
const columns = 3;
|
|
||||||
const spriteSize = 300;
|
|
||||||
const x = (index % columns) * spriteSize;
|
|
||||||
const y = Math.floor(index / columns) * spriteSize;
|
|
||||||
return {
|
|
||||||
backgroundImage: `url(${imageUrl})`,
|
|
||||||
backgroundPosition: `-${x}px -${y}px`,
|
|
||||||
backgroundSize: `${columns * spriteSize}px auto`,
|
|
||||||
width: `300px`,
|
|
||||||
height: `300px`,
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
borderRadius: '4px',
|
|
||||||
imageRendering: 'crisp-edges',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
moneyValue() {
|
moneyValue() {
|
||||||
const m = this.falukantUser?.money;
|
const m = this.falukantUser?.money;
|
||||||
|
|||||||
Reference in New Issue
Block a user