document.addEventListener('DOMContentLoaded', () => { const BIOME_TYPE = { WATER_DEEP: { sprite:null, design: {frames: 8,duration: 200, drawer: drawDeepWaterFrame}, acceptStructure:false, movements:['swim'], affinities:[('water',0.8),('dark',0.2)], name: 'Eau Profonde', winterColor: '#3D5A80', fallColor: '#3D5A80', summerColor: '#3D5A80', autumnColor: '#3D5A80',maxElevation:0,minElevation:0 }, WATER_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:[('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:[('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:[('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:[('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:[('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:[('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:[('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:[('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:[('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:[('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 = { 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, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } }, BOAR: { name: 'Sanglier', movement: 'walk', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } }, BIRD: { name: 'Aigle', movement: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.BEACH], spawnChance: 0.03, size: { w: 15, h: 12 }, offset: { x: -22, y: -45 }, flightHeight: 40 }, }; // --- Global Asset Variables --- let forestSVG, villageSVG, citySVG, playerSVG, enchantedForestSVG, swampSVG, wolfPackSVG, boarSVG; 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) { 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; } update(currentTime, gameMap) { 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.movements.includes('walk') || targetTile.biome.movements.includes('climb')) && this.type.biomes.includes(targetTile.biome)) { this.tile = targetTile; } } } setDesign(ctx) { if (!this.tile || this.tile.visibility !== 2) return; const svgImage = this.type.svgAsset(); if (!svgImage) return; const screenPos = this.tile.position.cartToIso(); const elevationHeight = this.tile.position.h * ELEVATION_STEP; const size = this.type.size || { w: 40, h: 40 }; const offset = this.type.offset || { x: -20, y: -35 }; ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight); 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.biome.sprite) { this.biome.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.biome === BIOME_TYPE.ENCHANTED_FOREST && enchantedForestSVG) { ctx.drawImage(enchantedForestSVG, -25, -25, 50, 50); } else if (this.biome === 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.animal = null; this.npc=[]; this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible this.setBiome(); this.setElevation(); this.setStructure(); this.setEntity(); } 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 if(this.visibility === 2) { if (this.structure ) { this.structure.setDesign(ctx); } if (this.animal ) { this.animal.setDesign(ctx); } if (this.npc ) { this.npc.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.maxElevation - this.biome.minElevation+ 1) + this.biome.minElevation); } setStructure(){ var rand = seededRandom(this.position.x * 13 + this.position.y * 59); var structureChance = rand(); if (structureChance < 0.01 && this.biome.acceptStructure) { this.structure=new structure(this.position); } } setEntity(){ // a changer! if(this.structure) { 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++) { this.npc.push(new Npc(job,new Creature(job.name,null,1,null,null,this.position,'HUMAN',null,10,null),this,null )); } } else { if (this.biome) { for (const key in ANIMAL_TYPES) { const animalType = ANIMAL_TYPES[key]; if (animalType.biomes.includes(this.biome) && Math.random() < animalType.spawnChance) { if( this.biome.movements.includes(animalType.movement)) { this.animal = new Animal(animalType, this.position); } } } } } } } class Map { constructor(size) { this.size = size; this.tiles = []; this.initializeMap(); } 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))); console.timeEnd('Map Generation'); } } 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,position,species,race,hp,items) { this.name = name; this.attributes = attributes; this.level = level; this.affinities = affinities; this.name = name; this.alignments = alignments; this.species = species; this.race = race; this.position=position; this.items=items; this.hp=hp; } } class Npc { constructor(job,creature,settlement,equipments) { this.job = job; this.tile = creature.tile; this.settlement = settlement; this.lastMoveTime = 0; this.moveCooldown = 3000 + Math.random() * 4000; //f (!this.settlement){this.setSettlement()} //if (!this.equipments){this.setEquipments()} } 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) { // Remplir le tableau pondéré en fonction des occurrences 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); } } }); // Sélectionner un type aléatoire dans le tableau pondéré 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) { const wanderRadius = 3; const possibleMoves = []; for (let y = -wanderRadius; y <= wanderRadius; y++) { for (let x = -wanderRadius; x <= wanderRadius; x++) { const newX = this.settlement.position.x + x; const newY = this.settlement.position.y + 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) { this.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; } } setDesign(ctx) { if (!this.tile || this.tile.visibility !== 2) return; const screenPos = this.tile.position.cartToIso(); const elevationHeight = this.tile.position.h * ELEVATION_STEP; ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight); ctx.drawImage(npcSVG, -25, -50, 50, 60); ctx.restore(); } interact() { const dialogue = this.type.dialogues[Math.floor(Math.random() * this.type.dialogues.length)]; return { name: this.job.name, text: dialogue }; } } class Player { constructor(creature,equipments) { this.creature=creature; this.equipments=equipments; } setDesign(ctx) { const position = this.creature.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.position.x + dx; const newY = this.creature.position.y + dy; if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) { const targetTile = gameMap.tiles[newY][newX].position; if (targetTile.biome && targetTile.biome.type.movements.includes('walk')) { 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 { 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.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.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.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.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 { 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.setControls(); this.loop = this.loop.bind(this); this.setInitial(); } loop(currentTime) { 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') ); this.world.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); } setControls() { document.addEventListener('keydown', e => { const key = e.key.toLowerCase(); 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; } }); 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(); controls[dir] = true; }, { passive: false }); btn.addEventListener('touchend', e => { e.preventDefault(); controls[dir] = false; }); } } 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; } handleInteraction() { if (!controls.interact) return; controls.interact = false; // Consume the action const px = this.world.player.creature.position.x; const py = this.world.player.creature.position.y; let targetNpc = null; for (const npc of this.world.npcs) { const dx = Math.abs(npc.tile.position.x - px); const dy = Math.abs(npc.tile.position.y - py); if (dx <= 1 && dy <= 1) { // Check adjacent tiles targetNpc = npc; break; } } if(targetNpc) { const dialogueData = targetNpc.interact(); this.showDialogue(dialogueData.name, dialogueData.text); } } 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.isDialogueActive = true; } hideDialogue() { document.getElementById('dialogue-box').classList.add('hidden'); this.isDialogueActive = false; } } 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 = ['#A9A9A9', '#808080', '#696969']; 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 < 70; 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) > 1) continue; const w = 2 + Math.random() * 4; const h = 2 + Math.random() * 4; ctx.fillStyle = colors[1 + Math.floor(Math.random() * 2)]; ctx.fillRect(x - w / 2, y - h / 2, w, h); } } 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; }); } 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(); });