defiance/js/test.js
2025-09-08 16:08:34 +02:00

1385 lines
55 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
// --- 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();
}
}
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"];
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 ItemType = {
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 = {
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 = {
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', 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;
}
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];
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 {
constructor(x, y) {
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();
}
setBiome() {
let scale = 0.05;
let eRaw = (simplex.noise2D(this.position.x * scale, this.position.y * scale) + 1) / 2;
let tRaw = (simplex.noise2D(this.position.x * scale * 0.8, this.position.y * scale * 0.8) + 1) / 2;
let mRaw = (simplex.noise2D(this.position.x * scale * 1.5, this.position.y * scale * 1.5) + 1) / 2;
if (eRaw < 0.25) this.biome = Biome.WATER_DEEP;
else if (eRaw < 0.3) this.biome = Biome.WATER_SHALLOW;
else if (eRaw < 0.32) this.biome = Biome.SWAMP;
else if (eRaw < 0.35) this.biome = Biome.BEACH;
else if (eRaw > 0.85) this.biome = Biome.SNOWMOUNTAIN;
else if (eRaw > 0.75) this.biome = Biome.MOUNTAIN;
else {
if (tRaw < 0.3) this.biome = Biome.DESERT;
else if (tRaw > 0.6) this.biome = (mRaw > 0.7) ? Biome.ENCHANTED_FOREST : Biome.FOREST;
else this.biome = Biome.GRASSLAND;
}
}
setDesign(ctx){
if (!this.biome || this.visibility === 0) return;
const screenPos = this.position.cartToIso();
const elevationHeight = this.position.h * ELEVATION_STEP;
ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
const baseColor = this.biome.summerColor;
const shadowColor = shadeColor(baseColor, -30);
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();
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.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();
}
setElevation(){
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 || !this.biome.acceptStructure) return;
var rand = seededRandom(this.position.x * 13 + this.position.y * 59);
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;
}
}
}
}
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');
}
generateRivers() {
const numRivers = 10;
for (let i = 0; i < numRivers; i++) {
let currentX = Math.floor(Math.random() * this.size);
let currentY = Math.floor(Math.random() * this.size);
let sourceTile = this.tiles[currentY][currentX];
if (sourceTile.biome !== Biome.MOUNTAIN) continue;
let maxLength = 200;
while (maxLength > 0) {
maxLength--;
const currentTile = this.tiles[currentY][currentX];
if (currentTile.biome === Biome.WATER_DEEP || currentTile.biome === Biome.WATER_SHALLOW) break;
currentTile.biome = Biome.RIVER;
currentTile.elevation = Math.max(1, currentTile.elevation -1);
const neighbors = [ {x:0, y:-1}, {x:0, y:1}, {x:-1, y:0}, {x:1, y:0} ];
let lowestNeighbor = null;
let lowestElevation = currentTile.elevation;
for (const n of neighbors) {
const nx = currentX + n.x;
const ny = currentY + n.y;
if (nx >= 0 && nx < this.size && ny >= 0 && ny < this.size) {
const neighborTile = this.tiles[ny][nx];
if (neighborTile.elevation < lowestElevation) {
lowestElevation = neighborTile.elevation;
lowestNeighbor = {x: nx, y: ny};
}
}
}
if (lowestNeighbor) {
currentX = lowestNeighbor.x;
currentY = lowestNeighbor.y;
} else {
break;
}
}
}
}
generateRoads(locations) {
//Location to transform into settlement
if (locations.length < 2) return;
for (let i = 0; i < locations.length - 1; i++) {
let start = locations[i];
let end = locations[i+1];
let currentX = start.x;
let currentY = start.y;
while(Math.abs(currentX - end.x) > 0 || Math.abs(currentY - end.y) > 0) {
if (Math.abs(currentX - end.x) > Math.abs(currentY - end.y)) {
currentX += Math.sign(end.x - currentX);
} else {
currentY += Math.sign(end.y - currentY);
}
if (currentX >= 0 && currentX < this.size && currentY >= 0 && currentY < this.size) {
const tile = this.tiles[currentY][currentX];
if (tile.biome === Biome.RIVER) {
tile.hasBridge = true;
} else if (tile.biome !== Biome.WATER_DEEP && tile.biome !== Biome.WATER_SHALLOW) {
tile.isRoad = true;
}
}
}
}
}
}
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 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;
this.defense=race.defense;
}
}
class Creature {
constructor(name,attributes,level,affinities,alignments,tile,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.tile=tile;
this.items=items;
this.hp=hp;
}
}
class Npc {
constructor(type, homeTile) {
this.type = type;
this.tile = homeTile;
this.homeTile = homeTile;
this.lastMoveTime = 0;
this.moveCooldown = 3000 + Math.random() * 4000;
this.questId = null;
}
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();
}
getDialogue(quests) {
if(this.questId) {
let quest = quests[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: [] };
}
}
class Player {
constructor(creature,equipments) {
this.creature=creature;
this.equipments=equipments;
this.quests={};
}
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 * 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.position.x + dx;
const newY = this.position.y + dy;
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
const targetTile = gameMap.tiles[newY][newX];
if (targetTile.biome && targetTile.biome.movements.includes('walk')) {
this.creature.tile = targetTile;
}
}
}
setQuest(questDefinition,questId) {
if(!this.quests[questId]) {
this.quests[questId] = new Quest(questId, questDefinition);
}
this.quests[questId];
}
}
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.animals = [];
this.npcs = [];
this.quests = [];
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;
}
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();
}
loop(currentTime) {
this.update(currentTime);
this.setDesign();
requestAnimationFrame(this.loop);
}
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 {
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.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; }
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.save();
ctx.translate(this.camera.x, this.camera.y);
const [startX, endX, startY, endY] = this.getVisibleTileBounds();
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
if (this.world.map.tiles[y] && this.world.map.tiles[y][x]) {
this.world.map.tiles[y][x].setDesign(ctx);
}
}
}
this.world.animals.forEach(animal => animal.setDesign(ctx));
this.world.npcs.forEach(npc => npc.setDesign(ctx));
this.player.setDesign(ctx);
ctx.restore();
ctx.save();
this.world.currentTime.setDesign(ctx);
ctx.restore();
}
getVisibleTileBounds() {
const margin = 5;
const viewWidth = canvas.width;
const viewHeight = canvas.height;
const isoToCart = (isoX, isoY) => {
const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
return { x: Math.floor(cartX), y: Math.floor(cartY) };
};
const topLeft = isoToCart(-this.camera.x, -this.camera.y);
const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
const startX = Math.max(0, topLeft.x - margin);
const endX = Math.min(this.world.map.size - 1, bottomRight.x + margin);
const startY = Math.max(0, topLeft.y - margin);
const endY = Math.min(this.world.map.size - 1, bottomRight.y + margin);
return [startX, endX, startY, endY];
}
handleMovement(currentTime) {
if (currentTime - lastMoveTime < 150) return;
let moved = false;
if (controls.up) { this.player.move(0, -1,this.world.map); moved = true; }
if (controls.down) { this.player.move(0, 1,this.world.map); moved = true; }
if (controls.left) { this.player.move(-1, 0,this.world.map); moved = true; }
if (controls.right) { this.player.move(1, 0,this.world.map); moved = true; }
if(moved) lastMoveTime = currentTime;
}
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 = '<p class="text-gray-400">Aucune quête active.</p>';
questLogDesktop.innerHTML = noQuestText;
questLogMobile.innerHTML = noQuestText;
return;
}
activeQuests.forEach(quest => {
const stage = quest.getCurrentStage();
const questElement = document.createElement('div');
questElement.innerHTML = `<h4 class="font-bold text-cyan-300">${quest.definition.title}</h4><p class="text-sm text-gray-300">${stage.text}</p>`;
questLogDesktop.appendChild(questElement);
questLogMobile.appendChild(questElement.cloneNode(true));
});
}
}
const controls = { up: false, down: false, left: false, right: false, interact: 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 createAnimatedSheet(width, height, frameCount, drawFrameFn) {
const sheetCanvas = document.createElement('canvas');
sheetCanvas.width = width;
sheetCanvas.height = height * frameCount;
const sheetCtx = sheetCanvas.getContext('2d');
sheetCtx.imageSmoothingEnabled = false;
for (let frame = 0; frame < frameCount; frame++) {
sheetCtx.save();
sheetCtx.translate(0, frame * height);
drawFrameFn(sheetCtx, frame, width, height);
sheetCtx.restore();
}
return sheetCanvas;
}
function loadSvgAsImage(gElement) {
return new Promise((resolve, reject) => {
if (!gElement) return reject(new Error("SVG <g> 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) {
const biome = Biome[key];
if (biome.design) {
const sheet = createAnimatedSheet(TILE_WIDTH, TILE_HEIGHT, biome.design.frames, biome.design.drawer);
biome.sprite = new Sprite(sheet, TILE_WIDTH, TILE_HEIGHT, biome.design.frames, biome.design.duration);
}
}
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'];
const assetElements = assetIds.map(id => document.getElementById(id));
[forestSVG, villageSVG, citySVG, playerSVG, enchantedForestSVG, swampSVG, wolfPackSVG, boarSVG, birdSVG, farmSVG, campSVG, npcSVG] = 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();
});