Compare commits
2 commits
3bcf836c63
...
b27b5ea041
Author | SHA1 | Date | |
---|---|---|---|
b27b5ea041 | |||
a82cabac15 |
3 changed files with 1820 additions and 1091 deletions
|
@ -303,7 +303,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="../js/scriptalpha.js"></script>
|
<script src="../js/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
2034
js/script.js
2034
js/script.js
File diff suppressed because it is too large
Load diff
|
@ -1,19 +1,18 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const Biome = {
|
const BIOME_TYPE = {
|
||||||
WATER_DEEP: { sprite:null, design: {frames: 8,duration: 200, drawer: drawDeepWaterFrame}, acceptStructure:false, movements:['swim'], affinities:[('water',0.8),('dark',0.2)], name: 'Eau Profonde', winterColor: '#3D5A80', fallColor: '#3D5A80', summerColor: '#3D5A80', autumnColor: '#3D5A80',maxElevation:0,minElevation:0 },
|
WATER_DEEP: { sprite: null, design: { frames: 8, duration: 200, drawer: drawDeepWaterFrame }, acceptStructure: false, movements: ['swim'], affinities: [{ type: 'water', value: 0.8 }, { type: 'dark', value: 0.2 }], name: 'Eau Profonde', winterColor: '#3D5A80', fallColor: '#3D5A80', summerColor: '#3D5A80', autumnColor: '#3D5A80', maxElevation: 0, minElevation: 0 },
|
||||||
WATER_SHALLOW: { sprite:null, design: {frames: 6,duration: 150, drawer: drawWaterFrame}, acceptStructure:false,movements:['swim','ride','navigate','fly'], affinities:[('water',0.8),('life',0.2)],name: 'Eau Peu Profonde', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8',maxElevation:1,minElevation:1 },
|
WATER_SHALLOW: { sprite: null, design: { frames: 6, duration: 150, drawer: drawWaterFrame }, acceptStructure: false, movements: ['swim', 'ride', 'navigate', 'fly'], affinities: [{ type: 'water', value: 0.8 }, { type: 'life', value: 0.2 }], name: 'Eau Peu Profonde', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8', maxElevation: 1, minElevation: 1 },
|
||||||
BEACH: { sprite:null, design: {frames: 4,duration: 200, drawer: drawBeachFrame}, acceptStructure:true, movements:['walk','ride','fly'],affinities:[('sand',0.8),('water',0.2)],name: 'Sable', winterColor: '#E9D9A1', fallColor: '#E9D9A1', summerColor: '#E9D9A1', autumnColor: '#E9D9A1',maxElevation:2 ,minElevation:2 },
|
BEACH: { sprite: null, design: { frames: 4, duration: 200, drawer: drawBeachFrame }, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'sand', value: 0.8 }, { type: 'water', value: 0.2 }], name: 'Sable', winterColor: '#E9D9A1', fallColor: '#E9D9A1', summerColor: '#E9D9A1', autumnColor: '#E9D9A1', maxElevation: 2, minElevation: 2 },
|
||||||
GRASSLAND: {sprite:null, design: { frames: 4,duration: 200, drawer: drawGrasslandFrame}, acceptStructure:true, movements:['walk','ride','fly'],affinities:[('life',0.6),('earth',0.2)],name: 'Plaine', winterColor: '#ecf1e3ff', fallColor: '#98C159', summerColor: '#a5a450ff', autumnColor: '#455e21ff',maxElevation:3,minElevation:2 },
|
GRASSLAND: { sprite: null, design: { frames: 4, duration: 200, drawer: drawGrasslandFrame }, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'life', value: 0.6 }, { type: 'earth', value: 0.2 }], name: 'Plaine', winterColor: '#ecf1e3ff', fallColor: '#98C159', summerColor: '#a5a450ff', autumnColor: '#455e21ff', maxElevation: 3, minElevation: 2 },
|
||||||
FOREST: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('wood',0.6),('earth',0.4)], name: 'Forêt', winterColor: '#92ac83ff', fallColor: '#21a32cff', summerColor: '#6A994E', autumnColor: '#b88a28ff',maxElevation:3,minElevation:2 },
|
FOREST: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'wood', value: 0.6 }, { type: 'earth', value: 0.4 }], name: 'Forêt', winterColor: '#92ac83ff', fallColor: '#21a32cff', summerColor: '#6A994E', autumnColor: '#b88a28ff', maxElevation: 3, minElevation: 2 },
|
||||||
ENCHANTED_FOREST: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('wood',0.8),('dark',0.2),('life',0.2)], name: 'Forêt Enchantée', winterColor: '#7B6094', fallColor: '#7B6094', summerColor: '#7B6094', autumnColor: '#7B6094',maxElevation:3,minElevation:2 },
|
ENCHANTED_FOREST: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'wood', value: 0.8 }, { type: 'dark', value: 0.2 }, { type: 'life', value: 0.2 }], name: 'Forêt Enchantée', winterColor: '#7B6094', fallColor: '#7B6094', summerColor: '#7B6094', autumnColor: '#7B6094', maxElevation: 3, minElevation: 2 },
|
||||||
MOUNTAIN: { sprite:null, design: { frames: 1,duration: 9999, drawer: drawMountainFrame}, acceptStructure:false, movements:['climb','fly'],affinities:[('rock',0.6),('wind',0.4)],name: 'Montagne', winterColor: '#F7F7F7', fallColor: '#A9A9A9', summerColor: '#A9A9A9', autumnColor: '#A9A9A9',maxElevation:5,minElevation:3 },
|
MOUNTAIN: { sprite: null, design: { frames: 1, duration: 9999, drawer: drawMountainFrame }, acceptStructure: false, movements: ['climb', 'fly'], affinities: [{ type: 'rock', value: 0.6 }, { type: 'wind', value: 0.4 }], name: 'Montagne', winterColor: '#F7F7F7', fallColor: '#A9A9A9', summerColor: '#A9A9A9', autumnColor: '#A9A9A9', maxElevation: 5, minElevation: 3 },
|
||||||
SNOWLAND: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'], affinities:[('ice',0.8),('earth',0.2)],name: 'Toundra', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7',maxElevation:2,minElevation:2 },
|
SNOWLAND: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'ice', value: 0.8 }, { type: 'earth', value: 0.2 }], name: 'Toundra', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7', maxElevation: 2, minElevation: 2 },
|
||||||
SNOWMOUNTAIN: { sprite: null, acceptStructure:false, movements:['climb','fly'],affinities:[('ice',0.4),('rock',0.4),('wind',0.2)],name: 'Mont enneigé', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7',maxElevation:8,minElevation:4 },
|
SNOWMOUNTAIN: { sprite: null, acceptStructure: false, movements: ['climb', 'fly'], affinities: [{ type: 'ice', value: 0.4 }, { type: 'rock', value: 0.4 }, { type: 'wind', value: 0.2 }], name: 'Mont enneigé', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7', maxElevation: 8, minElevation: 4 },
|
||||||
DESERT: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('sand',0.8),('life',0.1),('fire',0.1)], name: 'Désert', winterColor: '#D4A373', fallColor: '#D4A373', summerColor: '#D4A373', autumnColor: '#D4A373',maxElevation:2,minElevation:2 },
|
DESERT: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'sand', value: 0.8 }, { type: 'life', value: 0.1 }, { type: 'fire', value: 0.1 }], name: 'Désert', winterColor: '#D4A373', fallColor: '#D4A373', summerColor: '#D4A373', autumnColor: '#D4A373', maxElevation: 2, minElevation: 2 },
|
||||||
RIVER: { sprite: null, acceptStructure:false,movements:['navigate','swim','fly'],affinities:[('water',0.6),('earth',0.2),('life',0.2)],name: 'Rivière', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8',maxElevation:2,minElevation:2 },
|
RIVER: { sprite: null, acceptStructure: false, movements: ['navigate', 'swim', 'fly'], affinities: [{ type: 'water', value: 0.6 }, { type: 'earth', value: 0.2 }, { type: 'life', value: 0.2 }], name: 'Rivière', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8', maxElevation: 2, minElevation: 2 },
|
||||||
SWAMP: { sprite:null, design: { frames: 4,duration: 300, drawer: drawSwampFrame}, acceptStructure:false, movements:['fly'],affinities:[('water',0.6),('earth',0.2),('dark',0.2)],name: 'Marais', winterColor: '#0b2e10ff', fallColor: '#0b2e10ff', summerColor: '#0b2e10ff', autumnColor: '#0b2e10ff',maxElevation:1,minElevation:1 }
|
SWAMP: { sprite: null, design: { frames: 4, duration: 300, drawer: drawSwampFrame }, acceptStructure: false, movements: ['fly'], affinities: [{ type: 'water', value: 0.6 }, { type: 'earth', value: 0.2 }, { type: 'dark', value: 0.2 }], name: 'Marais', winterColor: '#0b2e10ff', fallColor: '#0b2e10ff', summerColor: '#0b2e10ff', autumnColor: '#0b2e10ff', maxElevation: 1, minElevation: 1 }
|
||||||
}
|
};
|
||||||
|
|
||||||
const JOB = {
|
const JOB = {
|
||||||
VILLAGER: {
|
VILLAGER: {
|
||||||
name: "Habitant",
|
name: "Habitant",
|
||||||
|
@ -103,9 +102,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMAL_TYPES = {
|
const ANIMAL_TYPES = {
|
||||||
WOLF: { name: 'Loup', movement: 'walk', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [Biome.FOREST, Biome.MOUNTAIN, Biome.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } },
|
WOLF: { name: 'Loup', movement: 'walk', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } },
|
||||||
BOAR: { name: 'Sanglier', movement: 'walk', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [Biome.FOREST, Biome.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } },
|
BOAR: { name: 'Sanglier', movement: 'walk', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } },
|
||||||
BIRD: { name: 'Aigle', movement: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [Biome.FOREST, Biome.GRASSLAND, Biome.MOUNTAIN, Biome.BEACH], spawnChance: 0.03, size: { w: 15, h: 12 }, offset: { x: -22, y: -45 }, flightHeight: 40 },
|
BIRD: { name: 'Aigle', movement: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.BEACH], spawnChance: 0.03, size: { w: 15, h: 12 }, offset: { x: -22, y: -45 }, flightHeight: 40 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Global Asset Variables ---
|
// --- Global Asset Variables ---
|
||||||
|
@ -300,71 +299,65 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
||||||
const targetTile = gameMap.tiles[newY][newX];
|
const targetTile = gameMap.tiles[newY][newX];
|
||||||
if (targetTile.biome && (targetTile.biome.movements.includes('walk') || targetTile.biome.movements.includes('climb')) && this.type.biomes.includes(targetTile.biome)) {
|
if (targetTile.biome && (targetTile.biome.type.movements.includes('walk') || targetTile.biome.type.movements.includes('climb')) && this.type.biomes.type.includes(targetTile.biome.type)) {
|
||||||
this.tile = targetTile;
|
this.tile = targetTile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDesign(ctx) {
|
setDesign(ctx) {
|
||||||
|
|
||||||
if (!this.tile || this.tile.visibility !== 2) return;
|
if (!this.tile || this.tile.visibility !== 2) return;
|
||||||
const svgImage = this.type.svgAsset();
|
const svgImage = this.type.svgAsset();
|
||||||
if (!svgImage) return;
|
if (!svgImage) return;
|
||||||
|
|
||||||
const screenPos = this.tile.position.cartToIso();
|
const screenPos = this.tile.position.cartToIso();
|
||||||
const elevationHeight = this.tile.position.h * ELEVATION_STEP;
|
const elevationHeight = this.tile.position.h * ELEVATION_STEP;
|
||||||
|
const flightHeight = this.type.flightHeight || 0;
|
||||||
const size = this.type.size || { w: 40, h: 40 };
|
const size = this.type.size || { w: 40, h: 40 };
|
||||||
const offset = this.type.offset || { x: -20, y: -35 };
|
const offset = this.type.offset || { x: -20, y: -35 };
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight - flightHeight);
|
||||||
|
if (flightHeight > 0) {
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(0, flightHeight, 8, 4, 0, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
ctx.drawImage(svgImage, offset.x, offset.y, size.w, size.h);
|
ctx.drawImage(svgImage, offset.x, offset.y, size.w, size.h);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Biome {
|
||||||
class Tile {
|
constructor() {
|
||||||
|
this.type=null;
|
||||||
constructor(x, y) {
|
|
||||||
this.position = new Position(x, y);
|
|
||||||
this.biome = null;
|
|
||||||
this.structure = null;
|
|
||||||
this.animal = null;
|
|
||||||
this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible
|
|
||||||
this.setBiome();
|
|
||||||
this.setElevation();
|
|
||||||
this.setStructure();
|
|
||||||
this.setEntity();
|
|
||||||
}
|
}
|
||||||
|
setType(position){
|
||||||
setBiome() {
|
|
||||||
let scale = 0.05;
|
let scale = 0.05;
|
||||||
let eRaw = (simplex.noise2D(this.position.x * scale, this.position.y * scale) + 1) / 2;
|
let eRaw = (simplex.noise2D(position.x * scale, position.y * scale) + 1) / 2;
|
||||||
let tRaw = (simplex.noise2D(this.position.x * scale * 0.8, this.position.y * scale * 0.8) + 1) / 2;
|
let tRaw = (simplex.noise2D(position.x * scale * 0.8, position.y * scale * 0.8) + 1) / 2;
|
||||||
let mRaw = (simplex.noise2D(this.position.x * scale * 1.5, this.position.y * scale * 1.5) + 1) / 2;
|
let mRaw = (simplex.noise2D(position.x * scale * 1.5, position.y * scale * 1.5) + 1) / 2;
|
||||||
if (eRaw < 0.25) this.biome = Biome.WATER_DEEP;
|
if (eRaw < 0.25) this.type = BIOME_TYPE.WATER_DEEP;
|
||||||
else if (eRaw < 0.3) this.biome = Biome.WATER_SHALLOW;
|
else if (eRaw < 0.3) this.type = BIOME_TYPE.WATER_SHALLOW;
|
||||||
else if (eRaw < 0.32) this.biome = Biome.SWAMP;
|
else if (eRaw < 0.32) this.type = BIOME_TYPE.SWAMP;
|
||||||
else if (eRaw < 0.35) this.biome = Biome.BEACH;
|
else if (eRaw < 0.35) this.type = BIOME_TYPE.BEACH;
|
||||||
else if (eRaw > 0.85) this.biome = Biome.SNOWMOUNTAIN;
|
else if (eRaw > 0.85) this.type = BIOME_TYPE.SNOWMOUNTAIN;
|
||||||
else if (eRaw > 0.75) this.biome = Biome.MOUNTAIN;
|
else if (eRaw > 0.75) this.type = BIOME_TYPE.MOUNTAIN;
|
||||||
else {
|
else {
|
||||||
if (tRaw < 0.3) this.biome = Biome.DESERT;
|
if (tRaw < 0.3) this.type = BIOME_TYPE.DESERT;
|
||||||
else if (tRaw > 0.6) this.biome = (mRaw > 0.7) ? Biome.ENCHANTED_FOREST : Biome.FOREST;
|
else if (tRaw > 0.6) this.type = (mRaw > 0.7) ? BIOME_TYPE.ENCHANTED_FOREST : BIOME_TYPE.FOREST;
|
||||||
else this.biome = Biome.GRASSLAND;
|
else this.type = BIOME_TYPE.GRASSLAND;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
setDesign(ctx, position){
|
||||||
setDesign(ctx){
|
const screenPos = position.cartToIso();
|
||||||
if (!this.biome || this.visibility === 0) return;
|
const elevationHeight = position.h * ELEVATION_STEP;
|
||||||
|
|
||||||
const screenPos = this.position.cartToIso();
|
|
||||||
const elevationHeight = this.position.h * ELEVATION_STEP;
|
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
||||||
|
|
||||||
const baseColor = this.biome.summerColor;
|
const baseColor = this.type.summerColor;
|
||||||
const shadowColor = shadeColor(baseColor, -30);
|
const shadowColor = shadeColor(baseColor, -30);
|
||||||
|
|
||||||
// Draw tile faces
|
// Draw tile faces
|
||||||
|
@ -381,8 +374,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Draw top surface using a sprite if available
|
// Draw top surface using a sprite if available
|
||||||
if (this.biome.sprite) {
|
if (this.type.sprite) {
|
||||||
this.biome.sprite.draw(ctx, -TILE_WIDTH / 2, 0);
|
this.type.sprite.draw(ctx, -TILE_WIDTH / 2, 0);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = baseColor;
|
ctx.fillStyle = baseColor;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
@ -390,20 +383,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
if (this.type === BIOME_TYPE.FOREST && forestSVG) {
|
||||||
|
ctx.drawImage(forestSVG, -25, -25, 50, 50);
|
||||||
|
} else if (this.type === BIOME_TYPE.ENCHANTED_FOREST && enchantedForestSVG) {
|
||||||
|
ctx.drawImage(enchantedForestSVG, -25, -25, 50, 50);
|
||||||
|
} else if (this.type === BIOME_TYPE.SWAMP && swampSVG) {
|
||||||
|
ctx.drawImage(swampSVG, -25, -5, 50, 30);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
class Tile {
|
||||||
|
|
||||||
|
constructor(x, y) {
|
||||||
|
this.position = new Position(x, y);
|
||||||
|
this.biome = null;
|
||||||
|
this.structure = null;
|
||||||
|
this.entities = [];
|
||||||
|
this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible
|
||||||
|
this.setBiome();
|
||||||
|
this.setElevation();
|
||||||
|
this.setStructure();
|
||||||
|
this.setEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
setBiome() {
|
||||||
|
this.biome=new Biome();
|
||||||
|
this.biome.setType(this.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDesign(ctx){
|
||||||
|
if (!this.biome || this.visibility === 0) return;
|
||||||
|
|
||||||
|
this.biome.setDesign(ctx,this.position);
|
||||||
|
|
||||||
// Draw features if fully visible
|
// Draw features if fully visible
|
||||||
if(this.visibility === 2) {
|
|
||||||
if (this.biome === Biome.FOREST && forestSVG) {
|
|
||||||
ctx.drawImage(forestSVG, -25, -25, 50, 50);
|
|
||||||
} else if (this.biome === Biome.ENCHANTED_FOREST && enchantedForestSVG) {
|
|
||||||
ctx.drawImage(enchantedForestSVG, -25, -25, 50, 50);
|
|
||||||
} else if (this.biome === Biome.SWAMP && swampSVG) {
|
|
||||||
ctx.drawImage(swampSVG, -25, -5, 50, 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.structure ) {
|
if(this.visibility === 2) {
|
||||||
this.structure.setDesign(ctx);
|
if (this.structure) this.structure.setDesign(ctx);
|
||||||
}
|
this.entities.forEach(e => e.setDesign(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw fog overlay if seen but not currently visible
|
// Draw fog overlay if seen but not currently visible
|
||||||
|
@ -420,41 +437,58 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
setElevation(){
|
setElevation(){
|
||||||
if (!this.biome) return;
|
if (!this.biome) return;
|
||||||
this.position.h = Math.floor(Math.random() * (this.biome.maxElevation - this.biome.minElevation+ 1) + this.biome.minElevation);
|
this.position.h = Math.floor(Math.random() * (this.biome.type.maxElevation - this.biome.type.minElevation+ 1) + this.biome.type.minElevation);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStructure(){
|
setStructure(){
|
||||||
var rand = seededRandom(this.position.x * 13 + this.position.y * 59);
|
var rand = seededRandom(this.position.x * 13 + this.position.y * 59);
|
||||||
var structureChance = rand();
|
var structureChance = rand();
|
||||||
if (structureChance < 0.01 && this.biome.acceptStructure) {
|
if (structureChance < 0.01 && this.biome.type.acceptStructure) {
|
||||||
this.structure=new structure(this.position);
|
this.structure=new structure(this.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEntity(){
|
setEntities() {
|
||||||
// a changer!
|
// --- NPC liés aux structures ---
|
||||||
if(this.structure) {
|
if (this.structure && this.structure.type) {
|
||||||
const structureType = this.structure.type;
|
const structureType = this.structure.type;
|
||||||
let job = JOB.VILLAGER;
|
let job = JOB.VILLAGER;
|
||||||
if(structureType === STRUCTURE_TYPE.FARM) job = JOB.FARMER;
|
if (structureType === STRUCTURE_TYPE.FARM) job = JOB.FARMER;
|
||||||
if(structureType === STRUCTURE_TYPE.CAMP) job = JOB.BANDIT;
|
if (structureType === STRUCTURE_TYPE.CAMP) job = JOB.BANDIT;
|
||||||
|
|
||||||
for(let i = 0; i < structureType.population; i++) {
|
for (let i = 0; i < structureType.population; i++) {
|
||||||
this.npc.push(new Npc(job,new Creature(job.name,null,1,null,null,this.position,'HUMAN',null,10,null),this,null ));
|
const creature = new Creature(
|
||||||
}
|
job.name,
|
||||||
}
|
new Attributes(RACE.HUMAN),
|
||||||
else {
|
1,
|
||||||
if (this.biome) {
|
null,
|
||||||
for (const key in ANIMAL_TYPES) {
|
null,
|
||||||
const animalType = ANIMAL_TYPES[key];
|
this, // la tuile du NPC
|
||||||
if (animalType.biomes.includes(this.biome) && Math.random() < animalType.spawnChance) {
|
'HUMAN',
|
||||||
if( this.biome.movements.includes(animalType.movement)) {
|
RACE.HUMAN,
|
||||||
this.animal.push(new Animal(animalType, this.position));
|
10,
|
||||||
}
|
null
|
||||||
}
|
);
|
||||||
}
|
this.entities.push(new Npc(job, creature, this.structure, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Animaux sauvages ---
|
||||||
|
else if (this.biome) {
|
||||||
|
for (const key in ANIMAL_TYPES) {
|
||||||
|
const animalType = ANIMAL_TYPES[key];
|
||||||
|
|
||||||
|
// 🔑 Vérif par nom au lieu d’objet
|
||||||
|
if (animalType.biomes.some(b => b.name === this.biome.type.name) && Math.random() < animalType.spawnChance) {
|
||||||
|
if (this.biome.type.movements.includes(animalType.movement)) {
|
||||||
|
this.entities.push(new Animal(animalType, this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
class Map {
|
class Map {
|
||||||
constructor(size) {
|
constructor(size) {
|
||||||
|
@ -512,6 +546,74 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
ctx.globalAlpha = 1.0;
|
ctx.globalAlpha = 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
generateRoads(locations) {
|
||||||
|
//Location to transform into settlement
|
||||||
|
if (locations.length < 2) return;
|
||||||
|
for (let i = 0; i < locations.length - 1; i++) {
|
||||||
|
let start = locations[i];
|
||||||
|
let end = locations[i+1];
|
||||||
|
let currentX = start.x;
|
||||||
|
let currentY = start.y;
|
||||||
|
while(Math.abs(currentX - end.x) > 0 || Math.abs(currentY - end.y) > 0) {
|
||||||
|
if (Math.abs(currentX - end.x) > Math.abs(currentY - end.y)) {
|
||||||
|
currentX += Math.sign(end.x - currentX);
|
||||||
|
} else {
|
||||||
|
currentY += Math.sign(end.y - currentY);
|
||||||
|
}
|
||||||
|
if (currentX >= 0 && currentX < this.size && currentY >= 0 && currentY < this.size) {
|
||||||
|
const tile = this.tiles[currentY][currentX];
|
||||||
|
if (tile.biome === Biome.RIVER) {
|
||||||
|
tile.hasBridge = true;
|
||||||
|
} else if (tile.biome !== Biome.WATER_DEEP && tile.biome !== Biome.WATER_SHALLOW) {
|
||||||
|
tile.isRoad = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateRivers() {
|
||||||
|
const numRivers = 10;
|
||||||
|
for (let i = 0; i < numRivers; i++) {
|
||||||
|
let currentX = Math.floor(Math.random() * this.size);
|
||||||
|
let currentY = Math.floor(Math.random() * this.size);
|
||||||
|
let sourceTile = this.tiles[currentY][currentX];
|
||||||
|
|
||||||
|
if (sourceTile.biome !== Biome.MOUNTAIN) continue;
|
||||||
|
|
||||||
|
let maxLength = 200;
|
||||||
|
while (maxLength > 0) {
|
||||||
|
maxLength--;
|
||||||
|
const currentTile = this.tiles[currentY][currentX];
|
||||||
|
if (currentTile.biome === Biome.WATER_DEEP || currentTile.biome === Biome.WATER_SHALLOW) break;
|
||||||
|
|
||||||
|
currentTile.biome = Biome.RIVER;
|
||||||
|
currentTile.elevation = Math.max(1, currentTile.elevation -1);
|
||||||
|
|
||||||
|
const neighbors = [ {x:0, y:-1}, {x:0, y:1}, {x:-1, y:0}, {x:1, y:0} ];
|
||||||
|
let lowestNeighbor = null;
|
||||||
|
let lowestElevation = currentTile.elevation;
|
||||||
|
|
||||||
|
for (const n of neighbors) {
|
||||||
|
const nx = currentX + n.x;
|
||||||
|
const ny = currentY + n.y;
|
||||||
|
if (nx >= 0 && nx < this.size && ny >= 0 && ny < this.size) {
|
||||||
|
const neighborTile = this.tiles[ny][nx];
|
||||||
|
if (neighborTile.elevation < lowestElevation) {
|
||||||
|
lowestElevation = neighborTile.elevation;
|
||||||
|
lowestNeighbor = {x: nx, y: ny};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowestNeighbor) {
|
||||||
|
currentX = lowestNeighbor.x;
|
||||||
|
currentY = lowestNeighbor.y;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
class Attributes {
|
class Attributes {
|
||||||
|
@ -526,7 +628,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Creature {
|
class Creature {
|
||||||
constructor(name,attributes,level,affinities,alignments,position,species,race,hp,items) {
|
constructor(name,attributes,level,affinities,alignments,tile,species,race,hp,items) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
this.level = level;
|
this.level = level;
|
||||||
|
@ -535,15 +637,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
this.alignments = alignments;
|
this.alignments = alignments;
|
||||||
this.species = species;
|
this.species = species;
|
||||||
this.race = race;
|
this.race = race;
|
||||||
this.position=position;
|
this.tile=tile;
|
||||||
this.items=items;
|
this.items=items;
|
||||||
this.hp=hp;
|
this.hp=hp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Npc {
|
||||||
|
constructor(job, creature, settlement, equipments) {
|
||||||
|
this.job = job;
|
||||||
|
this.creature = creature; // ✅ on stocke bien la créature
|
||||||
|
this.tile = creature.tile; // ✅ le NPC est placé sur la tuile de la créature
|
||||||
|
this.settlement = settlement; // ✅ lien avec le village/structure
|
||||||
|
this.equipments = equipments || [];
|
||||||
|
this.lastMoveTime = 0;
|
||||||
|
this.moveCooldown = 3000 + Math.random() * 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
class Npc {
|
class Npc {
|
||||||
constructor(job,creature,settlement,equipments) {
|
constructor(job,creature,settlement,equipments) {
|
||||||
this.job = job;
|
this.job = job;
|
||||||
this.tile = creature.tile;
|
this.creature = creature;
|
||||||
this.settlement = settlement;
|
this.settlement = settlement;
|
||||||
this.lastMoveTime = 0;
|
this.lastMoveTime = 0;
|
||||||
this.moveCooldown = 3000 + Math.random() * 4000;
|
this.moveCooldown = 3000 + Math.random() * 4000;
|
||||||
|
@ -591,22 +706,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const newY = this.settlement.position.y + y;
|
const newY = this.settlement.position.y + y;
|
||||||
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
||||||
const targetTile = gameMap.tiles[newY][newX];
|
const targetTile = gameMap.tiles[newY][newX];
|
||||||
if(targetTile.biome.movements.includes('walk') && !targetTile.structure) {
|
if(targetTile.biome.type.movements.includes('walk') && !targetTile.structure) {
|
||||||
possibleMoves.push(targetTile);
|
possibleMoves.push(targetTile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(possibleMoves.length > 0) {
|
if(possibleMoves.length > 0) {
|
||||||
this.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
|
this.creature.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDesign(ctx) {
|
setDesign(ctx) {
|
||||||
if (!this.tile || this.tile.visibility !== 2) return;
|
if (!this.creature.tile || this.creature.tile.visibility !== 2) return;
|
||||||
|
|
||||||
const screenPos = this.tile.position.cartToIso();
|
const screenPos = this.creature.tile.position.cartToIso();
|
||||||
const elevationHeight = this.tile.position.h * ELEVATION_STEP;
|
const elevationHeight = this.creature.tile.position.h * ELEVATION_STEP;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
||||||
|
@ -627,7 +742,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setDesign(ctx) {
|
setDesign(ctx) {
|
||||||
const position = this.creature.position
|
const position = this.creature.tile.position
|
||||||
if(!position) return;
|
if(!position) return;
|
||||||
|
|
||||||
const screenPos = position.cartToIso();
|
const screenPos = position.cartToIso();
|
||||||
|
@ -644,26 +759,327 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
move(dx, dy, gameMap) {
|
move(dx, dy, gameMap) {
|
||||||
const newX = this.creature.position.x + dx;
|
const newX = this.creature.tile.position.x + dx;
|
||||||
const newY = this.creature.position.y + dy;
|
const newY = this.creature.tile.position.y + dy;
|
||||||
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
|
||||||
const targetTile = gameMap.tiles[newY][newX].position;
|
const targetTile = gameMap.tiles[newY][newX];
|
||||||
if (targetTile.biome && targetTile.biome.movements.includes('walk')) {
|
if (targetTile.biome && targetTile.biome.type.movements.includes('walk')) {
|
||||||
this.creature.position = targetTile;
|
this.creature.tile = targetTile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Combat {
|
||||||
|
constructor(player, opponent) {
|
||||||
|
this.player=player;
|
||||||
|
this.opponent=opponent;
|
||||||
|
this.state='initiating'
|
||||||
|
this.startCombat();
|
||||||
|
}
|
||||||
|
startCombat() {
|
||||||
|
this.state= 'started';
|
||||||
|
document.getElementById('combat-screen').classList.remove('hidden');
|
||||||
|
document.getElementById('combat-screen').classList.add('flex');
|
||||||
|
updateCombatUI();
|
||||||
|
document.getElementById('combat-log').textContent = `Le combat commence`;
|
||||||
|
document.getElementById('attack-btn').addEventListener('click', handleAttack);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAttack(camera) {
|
||||||
|
|
||||||
|
const playerDamage = Math.max(1, player.attributes.strength + Math.floor(Math.random() * 4) - this.opponent.attributes.defense);
|
||||||
|
this.opponent.currentHp = Math.max(0, this.opponent.currentHp - playerDamage);
|
||||||
|
document.getElementById('combat-log').textContent = `Vous infligez ${playerDamage} dégâts.`;
|
||||||
|
floatingTexts.push({ content: `-${playerDamage}`, x: camera.x + canvas.width * 0.7, y: camera.y + canvas.height * 0.4, endTime: performance.now() + 1000, duration: 1000, color: '#ff4444' });
|
||||||
|
|
||||||
|
updateCombatUI();
|
||||||
|
|
||||||
|
if (this.opponent.currentHp <= 0) {
|
||||||
|
endCombat(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const opponentDamage = Math.max(1, this.opponent.attributes.strength + Math.floor(Math.random() * 4) - 2);
|
||||||
|
this.player.creature.hp = Math.max(0, this.player.creature.hp - opponentDamage);
|
||||||
|
document.getElementById('combat-log').textContent += `\nLe ${this.opponent.name} riposte et vous inflige ${opponentDamage} dégâts.`;
|
||||||
|
floatingTexts.push({ content: `-${opponentDamage}`, x: camera.x + canvas.width * 0.3, y: camera.y + canvas.height * 0.4, endTime: performance.now() + 1000, duration: 1000, color: '#ff4444' });
|
||||||
|
|
||||||
|
updateCombatUI();
|
||||||
|
updateAllInfoPanels();
|
||||||
|
|
||||||
|
if ( this.player.creature.hp <= 0) {
|
||||||
|
endCombat(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
endCombat(isVictory) {
|
||||||
|
const combatLog = document.getElementById('combat-log');
|
||||||
|
if (isVictory) {
|
||||||
|
combatLog.textContent = `Vous avez vaincu le ${currentOpponent.name} !`;
|
||||||
|
addXp(currentOpponent.xp);
|
||||||
|
Object.entries(currentOpponent.loot).forEach(([item, amount]) => {
|
||||||
|
player.inventory[item] = (player.inventory[item] || 0) + amount;
|
||||||
|
combatLog.textContent += `\nVous trouvez ${amount} ${item}.`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const opponentTile = mapData[player.y][player.x];
|
||||||
|
if (currentOpponent.isAnimal) {
|
||||||
|
opponentTile.animals.shift();
|
||||||
|
} else {
|
||||||
|
opponentTile.monsters.shift();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
combatLog.textContent = `Vous avez été vaincu...`;
|
||||||
|
player.derivedStats.currentHp = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOpponent = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('combat-screen').classList.add('hidden');
|
||||||
|
document.getElementById('combat-screen').classList.remove('flex');
|
||||||
|
gameState = 'exploring';
|
||||||
|
updateAllInfoPanels();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCombatUI() {
|
||||||
|
document.getElementById('combat-player-hp-text').textContent = `${this.player.creature.hp} / ${player.derivedStats.maxHp}`;
|
||||||
|
document.getElementById('combat-player-hp-bar').style.width = `${(this.player.creature.hp / player.derivedStats.maxHp) * 100}%`;
|
||||||
|
if (this.opponent) {
|
||||||
|
document.getElementById('combat-opponent-name').textContent = this.opponent.name;
|
||||||
|
document.getElementById('combat-opponent-icon').textContent = currentOpponent.icon;
|
||||||
|
document.getElementById('combat-opponent-hp-text').textContent = `${this.opponent.currentHp} / ${this.opponent.hp}`;
|
||||||
|
document.getElementById('combat-opponent-hp-bar').style.width = `${(this.opponent.currentHp / this.opponent.hp) * 100}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
class Quest {
|
||||||
|
constructor( ) {
|
||||||
|
this.id = null;
|
||||||
|
this.definition = null;
|
||||||
|
this.currentStage = 0;
|
||||||
|
this.status = 'inactive'; // inactive, active, completed
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentStage() {
|
||||||
|
return this.definition.stages[this.currentStage];
|
||||||
|
}
|
||||||
|
|
||||||
|
advance(choiceIndex) {
|
||||||
|
const stage = this.getCurrentStage();
|
||||||
|
if(stage.choices && stage.choices[choiceIndex]) {
|
||||||
|
const nextStageIndex = stage.choices[choiceIndex].nextStage;
|
||||||
|
if (nextStageIndex !== null) {
|
||||||
|
this.currentStage = nextStageIndex;
|
||||||
|
if(this.status === 'inactive') this.status = 'active';
|
||||||
|
if(this.getCurrentStage().isEnd) {
|
||||||
|
this.status = 'completed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async setQuest(npc) {
|
||||||
|
if(npc.questId && this.quests[npc.questId]) {
|
||||||
|
return this.quests[npc.questId];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const questDefinition = await this.generateQuestForNpc(npc);
|
||||||
|
if(questDefinition) {
|
||||||
|
const questId = `GEMINI_${Date.now()}`;
|
||||||
|
npc.assignQuest(questId);
|
||||||
|
this.id=questId;
|
||||||
|
this.definition=questDefinition;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Gemini quest generation failed, using fallback.", e);
|
||||||
|
const fallbackId = 'WOLF_MENACE';
|
||||||
|
npc.assignQuest(fallbackId);
|
||||||
|
this.id=fallbackId;
|
||||||
|
this.definition=QUEST_DATABASE[fallbackId];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async generateQuestForNpc(npc) {
|
||||||
|
if (!geminiApiKey) {
|
||||||
|
throw new Error("API key is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `Génère une quête simple pour un jeu RPG en 2D isométrique.
|
||||||
|
Le PNJ est un "${npc.type.name}".
|
||||||
|
Il se trouve près d'une structure de type "${npc.homeTile.structure.type.name}" dans un biome de type "${npc.homeTile.biome.name}".
|
||||||
|
|
||||||
|
Crée une quête avec un titre, et 3 à 4 étapes.
|
||||||
|
La première étape (index 0) doit proposer d'accepter ou de refuser la quête.
|
||||||
|
L'étape suivante (index 1) doit décrire l'objectif. L'objectif doit être simple, comme tuer un certain nombre de monstres ou collecter des objets.
|
||||||
|
La dernière étape doit être la récompense (isEnd: true).
|
||||||
|
|
||||||
|
Réponds UNIQUEMENT avec un objet JSON valide qui suit le schéma ci-dessous. N'ajoute aucun texte avant ou après le JSON.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
contents: [{ parts: [{ text: prompt }] }],
|
||||||
|
generationConfig: {
|
||||||
|
responseMimeType: "application/json",
|
||||||
|
responseSchema: {
|
||||||
|
type: "OBJECT",
|
||||||
|
properties: {
|
||||||
|
"title": { "type": "STRING" },
|
||||||
|
"stages": {
|
||||||
|
"type": "ARRAY",
|
||||||
|
"items": {
|
||||||
|
"type": "OBJECT",
|
||||||
|
"properties": {
|
||||||
|
"text": { "type": "STRING" },
|
||||||
|
"choices": {
|
||||||
|
"type": "ARRAY",
|
||||||
|
"items": {
|
||||||
|
"type": "OBJECT",
|
||||||
|
"properties": {
|
||||||
|
"text": { "type": "STRING" },
|
||||||
|
"nextStage": { "type": ["NUMBER", "NULL"] }
|
||||||
|
},
|
||||||
|
"required": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objective": { "type": ["OBJECT", "NULL"], "properties": {"type": {"type": "STRING"}, "target": {"type": "STRING"}, "count": {"type": "NUMBER"}} },
|
||||||
|
"reward": { "type": ["OBJECT", "NULL"], "properties": {"xp": {"type": "NUMBER"}, "items": {"type": "ARRAY", "items": { "type": "STRING" }}} },
|
||||||
|
"isEnd": { "type": ["BOOLEAN", "NULL"] }
|
||||||
|
},
|
||||||
|
"required": ["text", "choices"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "stages"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${geminiApiKey}`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorBody = await response.text();
|
||||||
|
console.error("API Error Body:", errorBody);
|
||||||
|
throw new Error(`API call failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
const jsonText = result.candidates?.[0]?.content?.parts?.[0]?.text;
|
||||||
|
if(jsonText) {
|
||||||
|
return JSON.parse(jsonText);
|
||||||
|
}
|
||||||
|
throw new Error("Invalid response from API.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class World {
|
class World {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.map = new Map(200);
|
this.map = new Map(200);
|
||||||
this.currentTime=new Time();
|
this.currentTime=new Time();
|
||||||
this.animals = [];
|
this.camera = new Camera(0,0, canvas);
|
||||||
this.npcs = [];
|
this.player=null;
|
||||||
}
|
}
|
||||||
|
setDesign(){
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(this.camera.x, this.camera.y);
|
||||||
|
|
||||||
|
const [startX, endX, startY, endY] = this.getVisibleTileBounds();
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
for (let x = startX; x < endX; x++) {
|
||||||
|
if (this.map.tiles[y] && this.map.tiles[y][x]) {
|
||||||
|
this.map.tiles[y][x].setDesign(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//this.world.animals.forEach(animal => animal.setDesign(ctx));
|
||||||
|
//this.world.npcs.forEach(npc => npc.setDesign(ctx));
|
||||||
|
this.player.setDesign(ctx);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
ctx.save();
|
||||||
|
this.currentTime.setDesign(ctx);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
}
|
||||||
|
getVisibleTileBounds() {
|
||||||
|
const margin = 5;
|
||||||
|
const viewWidth = canvas.width;
|
||||||
|
const viewHeight = canvas.height;
|
||||||
|
|
||||||
|
const isoToCart = (isoX, isoY) => {
|
||||||
|
const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
|
||||||
|
const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
|
||||||
|
return { x: Math.floor(cartX), y: Math.floor(cartY) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const topLeft = isoToCart(-this.camera.x, -this.camera.y);
|
||||||
|
const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
|
||||||
|
|
||||||
|
const startX = Math.max(0, topLeft.x - margin);
|
||||||
|
const endX = Math.min(this.map.size - 1, bottomRight.x + margin);
|
||||||
|
const startY = Math.max(0, topLeft.y - margin);
|
||||||
|
const endY = Math.min(this.map.size - 1, bottomRight.y + margin);
|
||||||
|
|
||||||
|
return [startX, endX, startY, endY];
|
||||||
|
}
|
||||||
|
update(currentTime) {
|
||||||
|
|
||||||
|
this.updateVisibility(this.player.creature.tile.position);
|
||||||
|
// a changer!
|
||||||
|
//this.world.animals.forEach(animal => animal.update(currentTime, this.world.map));
|
||||||
|
//this.world.npcs.forEach(npc => npc.update(currentTime, this.world.map));
|
||||||
|
|
||||||
|
this.camera.setCamera(this.player.creature.tile.position);
|
||||||
|
|
||||||
|
// Update all biome sprites
|
||||||
|
for (const key in BIOME_TYPE) {
|
||||||
|
if (BIOME_TYPE[key].sprite) {
|
||||||
|
BIOME_TYPE[key].sprite.update(currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentTime.tick();
|
||||||
|
}
|
||||||
|
updateVisibility(position) {
|
||||||
|
const px = position.x;
|
||||||
|
const py = position.y;
|
||||||
|
const radius = VISION_RADIUS;
|
||||||
|
const radiusSq = radius * radius;
|
||||||
|
|
||||||
|
const viewBox = {
|
||||||
|
minX: Math.max(0, px - radius - 2),
|
||||||
|
maxX: Math.min(this.map.size - 1, px + radius + 2),
|
||||||
|
minY: Math.max(0, py - radius - 2),
|
||||||
|
maxY: Math.min(this.map.size - 1, py + radius + 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
for(let y = viewBox.minY; y <= viewBox.maxY; y++) {
|
||||||
|
for(let x = viewBox.minX; x <= viewBox.maxX; x++) {
|
||||||
|
const tile = this.map.tiles[y][x];
|
||||||
|
if(tile.visibility === 2) tile.visibility = 1;
|
||||||
|
|
||||||
|
const dx = px - x;
|
||||||
|
const dy = py - y;
|
||||||
|
if (dx * dx + dy * dy <= radiusSq) {
|
||||||
|
tile.visibility = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class Camera {
|
class Camera {
|
||||||
constructor(x=0,y=0,canvas) {
|
constructor(x=0,y=0,canvas) {
|
||||||
|
@ -682,74 +1098,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.world = new World('Defiance');
|
this.world = new World('Defiance');
|
||||||
this.camera = new Camera(0,0, canvas);
|
|
||||||
this.setControls();
|
this.setControls();
|
||||||
this.loop = this.loop.bind(this);
|
this.loop = this.loop.bind(this);
|
||||||
this.player=null;
|
|
||||||
this.setInitial();
|
this.setInitial();
|
||||||
}
|
}
|
||||||
loop(currentTime) {
|
loop(currentTime) {
|
||||||
this.update(currentTime);
|
this.handleMovement(currentTime);
|
||||||
this.setDesign();
|
this.world.update(currentTime);
|
||||||
|
this.world.setDesign();
|
||||||
requestAnimationFrame(this.loop);
|
requestAnimationFrame(this.loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(currentTime) {
|
|
||||||
this.handleMovement(currentTime);
|
|
||||||
this.updateVisibility();
|
|
||||||
// a changer!
|
|
||||||
this.world.animals.forEach(animal => animal.update(currentTime, this.world.map));
|
|
||||||
this.world.npcs.forEach(npc => npc.update(currentTime, this.world.map));
|
|
||||||
|
|
||||||
this.camera.setCamera(this.player.creature.position);
|
|
||||||
|
|
||||||
// Update all biome sprites
|
|
||||||
for (const key in Biome) {
|
|
||||||
if (Biome[key].sprite) {
|
|
||||||
Biome[key].sprite.update(currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.world.currentTime.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisibility() {
|
|
||||||
const map = this.world.map;
|
|
||||||
const px = this.player.creature.position.x;
|
|
||||||
const py = this.player.creature.position.y;
|
|
||||||
const radius = VISION_RADIUS;
|
|
||||||
const radiusSq = radius * radius;
|
|
||||||
|
|
||||||
const viewBox = {
|
|
||||||
minX: Math.max(0, px - radius - 2),
|
|
||||||
maxX: Math.min(map.size - 1, px + radius + 2),
|
|
||||||
minY: Math.max(0, py - radius - 2),
|
|
||||||
maxY: Math.min(map.size - 1, py + radius + 2)
|
|
||||||
};
|
|
||||||
|
|
||||||
for(let y = viewBox.minY; y <= viewBox.maxY; y++) {
|
|
||||||
for(let x = viewBox.minX; x <= viewBox.maxX; x++) {
|
|
||||||
const tile = map.tiles[y][x];
|
|
||||||
if(tile.visibility === 2) tile.visibility = 1;
|
|
||||||
|
|
||||||
const dx = px - x;
|
|
||||||
const dy = py - y;
|
|
||||||
if (dx * dx + dy * dy <= radiusSq) {
|
|
||||||
tile.visibility = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setInitial(){
|
setInitial(){
|
||||||
let spawnX, spawnY;
|
let spawnX, spawnY;
|
||||||
do {
|
do {
|
||||||
spawnX = Math.floor(Math.random() * this.world.map.size);
|
spawnX = Math.floor(Math.random() * this.world.map.size);
|
||||||
spawnY = Math.floor(Math.random() * this.world.map.size);
|
spawnY = Math.floor(Math.random() * this.world.map.size);
|
||||||
} while (!this.world.map.tiles[spawnY][spawnX].biome || !this.world.map.tiles[spawnY][spawnX].biome.movements.includes('walk') );
|
} while (!this.world.map.tiles[spawnY][spawnX].biome || !this.world.map.tiles[spawnY][spawnX].biome.type.movements.includes('walk') );
|
||||||
this.player = new Player(new Creature('player',new Attributes(RACE.HUMAN),1,null,null,this.world.map.tiles[spawnY][spawnX].position,'human',RACE.HUMAN,10,null),);
|
this.world.player = new Player(new Creature('player',new Attributes(RACE.HUMAN),1,null,null,this.world.map.tiles[spawnY][spawnX],'human',RACE.HUMAN,10,null),);
|
||||||
this.updateVisibility();
|
this.world.updateVisibility(this.world.map.tiles[spawnY][spawnX].position);
|
||||||
}
|
}
|
||||||
setControls() {
|
setControls() {
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
|
@ -775,71 +1142,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
btn.addEventListener('touchend', e => { e.preventDefault(); controls[dir] = false; });
|
btn.addEventListener('touchend', e => { e.preventDefault(); controls[dir] = false; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDesign(){
|
|
||||||
ctx.fillStyle = '#000000';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(this.camera.x, this.camera.y);
|
|
||||||
|
|
||||||
const [startX, endX, startY, endY] = this.getVisibleTileBounds();
|
|
||||||
|
|
||||||
for (let y = startY; y < endY; y++) {
|
|
||||||
for (let x = startX; x < endX; x++) {
|
|
||||||
if (this.world.map.tiles[y] && this.world.map.tiles[y][x]) {
|
|
||||||
this.world.map.tiles[y][x].setDesign(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.world.animals.forEach(animal => animal.setDesign(ctx));
|
|
||||||
this.world.npcs.forEach(npc => npc.setDesign(ctx));
|
|
||||||
this.player.setDesign(ctx);
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
ctx.save();
|
|
||||||
this.world.currentTime.setDesign(ctx);
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisibleTileBounds() {
|
|
||||||
const margin = 5;
|
|
||||||
const viewWidth = canvas.width;
|
|
||||||
const viewHeight = canvas.height;
|
|
||||||
|
|
||||||
const isoToCart = (isoX, isoY) => {
|
|
||||||
const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
|
|
||||||
const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
|
|
||||||
return { x: Math.floor(cartX), y: Math.floor(cartY) };
|
|
||||||
};
|
|
||||||
|
|
||||||
const topLeft = isoToCart(-this.camera.x, -this.camera.y);
|
|
||||||
const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
|
|
||||||
|
|
||||||
const startX = Math.max(0, topLeft.x - margin);
|
|
||||||
const endX = Math.min(this.world.map.size - 1, bottomRight.x + margin);
|
|
||||||
const startY = Math.max(0, topLeft.y - margin);
|
|
||||||
const endY = Math.min(this.world.map.size - 1, bottomRight.y + margin);
|
|
||||||
|
|
||||||
return [startX, endX, startY, endY];
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMovement(currentTime) {
|
handleMovement(currentTime) {
|
||||||
if (currentTime - lastMoveTime < 150) return;
|
if (currentTime - lastMoveTime < 150) return;
|
||||||
let moved = false;
|
let moved = false;
|
||||||
if (controls.up) { this.player.move(0, -1,this.world.map); moved = true; }
|
if (controls.up) { this.world.player.move(0, -1,this.world.map); moved = true; }
|
||||||
if (controls.down) { this.player.move(0, 1,this.world.map); moved = true; }
|
if (controls.down) { this.world.player.move(0, 1,this.world.map); moved = true; }
|
||||||
if (controls.left) { this.player.move(-1, 0,this.world.map); moved = true; }
|
if (controls.left) { this.world.player.move(-1, 0,this.world.map); moved = true; }
|
||||||
if (controls.right) { this.player.move(1, 0,this.world.map); moved = true; }
|
if (controls.right) { this.world.player.move(1, 0,this.world.map); moved = true; }
|
||||||
if(moved) lastMoveTime = currentTime;
|
if(moved) lastMoveTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInteraction() {
|
handleInteraction() {
|
||||||
if (!controls.interact) return;
|
if (!controls.interact) return;
|
||||||
controls.interact = false; // Consume the action
|
controls.interact = false; // Consume the action
|
||||||
|
|
||||||
const px = this.player.creature.position.x;
|
const px = this.world.player.creature.position.x;
|
||||||
const py = this.player.creature.position.y;
|
const py = this.world.player.creature.position.y;
|
||||||
|
|
||||||
let targetNpc = null;
|
let targetNpc = null;
|
||||||
for (const npc of this.world.npcs) {
|
for (const npc of this.world.npcs) {
|
||||||
|
@ -1048,8 +1367,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
canvas.height = canvas.parentElement.clientHeight;
|
canvas.height = canvas.parentElement.clientHeight;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const key in Biome) {
|
for (const key in BIOME_TYPE) {
|
||||||
if (Biome[key].design) { Biome[key].sprite = new Sprite(Biome[key]);}
|
if (BIOME_TYPE[key].design) { BIOME_TYPE[key].sprite = new Sprite(BIOME_TYPE[key]);}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetIds = ['forest-svg', 'village-svg', 'city-svg', 'player-svg', 'enchanted-forest-svg', 'swamp-svg', 'wolf-pack-svg', 'boar-svg', 'bird-svg', 'farm-svg', 'camp-svg', 'npc-svg','house-svg','cult-svg','market-svg','mine-svg'];
|
const assetIds = ['forest-svg', 'village-svg', 'city-svg', 'player-svg', 'enchanted-forest-svg', 'swamp-svg', 'wolf-pack-svg', 'boar-svg', 'bird-svg', 'farm-svg', 'camp-svg', 'npc-svg','house-svg','cult-svg','market-svg','mine-svg'];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue