diff --git a/html/main.html b/html/main.html
index 1a104b6..59463a4 100644
--- a/html/main.html
+++ b/html/main.html
@@ -9,7 +9,7 @@
COMBAT !
diff --git a/js/script.js b/js/script.js
index f9743a6..62e5287 100644
--- a/js/script.js
+++ b/js/script.js
@@ -1,22 +1,153 @@
document.addEventListener('DOMContentLoaded', () => {
- const Biome = {
- WATER_DEEP: { 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: { 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: { 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:{frame:4,animationSpeed:200 }, 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: { 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: { 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: { 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: { 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: { 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: { 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: { 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: { acceptStructure:false, movements:['fly'],affinities:[('water',0.6),('earth',0.2),('dark',0.2)],name: 'Marais', winterColor: '#15521eff', fallColor: '#15521eff', summerColor: '#15521eff', autumnColor: '#15521eff',maxElevation:1,minElevation:1 }
+
+ // --- Sprite Drawing Functions ---
+
+ 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();
+ }
}
- let grasslandSheet;
- let grasslandFrame = 0;
- let lastGrassFrameTime = 0;
+ 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);
+ }
+ }
+
+ // --- Biome Configuration ---
+ const Biome = {
+ WATER_DEEP: { sprite: null, design: { frames: 8, duration: 250, 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: 8, 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: 99999, 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: 10, 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 NAME_PREFIXES = ["Chêne", "Sombre", "Pierre", "Haut", "Val", "Mur", "Guet", "Clair"];
const NAME_SUFFIXES = ["bourg", "fort", "ville", "mont", "port", "bois", "rivage", "gard"];
@@ -26,9 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
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 ItemType = {
@@ -64,11 +193,32 @@ document.addEventListener('DOMContentLoaded', () => {
};
- const StructureType = {
- TOWN: { name: 'Ville', population:15, icon: '🗡️', occurence:0.001,design: `
`},
- VILLAGE: { name: 'Village', population:5, icon: '🗡️',occurence:0.005, design: `
`},
- FARM: { name: 'Ferme', population:5, icon: '🗡️',occurence:0.010 },
- CAMP:{ name: 'Campement', population:2, icon: '⛺',occurence:0.002 },
+ const STRUCTURE_TYPE = {
+ CITY: { name: 'Ville', population: 15, spawnChance: 0.005, svg: () => citySVG },
+ VILLAGE: { name: 'Village', population: 5, spawnChance: 0.02, svg: () => villageSVG},
+ FARM: { name: 'Ferme', population: 3, spawnChance: 0.01, svg: () => farmSVG },
+ CAMP: { name: 'Campement', population: 2, spawnChance: 0.002, svg: () => campSVG },
+ };
+
+ const NPC_TYPES = {
+ 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 MONSTER_TYPES = {
@@ -77,36 +227,150 @@ document.addEventListener('DOMContentLoaded', () => {
};
const ANIMAL_TYPES = {
- WOLF: { name: 'Loup', svgAsset: () => wolfsSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [Biome.FOREST, Biome.MOUNTAIN, Biome.SNOWLAND], spawnChance: 0.01 },
- BOAR: { name: 'Sanglier', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [Biome.FOREST, Biome.GRASSLAND], spawnChance: 0.02 },
+ WOLF: { name: 'Loup', movementClass: 'ground', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [Biome.FOREST, Biome.MOUNTAIN, Biome.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } },
+ BOAR: { name: 'Sanglier', movementClass: 'ground', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [Biome.FOREST, Biome.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } },
+ BIRD: { name: 'Aigle', movementClass: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [Biome.FOREST, Biome.GRASSLAND, Biome.MOUNTAIN, Biome.BEACH], spawnChance: 0.03, size: { w: 45, h: 40 }, offset: { x: -22, y: -45 }, flightHeight: 40 },
};
-
-
-
+ // --- Global Asset Variables ---
+ let forestSVG, villageSVG, citySVG, playerSVG, enchantedForestSVG, swampSVG, wolfPackSVG, boarSVG, birdSVG, farmSVG, campSVG, npcSVG;
+ let geminiApiKey = "";
+
+ // --- QUEST DATABASE ---
+ const QUEST_DATABASE = {
+ 'WOLF_MENACE': {
+ title: "La menace des loups",
+ stages: {
+ 0: {
+ text: "Les loups sont devenus bien trop agressifs ces derniers temps. Ils s'approchent du village et effraient les enfants. Pourriez-vous nous aider à chasser quelques-uns d'entre eux ?",
+ choices: [
+ { text: "J'accepte de vous aider.", nextStage: 1 },
+ { text: "Désolé, je n'ai pas le temps.", nextStage: null }
+ ]
+ },
+ 1: {
+ text: "Merci ! S'il vous plaît, éliminez 3 meutes de loups dans les environs et revenez me voir. Soyez prudent.",
+ objective: { type: 'kill', target: 'WOLF', count: 3 },
+ choices: [
+ { text: "Je suis dessus.", nextStage: null }
+ ]
+ },
+ 2: {
+ text: "Vous l'avez fait ! Merci, vous avez rendu un grand service au village.",
+ choices: [
+ { text: "Ce n'était rien.", nextStage: 3 }
+ ]
+ },
+ 3: {
+ text: "Voici une petite récompense pour vos efforts. Revenez nous voir quand vous voulez !",
+ reward: { xp: 100, items: [] },
+ isEnd: true
+ }
+ }
+ }
+ };
+
+
+ class Sprite {
+ constructor(sheet, frameWidth, frameHeight, frameCount, animationSpeed) {
+ this.sheet = sheet;
+ this.frameWidth = frameWidth;
+ this.frameHeight = frameHeight;
+ this.frameCount = frameCount;
+ this.animationSpeed = animationSpeed;
+ this.currentFrame = 0;
+ this.lastFrameTime = 0;
+ }
+
+ update(currentTime) {
+ if (this.frameCount <= 1) return;
+ 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
+ );
+ }
+ }
+
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) * 32,
- y: (this.x + this.y) * 16
+ x: (this.x - this.y) * (TILE_WIDTH / 2),
+ y: (this.x + this.y) * (TILE_HEIGHT / 2)
}
}
- // Méthode de l'objet
+ }
+
+ 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];
+ const canMoveGround = this.type.movementClass === 'ground' && targetTile.biome && (targetTile.biome.movements.includes('walk') || targetTile.biome.movements.includes('climb'));
+ const canFly = this.type.movementClass === 'fly' && targetTile.biome;
+
+ if ((canMoveGround || canFly) && 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 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(screenPos.x, screenPos.y - elevationHeight - flightHeight);
+ if (flightHeight > 0) {
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
+ ctx.beginPath();
+ ctx.ellipse(0, flightHeight, 8, 4, 0, 0, 2 * Math.PI);
+ ctx.fill();
+ }
+ ctx.drawImage(svgImage, offset.x, offset.y, size.w, size.h);
+ ctx.restore();
+ }
}
class Tile {
@@ -115,15 +379,12 @@ document.addEventListener('DOMContentLoaded', () => {
this.position = new Position(x, y);
this.biome = null;
this.structure = null;
+ this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible
this.setBiome();
this.setElevation();
this.setStructure();
- //this.setDesign(ctx);
- //this.forestSvg=null;
- //this.setSvg();
}
- // Méthode de l'objet
setBiome() {
let scale = 0.05;
let eRaw = (simplex.noise2D(this.position.x * scale, this.position.y * scale) + 1) / 2;
@@ -141,88 +402,96 @@ document.addEventListener('DOMContentLoaded', () => {
else this.biome = Biome.GRASSLAND;
}
}
- setDesign(ctx,currentTime){
- if (!this.biome) return;
+
+ setDesign(ctx){
+ if (!this.biome || this.visibility === 0) return;
+
const screenPos = this.position.cartToIso();
- const elevationHeight = this.position.h * 8;
- let baseColor=null;
+ const elevationHeight = this.position.h * ELEVATION_STEP;
+
ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
- if (currentTime.season == 'winter') { baseColor = this.biome.winterColor;}
- if (currentTime.season == 'summer') { baseColor = this.biome.summerColor;}
- if (currentTime.season == 'fall') { baseColor = this.biome.fallColor;}
- if (currentTime.season == 'autumn') { baseColor = this.biome.autumnColor;}
+
+ const baseColor = this.biome.summerColor;
const shadowColor = shadeColor(baseColor, -30);
- // Right Face
ctx.fillStyle = shadowColor;
ctx.beginPath();
- ctx.moveTo(0, 32); ctx.lineTo(32, 16); ctx.lineTo(32, 16 + elevationHeight); ctx.lineTo(0, 32 + elevationHeight);
+ 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();
- // Left Face
ctx.fillStyle = shadeColor(shadowColor, -10);
ctx.beginPath();
- ctx.moveTo(0, 32); ctx.lineTo(-32, 16); ctx.lineTo(-32, 16 + elevationHeight); ctx.lineTo(0, 32 + elevationHeight);
+ 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();
- // Top Face
- ctx.fillStyle = baseColor;
- ctx.beginPath();
- ctx.moveTo(0, 0); ctx.lineTo(32, 16); ctx.lineTo(0, 32); ctx.lineTo(-32, 16);
- ctx.closePath();
- ctx.fill();
- if (this.biome === Biome.FOREST && forestSVG) {
- ctx.drawImage(forestSVG, -25, -25, 50, 50);
- } else if (this.biome === Biome.ENCHANTED_FOREST && mforestSVG) {
- ctx.drawImage(mforestSVG, -25, -25, 50, 50);
- } else if (this.biome === Biome.SWAMP && swampSVG) {
- ctx.drawImage(swampSVG, -25, -5, 50, 30);
+ 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.structure) {
- if (this.structure.type === 'village' && villageSVG) {
- ctx.drawImage(villageSVG, -20, -16, 40, 40);
- } else if (this.structure.type === 'city' && citySVG) {
- ctx.drawImage(citySVG, -24, -28, 48, 48);
- }
+
+ if(this.visibility === 2) {
+ if (this.biome === Biome.FOREST && forestSVG) {
+ ctx.drawImage(forestSVG, -25, -25, 50, 50);
+ } else if (this.biome === Biome.ENCHANTED_FOREST && enchantedForestSVG) {
+ ctx.drawImage(enchantedForestSVG, -25, -25, 50, 50);
+ } else if (this.biome === Biome.SWAMP && swampSVG) {
+ ctx.drawImage(swampSVG, -25, -5, 50, 30);
+ }
+
+ if (this.structure) {
+ const svg = this.structure.type.svg();
+ ctx.drawImage(svg, -20, -20, 40, 40);
+ }
}
+
+ 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();
}
- setExplored(explored) {
- this.explored=explored;
- }
setElevation(){
- if (!this.biome) {
- console.error("Impossible de définir l'élévation car le biome n'est pas défini pour la tuile", this.position);
- return;
- }
+ if (!this.biome) return;
this.position.h = Math.floor(Math.random() * (this.biome.maxElevation - this.biome.minElevation+ 1) + this.biome.minElevation);
}
+
setStructure(){
- if (!this.biome) {
- console.error("Impossible de définir une structure car le biome n'est pas défini pour la tuile", this.position);
- return;
- }
+ if (!this.biome || !this.biome.acceptStructure) return;
var rand = seededRandom(this.position.x * 13 + this.position.y * 59);
- var structureChance = rand();
- if (structureChance < 0.005 && this.biome.acceptStructure) {
- this.structure = { type: 'city', name: NAME_PREFIXES[Math.floor(rand() * NAME_PREFIXES.length)] + NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)], population: StructureType.TOWN.population + Math.floor(rand() * 150), buildings: ['Remparts', 'Place Forte (Château)', 'Grand Marché', 'Forge', 'Alchimiste', 'Enchanteur', 'Écurie', 'Ferme', 'Ferme', 'Ferme', 'Ferme', 'Ferme', 'Lieu de Culte', 'Nombreuses Maisons'] };
- } else if (structureChance < 0.02 && this.biome.acceptStructure) {
- this.structure = { type: 'village', name: NAME_PREFIXES[Math.floor(rand() * NAME_PREFIXES.length)] + NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)], population: StructureType.VILLAGE.population + Math.floor(rand() * 20), buildings: ['Maison du Chef', 'Marchand', 'Lieu de Culte', 'Ferme', 'Ferme', 'Plusieurs Maisons'] };
+
+ let chance = rand();
+ let cumulativeChance = 0;
+
+ for(const key in STRUCTURE_TYPE) {
+ const type = STRUCTURE_TYPE[key];
+ cumulativeChance += type.spawnChance;
+ if (chance < cumulativeChance) {
+ this.structure = {
+ type: type,
+ name: type.name === 'Campement' ? 'Campement de bandits' : (NAME_PREFIXES[Math.floor(rand() * NAME_PREFIXES.length)] + " " + NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)])
+ };
+ return;
+ }
}
- if ( this.biome.acceptStructure && Math.random() < 0.2) {
- this.structure={type:'farm', name: 'farm', population: 2, buildings: []}
- // npcs.push({ x: nx, y: ny, homeX: loc.x, homeY: loc.y, workX: nx, workY: ny, type: 'farmer', icon: '🧑🌾' });
- }
}
}
class Map {
constructor(size) {
this.size = size;
- this.tiles = null
+ this.tiles = [];
this.initializeMap();
}
@@ -245,43 +514,35 @@ document.addEventListener('DOMContentLoaded', () => {
tick(){
this.minute=(this.minute+1) % 60
- if (this.minute==1) this.hour=(this.hour+1) % 24
- if (this.hour==1) this.day=(this.day+1) % 30
- if (this.day==1) {this.month=(this.month+1) % 12;}
- if (this.month==1) {this.year=(this.year+1); }
- if (this.month==6 && this.day==21) {this.season='summer'; }
- if (this.month==9 && this.day==21) {this.season='autumn'; }
- if (this.month==12 && this.day==21) {this.season='winter'; }
- if (this.month==3 && this.day==21) {this.season='fall'; }
+ 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;
}
- wait(nbrHour){
- for (let i = 0; i < nbrHour; i++) {
- this.tick();
- }
- }
- setDesign(){
- let overlayColor = 'rgba(0,0,0,0)';
+
+ setDesign(ctx){
let opacity = 0;
- if (this.hour > 20 || this.hour < 6) { // Nuit (20h -> 6h)
- overlayColor = '#000033';
+ if (this.hour >= 20 || this.hour < 6) {
opacity = 0.5;
- } else if (this.hour > 18) { // Soir (18h -> 20h)
- overlayColor = '#FF8C00';
- opacity = 0.3 * ( ((this.hour/24) - 0.75) / 0.08 );
- } else if (this.hour < 8) { // Matin (6h -> 8h)
- overlayColor = '#FFD700';
- opacity = 0.3 * ( (0.33 - (this.hour/24) ) / 0.08 );
+ } else if (this.hour >= 18) {
+ opacity = 0.3 * ( (this.hour - 18) / 2 );
+ } else if (this.hour < 8) {
+ opacity = 0.3 * ( (8 - this.hour) / 2 );
}
- ctx.fillStyle = overlayColor;
- ctx.globalAlpha = opacity;
- document.getElementById('time-display').textContent = `Jour ${this.day}, ${this.hour}:${this.minute}`;
- document.getElementById('season-display').textContent = this.season;
-
+ if (opacity > 0) {
+ ctx.fillStyle = '#000033';
+ ctx.globalAlpha = opacity;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.globalAlpha = 1.0;
+ }
}
- // Méthode de l'objet
}
class Attributes {
@@ -293,58 +554,8 @@ document.addEventListener('DOMContentLoaded', () => {
this.wisdom = race.wisdom;
this.luck = race.luck;
}
-
- // Méthode de l'objet
-
- }
- class Animal {
- constructor(type, tile) {
- this.type = type;
- this.tile = tile; // The tile the animal is currently on
- this.lastMoveTime = 0;
- this.moveCooldown = 2000 + Math.random() * 3000; // Move every 2-5 seconds
- }
-
- 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) {
- const svgImage = this.type.svgAsset();
- if (!this.tile || !svgImage) return;
-
- const screenPos = this.tile.position.cartToIso();
- const elevationHeight = this.tile.position.h * 8;
-
- 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 Creature {
constructor(name,attributes,level,affinities,alignments,tile,species,race,hp,items) {
this.name = name;
@@ -359,61 +570,96 @@ document.addEventListener('DOMContentLoaded', () => {
this.items=items;
this.hp=hp;
}
-
- fight(){
-
- }
- // Méthode de l'objet
-
}
+
class Npc {
- constructor(name, creature, settlement, equipments) {
- this.creature = creature;
- this.settlement = settlement;
- this.equipments = equipments;
+ constructor(type, homeTile) {
+ this.type = type;
+ this.tile = homeTile;
+ this.homeTile = homeTile;
+ this.lastMoveTime = 0;
+ this.moveCooldown = 3000 + Math.random() * 4000;
+ this.questId = null;
}
-
- talk() {
+
+ assignQuest(questId) {
+ this.questId = questId;
+ }
+
+ 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.homeTile.position.x + x;
+ const newY = this.homeTile.position.y + y;
+ if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
+ const targetTile = gameMap.tiles[newY][newX];
+ if(targetTile.biome.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();
}
- steal() {
-
+ getDialogue(questManager) {
+ if(this.questId) {
+ let quest = questManager.getQuest(this.questId);
+ if(quest && quest.status !== 'completed') {
+ return { name: this.type.name, ...quest.getCurrentStage() };
+ }
+ }
+ const randomDialogue = this.type.dialogues[Math.floor(Math.random() * this.type.dialogues.length)];
+ return { name: this.type.name, text: randomDialogue, choices: [] };
}
-
- trade() {
-
- }
- // Méthode de l'objet
-
}
class Player {
- constructor(creature,equipments,playerImage) {
+ constructor(creature,equipments) {
this.creature=creature;
this.equipments=equipments;
- this.color = '#E53E3E';
- //this.playerImage = playerImage ;
- //this.setSvg();
}
get position() {
return this.creature.tile.position;
}
-
+
setDesign(ctx) {
const tile = this.creature.tile
if(!tile) return;
const screenPos = tile.position.cartToIso();
- const elevationHeight = tile.position.h * 8;
+ const elevationHeight = tile.position.h * ELEVATION_STEP;
ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
- if (playerSVG && playerSVG.complete) {
+ if (playerSVG) {
ctx.drawImage(playerSVG, -25, -50, 50, 60);
- } else { // Fallback to circle
- ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(0, TILE_HEIGHT / 2 - 6, 8, 0, Math.PI * 2); ctx.fill();
+ } else {
+ ctx.fillStyle = '#E53E3E'; ctx.beginPath(); ctx.arc(0, TILE_HEIGHT / 2 - 6, 8, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
}
@@ -427,180 +673,278 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
}
-
- rename(name) {
- this.name = name;
- }
-
- equipItem(item,slot) {
- equipment.equip(item,slot);
- updateAttribute();
- }
}
- class Settlement {
- constructor(level, affinities, name, alignments, position, structures) {
- this.level = level;
- this.affinities = affinities;
- this.name = name;
- this.alignments = alignments;
- this.structures = structures;
+ class Quest {
+ constructor(id, definition) {
+ this.id = id;
+ this.definition = definition;
+ this.currentStage = 0;
+ this.status = 'inactive'; // inactive, active, completed
}
-
- // Méthode de l'objet
-
- }
- class Resource {
- constructor(resourceType, position) {
- this.resourceType = resourceType;
- this.position = position;
+
+ getCurrentStage() {
+ return this.definition.stages[this.currentStage];
}
- collect(tile){
-
- }
- collect(creature){
- // check if creature can collect
- // transfer resulting items creature.items[]=this.resourceType.item
- //end object
- }
- // Méthode de l'objet
-
- }
- class Equipment {
-
- constructor(item,slot) {
- this.item = item;
- this.slot = slot;
- }
- // Méthode de l'objet
- equip(slot) {
- //replace current equipment
- //check if compatible
- //check if equipable
- this.slot = slot;
- }
- unequip() {
- this.slot = null;
-
+
+ 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';
+ }
+ }
+ }
}
}
- class Soul {
-
- constructor(name,description) {
- this.name = name;
- this.description = description;
- }
- // Méthode de l'objet
- invoke() {
- //
-
- }
- }
- class Item {
-
- constructor(name,duration,itemType) {
- this.name = name;
- if (!Object.values(ItemType).includes(itemType)) {
- throw new Error('Item type invalide !');
+
+ class QuestManager {
+ constructor(questGenerator) {
+ this.quests = {};
+ this.generator = questGenerator;
}
- this.itemType = itemType;
- if (duration){
- this.duration = itemType.duration
- } else {
- this.duration = duration
+
+ getQuest(questId) {
+ return this.quests[questId];
+ }
+
+ startQuest(questId, questDefinition) {
+ if(!this.quests[questId]) {
+ this.quests[questId] = new Quest(questId, questDefinition);
+ }
+ return this.quests[questId];
}
- }
- // Méthode de l'objet
- throw() {
- // delete after x Tick
- }
- destroy() {
-
- }
- use(){
- if (this.duration >1) {
- this.duration = this.duration-1
- }
- else {
- this.destroy();
+ async getQuestForNpc(npc) {
+ if(npc.questId && this.quests[npc.questId]) {
+ return this.quests[npc.questId];
+ }
+
+ try {
+ const questDefinition = await this.generator.generateQuestForNpc(npc);
+ if(questDefinition) {
+ const questId = `GEMINI_${Date.now()}`;
+ npc.assignQuest(questId);
+ return this.startQuest(questId, questDefinition);
+ }
+ } catch(e) {
+ console.error("Gemini quest generation failed, using fallback.", e);
+ const fallbackId = 'WOLF_MENACE';
+ npc.assignQuest(fallbackId);
+ return this.startQuest(fallbackId, QUEST_DATABASE[fallbackId]);
+ }
+ return null;
}
- }
}
-
+ class GeminiQuestGenerator {
+ 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.map = new Map(200);
this.currentTime=new Time();
this.animals = [];
- this.setAnimals();
- }
- // Méthode de l'objet
- save() {
-
- }
- setAnimals() {
- console.time('Animal Spawning');
- for (let y = 0; y < this.map.size; y++) {
- for (let x = 0; x < this.map.size; x++) {
- const tile = this.map.tiles[y][x];
- if (tile.biome && tile.biome.movements.includes('walk')) {
- for (const key in ANIMAL_TYPES) {
- const animalType = ANIMAL_TYPES[key];
- if (animalType.biomes.includes(tile.biome) && Math.random() < animalType.spawnChance) {
- this.animals.push(new Animal(animalType, tile));
- }
- }
- }
- }
- }
- console.log(`Spawned ${this.animals.length} animals.`);
- console.timeEnd('Animal Spawning');
+ this.npcs = [];
+ this.questManager = new QuestManager(new GeminiQuestGenerator());
+ this.spawnEntities();
}
+ spawnEntities() {
+ console.time('Entity Spawning');
+ for (let y = 0; y < this.map.size; y++) {
+ for (let x = 0; x < this.map.size; x++) {
+ const tile = this.map.tiles[y][x];
+
+ if(tile.structure) {
+ const structureType = tile.structure.type;
+ let npcType = NPC_TYPES.VILLAGER;
+ if(structureType === STRUCTURE_TYPE.FARM) npcType = NPC_TYPES.FARMER;
+ if(structureType === STRUCTURE_TYPE.CAMP) npcType = NPC_TYPES.BANDIT;
+
+ for(let i = 0; i < structureType.population; i++) {
+ this.npcs.push(new Npc(npcType, tile));
+ }
+ }
+
+ if (tile.biome) {
+ for (const key in ANIMAL_TYPES) {
+ const animalType = ANIMAL_TYPES[key];
+ if (animalType.biomes.includes(tile.biome) && Math.random() < animalType.spawnChance) {
+ if(animalType.movementClass === 'ground' && tile.biome.movements.includes('walk')) {
+ this.animals.push(new Animal(animalType, tile));
+ } else if (animalType.movementClass === 'fly') {
+ this.animals.push(new Animal(animalType, tile));
+ }
+ }
+ }
+ }
+ }
+ }
+ console.log(`Spawned ${this.animals.length} animals and ${this.npcs.length} NPCs.`);
+ console.timeEnd('Entity Spawning');
+ }
}
class Camera {
-
constructor(x=0,y=0,canvas) {
this.x=x;
this.y=y;
this.canvas = canvas;
}
- // Méthode de l'objet
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.camera = new Camera(0,0, canvas);
+ this.isDialogueActive = false;
this.setControls();
this.loop = this.loop.bind(this);
this.player=null;
this.setInitial();
}
- // Méthode de l'objet
loop(currentTime) {
- this.updateAnimals(currentTime);
- this.handleMovement(currentTime);
- this.camera.setCamera(this.player.position);
+ this.update(currentTime);
this.setDesign();
requestAnimationFrame(this.loop);
}
- updateAnimals(currentTime) {
- this.world.animals.forEach(animal => {
- animal.update(currentTime, this.world.map);
- });
+
+ update(currentTime) {
+ if(this.isDialogueActive) return;
+
+ this.handleMovement(currentTime);
+ this.updateVisibility();
+
+ 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.position);
+
+ for (const key in Biome) {
+ if (Biome[key].sprite) {
+ Biome[key].sprite.update(currentTime);
+ }
+ }
+
+ this.world.currentTime.tick();
}
+
+ updateVisibility() {
+ const map = this.world.map;
+ const px = this.player.position.x;
+ const py = this.player.position.y;
+ const radius = VISION_RADIUS;
+ const radiusSq = radius * radius;
+
+ const viewBox = {
+ minX: Math.max(0, px - radius - 2),
+ maxX: Math.min(map.size - 1, px + radius + 2),
+ minY: Math.max(0, py - radius - 2),
+ maxY: Math.min(map.size - 1, py + radius + 2)
+ };
+
+ for(let y = viewBox.minY; y <= viewBox.maxY; y++) {
+ for(let x = viewBox.minX; x <= viewBox.maxX; x++) {
+ const tile = map.tiles[y][x];
+ if(tile.visibility === 2) tile.visibility = 1;
+
+ const dx = px - x;
+ const dy = py - y;
+ if (dx * dx + dy * dy <= radiusSq) {
+ tile.visibility = 2;
+ }
+ }
+ }
+ }
+
+
setInitial(){
let spawnX, spawnY;
do {
@@ -608,48 +952,99 @@ document.addEventListener('DOMContentLoaded', () => {
spawnY = Math.floor(Math.random() * this.world.map.size);
} while (!this.world.map.tiles[spawnY][spawnX].biome || !this.world.map.tiles[spawnY][spawnX].biome.movements.includes('walk') );
this.player = new Player(new Creature('player',new Attributes(RACE.HUMAN),1,null,null,this.world.map.tiles[spawnY][spawnX],'human',RACE.HUMAN,10,null),);
+ this.updateVisibility();
}
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; });
- }
+ 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; }
+ else if (key === 'e') {
+ if(!this.isDialogueActive) this.handleInteraction();
+ }
+ });
+ 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; });
+ }
+ document.getElementById('btn-action').addEventListener('click', () => { if(!this.isDialogueActive) this.handleInteraction(); });
+ document.getElementById('dialogue-close').addEventListener('click', () => this.hideDialogue());
+
+ const apiKeyInputDesktop = document.getElementById('apiKeyInputDesktop');
+ const apiKeyInputMobile = document.getElementById('apiKeyInputMobile');
+
+ apiKeyInputDesktop.addEventListener('input', (e) => {
+ geminiApiKey = e.target.value;
+ apiKeyInputMobile.value = geminiApiKey;
+ });
+ apiKeyInputMobile.addEventListener('input', (e) => {
+ geminiApiKey = e.target.value;
+ apiKeyInputDesktop.value = geminiApiKey;
+ });
}
setDesign(){
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(this.camera.x, this.camera.y);
- for (let y = 0; y < this.world.map.size; y++) for (let x = 0; x < this.world.map.size; x++) this.world.map.tiles[y][x].setDesign(ctx,this.world.currentTime);
+ 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);
- this.world.currentTime.setDesign(ctx);
- ctx.fillRect(this.camera.x * -1, this.camera.y * -1, canvas.width, canvas.height);
- ctx.globalAlpha = 1.0;
- //drawFloatingTexts();
- ctx.restore();
- this.world.currentTime.tick();
+ ctx.restore();
+ ctx.save();
+ this.world.currentTime.setDesign(ctx);
+ ctx.restore();
+
}
+
+ getVisibleTileBounds() {
+ const margin = 5;
+ const viewWidth = canvas.width;
+ const viewHeight = canvas.height;
+
+ const isoToCart = (isoX, isoY) => {
+ const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
+ const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
+ return { x: Math.floor(cartX), y: Math.floor(cartY) };
+ };
+
+ const topLeft = isoToCart(-this.camera.x, -this.camera.y);
+ const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
+
+ const startX = Math.max(0, topLeft.x - margin);
+ const endX = Math.min(this.world.map.size - 1, bottomRight.x + margin);
+ const startY = Math.max(0, topLeft.y - margin);
+ const endY = Math.min(this.world.map.size - 1, bottomRight.y + margin);
+
+ return [startX, endX, startY, endY];
+ }
+
handleMovement(currentTime) {
if (currentTime - lastMoveTime < 150) return;
let moved = false;
@@ -659,104 +1054,186 @@ document.addEventListener('DOMContentLoaded', () => {
if (controls.right) { this.player.move(1, 0,this.world.map); moved = true; }
if(moved) lastMoveTime = currentTime;
}
+
+ async handleInteraction() {
+ const px = this.player.position.x;
+ const py = this.player.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) {
+ targetNpc = npc;
+ break;
+ }
+ }
+
+ if(targetNpc) {
+ this.showDialogue(targetNpc, {name: targetNpc.type.name, text: "Laissez-moi réfléchir..."});
+ const quest = await this.world.questManager.getQuestForNpc(targetNpc);
+ const dialogueData = targetNpc.getDialogue(this.world.questManager);
+ this.showDialogue(targetNpc, dialogueData);
+ }
+ }
+
+ showDialogue(npc, dialogueData) {
+ this.isDialogueActive = true;
+ const dialogueBox = document.getElementById('dialogue-box');
+ document.getElementById('dialogue-name').textContent = dialogueData.name;
+ document.getElementById('dialogue-text').textContent = dialogueData.text;
+
+ const choicesContainer = document.getElementById('dialogue-choices');
+ choicesContainer.innerHTML = '';
+
+ if(dialogueData.choices && dialogueData.choices.length > 0) {
+ document.getElementById('dialogue-close').classList.add('hidden');
+ dialogueData.choices.forEach((choice, index) => {
+ const button = document.createElement('button');
+ button.textContent = choice.text;
+ button.className = 'quest-choice-btn w-full';
+ button.onclick = () => {
+ const quest = this.world.questManager.getQuest(npc.questId);
+ if (quest) {
+ quest.advance(index);
+ this.updateQuestLogUI();
+ }
+ if(choice.nextStage !== null) {
+ const nextDialogue = npc.getDialogue(this.world.questManager);
+ this.showDialogue(npc, nextDialogue);
+ } else {
+ this.hideDialogue();
+ }
+ };
+ choicesContainer.appendChild(button);
+ });
+ } else {
+ document.getElementById('dialogue-close').classList.remove('hidden');
+ }
+
+ dialogueBox.classList.remove('hidden');
+ }
+
+ hideDialogue() {
+ document.getElementById('dialogue-box').classList.add('hidden');
+ this.isDialogueActive = false;
+ }
+
+ updateQuestLogUI() {
+ const questLogDesktop = document.getElementById('quest-log-desktop');
+ const questLogMobile = document.getElementById('quest-log-mobile');
+ questLogDesktop.innerHTML = '';
+ questLogMobile.innerHTML = '';
+
+ const activeQuests = Object.values(this.world.questManager.quests).filter(q => q.status === 'active');
+
+ if (activeQuests.length === 0) {
+ const noQuestText = '
Aucune quête active.
';
+ questLogDesktop.innerHTML = noQuestText;
+ questLogMobile.innerHTML = noQuestText;
+ return;
+ }
+
+ activeQuests.forEach(quest => {
+ const stage = quest.getCurrentStage();
+ const questElement = document.createElement('div');
+ questElement.innerHTML = `
${quest.definition.title}
${stage.text}
`;
+ questLogDesktop.appendChild(questElement);
+ questLogMobile.appendChild(questElement.cloneNode(true));
+ });
+ }
}
-
- const controls = { up: false, down: false, left: false, right: false };
+ const controls = { up: false, down: false, left: false, right: false, interact: false };
const simplex = new SimplexNoise();
let lastMoveTime = 0;
- let isGameReady = false;
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;
- let forestSVG, villageSVG, citySVG, boarSVG,wolfsSVG,swampSVG, playerSVG,mforestSVG;
+ const VISION_RADIUS = 8;
+
function seededRandom(seed) {
let s = seed;
return () => { s = (s * 9301 + 49297) % 233280; return s / 233280.0; };
}
- 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';
-
- // Create a new, standalone