Compare commits

..

2 commits

Author SHA1 Message Date
b27b5ea041 Combat enabled 2025-09-12 23:32:53 +02:00
a82cabac15 bug session 2025-09-10 22:38:09 +02:00
3 changed files with 1820 additions and 1091 deletions

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,18 @@
document.addEventListener('DOMContentLoaded', () => {
const Biome = {
WATER_DEEP: { sprite:null, design: {frames: 8,duration: 200, drawer: drawDeepWaterFrame}, acceptStructure:false, movements:['swim'], affinities:[('water',0.8),('dark',0.2)], name: 'Eau Profonde', winterColor: '#3D5A80', fallColor: '#3D5A80', summerColor: '#3D5A80', autumnColor: '#3D5A80',maxElevation:0,minElevation:0 },
WATER_SHALLOW: { sprite:null, design: {frames: 6,duration: 150, drawer: drawWaterFrame}, acceptStructure:false,movements:['swim','ride','navigate','fly'], affinities:[('water',0.8),('life',0.2)],name: 'Eau Peu Profonde', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8',maxElevation:1,minElevation:1 },
BEACH: { sprite:null, design: {frames: 4,duration: 200, drawer: drawBeachFrame}, acceptStructure:true, movements:['walk','ride','fly'],affinities:[('sand',0.8),('water',0.2)],name: 'Sable', winterColor: '#E9D9A1', fallColor: '#E9D9A1', summerColor: '#E9D9A1', autumnColor: '#E9D9A1',maxElevation:2 ,minElevation:2 },
GRASSLAND: {sprite:null, design: { frames: 4,duration: 200, drawer: drawGrasslandFrame}, acceptStructure:true, movements:['walk','ride','fly'],affinities:[('life',0.6),('earth',0.2)],name: 'Plaine', winterColor: '#ecf1e3ff', fallColor: '#98C159', summerColor: '#a5a450ff', autumnColor: '#455e21ff',maxElevation:3,minElevation:2 },
FOREST: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('wood',0.6),('earth',0.4)], name: 'Forêt', winterColor: '#92ac83ff', fallColor: '#21a32cff', summerColor: '#6A994E', autumnColor: '#b88a28ff',maxElevation:3,minElevation:2 },
ENCHANTED_FOREST: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('wood',0.8),('dark',0.2),('life',0.2)], name: 'Forêt Enchantée', winterColor: '#7B6094', fallColor: '#7B6094', summerColor: '#7B6094', autumnColor: '#7B6094',maxElevation:3,minElevation:2 },
MOUNTAIN: { sprite:null, design: { frames: 1,duration: 9999, drawer: drawMountainFrame}, acceptStructure:false, movements:['climb','fly'],affinities:[('rock',0.6),('wind',0.4)],name: 'Montagne', winterColor: '#F7F7F7', fallColor: '#A9A9A9', summerColor: '#A9A9A9', autumnColor: '#A9A9A9',maxElevation:5,minElevation:3 },
SNOWLAND: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'], affinities:[('ice',0.8),('earth',0.2)],name: 'Toundra', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7',maxElevation:2,minElevation:2 },
SNOWMOUNTAIN: { sprite: null, acceptStructure:false, movements:['climb','fly'],affinities:[('ice',0.4),('rock',0.4),('wind',0.2)],name: 'Mont enneigé', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7',maxElevation:8,minElevation:4 },
DESERT: { sprite: null, acceptStructure:true,movements:['walk','ride','fly'],affinities:[('sand',0.8),('life',0.1),('fire',0.1)], name: 'Désert', winterColor: '#D4A373', fallColor: '#D4A373', summerColor: '#D4A373', autumnColor: '#D4A373',maxElevation:2,minElevation:2 },
RIVER: { sprite: null, acceptStructure:false,movements:['navigate','swim','fly'],affinities:[('water',0.6),('earth',0.2),('life',0.2)],name: 'Rivière', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8',maxElevation:2,minElevation:2 },
SWAMP: { sprite:null, design: { frames: 4,duration: 300, drawer: drawSwampFrame}, acceptStructure:false, movements:['fly'],affinities:[('water',0.6),('earth',0.2),('dark',0.2)],name: 'Marais', winterColor: '#0b2e10ff', fallColor: '#0b2e10ff', summerColor: '#0b2e10ff', autumnColor: '#0b2e10ff',maxElevation:1,minElevation:1 }
}
const BIOME_TYPE = {
WATER_DEEP: { sprite: null, design: { frames: 8, duration: 200, drawer: drawDeepWaterFrame }, acceptStructure: false, movements: ['swim'], affinities: [{ type: 'water', value: 0.8 }, { type: 'dark', value: 0.2 }], name: 'Eau Profonde', winterColor: '#3D5A80', fallColor: '#3D5A80', summerColor: '#3D5A80', autumnColor: '#3D5A80', maxElevation: 0, minElevation: 0 },
WATER_SHALLOW: { sprite: null, design: { frames: 6, duration: 150, drawer: drawWaterFrame }, acceptStructure: false, movements: ['swim', 'ride', 'navigate', 'fly'], affinities: [{ type: 'water', value: 0.8 }, { type: 'life', value: 0.2 }], name: 'Eau Peu Profonde', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8', maxElevation: 1, minElevation: 1 },
BEACH: { sprite: null, design: { frames: 4, duration: 200, drawer: drawBeachFrame }, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'sand', value: 0.8 }, { type: 'water', value: 0.2 }], name: 'Sable', winterColor: '#E9D9A1', fallColor: '#E9D9A1', summerColor: '#E9D9A1', autumnColor: '#E9D9A1', maxElevation: 2, minElevation: 2 },
GRASSLAND: { sprite: null, design: { frames: 4, duration: 200, drawer: drawGrasslandFrame }, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'life', value: 0.6 }, { type: 'earth', value: 0.2 }], name: 'Plaine', winterColor: '#ecf1e3ff', fallColor: '#98C159', summerColor: '#a5a450ff', autumnColor: '#455e21ff', maxElevation: 3, minElevation: 2 },
FOREST: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'wood', value: 0.6 }, { type: 'earth', value: 0.4 }], name: 'Forêt', winterColor: '#92ac83ff', fallColor: '#21a32cff', summerColor: '#6A994E', autumnColor: '#b88a28ff', maxElevation: 3, minElevation: 2 },
ENCHANTED_FOREST: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'wood', value: 0.8 }, { type: 'dark', value: 0.2 }, { type: 'life', value: 0.2 }], name: 'Forêt Enchantée', winterColor: '#7B6094', fallColor: '#7B6094', summerColor: '#7B6094', autumnColor: '#7B6094', maxElevation: 3, minElevation: 2 },
MOUNTAIN: { sprite: null, design: { frames: 1, duration: 9999, drawer: drawMountainFrame }, acceptStructure: false, movements: ['climb', 'fly'], affinities: [{ type: 'rock', value: 0.6 }, { type: 'wind', value: 0.4 }], name: 'Montagne', winterColor: '#F7F7F7', fallColor: '#A9A9A9', summerColor: '#A9A9A9', autumnColor: '#A9A9A9', maxElevation: 5, minElevation: 3 },
SNOWLAND: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'ice', value: 0.8 }, { type: 'earth', value: 0.2 }], name: 'Toundra', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7', maxElevation: 2, minElevation: 2 },
SNOWMOUNTAIN: { sprite: null, acceptStructure: false, movements: ['climb', 'fly'], affinities: [{ type: 'ice', value: 0.4 }, { type: 'rock', value: 0.4 }, { type: 'wind', value: 0.2 }], name: 'Mont enneigé', winterColor: '#F7F7F7', fallColor: '#F7F7F7', summerColor: '#F7F7F7', autumnColor: '#F7F7F7', maxElevation: 8, minElevation: 4 },
DESERT: { sprite: null, acceptStructure: true, movements: ['walk', 'ride', 'fly'], affinities: [{ type: 'sand', value: 0.8 }, { type: 'life', value: 0.1 }, { type: 'fire', value: 0.1 }], name: 'Désert', winterColor: '#D4A373', fallColor: '#D4A373', summerColor: '#D4A373', autumnColor: '#D4A373', maxElevation: 2, minElevation: 2 },
RIVER: { sprite: null, acceptStructure: false, movements: ['navigate', 'swim', 'fly'], affinities: [{ type: 'water', value: 0.6 }, { type: 'earth', value: 0.2 }, { type: 'life', value: 0.2 }], name: 'Rivière', winterColor: '#97aabdff', fallColor: '#5A8AB8', summerColor: '#5A8AB8', autumnColor: '#5A8AB8', maxElevation: 2, minElevation: 2 },
SWAMP: { sprite: null, design: { frames: 4, duration: 300, drawer: drawSwampFrame }, acceptStructure: false, movements: ['fly'], affinities: [{ type: 'water', value: 0.6 }, { type: 'earth', value: 0.2 }, { type: 'dark', value: 0.2 }], name: 'Marais', winterColor: '#0b2e10ff', fallColor: '#0b2e10ff', summerColor: '#0b2e10ff', autumnColor: '#0b2e10ff', maxElevation: 1, minElevation: 1 }
};
const JOB = {
VILLAGER: {
name: "Habitant",
@ -103,9 +102,9 @@ document.addEventListener('DOMContentLoaded', () => {
};
const ANIMAL_TYPES = {
WOLF: { name: 'Loup', movement: 'walk', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [Biome.FOREST, Biome.MOUNTAIN, Biome.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } },
BOAR: { name: 'Sanglier', movement: 'walk', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [Biome.FOREST, Biome.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } },
BIRD: { name: 'Aigle', movement: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [Biome.FOREST, Biome.GRASSLAND, Biome.MOUNTAIN, Biome.BEACH], spawnChance: 0.03, size: { w: 15, h: 12 }, offset: { x: -22, y: -45 }, flightHeight: 40 },
WOLF: { name: 'Loup', movement: 'walk', svgAsset: () => wolfPackSVG, hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.SNOWLAND], spawnChance: 0.01, size: { w: 40, h: 40 }, offset: { x: -20, y: -35 } },
BOAR: { name: 'Sanglier', movement: 'walk', svgAsset: () => boarSVG, hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND], spawnChance: 0.02, size: { w: 30, h: 30 }, offset: { x: -15, y: -28 } },
BIRD: { name: 'Aigle', movement: 'fly', svgAsset: () => birdSVG, hp: 10, strength: 1, xp: 5, loot: { 'Plume': 1 }, biomes: [BIOME_TYPE.FOREST, BIOME_TYPE.GRASSLAND, BIOME_TYPE.MOUNTAIN, BIOME_TYPE.BEACH], spawnChance: 0.03, size: { w: 15, h: 12 }, offset: { x: -22, y: -45 }, flightHeight: 40 },
};
// --- Global Asset Variables ---
@ -300,71 +299,65 @@ document.addEventListener('DOMContentLoaded', () => {
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)) {
if (targetTile.biome && (targetTile.biome.type.movements.includes('walk') || targetTile.biome.type.movements.includes('climb')) && this.type.biomes.type.includes(targetTile.biome.type)) {
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);
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.animal = null;
this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible
this.setBiome();
this.setElevation();
this.setStructure();
this.setEntity();
class Biome {
constructor() {
this.type=null;
}
setBiome() {
setType(position){
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;
let eRaw = (simplex.noise2D(position.x * scale, position.y * scale) + 1) / 2;
let tRaw = (simplex.noise2D(position.x * scale * 0.8, position.y * scale * 0.8) + 1) / 2;
let mRaw = (simplex.noise2D(position.x * scale * 1.5, position.y * scale * 1.5) + 1) / 2;
if (eRaw < 0.25) this.type = BIOME_TYPE.WATER_DEEP;
else if (eRaw < 0.3) this.type = BIOME_TYPE.WATER_SHALLOW;
else if (eRaw < 0.32) this.type = BIOME_TYPE.SWAMP;
else if (eRaw < 0.35) this.type = BIOME_TYPE.BEACH;
else if (eRaw > 0.85) this.type = BIOME_TYPE.SNOWMOUNTAIN;
else if (eRaw > 0.75) this.type = BIOME_TYPE.MOUNTAIN;
else {
if (tRaw < 0.3) this.biome = Biome.DESERT;
else if (tRaw > 0.6) this.biome = (mRaw > 0.7) ? Biome.ENCHANTED_FOREST : Biome.FOREST;
else this.biome = Biome.GRASSLAND;
if (tRaw < 0.3) this.type = BIOME_TYPE.DESERT;
else if (tRaw > 0.6) this.type = (mRaw > 0.7) ? BIOME_TYPE.ENCHANTED_FOREST : BIOME_TYPE.FOREST;
else this.type = BIOME_TYPE.GRASSLAND;
}
}
setDesign(ctx){
if (!this.biome || this.visibility === 0) return;
const screenPos = this.position.cartToIso();
const elevationHeight = this.position.h * ELEVATION_STEP;
};
setDesign(ctx, position){
const screenPos = position.cartToIso();
const elevationHeight = position.h * ELEVATION_STEP;
ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
const baseColor = this.biome.summerColor;
const baseColor = this.type.summerColor;
const shadowColor = shadeColor(baseColor, -30);
// Draw tile faces
@ -381,8 +374,8 @@ document.addEventListener('DOMContentLoaded', () => {
ctx.fill();
// Draw top surface using a sprite if available
if (this.biome.sprite) {
this.biome.sprite.draw(ctx, -TILE_WIDTH / 2, 0);
if (this.type.sprite) {
this.type.sprite.draw(ctx, -TILE_WIDTH / 2, 0);
} else {
ctx.fillStyle = baseColor;
ctx.beginPath();
@ -390,20 +383,44 @@ document.addEventListener('DOMContentLoaded', () => {
ctx.closePath();
ctx.fill();
}
if (this.type === BIOME_TYPE.FOREST && forestSVG) {
ctx.drawImage(forestSVG, -25, -25, 50, 50);
} else if (this.type === BIOME_TYPE.ENCHANTED_FOREST && enchantedForestSVG) {
ctx.drawImage(enchantedForestSVG, -25, -25, 50, 50);
} else if (this.type === BIOME_TYPE.SWAMP && swampSVG) {
ctx.drawImage(swampSVG, -25, -5, 50, 30);
}
};
}
class Tile {
constructor(x, y) {
this.position = new Position(x, y);
this.biome = null;
this.structure = null;
this.entities = [];
this.visibility = 0; // 0: Unseen, 1: Seen, 2: Visible
this.setBiome();
this.setElevation();
this.setStructure();
this.setEntities();
}
setBiome() {
this.biome=new Biome();
this.biome.setType(this.position);
}
setDesign(ctx){
if (!this.biome || this.visibility === 0) return;
this.biome.setDesign(ctx,this.position);
// Draw features if fully visible
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 ) {
this.structure.setDesign(ctx);
}
if(this.visibility === 2) {
if (this.structure) this.structure.setDesign(ctx);
this.entities.forEach(e => e.setDesign(ctx));
}
// Draw fog overlay if seen but not currently visible
@ -420,41 +437,58 @@ document.addEventListener('DOMContentLoaded', () => {
setElevation(){
if (!this.biome) return;
this.position.h = Math.floor(Math.random() * (this.biome.maxElevation - this.biome.minElevation+ 1) + this.biome.minElevation);
this.position.h = Math.floor(Math.random() * (this.biome.type.maxElevation - this.biome.type.minElevation+ 1) + this.biome.type.minElevation);
}
setStructure(){
var rand = seededRandom(this.position.x * 13 + this.position.y * 59);
var structureChance = rand();
if (structureChance < 0.01 && this.biome.acceptStructure) {
if (structureChance < 0.01 && this.biome.type.acceptStructure) {
this.structure=new structure(this.position);
}
}
setEntity(){
// a changer!
if(this.structure) {
const structureType = this.structure.type;
let job = JOB.VILLAGER;
if(structureType === STRUCTURE_TYPE.FARM) job = JOB.FARMER;
if(structureType === STRUCTURE_TYPE.CAMP) job = JOB.BANDIT;
setEntities() {
// --- NPC liés aux structures ---
if (this.structure && this.structure.type) {
const structureType = this.structure.type;
let job = JOB.VILLAGER;
if (structureType === STRUCTURE_TYPE.FARM) job = JOB.FARMER;
if (structureType === STRUCTURE_TYPE.CAMP) job = JOB.BANDIT;
for(let i = 0; i < structureType.population; i++) {
this.npc.push(new Npc(job,new Creature(job.name,null,1,null,null,this.position,'HUMAN',null,10,null),this,null ));
}
}
else {
if (this.biome) {
for (const key in ANIMAL_TYPES) {
const animalType = ANIMAL_TYPES[key];
if (animalType.biomes.includes(this.biome) && Math.random() < animalType.spawnChance) {
if( this.biome.movements.includes(animalType.movement)) {
this.animal.push(new Animal(animalType, this.position));
}
}
}
for (let i = 0; i < structureType.population; i++) {
const creature = new Creature(
job.name,
new Attributes(RACE.HUMAN),
1,
null,
null,
this, // la tuile du NPC
'HUMAN',
RACE.HUMAN,
10,
null
);
this.entities.push(new Npc(job, creature, this.structure, null));
}
}
// --- Animaux sauvages ---
else if (this.biome) {
for (const key in ANIMAL_TYPES) {
const animalType = ANIMAL_TYPES[key];
// 🔑 Vérif par nom au lieu d’objet
if (animalType.biomes.some(b => b.name === this.biome.type.name) && Math.random() < animalType.spawnChance) {
if (this.biome.type.movements.includes(animalType.movement)) {
this.entities.push(new Animal(animalType, this));
}
}
}
}
}
}
class Map {
constructor(size) {
@ -512,6 +546,74 @@ document.addEventListener('DOMContentLoaded', () => {
ctx.globalAlpha = 1.0;
}
}
generateRoads(locations) {
//Location to transform into settlement
if (locations.length < 2) return;
for (let i = 0; i < locations.length - 1; i++) {
let start = locations[i];
let end = locations[i+1];
let currentX = start.x;
let currentY = start.y;
while(Math.abs(currentX - end.x) > 0 || Math.abs(currentY - end.y) > 0) {
if (Math.abs(currentX - end.x) > Math.abs(currentY - end.y)) {
currentX += Math.sign(end.x - currentX);
} else {
currentY += Math.sign(end.y - currentY);
}
if (currentX >= 0 && currentX < this.size && currentY >= 0 && currentY < this.size) {
const tile = this.tiles[currentY][currentX];
if (tile.biome === Biome.RIVER) {
tile.hasBridge = true;
} else if (tile.biome !== Biome.WATER_DEEP && tile.biome !== Biome.WATER_SHALLOW) {
tile.isRoad = true;
}
}
}
}
}
generateRivers() {
const numRivers = 10;
for (let i = 0; i < numRivers; i++) {
let currentX = Math.floor(Math.random() * this.size);
let currentY = Math.floor(Math.random() * this.size);
let sourceTile = this.tiles[currentY][currentX];
if (sourceTile.biome !== Biome.MOUNTAIN) continue;
let maxLength = 200;
while (maxLength > 0) {
maxLength--;
const currentTile = this.tiles[currentY][currentX];
if (currentTile.biome === Biome.WATER_DEEP || currentTile.biome === Biome.WATER_SHALLOW) break;
currentTile.biome = Biome.RIVER;
currentTile.elevation = Math.max(1, currentTile.elevation -1);
const neighbors = [ {x:0, y:-1}, {x:0, y:1}, {x:-1, y:0}, {x:1, y:0} ];
let lowestNeighbor = null;
let lowestElevation = currentTile.elevation;
for (const n of neighbors) {
const nx = currentX + n.x;
const ny = currentY + n.y;
if (nx >= 0 && nx < this.size && ny >= 0 && ny < this.size) {
const neighborTile = this.tiles[ny][nx];
if (neighborTile.elevation < lowestElevation) {
lowestElevation = neighborTile.elevation;
lowestNeighbor = {x: nx, y: ny};
}
}
}
if (lowestNeighbor) {
currentX = lowestNeighbor.x;
currentY = lowestNeighbor.y;
} else {
break;
}
}
}
}
}
class Attributes {
@ -526,7 +628,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
class Creature {
constructor(name,attributes,level,affinities,alignments,position,species,race,hp,items) {
constructor(name,attributes,level,affinities,alignments,tile,species,race,hp,items) {
this.name = name;
this.attributes = attributes;
this.level = level;
@ -535,15 +637,28 @@ document.addEventListener('DOMContentLoaded', () => {
this.alignments = alignments;
this.species = species;
this.race = race;
this.position=position;
this.tile=tile;
this.items=items;
this.hp=hp;
}
}
class Npc {
constructor(job, creature, settlement, equipments) {
this.job = job;
this.creature = creature; // ✅ on stocke bien la créature
this.tile = creature.tile; // ✅ le NPC est placé sur la tuile de la créature
this.settlement = settlement; // ✅ lien avec le village/structure
this.equipments = equipments || [];
this.lastMoveTime = 0;
this.moveCooldown = 3000 + Math.random() * 4000;
}
}
class Npc {
constructor(job,creature,settlement,equipments) {
this.job = job;
this.tile = creature.tile;
this.creature = creature;
this.settlement = settlement;
this.lastMoveTime = 0;
this.moveCooldown = 3000 + Math.random() * 4000;
@ -591,22 +706,22 @@ document.addEventListener('DOMContentLoaded', () => {
const newY = this.settlement.position.y + y;
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
const targetTile = gameMap.tiles[newY][newX];
if(targetTile.biome.movements.includes('walk') && !targetTile.structure) {
if(targetTile.biome.type.movements.includes('walk') && !targetTile.structure) {
possibleMoves.push(targetTile);
}
}
}
}
if(possibleMoves.length > 0) {
this.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
this.creature.tile = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
}
}
setDesign(ctx) {
if (!this.tile || this.tile.visibility !== 2) return;
if (!this.creature.tile || this.creature.tile.visibility !== 2) return;
const screenPos = this.tile.position.cartToIso();
const elevationHeight = this.tile.position.h * ELEVATION_STEP;
const screenPos = this.creature.tile.position.cartToIso();
const elevationHeight = this.creature.tile.position.h * ELEVATION_STEP;
ctx.save();
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
@ -627,7 +742,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
setDesign(ctx) {
const position = this.creature.position
const position = this.creature.tile.position
if(!position) return;
const screenPos = position.cartToIso();
@ -644,26 +759,327 @@ document.addEventListener('DOMContentLoaded', () => {
ctx.restore();
}
move(dx, dy, gameMap) {
const newX = this.creature.position.x + dx;
const newY = this.creature.position.y + dy;
const newX = this.creature.tile.position.x + dx;
const newY = this.creature.tile.position.y + dy;
if (newX >= 0 && newX < gameMap.size && newY >= 0 && newY < gameMap.size) {
const targetTile = gameMap.tiles[newY][newX].position;
if (targetTile.biome && targetTile.biome.movements.includes('walk')) {
this.creature.position = targetTile;
const targetTile = gameMap.tiles[newY][newX];
if (targetTile.biome && targetTile.biome.type.movements.includes('walk')) {
this.creature.tile = targetTile;
}
}
}
}
class Combat {
constructor(player, opponent) {
this.player=player;
this.opponent=opponent;
this.state='initiating'
this.startCombat();
}
startCombat() {
this.state= 'started';
document.getElementById('combat-screen').classList.remove('hidden');
document.getElementById('combat-screen').classList.add('flex');
updateCombatUI();
document.getElementById('combat-log').textContent = `Le combat commence`;
document.getElementById('attack-btn').addEventListener('click', handleAttack);
}
handleAttack(camera) {
const playerDamage = Math.max(1, player.attributes.strength + Math.floor(Math.random() * 4) - this.opponent.attributes.defense);
this.opponent.currentHp = Math.max(0, this.opponent.currentHp - playerDamage);
document.getElementById('combat-log').textContent = `Vous infligez ${playerDamage} dégâts.`;
floatingTexts.push({ content: `-${playerDamage}`, x: camera.x + canvas.width * 0.7, y: camera.y + canvas.height * 0.4, endTime: performance.now() + 1000, duration: 1000, color: '#ff4444' });
updateCombatUI();
if (this.opponent.currentHp <= 0) {
endCombat(true);
return;
}
setTimeout(() => {
const opponentDamage = Math.max(1, this.opponent.attributes.strength + Math.floor(Math.random() * 4) - 2);
this.player.creature.hp = Math.max(0, this.player.creature.hp - opponentDamage);
document.getElementById('combat-log').textContent += `\nLe ${this.opponent.name} riposte et vous inflige ${opponentDamage} dégâts.`;
floatingTexts.push({ content: `-${opponentDamage}`, x: camera.x + canvas.width * 0.3, y: camera.y + canvas.height * 0.4, endTime: performance.now() + 1000, duration: 1000, color: '#ff4444' });
updateCombatUI();
updateAllInfoPanels();
if ( this.player.creature.hp <= 0) {
endCombat(false);
}
}, 500);
}
endCombat(isVictory) {
const combatLog = document.getElementById('combat-log');
if (isVictory) {
combatLog.textContent = `Vous avez vaincu le ${currentOpponent.name} !`;
addXp(currentOpponent.xp);
Object.entries(currentOpponent.loot).forEach(([item, amount]) => {
player.inventory[item] = (player.inventory[item] || 0) + amount;
combatLog.textContent += `\nVous trouvez ${amount} ${item}.`;
});
const opponentTile = mapData[player.y][player.x];
if (currentOpponent.isAnimal) {
opponentTile.animals.shift();
} else {
opponentTile.monsters.shift();
}
} else {
combatLog.textContent = `Vous avez été vaincu...`;
player.derivedStats.currentHp = 1;
}
currentOpponent = null;
setTimeout(() => {
document.getElementById('combat-screen').classList.add('hidden');
document.getElementById('combat-screen').classList.remove('flex');
gameState = 'exploring';
updateAllInfoPanels();
}, 2000);
}
updateCombatUI() {
document.getElementById('combat-player-hp-text').textContent = `${this.player.creature.hp} / ${player.derivedStats.maxHp}`;
document.getElementById('combat-player-hp-bar').style.width = `${(this.player.creature.hp / player.derivedStats.maxHp) * 100}%`;
if (this.opponent) {
document.getElementById('combat-opponent-name').textContent = this.opponent.name;
document.getElementById('combat-opponent-icon').textContent = currentOpponent.icon;
document.getElementById('combat-opponent-hp-text').textContent = `${this.opponent.currentHp} / ${this.opponent.hp}`;
document.getElementById('combat-opponent-hp-bar').style.width = `${(this.opponent.currentHp / this.opponent.hp) * 100}%`;
}
}
}
class Quest {
constructor( ) {
this.id = null;
this.definition = null;
this.currentStage = 0;
this.status = 'inactive'; // inactive, active, completed
}
getCurrentStage() {
return this.definition.stages[this.currentStage];
}
advance(choiceIndex) {
const stage = this.getCurrentStage();
if(stage.choices && stage.choices[choiceIndex]) {
const nextStageIndex = stage.choices[choiceIndex].nextStage;
if (nextStageIndex !== null) {
this.currentStage = nextStageIndex;
if(this.status === 'inactive') this.status = 'active';
if(this.getCurrentStage().isEnd) {
this.status = 'completed';
}
}
}
}
async setQuest(npc) {
if(npc.questId && this.quests[npc.questId]) {
return this.quests[npc.questId];
}
try {
const questDefinition = await this.generateQuestForNpc(npc);
if(questDefinition) {
const questId = `GEMINI_${Date.now()}`;
npc.assignQuest(questId);
this.id=questId;
this.definition=questDefinition;
}
} catch(e) {
console.error("Gemini quest generation failed, using fallback.", e);
const fallbackId = 'WOLF_MENACE';
npc.assignQuest(fallbackId);
this.id=fallbackId;
this.definition=QUEST_DATABASE[fallbackId];
}
return null;
}
async generateQuestForNpc(npc) {
if (!geminiApiKey) {
throw new Error("API key is missing.");
}
const prompt = `Génère une quête simple pour un jeu RPG en 2D isométrique.
Le PNJ est un "${npc.type.name}".
Il se trouve près d'une structure de type "${npc.homeTile.structure.type.name}" dans un biome de type "${npc.homeTile.biome.name}".
Crée une quête avec un titre, et 3 à 4 étapes.
La première étape (index 0) doit proposer d'accepter ou de refuser la quête.
L'étape suivante (index 1) doit décrire l'objectif. L'objectif doit être simple, comme tuer un certain nombre de monstres ou collecter des objets.
La dernière étape doit être la récompense (isEnd: true).
Réponds UNIQUEMENT avec un objet JSON valide qui suit le schéma ci-dessous. N'ajoute aucun texte avant ou après le JSON.
`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "OBJECT",
properties: {
"title": { "type": "STRING" },
"stages": {
"type": "ARRAY",
"items": {
"type": "OBJECT",
"properties": {
"text": { "type": "STRING" },
"choices": {
"type": "ARRAY",
"items": {
"type": "OBJECT",
"properties": {
"text": { "type": "STRING" },
"nextStage": { "type": ["NUMBER", "NULL"] }
},
"required": ["text"]
}
},
"objective": { "type": ["OBJECT", "NULL"], "properties": {"type": {"type": "STRING"}, "target": {"type": "STRING"}, "count": {"type": "NUMBER"}} },
"reward": { "type": ["OBJECT", "NULL"], "properties": {"xp": {"type": "NUMBER"}, "items": {"type": "ARRAY", "items": { "type": "STRING" }}} },
"isEnd": { "type": ["BOOLEAN", "NULL"] }
},
"required": ["text", "choices"]
}
}
},
"required": ["title", "stages"]
}
}
};
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${geminiApiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
console.error("API Error Body:", errorBody);
throw new Error(`API call failed with status: ${response.status}`);
}
const result = await response.json();
const jsonText = result.candidates?.[0]?.content?.parts?.[0]?.text;
if(jsonText) {
return JSON.parse(jsonText);
}
throw new Error("Invalid response from API.");
}
}
class World {
constructor(name) {
this.name = name;
this.map = new Map(200);
this.currentTime=new Time();
this.animals = [];
this.npcs = [];
this.camera = new Camera(0,0, canvas);
this.player=null;
}
setDesign(){
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(this.camera.x, this.camera.y);
const [startX, endX, startY, endY] = this.getVisibleTileBounds();
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
if (this.map.tiles[y] && this.map.tiles[y][x]) {
this.map.tiles[y][x].setDesign(ctx);
}
}
}
//this.world.animals.forEach(animal => animal.setDesign(ctx));
//this.world.npcs.forEach(npc => npc.setDesign(ctx));
this.player.setDesign(ctx);
ctx.restore();
ctx.save();
this.currentTime.setDesign(ctx);
ctx.restore();
}
getVisibleTileBounds() {
const margin = 5;
const viewWidth = canvas.width;
const viewHeight = canvas.height;
const isoToCart = (isoX, isoY) => {
const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
return { x: Math.floor(cartX), y: Math.floor(cartY) };
};
const topLeft = isoToCart(-this.camera.x, -this.camera.y);
const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
const startX = Math.max(0, topLeft.x - margin);
const endX = Math.min(this.map.size - 1, bottomRight.x + margin);
const startY = Math.max(0, topLeft.y - margin);
const endY = Math.min(this.map.size - 1, bottomRight.y + margin);
return [startX, endX, startY, endY];
}
update(currentTime) {
this.updateVisibility(this.player.creature.tile.position);
// a changer!
//this.world.animals.forEach(animal => animal.update(currentTime, this.world.map));
//this.world.npcs.forEach(npc => npc.update(currentTime, this.world.map));
this.camera.setCamera(this.player.creature.tile.position);
// Update all biome sprites
for (const key in BIOME_TYPE) {
if (BIOME_TYPE[key].sprite) {
BIOME_TYPE[key].sprite.update(currentTime);
}
}
this.currentTime.tick();
}
updateVisibility(position) {
const px = position.x;
const py = position.y;
const radius = VISION_RADIUS;
const radiusSq = radius * radius;
const viewBox = {
minX: Math.max(0, px - radius - 2),
maxX: Math.min(this.map.size - 1, px + radius + 2),
minY: Math.max(0, py - radius - 2),
maxY: Math.min(this.map.size - 1, py + radius + 2)
};
for(let y = viewBox.minY; y <= viewBox.maxY; y++) {
for(let x = viewBox.minX; x <= viewBox.maxX; x++) {
const tile = this.map.tiles[y][x];
if(tile.visibility === 2) tile.visibility = 1;
const dx = px - x;
const dy = py - y;
if (dx * dx + dy * dy <= radiusSq) {
tile.visibility = 2;
}
}
}
}
}
class Camera {
constructor(x=0,y=0,canvas) {
@ -682,74 +1098,25 @@ document.addEventListener('DOMContentLoaded', () => {
constructor() {
this.world = new World('Defiance');
this.camera = new Camera(0,0, canvas);
this.setControls();
this.loop = this.loop.bind(this);
this.player=null;
this.setInitial();
}
loop(currentTime) {
this.update(currentTime);
this.setDesign();
this.handleMovement(currentTime);
this.world.update(currentTime);
this.world.setDesign();
requestAnimationFrame(this.loop);
}
update(currentTime) {
this.handleMovement(currentTime);
this.updateVisibility();
// a changer!
this.world.animals.forEach(animal => animal.update(currentTime, this.world.map));
this.world.npcs.forEach(npc => npc.update(currentTime, this.world.map));
this.camera.setCamera(this.player.creature.position);
// Update all biome sprites
for (const key in Biome) {
if (Biome[key].sprite) {
Biome[key].sprite.update(currentTime);
}
}
this.world.currentTime.tick();
}
updateVisibility() {
const map = this.world.map;
const px = this.player.creature.position.x;
const py = this.player.creature.position.y;
const radius = VISION_RADIUS;
const radiusSq = radius * radius;
const viewBox = {
minX: Math.max(0, px - radius - 2),
maxX: Math.min(map.size - 1, px + radius + 2),
minY: Math.max(0, py - radius - 2),
maxY: Math.min(map.size - 1, py + radius + 2)
};
for(let y = viewBox.minY; y <= viewBox.maxY; y++) {
for(let x = viewBox.minX; x <= viewBox.maxX; x++) {
const tile = map.tiles[y][x];
if(tile.visibility === 2) tile.visibility = 1;
const dx = px - x;
const dy = py - y;
if (dx * dx + dy * dy <= radiusSq) {
tile.visibility = 2;
}
}
}
}
setInitial(){
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].position,'human',RACE.HUMAN,10,null),);
this.updateVisibility();
} while (!this.world.map.tiles[spawnY][spawnX].biome || !this.world.map.tiles[spawnY][spawnX].biome.type.movements.includes('walk') );
this.world.player = new Player(new Creature('player',new Attributes(RACE.HUMAN),1,null,null,this.world.map.tiles[spawnY][spawnX],'human',RACE.HUMAN,10,null),);
this.world.updateVisibility(this.world.map.tiles[spawnY][spawnX].position);
}
setControls() {
document.addEventListener('keydown', e => {
@ -775,71 +1142,23 @@ document.addEventListener('DOMContentLoaded', () => {
btn.addEventListener('touchend', e => { e.preventDefault(); controls[dir] = false; });
}
}
setDesign(){
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(this.camera.x, this.camera.y);
const [startX, endX, startY, endY] = this.getVisibleTileBounds();
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
if (this.world.map.tiles[y] && this.world.map.tiles[y][x]) {
this.world.map.tiles[y][x].setDesign(ctx);
}
}
}
this.world.animals.forEach(animal => animal.setDesign(ctx));
this.world.npcs.forEach(npc => npc.setDesign(ctx));
this.player.setDesign(ctx);
ctx.restore();
ctx.save();
this.world.currentTime.setDesign(ctx);
ctx.restore();
}
getVisibleTileBounds() {
const margin = 5;
const viewWidth = canvas.width;
const viewHeight = canvas.height;
const isoToCart = (isoX, isoY) => {
const cartX = (isoX / (TILE_WIDTH / 2) + isoY / (TILE_HEIGHT / 2)) / 2;
const cartY = (isoY / (TILE_HEIGHT / 2) - isoX / (TILE_WIDTH / 2)) / 2;
return { x: Math.floor(cartX), y: Math.floor(cartY) };
};
const topLeft = isoToCart(-this.camera.x, -this.camera.y);
const bottomRight = isoToCart(-this.camera.x + viewWidth, -this.camera.y + viewHeight);
const startX = Math.max(0, topLeft.x - margin);
const endX = Math.min(this.world.map.size - 1, bottomRight.x + margin);
const startY = Math.max(0, topLeft.y - margin);
const endY = Math.min(this.world.map.size - 1, bottomRight.y + margin);
return [startX, endX, startY, endY];
}
handleMovement(currentTime) {
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 (controls.up) { this.world.player.move(0, -1,this.world.map); moved = true; }
if (controls.down) { this.world.player.move(0, 1,this.world.map); moved = true; }
if (controls.left) { this.world.player.move(-1, 0,this.world.map); moved = true; }
if (controls.right) { this.world.player.move(1, 0,this.world.map); moved = true; }
if(moved) lastMoveTime = currentTime;
}
handleInteraction() {
if (!controls.interact) return;
controls.interact = false; // Consume the action
const px = this.player.creature.position.x;
const py = this.player.creature.position.y;
const px = this.world.player.creature.position.x;
const py = this.world.player.creature.position.y;
let targetNpc = null;
for (const npc of this.world.npcs) {
@ -1048,8 +1367,8 @@ document.addEventListener('DOMContentLoaded', () => {
canvas.height = canvas.parentElement.clientHeight;
try {
for (const key in Biome) {
if (Biome[key].design) { Biome[key].sprite = new Sprite(Biome[key]);}
for (const key in BIOME_TYPE) {
if (BIOME_TYPE[key].design) { BIOME_TYPE[key].sprite = new Sprite(BIOME_TYPE[key]);}
}
const assetIds = ['forest-svg', 'village-svg', 'city-svg', 'player-svg', 'enchanted-forest-svg', 'swamp-svg', 'wolf-pack-svg', 'boar-svg', 'bird-svg', 'farm-svg', 'camp-svg', 'npc-svg','house-svg','cult-svg','market-svg','mine-svg'];