Compare commits

..

No commits in common. "b27b5ea041c99f6ec56d3453fdb85a3bb758b513" and "3bcf836c63b411de16d9281f96c80865a391a8c3" have entirely different histories.

3 changed files with 1084 additions and 1813 deletions

View file

@ -303,7 +303,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="../js/script.js"></script> <script src="../js/scriptalpha.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,19 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const BIOME_TYPE = { const Biome = {
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_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_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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 }, 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 },
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 } 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 }
}; }
const JOB = { const JOB = {
VILLAGER: { VILLAGER: {
name: "Habitant", name: "Habitant",
@ -102,9 +103,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_TYPE.FOREST, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.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.FOREST, Biome.MOUNTAIN, Biome.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_TYPE.FOREST, BIOME_TYPE.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.FOREST, Biome.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_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 }, 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 },
}; };
// --- Global Asset Variables --- // --- Global Asset Variables ---
@ -299,65 +300,71 @@ 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.type.movements.includes('walk') || targetTile.biome.type.movements.includes('climb')) && this.type.biomes.type.includes(targetTile.biome.type)) { if (targetTile.biome && (targetTile.biome.movements.includes('walk') || targetTile.biome.movements.includes('climb')) && this.type.biomes.includes(targetTile.biome)) {
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 - flightHeight); ctx.translate(screenPos.x, screenPos.y - elevationHeight);
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 {
constructor() { class Tile {
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(position.x * scale, position.y * scale) + 1) / 2; let eRaw = (simplex.noise2D(this.position.x * scale, this.position.y * scale) + 1) / 2;
let tRaw = (simplex.noise2D(position.x * scale * 0.8, position.y * scale * 0.8) + 1) / 2; let tRaw = (simplex.noise2D(this.position.x * scale * 0.8, this.position.y * scale * 0.8) + 1) / 2;
let mRaw = (simplex.noise2D(position.x * scale * 1.5, position.y * scale * 1.5) + 1) / 2; let mRaw = (simplex.noise2D(this.position.x * scale * 1.5, this.position.y * scale * 1.5) + 1) / 2;
if (eRaw < 0.25) this.type = BIOME_TYPE.WATER_DEEP; if (eRaw < 0.25) this.biome = Biome.WATER_DEEP;
else if (eRaw < 0.3) this.type = BIOME_TYPE.WATER_SHALLOW; else if (eRaw < 0.3) this.biome = Biome.WATER_SHALLOW;
else if (eRaw < 0.32) this.type = BIOME_TYPE.SWAMP; else if (eRaw < 0.32) this.biome = Biome.SWAMP;
else if (eRaw < 0.35) this.type = BIOME_TYPE.BEACH; else if (eRaw < 0.35) this.biome = Biome.BEACH;
else if (eRaw > 0.85) this.type = BIOME_TYPE.SNOWMOUNTAIN; else if (eRaw > 0.85) this.biome = Biome.SNOWMOUNTAIN;
else if (eRaw > 0.75) this.type = BIOME_TYPE.MOUNTAIN; else if (eRaw > 0.75) this.biome = Biome.MOUNTAIN;
else { else {
if (tRaw < 0.3) this.type = BIOME_TYPE.DESERT; if (tRaw < 0.3) this.biome = Biome.DESERT;
else if (tRaw > 0.6) this.type = (mRaw > 0.7) ? BIOME_TYPE.ENCHANTED_FOREST : BIOME_TYPE.FOREST; else if (tRaw > 0.6) this.biome = (mRaw > 0.7) ? Biome.ENCHANTED_FOREST : Biome.FOREST;
else this.type = BIOME_TYPE.GRASSLAND; else this.biome = Biome.GRASSLAND;
} }
}; }
setDesign(ctx, position){
const screenPos = position.cartToIso(); setDesign(ctx){
const elevationHeight = position.h * ELEVATION_STEP; if (!this.biome || this.visibility === 0) return;
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.type.summerColor; const baseColor = this.biome.summerColor;
const shadowColor = shadeColor(baseColor, -30); const shadowColor = shadeColor(baseColor, -30);
// Draw tile faces // Draw tile faces
@ -374,8 +381,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.type.sprite) { if (this.biome.sprite) {
this.type.sprite.draw(ctx, -TILE_WIDTH / 2, 0); this.biome.sprite.draw(ctx, -TILE_WIDTH / 2, 0);
} else { } else {
ctx.fillStyle = baseColor; ctx.fillStyle = baseColor;
ctx.beginPath(); ctx.beginPath();
@ -383,44 +390,20 @@ 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.visibility === 2) {
if (this.structure) this.structure.setDesign(ctx); if (this.biome === Biome.FOREST && forestSVG) {
this.entities.forEach(e => e.setDesign(ctx)); 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 ) {
this.structure.setDesign(ctx);
}
} }
// Draw fog overlay if seen but not currently visible // Draw fog overlay if seen but not currently visible
@ -437,58 +420,41 @@ document.addEventListener('DOMContentLoaded', () => {
setElevation(){ setElevation(){
if (!this.biome) return; if (!this.biome) return;
this.position.h = Math.floor(Math.random() * (this.biome.type.maxElevation - this.biome.type.minElevation+ 1) + this.biome.type.minElevation); this.position.h = Math.floor(Math.random() * (this.biome.maxElevation - this.biome.minElevation+ 1) + this.biome.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.type.acceptStructure) { if (structureChance < 0.01 && this.biome.acceptStructure) {
this.structure=new structure(this.position); this.structure=new structure(this.position);
} }
} }
setEntities() { setEntity(){
// --- NPC liés aux structures --- // a changer!
if (this.structure && this.structure.type) { if(this.structure) {
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++) {
const creature = new Creature( this.npc.push(new Npc(job,new Creature(job.name,null,1,null,null,this.position,'HUMAN',null,10,null),this,null ));
job.name,
new Attributes(RACE.HUMAN),
1,
null,
null,
this, // la tuile du NPC
'HUMAN',
RACE.HUMAN,
10,
null
);
this.entities.push(new Npc(job, creature, this.structure, null));
} }
} }
// --- Animaux sauvages --- else {
else if (this.biome) { if (this.biome) {
for (const key in ANIMAL_TYPES) { for (const key in ANIMAL_TYPES) {
const animalType = ANIMAL_TYPES[key]; const animalType = ANIMAL_TYPES[key];
if (animalType.biomes.includes(this.biome) && Math.random() < animalType.spawnChance) {
// 🔑 Vérif par nom au lieu d’objet if( this.biome.movements.includes(animalType.movement)) {
if (animalType.biomes.some(b => b.name === this.biome.type.name) && Math.random() < animalType.spawnChance) { this.animal.push(new Animal(animalType, this.position));
if (this.biome.type.movements.includes(animalType.movement)) { }
this.entities.push(new Animal(animalType, this)); }
} }
} }
} }
} }
}
} }
class Map { class Map {
constructor(size) { constructor(size) {
@ -546,74 +512,6 @@ setEntities() {
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 {
@ -628,7 +526,7 @@ setEntities() {
} }
class Creature { class Creature {
constructor(name,attributes,level,affinities,alignments,tile,species,race,hp,items) { constructor(name,attributes,level,affinities,alignments,position,species,race,hp,items) {
this.name = name; this.name = name;
this.attributes = attributes; this.attributes = attributes;
this.level = level; this.level = level;
@ -637,28 +535,15 @@ setEntities() {
this.alignments = alignments; this.alignments = alignments;
this.species = species; this.species = species;
this.race = race; this.race = race;
this.tile=tile; this.position=position;
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.creature = creature; this.tile = creature.tile;
this.settlement = settlement; this.settlement = settlement;
this.lastMoveTime = 0; this.lastMoveTime = 0;
this.moveCooldown = 3000 + Math.random() * 4000; this.moveCooldown = 3000 + Math.random() * 4000;
@ -706,22 +591,22 @@ setEntities() {
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.type.movements.includes('walk') && !targetTile.structure) { if(targetTile.biome.movements.includes('walk') && !targetTile.structure) {
possibleMoves.push(targetTile); possibleMoves.push(targetTile);
} }
} }
} }
} }
if(possibleMoves.length > 0) { if(possibleMoves.length > 0) {
this.creature.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; this.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
} }
} }
setDesign(ctx) { setDesign(ctx) {
if (!this.creature.tile || this.creature.tile.visibility !== 2) return; if (!this.tile || this.tile.visibility !== 2) return;
const screenPos = this.creature.tile.position.cartToIso(); const screenPos = this.tile.position.cartToIso();
const elevationHeight = this.creature.tile.position.h * ELEVATION_STEP; const elevationHeight = this.tile.position.h * ELEVATION_STEP;
ctx.save(); ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight); ctx.translate(screenPos.x, screenPos.y - elevationHeight);
@ -742,7 +627,7 @@ setEntities() {
} }
setDesign(ctx) { setDesign(ctx) {
const position = this.creature.tile.position const position = this.creature.position
if(!position) return; if(!position) return;
const screenPos = position.cartToIso(); const screenPos = position.cartToIso();
@ -759,327 +644,26 @@ setEntities() {
ctx.restore(); ctx.restore();
} }
move(dx, dy, gameMap) { move(dx, dy, gameMap) {
const newX = this.creature.tile.position.x + dx; const newX = this.creature.position.x + dx;
const newY = this.creature.tile.position.y + dy; const newY = this.creature.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]; const targetTile = gameMap.tiles[newY][newX].position;
if (targetTile.biome && targetTile.biome.type.movements.includes('walk')) { if (targetTile.biome && targetTile.biome.movements.includes('walk')) {
this.creature.tile = targetTile; this.creature.position = 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.camera = new Camera(0,0, canvas); this.animals = [];
this.player=null; this.npcs = [];
}
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) {
@ -1098,25 +682,74 @@ setEntities() {
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.handleMovement(currentTime); this.update(currentTime);
this.world.update(currentTime); this.setDesign();
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.type.movements.includes('walk') ); } while (!this.world.map.tiles[spawnY][spawnX].biome || !this.world.map.tiles[spawnY][spawnX].biome.movements.includes('walk') );
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.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.updateVisibility(this.world.map.tiles[spawnY][spawnX].position); this.updateVisibility();
} }
setControls() { setControls() {
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
@ -1142,23 +775,71 @@ setEntities() {
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.world.player.move(0, -1,this.world.map); moved = true; } if (controls.up) { 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.down) { this.player.move(0, 1,this.world.map); moved = true; }
if (controls.left) { this.world.player.move(-1, 0,this.world.map); moved = true; } if (controls.left) { 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 (controls.right) { this.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.world.player.creature.position.x; const px = this.player.creature.position.x;
const py = this.world.player.creature.position.y; const py = this.player.creature.position.y;
let targetNpc = null; let targetNpc = null;
for (const npc of this.world.npcs) { for (const npc of this.world.npcs) {
@ -1367,8 +1048,8 @@ setEntities() {
canvas.height = canvas.parentElement.clientHeight; canvas.height = canvas.parentElement.clientHeight;
try { try {
for (const key in BIOME_TYPE) { for (const key in Biome) {
if (BIOME_TYPE[key].design) { BIOME_TYPE[key].sprite = new Sprite(BIOME_TYPE[key]);} if (Biome[key].design) { Biome[key].sprite = new Sprite(Biome[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'];