document.addEventListener('DOMContentLoaded', () => { const BIOME_TYPE = { 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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: [{ 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 = { VILLAGER: { name: "Habitant", dialogues: [ "Bien le bonjour, étranger.", "J'espère que la récolte sera bonne cette année.", "Faites attention aux loups dans la forêt.", "Le forgeron a de nouvelles marchandises, je crois." ] }, FARMER: { name: "Fermier", dialogues: [ "Le temps est parfait pour les cultures.", "Ces sangliers n'arrêtent pas de saccager mes champs !", "Une bonne terre, c'est tout ce qui compte." ] }, BANDIT: { name: "Bandit", dialogues: [ "Qu'est-ce que tu regardes ?", "Dégage d'ici avant que je me fâche.", "Ta bourse ou la vie !" ] } }; const Affinities = ['water','fire','sand','rock','dark','life','ice','wood','wind','metal','time','space','lava','light','spirit']; const RACE = { HUMAN: {strength:1,vitality:1,dexterity:1,intelligence:1,wisdom:1,luck:1} }; const SLOT = ['head','body','leg','foot','right-hand','left-hand','two-hands','finger1','finger2','neck','purse','backpack','belt1','belt2']; const ITEM_TYPE = { lightWeapon:{slot:['right-hand','left-hand','backpack'],soul:true,enchant:true}, heavyWeapon:{slot:['two-hands','backpack'],soul:true,enchant:true}, heavyArmor:{slot:['body','backpack'],soul:true,enchant:true}, lightArmor:{slot:['body','backpack'],soul:true,enchant:true}, shield:{slot:['right-hand','left-hand','backpack'],soul:true,enchant:true}, shoes:{slot:['foot','backpack'],soul:true,enchant:true}, pant:{slot:['leg','backpack'],soul:true,enchant:true}, helmet:{slot:['leg','backpack'],soul:true,enchant:true}, potion:{slot:['heal','backpack'],soul:false,enchant:false}, throwable:{slot:['belt','backpack'],soul:true,enchant:true}, craftMaterial:{slot:['backpack'],soul:false,enchant:false}, ring:{slot:['finger1','finger2','backpack'],soul:true,enchant:true}, amulet:{slot:['neck','backpack'],soul:true,enchant:true}, currency:{slot:['purse'],soul:true,enchant:true}, enchantMaterial:{slot:['backpack'],soul:false,enchant:false}, mercantMaterial:{slot:['backpack'],soul:false,enchant:false} } const ITEMS = { 'Épée en Bois': { name: 'Épée en Bois', itemType: 'lightWeapon', stats: { strength: 2 }, icon: '🗡️' ,enchantments:[] }, 'Armure de Cuir': { name: 'Armure de Cuir', itemType: 'lightArmor', stats: { vitality: 2 }, icon: '🧥',enchantments:[] }, 'Claymore': { name: 'Claymore', itemType: 'lightArmor', stats: { vitality: 2 }, icon: '🧥',enchantments:['primal'] }, 'Bois': { name: 'Bois', itemType: 'craftMaterial', icon: '🌲',biome:['FOREST'],occurence:0.9}, 'stone': { name: 'Pierre', itemType: 'craftMaterial', icon: '🗿',biome:['MOUNTAIN'],occurence:0.4}, 'Herbes': { name: 'Herbes', itemType: 'craftMaterial', icon: '🌿',biome:['GRASSLAND'],occurence:0.4}, 'Cristaux Magiques': { name: ' enchantMaterial',icon: '💎',biome:['MOUNTAIN','ENCHANTED_FOREST'],occurence:0.005}, 'Fer': { name: 'Fer', itemType: 'craftMaterial', icon: '🔩',biome:['MOUNTAIN'],occurence:0.001}, 'Mushroom': { name: 'Champignons', itemType: 'craftMaterial', icon: '🍄',biome:['FOREST'],occurence:0.01}, 'gold': { name: 'Champignons', itemType: 'mercantMaterial', icon: '💰',biome:['MOUNTAIN','RIVER'],occurence:0.001} }; const STRUCTURE_TYPE = { CASTLE: { name: 'Chateau', population: 15, spawnChance: 0.01, size: { w: 48, h: 48 }, offset: { x: -24, y: -28 } , svgAsset: () => citySVG }, HOUSE: { name: 'Maison', population: 5, spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -20, y: -16 } , svgAsset: () => houseSVG}, FARM: { name: 'Ferme', population: 5, spawnChance: 0.10, size: { w: 30, h: 30 }, offset: { x: -20, y: -16 }, svgAsset: () => farmSVG }, CAMP: { name: 'Campement', population: 2, spawnChance: 0.05, size: { w: 30, h: 30 }, offset: { x: -20, y: -20 } , svgAsset: () => campSVG }, MARKET: { name: 'Marché', population: 2, spawnChance: 0.05, size: { w: 30, h: 30 }, offset: { x: -20, y: -20 } , svgAsset: () => marketSVG }, CULT: { name: 'Lieu de culte', population: 2, spawnChance: 0.05, size: { w: 30, h: 30 }, offset: { x: -20, y: -20 } , svgAsset: () => cultSVG }, MINE: { name: 'Mine', population: 2, spawnChance: 0.05, size: { w: 30, h: 30 }, offset: { x: -20, y: -20 } , svgAsset: () => mineSVG } }; const SETTLEMENT_TYPE = { HUMAN_TOWN: { name: 'Ville', population: 100, spawnChance: 0.01, size: 10, structureType:[(STRUCTURE_TYPE.CASTLE,1),(STRUCTURE_TYPE.HOUSE,5),(STRUCTURE_TYPE.MARKET,2),(STRUCTURE_TYPE.CULT,2)] }, HUMAN_VILLAGE: { name: 'Village', population: 50, spawnChance: 0.02, size: 3, structureType:[(STRUCTURE_TYPE.CASTLE,1),(STRUCTURE_TYPE.HOUSE,5)]}, DEMON_CAMP: { name: 'Demon camp', population: 5, spawnChance: 0.10, size: 3, structureType:[(STRUCTURE_TYPE.CULT,1),(STRUCTURE_TYPE.HOUSE,10)] }, ORC_BASE: { name: 'Orc base', population: 2, spawnChance: 0.05, size: 5, structureType:[(STRUCTURE_TYPE.CASTLE,1),(STRUCTURE_TYPE.HOUSE,5)] } }; const MONSTER_TYPES = { GOBLIN: { name: 'Gobelin', icon: '👺', hp: 30, strength: 3, xp: 25, loot: { 'Or': 5 } }, ORC: { name: 'Orc', icon: '👹', hp: 60, strength: 6, xp: 50, loot: { 'Or': 15, 'Fer': 1 } }, }; const ANIMAL_TYPES = { WOLF: { name: 'Loup', movement: 'walk', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, defense: 1, 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, defense: 2, 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', design: { frames: 2, duration: 200, drawer: drawBirdFrame }, hp: 10, strength: 1, defense: 0, xp: 5, loot: { 'Plume': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.BEACH], spawnChance: 0.03, size: { w: 24, h: 16 }, offset: { x: -12, y: -50 }, flightHeight: 40, nonHostile: true }, }; // --- Global Asset Variables --- let forestSVG, villageSVG, citySVG, playerSVG, enchantedForestSVG, swampSVG, wolfPackSVG, boarSVG, birdSVG, farmSVG, campSVG, npcSVG, houseSVG, cultSVG, marketSVG, mineSVG; class Sprite { constructor(biome) { this.biome = biome; this.sheet = null; this.frameWidth = TILE_WIDTH; this.frameHeight = TILE_HEIGHT; this.frameCount = this.biome.design.frames; this.animationSpeed = this.biome.design.duration; this.currentFrame = 0; this.lastFrameTime = 0; this.setSheet(this.biome.design.drawer); } update(currentTime) { if (this.frameCount <= 1) return; // Don't animate static sprites if (currentTime - this.lastFrameTime > this.animationSpeed) { this.currentFrame = (this.currentFrame + 1) % this.frameCount; this.lastFrameTime = currentTime; } } draw(ctx, dx, dy) { if (!this.sheet) return; const sx = 0; const sy = this.currentFrame * this.frameHeight; ctx.drawImage(this.sheet, sx, sy, this.frameWidth, this.frameHeight, dx, dy, this.frameWidth, this.frameHeight ); } setSheet(drawer) { const sheetCanvas = document.createElement('canvas'); sheetCanvas.width = this.frameWidth; sheetCanvas.height = this.frameHeight * this.frameCount; const sheetCtx = sheetCanvas.getContext('2d'); sheetCtx.imageSmoothingEnabled = false; for (let frame = 0; frame < this.frameCount; frame++) { sheetCtx.save(); sheetCtx.translate(0, frame * this.frameHeight); drawer(sheetCtx,frame,this.frameWidth,this.frameHeight); sheetCtx.restore(); } this.sheet= sheetCanvas; } } class structure { constructor(position) { this.position = position; this.type = null; this.name = null; this.population=null; this.setType(); this.setName(); //this.setPopulation(); } setName() { var rand = seededRandom(this.position.x * 13 + this.position.y * 59); const NAME_SUFFIXES = ["bourg", "fort", "ville", "mont", "port", "bois", "rivage", "gard"]; this.name= this.type.name+ NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)] } setDesign(ctx) { // This function assumes the context is already translated to the tile's origin. const svgImage = this.type.svgAsset(); if (!svgImage) return; const size = this.type.size || { w: 40, h: 40 }; const offset = this.type.offset || { x: -20, y: -35 }; ctx.drawImage(svgImage, offset.x, offset.y, size.w, size.h); } setType() { const types = Object.values(STRUCTURE_TYPE); const weightedTypes = []; // Remplir le tableau pondéré en fonction des occurrences types.forEach(type => { const weight = Math.floor(type.spawnChance * 100); for (let i = 0; i < weight; i++) { weightedTypes.push(type); } }); // Sélectionner un type aléatoire dans le tableau pondéré this.type = weightedTypes[Math.floor(Math.random() * weightedTypes.length)]; } setPopulation() { for (let i = 0; i < this.type.population; i++) { //new NPC } } // Méthode de l'objet } class Settlement { constructor() { this.tiles = []; this.structures = []; this.name = null; this.population=null; this.setType(); this.setName(); //this.setPopulation(); } setName() { var rand = seededRandom(this.position.x * 13 + this.position.y * 59); const NAME_SUFFIXES = ["bourg", "fort", "ville", "mont", "port", "bois", "rivage", "gard"]; this.name= this.type.name+ NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)] } setType() { const types = Object.values(SETTLEMENT_TYPE); const weightedTypes = []; // Remplir le tableau pondéré en fonction des occurrences types.forEach(type => { const weight = Math.floor(type.spawnChance * 100); for (let i = 0; i < weight; i++) { weightedTypes.push(type); } }); // Sélectionner un type aléatoire dans le tableau pondéré this.type = weightedTypes[Math.floor(Math.random() * weightedTypes.length)]; } setPopulation() { for (let i = 0; i < this.type.population; i++) { //new NPC } } // Méthode de l'objet } class Position{ constructor(x,y,h=0) { this.x = x; this.y = y; this.h = h; } setPosition(x, y, h=0) { this.x = x; this.y = y; this.h = h; } getPosition() { return { x: this.x, y: this.y, h: this.h }; } cartToIso() { return { x: (this.x - this.y) * (TILE_WIDTH / 2), y: (this.x + this.y) * (TILE_HEIGHT / 2) } } } class Animal { constructor(type, tile) { this.type = type; this.tile = tile; this.lastMoveTime = 0; this.moveCooldown = 2000 + Math.random() * 3000; // Unifying with Creature class for stats const animalAttributes = new Attributes({strength: type.strength, vitality: type.hp, dexterity: 1, intelligence: 1, wisdom: 1, luck: 1}); this.creature = new Creature( type.name, animalAttributes, 1, null, null, tile, 'ANIMAL', null, type.hp, type.defense, type.xp, type.loot ); if (this.type.design) { this.anim = { frames: this.type.design.frames, duration: this.type.design.duration, drawer: this.type.design.drawer, currentFrame: 0, lastFrameTime: 0 } } } update(currentTime, gameMap) { if (this.anim) { if (currentTime - this.anim.lastFrameTime > this.anim.duration) { this.anim.currentFrame = (this.anim.currentFrame + 1) % this.anim.frames; this.anim.lastFrameTime = currentTime; } } if (currentTime - this.lastMoveTime > this.moveCooldown) { this.move(gameMap); this.lastMoveTime = currentTime; this.moveCooldown = 2000 + Math.random() * 3000; } } move(gameMap) { const possibleMoves = [ { dx: 0, dy: -1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }, { dx: 1, dy: 0 }]; const move = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; const newX = this.tile.position.x + move.dx; const newY = this.tile.position.y + move.dy; if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) { const targetTile = gameMap.tiles[newY][newX]; if (targetTile.biome && targetTile.biome.type.movements.includes(this.type.movement) && this.type.biomes.includes(targetTile.biome.type)) { const oldTile = this.tile; const entityIndex = oldTile.entities.indexOf(this); if (entityIndex > -1) { oldTile.entities.splice(entityIndex, 1); } targetTile.entities.push(this); this.tile = targetTile; this.creature.tile = targetTile; } } } setDesign(ctx) { if (!this.tile || this.tile.visibility !== 2) return; const flightHeight = this.type.flightHeight || 0; const size = this.type.size || { w: 40, h: 40 }; const offset = this.type.offset || { x: -20, y: -35 }; ctx.save(); ctx.translate(0, -flightHeight); if (flightHeight > 0) { ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.ellipse(0, flightHeight, size.w / 2, size.w / 4, 0, 0, 2 * Math.PI); ctx.fill(); } if (this.anim) { ctx.save(); ctx.translate(offset.x, offset.y); this.anim.drawer(ctx, this.anim.currentFrame, size.w, size.h); ctx.restore(); } else if (this.type.svgAsset) { const svgImage = this.type.svgAsset(); if (svgImage) { ctx.drawImage(svgImage, offset.x, offset.y, size.w, size.h); } } ctx.restore(); } } class Biome { constructor() { this.type=null; } setType(position){ let scale = 0.05; let eRaw = (simplex.noise2D(position.x * scale, position.y * scale) + 1) / 2; let tRaw = (simplex.noise2D(position.x * scale * 0.8, position.y * scale * 0.8) + 1) / 2; let mRaw = (simplex.noise2D(position.x * scale * 1.5, position.y * scale * 1.5) + 1) / 2; if (eRaw < 0.25) this.type = BIOME_TYPE.WATER_DEEP; else if (eRaw < 0.3) this.type = BIOME_TYPE.WATER_SHALLOW; else if (eRaw < 0.32) this.type = BIOME_TYPE.SWAMP; else if (eRaw < 0.35) this.type = BIOME_TYPE.BEACH; else if (eRaw > 0.85) this.type = BIOME_TYPE.SNOWMOUNTAIN; else if (eRaw > 0.75) this.type = BIOME_TYPE.MOUNTAIN; else { if (tRaw < 0.3) this.type = BIOME_TYPE.DESERT; else if (tRaw > 0.6) this.type = (mRaw > 0.7) ? BIOME_TYPE.ENCHANTED_FOREST : BIOME_TYPE.FOREST; else this.type = BIOME_TYPE.GRASSLAND; } }; setDesign(ctx, position){ const screenPos = position.cartToIso(); const elevationHeight = position.h * ELEVATION_STEP; ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight); const baseColor = this.type.summerColor; const shadowColor = shadeColor(baseColor, -30); // Draw tile faces ctx.fillStyle = shadowColor; ctx.beginPath(); ctx.moveTo(0, TILE_HEIGHT); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2 + elevationHeight); ctx.lineTo(0, TILE_HEIGHT + elevationHeight); ctx.closePath(); ctx.fill(); ctx.fillStyle = shadeColor(shadowColor, -10); ctx.beginPath(); ctx.moveTo(0, TILE_HEIGHT); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2 + elevationHeight); ctx.lineTo(0, TILE_HEIGHT + elevationHeight); ctx.closePath(); ctx.fill(); // Draw top surface using a sprite if available if (this.type.sprite) { this.type.sprite.draw(ctx, -TILE_WIDTH / 2, 0); } else { ctx.fillStyle = baseColor; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(0, TILE_HEIGHT); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.closePath(); 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); } ctx.restore(); // Correction: Restore the context after drawing }; } 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.isRoad = false; this.hasBridge = false; 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); const screenPos = this.position.cartToIso(); const elevationHeight = this.position.h * ELEVATION_STEP; // We need a dedicated save/restore block for entities and features on this tile ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight); if (this.isRoad) { ctx.fillStyle = '#a1662f'; // Dirt road color ctx.globalAlpha = 0.7; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(0, TILE_HEIGHT); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1.0; } else if (this.hasBridge) { ctx.fillStyle = '#8B4513'; // Wood color ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(0, TILE_HEIGHT); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1.0; } if(this.visibility === 2) { if (this.structure) this.structure.setDesign(ctx); this.entities.forEach(e => { if (e.setDesign) e.setDesign(ctx) }); } // Draw fog overlay if seen but not currently visible if (this.visibility === 1) { ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.lineTo(0, TILE_HEIGHT); ctx.lineTo(-TILE_WIDTH / 2, TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); } ctx.restore(); } setElevation(){ if (!this.biome) return; this.position.h = Math.floor(Math.random() * (this.biome.type.maxElevation - this.biome.type.minElevation+ 1) + this.biome.type.minElevation); } setStructure(){ var rand = seededRandom(this.position.x * 13 + this.position.y * 59); var structureChance = rand(); if (structureChance < 0.01 && this.biome.type.acceptStructure) { this.structure=new structure(this.position); } } setEntities() { // --- NPC liés aux structures --- if (this.structure && this.structure.type) { const structureType = this.structure.type; let job = JOB.VILLAGER; if (structureType === STRUCTURE_TYPE.FARM) job = JOB.FARMER; if (structureType === STRUCTURE_TYPE.CAMP) job = JOB.BANDIT; for (let i = 0; i < structureType.population; i++) { const creature = new Creature( job.name, new Attributes(RACE.HUMAN), 1, null, null, this, // la tuile du NPC 'HUMAN', RACE.HUMAN, 10, 1, 0, {} ); 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]; 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 { constructor(size) { this.size = size; this.tiles = []; this.initializeMap(); const structures = []; this.tiles.forEach(row => { row.forEach(tile => { if (tile.structure) { structures.push(tile.structure); } }); }); //const importantStructures = structures.filter(s => s.type !== STRUCTURE_TYPE.CAMP && s.type !== STRUCTURE_TYPE.MINE); //this.generateRoads(importantStructures); } initializeMap(){ console.time('Map Generation'); this.tiles = Array(this.size).fill(null).map((_, y) => Array(this.size).fill(null).map((_, x) => new Tile(x, y))); this.generateRivers(); console.timeEnd('Map Generation'); } generateRoads(locations) { if (locations.length < 2) return; for (let i = 0; i < locations.length - 1; i++) { let start = locations[i].position; let end = locations[i+1].position; 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.type === BIOME_TYPE.RIVER) { tile.hasBridge = true; } else if (tile.biome.type !== BIOME_TYPE.WATER_DEEP && tile.biome.type !== BIOME_TYPE.WATER_SHALLOW && tile.biome.type !== BIOME_TYPE.MOUNTAIN && tile.biome.type !== BIOME_TYPE.SNOWMOUNTAIN) { 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.type !== BIOME_TYPE.MOUNTAIN) continue; let maxLength = 200; while (maxLength > 0) { maxLength--; const currentTile = this.tiles[currentY][currentX]; if (currentTile.biome.type === BIOME_TYPE.WATER_DEEP || currentTile.biome.type === BIOME_TYPE.WATER_SHALLOW) break; currentTile.biome.type = BIOME_TYPE.RIVER; currentTile.position.h = Math.max(1, currentTile.position.h -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.position.h; 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.position.h < lowestElevation) { lowestElevation = neighborTile.position.h; lowestNeighbor = {x: nx, y: ny}; } } } if (lowestNeighbor) { currentX = lowestNeighbor.x; currentY = lowestNeighbor.y; } else { break; } } } } } class Time { constructor() { this.year = 812; this.month = 1; this.day = 13; this.season = 'winter'; this.hour = 12; this.daylight = true; this.minute=2 } tick(){ this.minute=(this.minute+1) % 60 if (this.minute==0) this.hour=(this.hour+1) % 24 if (this.hour==0 && this.minute == 0) this.day=(this.day+1) % 31; if (this.day==1 && this.hour == 0 && this.minute == 0) this.month=(this.month+1) % 13; if (this.month==1 && this.day == 1 && this.hour == 0 && this.minute == 0) this.year++; const timeStr = `${this.hour.toString().padStart(2, '0')}:${this.minute.toString().padStart(2, '0')}`; document.getElementById('time-display').textContent = `Jour ${this.day}, ${timeStr}`; document.getElementById('season-display').textContent = this.season; } setDesign(ctx){ let overlayColor = 'rgba(0,0,0,0)'; let opacity = 0; if (this.hour >= 20 || this.hour < 6) { opacity = 0.5; } else if (this.hour >= 18) { opacity = 0.3 * ( (this.hour - 18) / 2 ); } else if (this.hour < 8) { opacity = 0.3 * ( (8 - this.hour) / 2 ); } if (opacity > 0) { ctx.fillStyle = '#000033'; ctx.globalAlpha = opacity; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.globalAlpha = 1.0; } } } class Attributes { constructor(race) { this.strength = race.strength; this.vitality = race.vitality; this.dexterity = race.dexterity; this.intelligence = race.intelligence; this.wisdom = race.wisdom; this.luck = race.luck; } } class Creature { constructor(name,attributes,level,affinities,alignments,tile,species,race,hp,defense,xp,loot,items) { this.name = name; this.attributes = attributes; this.level = level; this.xp = xp || 0; this.affinities = affinities; this.alignments = alignments; this.species = species; this.race = race; this.tile=tile; this.items=items; this.maxHp = hp; this.currentHp = hp; this.defense = defense || 0; this.loot = loot || {}; } } class Npc { constructor(job,creature,settlement,equipments) { this.job = job; this.creature = creature; this.tile = creature.tile; this.structure = settlement; // The NPC's home structure this.equipments = equipments || []; this.lastMoveTime = 0; this.moveCooldown = 3000 + Math.random() * 4000; } setEquipments() { var i=0; const items = Object.values(ITEMS); const weightedTypes = []; var compatibleItem=null; for (const slot in SLOT) { var slotChance = rand(); if (slotChance < 0.005) { items.forEach(item => { if (ITEM_TYPE[item.itemType].slot.includes(slot)){ const weight = Math.floor(item.spawnChance * 100); for (let i = 0; i < weight; i++) { weightedTypes.push(item); } } }); compatibleItem = weightedTypes[Math.floor(Math.random() * weightedTypes.length)]; this.equipments[i]=new equipment(slot,compatibleItem); } }; } update(currentTime, gameMap) { if (currentTime - this.lastMoveTime > this.moveCooldown) { this.move(gameMap); this.lastMoveTime = currentTime; } } move(gameMap) { if (!this.tile || !this.structure) return; const wanderRadius = 5; // Let them wander a bit further const possibleMoves = []; const homeX = this.structure.position.x; const homeY = this.structure.position.y; for (let y = -wanderRadius; y <= wanderRadius; y++) { for (let x = -wanderRadius; x <= wanderRadius; x++) { const newX = homeX + x; const newY = homeY + y; if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) { const targetTile = gameMap.tiles[newY][newX]; if (targetTile.biome.type.movements.includes('walk') && !targetTile.structure) { possibleMoves.push(targetTile); } } } } if (possibleMoves.length > 0) { const targetTile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; // Remove from old tile const oldTile = this.tile; const entityIndex = oldTile.entities.indexOf(this); if (entityIndex > -1) { oldTile.entities.splice(entityIndex, 1); } // Add to new tile targetTile.entities.push(this); this.tile = targetTile; this.creature.tile = this.tile; } } setDesign(ctx) { if (!this.tile || this.tile.visibility !== 2) return; // The context is already translated by Tile.setDesign. We just draw the asset. ctx.drawImage(npcSVG, -25, -50, 50, 60); } interact() { const dialogue = this.job.dialogues[Math.floor(Math.random() * this.job.dialogues.length)]; return { name: this.job.name, text: dialogue }; } } class Player { constructor(creature,equipments) { this.creature=creature; this.equipments=equipments; this.inventory = {}; } addXp(amount) { this.creature.xp += amount; // Basic leveling up for demonstration const xpForNextLevel = 100 * this.creature.level; if (this.creature.xp >= xpForNextLevel) { this.creature.level++; this.creature.xp -= xpForNextLevel; this.creature.attributes.strength++; this.creature.attributes.vitality++; this.creature.maxHp += 10; this.creature.currentHp = this.creature.maxHp; // Full heal on level up } } setDesign(ctx) { const position = this.creature.tile.position if(!position) return; const screenPos = position.cartToIso(); const elevationHeight = position.h * ELEVATION_STEP; ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight); if (playerSVG) { ctx.drawImage(playerSVG, -25, -50, 50, 60); } else { ctx.fillStyle = '#E53E3E'; ctx.beginPath(); ctx.arc(0, TILE_HEIGHT / 2 - 6, 8, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } move(dx, dy, gameMap) { const newX = this.creature.tile.position.x + dx; const newY = this.creature.tile.position.y + dy; if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) { const targetTile = gameMap.tiles[newY][newX]; if (targetTile.biome && targetTile.biome.type.movements.includes('walk')) { this.creature.tile = targetTile; } } } } class Combat { constructor(player, opponent, session) { this.player = player; this.opponent = opponent; this.session = session; // To control game state this.attackBtnListener = () => this.handleAttack(); this.startCombat(); } startCombat() { this.session.gameState = 'combat'; document.getElementById('combat-screen').classList.remove('hidden'); document.getElementById('combat-screen').classList.add('flex'); document.getElementById('attack-btn').addEventListener('click', this.attackBtnListener); document.getElementById('combat-log').textContent = `Un ${this.opponent.creature.name} sauvage apparaît !`; this.updateCombatUI(); } handleAttack() { const playerDamage = Math.max(1, this.player.creature.attributes.strength - this.opponent.creature.defense); this.opponent.creature.currentHp = Math.max(0, this.opponent.creature.currentHp - playerDamage); document.getElementById('combat-log').textContent = `Vous infligez ${playerDamage} dégâts.`; this.updateCombatUI(); if (this.opponent.creature.currentHp <= 0) { this.endCombat(true); return; } document.getElementById('attack-btn').disabled = true; setTimeout(() => { const opponentDamage = Math.max(1, this.opponent.creature.attributes.strength - this.player.creature.defense); this.player.creature.currentHp = Math.max(0, this.player.creature.currentHp - opponentDamage); document.getElementById('combat-log').textContent += `\nLe ${this.opponent.creature.name} riposte et vous inflige ${opponentDamage} dégâts.`; this.updateCombatUI(); if (this.player.creature.currentHp <= 0) { this.endCombat(false); } else { document.getElementById('attack-btn').disabled = false; } }, 1000); } endCombat(isVictory) { document.getElementById('attack-btn').removeEventListener('click', this.attackBtnListener); const combatLog = document.getElementById('combat-log'); if (isVictory) { combatLog.textContent = `Vous avez vaincu le ${this.opponent.creature.name} !`; this.player.addXp(this.opponent.creature.xp); combatLog.textContent += `\nVous gagnez ${this.opponent.creature.xp} XP.`; Object.entries(this.opponent.creature.loot).forEach(([item, amount]) => { this.player.inventory[item] = (this.player.inventory[item] || 0) + amount; combatLog.textContent += `\nVous trouvez ${amount} ${item}.`; }); const opponentTile = this.opponent.tile; const entityIndex = opponentTile.entities.indexOf(this.opponent); if(entityIndex > -1) { opponentTile.entities.splice(entityIndex, 1); } } else { combatLog.textContent = `Vous avez été vaincu...`; this.player.creature.currentHp = 1; } setTimeout(() => { document.getElementById('combat-screen').classList.add('hidden'); document.getElementById('combat-screen').classList.remove('flex'); this.session.gameState = 'exploring'; this.session.currentCombat = null; this.session.updateAllInfoPanels(); }, 3000); } updateCombatUI() { const playerCreature = this.player.creature; const opponentCreature = this.opponent.creature; document.getElementById('combat-player-hp-text').textContent = `${playerCreature.currentHp} / ${playerCreature.maxHp}`; document.getElementById('combat-player-hp-bar').style.width = `${(playerCreature.currentHp / playerCreature.maxHp) * 100}%`; document.getElementById('combat-opponent-name').textContent = opponentCreature.name; // Use SVG asset for opponent icon if available if (this.opponent.type && this.opponent.type.svgAsset) { const svgImage = this.opponent.type.svgAsset(); document.getElementById('combat-opponent-icon').innerHTML = ``; } else { document.getElementById('combat-opponent-icon').textContent = '👹'; // Fallback } document.getElementById('combat-opponent-hp-text').textContent = `${opponentCreature.currentHp} / ${opponentCreature.maxHp}`; document.getElementById('combat-opponent-hp-bar').style.width = `${(opponentCreature.currentHp / opponentCreature.maxHp) * 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 { constructor(name) { this.name = name; this.map = new Map(200); this.currentTime=new Time(); this.camera = new Camera(0,0, canvas); 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.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); const [startX, endX, startY, endY] = this.getVisibleTileBounds(); for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { const tile = this.map.tiles[y]?.[x]; if (tile && tile.visibility === 2) { [...tile.entities].forEach(e => { if (e.update) { e.update(currentTime, this.map); } }); } } } this.camera.setCamera(this.player.creature.tile.position); 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 { constructor(x=0,y=0,canvas) { this.x=x; this.y=y; this.canvas = canvas; } setCamera(position) { var target = position.cartToIso(); var perspective = position.h * ELEVATION_STEP; this.x = this.canvas.width / 2 - target.x; this.y = this.canvas.height / 2 - (target.y - perspective); } } class Session { constructor() { this.world = new World('Defiance'); this.gameState = 'exploring'; // Can be 'exploring', 'dialogue', 'combat' this.currentCombat = null; this.setControls(); this.loop = this.loop.bind(this); this.setInitial(); this.updateAllInfoPanels(); } loop(currentTime) { if (this.gameState === 'exploring') { this.handleMovement(currentTime); } this.world.update(currentTime); this.world.setDesign(); requestAnimationFrame(this.loop); } setInitial(){ let spawnX, spawnY; do { spawnX = 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') ); const playerCreature = new Creature('Héros', new Attributes(RACE.HUMAN), 1, null, null, this.world.map.tiles[spawnY][spawnX], 'human', RACE.HUMAN, 50, 2, 0, {}); this.world.player = new Player(playerCreature); this.world.updateVisibility(this.world.map.tiles[spawnY][spawnX].position); } setControls() { document.addEventListener('keydown', e => { const key = e.key.toLowerCase(); if (this.gameState === 'exploring') { if (key.startsWith('arrow')) { controls[key.replace('arrow', '')] = true; } else if (['w', 'z'].includes(key)) { controls.up = true; } else if (['s'].includes(key)) { controls.down = true; } else if (['a', 'q'].includes(key)) { controls.left = true; } else if (['d'].includes(key)) { controls.right = true; } else if (key === 'e') { this.handleAction(); } } }); document.addEventListener('keyup', e => { const key = e.key.toLowerCase(); if (key.startsWith('arrow')) { controls[key.replace('arrow', '')] = false; } else if (['w', 'z'].includes(key)) { controls.up = false; } else if (['s'].includes(key)) { controls.down = false; } else if (['a', 'q'].includes(key)) { controls.left = false; } else if (['d'].includes(key)) { controls.right = false; } }); const controlMap = { 'btn-up': 'up', 'btn-down': 'down', 'btn-left': 'left', 'btn-right': 'right' }; for (const [id, dir] of Object.entries(controlMap)) { const btn = document.getElementById(id); btn.addEventListener('touchstart', e => { e.preventDefault(); if(this.gameState === 'exploring') controls[dir] = true; }, { passive: false }); btn.addEventListener('touchend', e => { e.preventDefault(); controls[dir] = false; }); } document.getElementById('btn-action').addEventListener('click', () => { if (this.gameState === 'exploring') this.handleAction(); }); } handleMovement(currentTime) { if (currentTime - lastMoveTime < 150) return; let moved = false; if (controls.up) { this.world.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.world.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; this.updateAllInfoPanels(); } } handleAction() { const playerTile = this.world.player.creature.tile; const px = playerTile.position.x; const py = playerTile.position.y; let targetEntity = null; // Check for entities in adjacent tiles for (let y = -1; y <= 1; y++) { for (let x = -1; x <= 1; x++) { if (x === 0 && y === 0) continue; // Skip player's own tile for now const checkX = px + x; const checkY = py + y; if (checkX >= 0 && checkX < this.world.map.size && checkY >= 0 && checkY < this.world.map.size) { const adjacentTile = this.world.map.tiles[checkY][checkX]; const hostileEntity = adjacentTile.entities.find(e => e instanceof Animal && !e.type.nonHostile); if(hostileEntity) { targetEntity = hostileEntity; break; } } } if (targetEntity) break; } if(targetEntity) { this.currentCombat = new Combat(this.world.player, targetEntity, this); } } showDialogue(name, text) { const dialogueBox = document.getElementById('dialogue-box'); document.getElementById('dialogue-name').textContent = name; document.getElementById('dialogue-text').textContent = text; dialogueBox.classList.remove('hidden'); this.gameState = 'dialogue'; } hideDialogue() { document.getElementById('dialogue-box').classList.add('hidden'); this.gameState = 'exploring'; } updateAllInfoPanels() { const player = this.world.player; const creature = player.creature; const xpForNextLevel = 100 * creature.level; let inventoryHtml = '

Inventaire

'; if (Object.keys(player.inventory).length === 0) { inventoryHtml += '

Vide

'; } else { inventoryHtml += ''; } inventoryHtml += '
'; const playerInfoHtml = `
Niveau: ${creature.level}
PV: ${creature.currentHp} / ${creature.maxHp}
XP: ${creature.xp} / ${xpForNextLevel}
Attributs:
${inventoryHtml}
`; const currentTile = player.creature.tile; const tileInfoHtml = `
Position: (${currentTile.position.x}, ${currentTile.position.y})
Biome: ${currentTile.biome.type.name}
${currentTile.structure ? `
Lieu: ${currentTile.structure.name} (${currentTile.structure.type.name})
` : ''} `; document.getElementById('player-info-desktop').innerHTML = playerInfoHtml; document.getElementById('player-info-mobile').innerHTML = playerInfoHtml; document.getElementById('tile-info-desktop').innerHTML = tileInfoHtml; document.getElementById('tile-info-mobile').innerHTML = tileInfoHtml; } } const controls = { up: false, down: false, left: false, right: false }; const simplex = new SimplexNoise(); let lastMoveTime = 0; const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const loadingScreen = document.getElementById('loading'); const TILE_WIDTH = 64; const TILE_HEIGHT = 32; const ELEVATION_STEP = TILE_HEIGHT / 4; const VISION_RADIUS = 8; function seededRandom(seed) { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280.0; }; } function shadeColor(c, p) { let R=parseInt(c.substring(1,3),16),G=parseInt(c.substring(3,5),16),B=parseInt(c.substring(5,7),16);R=parseInt(R*(100+p)/100);G=parseInt(G*(100+p)/100);B=parseInt(B*(100+p)/100);R=(R<255)?R:255;G=(G<255)?G:255;B=(B<255)?B:255; return "#"+("0"+R.toString(16)).slice(-2)+("0"+G.toString(16)).slice(-2)+("0"+B.toString(16)).slice(-2); } function drawGrasslandFrame(ctx, frame, width, height) { const PIXEL_SCALE = 2; const colors = ['#6A994E', '#588142', '#A5A450']; ctx.fillStyle = colors[1]; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); for (let i = 0; i < 150; i++) { const x = Math.random() * width; const y = (Math.random() * height / 2) + height / 4; const dx = Math.abs(x - width / 2); const dy = Math.abs(y - height / 2); if (dx / (width / 2) + dy / (height / 2) > 1) continue; const wave = Math.sin(x / 5 + frame * Math.PI / 2) * PIXEL_SCALE; ctx.fillStyle = colors[i % 2]; ctx.fillRect(x + wave, y - PIXEL_SCALE, PIXEL_SCALE, PIXEL_SCALE); } } function drawWaterFrame(ctx, frame, width, height) { const colors = ['#5A8AB8', '#4A7AA8', '#97aabdff']; ctx.fillStyle = colors[0]; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); for (let i = 0; i < 3; i++) { ctx.strokeStyle = colors[1]; ctx.lineWidth = 1; ctx.beginPath(); const startY = (height / 4) * (i + 1); const offset = (frame * 0.2) + i * 2; ctx.moveTo(0, startY + Math.sin(offset) * 2); for (let x = 0; x < width; x++) { const y = startY + Math.sin(x * 0.15 + offset) * 2; if (Math.abs(x - width / 2) / (width / 2) + Math.abs(y - height / 2) / (height / 2) < 1) { ctx.lineTo(x, y); } } ctx.stroke(); } } function drawDeepWaterFrame(ctx, frame, width, height) { const colors = ['#3D5A80', '#2A4A6D', '#1A202C']; ctx.fillStyle = colors[0]; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); for (let i = 0; i < 2; i++) { ctx.strokeStyle = colors[1]; ctx.lineWidth = 2; ctx.beginPath(); const startY = (height / 3) * (i + 1); const offset = (frame * 0.1) + i * 3; ctx.moveTo(0, startY + Math.sin(offset) * 1.5); for (let x = 0; x < width; x++) { const y = startY + Math.sin(x * 0.1 + offset) * 1.5; if (Math.abs(x - width / 2) / (width / 2) + Math.abs(y - height / 2) / (height / 2) < 1) { ctx.lineTo(x, y); } } ctx.stroke(); } } function drawMountainFrame(ctx, frame, width, height) { const colors = { base: '#A9A9A9', light: '#CCCCCC', shadow: '#808080', deepShadow: '#696969' }; // 1. Draw the base diamond shape ctx.fillStyle = colors.base; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); // 2. Draw the main "pointy" peak // The lit side (right) ctx.fillStyle = colors.light; ctx.beginPath(); ctx.moveTo(width / 2, 5); // Start slightly below the top point ctx.lineTo(width - 10, height / 2); ctx.lineTo(width / 2, height - 10); ctx.closePath(); ctx.fill(); // The shadowed side (left) ctx.fillStyle = colors.shadow; ctx.beginPath(); ctx.moveTo(width / 2, 5); ctx.lineTo(10, height / 2); ctx.lineTo(width / 2, height - 10); ctx.closePath(); ctx.fill(); // 3. Add some smaller, darker triangles for a more jagged look ctx.fillStyle = colors.deepShadow; ctx.beginPath(); ctx.moveTo(width / 2, 20); ctx.lineTo(25, height / 2 + 5); ctx.lineTo(width / 2, height - 15); ctx.closePath(); ctx.fill(); } function drawSwampFrame(ctx, frame, width, height) { const colors = ['#526044', '#36402D', '#6A994E']; ctx.fillStyle = colors[1]; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); for (let i = 0; i < 3; i++) { ctx.strokeStyle = colors[0]; ctx.lineWidth = 1; ctx.beginPath(); const startY = (height / 4) * (i + 1); const offset = (frame * 0.05) + i * 2.5; ctx.moveTo(0, startY + Math.sin(offset) * 1); for (let x = 0; x < width; x++) { const y = startY + Math.sin(x * 0.1 + offset) * 1; if (Math.abs(x - width / 2) / (width / 2) + Math.abs(y - height / 2) / (height / 2) < 1) { ctx.lineTo(x, y); } } ctx.stroke(); } } function drawBeachFrame(ctx, frame, width, height) { const colors = ['#E9D9A1', '#D4C092', '#FFFFFF']; ctx.fillStyle = colors[0]; ctx.beginPath(); ctx.moveTo(width / 2, 0); ctx.lineTo(width, height / 2); ctx.lineTo(width / 2, height); ctx.lineTo(0, height / 2); ctx.closePath(); ctx.fill(); for (let i = 0; i < 10; i++) { const x = Math.random() * width; const y = Math.random() * height; if (Math.abs(x - width / 2) / (width / 2) + Math.abs(y - height / 2) / (height / 2) > 0.9) continue; ctx.fillStyle = colors[2]; ctx.fillRect(x, y, 1, 1); } } function loadSvgAsImage(gElement) { return new Promise((resolve, reject) => { if (!gElement) return reject(new Error("SVG element not found.")); const viewBox = gElement.getAttribute('viewBox') || '0 0 100 100'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', viewBox); const defs = document.querySelector('svg > defs'); if (defs) svg.appendChild(defs.cloneNode(true)); svg.appendChild(gElement.cloneNode(true)); const svgString = new XMLSerializer().serializeToString(svg); const url = URL.createObjectURL(new Blob([svgString], { type: "image/svg+xml;charset=utf-8" })); const img = new Image(); img.onload = () => { URL.revokeObjectURL(url); resolve(img); }; img.onerror = (err) => { URL.revokeObjectURL(url); reject(err); }; img.src = url; }); } function drawBirdFrame(ctx, frame, width, height) { const bodyColor = '#4A5568'; // slate gray const wingColor = '#A0AEC0'; // lighter gray const centerX = width / 2; const centerY = height; // Anchor at the bottom center // Wing flap animation: a simple up/down motion for 2 frames const wingYOffset = (frame === 0) ? -3 : 0; // Body ctx.fillStyle = bodyColor; ctx.beginPath(); ctx.ellipse(centerX, centerY - 5, 4, 5, 0, 0, Math.PI * 2); ctx.fill(); // Wings ctx.fillStyle = wingColor; // Left Wing ctx.beginPath(); ctx.moveTo(centerX - 3, centerY - 6); ctx.lineTo(centerX - 12, centerY - 8 + wingYOffset); ctx.lineTo(centerX - 3, centerY - 4); ctx.closePath(); ctx.fill(); // Right Wing ctx.beginPath(); ctx.moveTo(centerX + 3, centerY - 6); ctx.lineTo(centerX + 12, centerY - 8 + wingYOffset); ctx.lineTo(centerX + 3, centerY - 4); ctx.closePath(); ctx.fill(); } async function initializeGame() { canvas.width = canvas.parentElement.clientWidth; canvas.height = canvas.parentElement.clientHeight; try { for (const key in BIOME_TYPE) { 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 assetElements = assetIds.map(id => document.getElementById(id)); [forestSVG, villageSVG, citySVG, playerSVG, enchantedForestSVG, swampSVG, wolfPackSVG, boarSVG, birdSVG, farmSVG, campSVG, npcSVG,houseSVG,cultSVG,marketSVG,mineSVG] = await Promise.all( assetElements.map(el => loadSvgAsImage(el)) ); console.log("All assets loaded."); let session = new Session(); loadingScreen.style.display = 'none'; session.loop(); } catch (error) { console.error("Failed to load assets:", error); loadingScreen.innerHTML = "Error loading assets. Please refresh."; } } initializeGame(); });