1091 lines
62 KiB
HTML
1091 lines
62 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>defiance v0.12.1</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; overflow: hidden; }
|
|
canvas { display: block; background-color: #1a202c; cursor: pointer; }
|
|
::-webkit-scrollbar { width: 8px; }
|
|
::-webkit-scrollbar-track { background: #2d3748; }
|
|
::-webkit-scrollbar-thumb { background: #4a5568; border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #718096; }
|
|
.control-btn {
|
|
background-color: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.4);
|
|
color: white; font-size: 1.5rem; font-weight: bold; display: flex;
|
|
align-items: center; justify-content: center; user-select: none;
|
|
-webkit-user-select: none; -webkit-tap-highlight-color: transparent;
|
|
}
|
|
.control-btn:active { background-color: rgba(255, 255, 255, 0.5); }
|
|
.equip-btn, .menu-btn {
|
|
background-color: #3182ce; color: white; padding: 2px 6px; border-radius: 4px;
|
|
font-size: 0.75rem; cursor: pointer; margin-left: 8px; border: none;
|
|
}
|
|
.equip-btn:hover, .menu-btn:hover { background-color: #2b6cb0; }
|
|
.menu-btn { padding: 8px 12px; font-size: 1rem; width: 100%; text-align: left; }
|
|
.menu-btn:disabled { background-color: #4a5568; cursor: not-allowed; }
|
|
@media (min-width: 1024px) { #mobile-controls, #info-toggle-btn { display: none; } }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-900 text-white flex h-screen">
|
|
|
|
|
|
<!-- Panneau d'information Desktop -->
|
|
<div id="desktop-panel" class="w-1/4 max-w-sm p-6 bg-gray-800 shadow-lg overflow-y-auto flex-col hidden md:flex">
|
|
<h1 class="text-2xl font-bold mb-4 text-cyan-400">defiance v0.12.1</h1>
|
|
<p class="text-sm text-gray-400 mb-6">ZQSD/Flèches: Bouger | E: Interagir | C: Camper</p>
|
|
|
|
<div class="space-y-4">
|
|
<h2 class="text-xl font-semibold border-b-2 border-gray-700 pb-2">Feuille de Personnage</h2>
|
|
<div id="player-info-desktop" class="bg-gray-700 p-4 rounded-lg text-sm space-y-3"></div>
|
|
<h2 class="text-xl font-semibold border-b-2 border-gray-700 pb-2 mt-4">Infos de la Tuile</h2>
|
|
<div id="tile-info-desktop" class="bg-gray-700 p-4 rounded-lg text-sm"></div>
|
|
</div>
|
|
<div class="mt-auto pt-6 text-center text-xs text-gray-500"><p>Généré par Gemini</p></div>
|
|
</div>
|
|
|
|
<!-- Zone du jeu -->
|
|
<div class="flex-1 relative">
|
|
<canvas id="gameCanvas"></canvas>
|
|
<div id="loading" class="absolute inset-0 bg-black bg-opacity-75 flex items-center justify-center text-2xl z-50">Le monde prend vie...</div>
|
|
<!-- UI Temps et Saison -->
|
|
<div id="time-ui" class="absolute top-4 left-4 bg-black/50 p-2 rounded-lg text-sm font-semibold z-20">
|
|
<div id="time-display">Jour 1, 06:00</div>
|
|
<div id="season-display">Printemps</div>
|
|
</div>
|
|
<!-- Bouton d'info Mobile -->
|
|
<button id="info-toggle-btn" class="control-btn absolute top-5 right-5 w-16 h-16 rounded-full text-3xl z-20">🎒</button>
|
|
|
|
<!-- Panneau d'info Mobile (Overlay) -->
|
|
<div id="mobile-info-panel" class="absolute inset-0 bg-black/75 p-4 flex-col items-center justify-center z-30 hidden">
|
|
<div class="w-full max-w-md max-h-full bg-gray-800 rounded-lg shadow-lg overflow-y-auto p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold border-b-2 border-gray-700 pb-2">Feuille de Personnage</h2>
|
|
<div id="player-info-mobile" class="bg-gray-700 p-4 rounded-lg text-sm space-y-3"></div>
|
|
<h2 class="text-xl font-semibold border-b-2 border-gray-700 pb-2 mt-4">Infos de la Tuile</h2>
|
|
<div id="tile-info-mobile" class="bg-gray-700 p-4 rounded-lg text-sm"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contrôles Mobiles -->
|
|
<div id="mobile-controls" class="absolute bottom-20 right-5 flex items-end gap-4 z-20">
|
|
<button id="btn-camp" class="control-btn rounded-full w-20 h-20 text-3xl">⛺</button>
|
|
<button id="btn-action" class="control-btn rounded-full w-20 h-20 text-3xl">✋</button>
|
|
<div class="grid grid-cols-3 grid-rows-3 w-36 h-36 gap-1">
|
|
<div></div><button id="btn-up" class="control-btn rounded-t-lg">▲</button><div></div>
|
|
<button id="btn-left" class="control-btn rounded-l-lg">◀</button><div></div><button id="btn-right" class="control-btn rounded-r-lg">▶</button>
|
|
<div></div><button id="btn-down" class="control-btn rounded-b-lg">▼</button><div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Écran de Combat -->
|
|
<div id="combat-screen" class="absolute inset-0 bg-black/80 flex-col items-center justify-center z-40 hidden">
|
|
<div class="text-center text-4xl mb-8 text-red-500 font-bold">COMBAT !</div>
|
|
<div class="flex justify-around w-full max-w-4xl">
|
|
<div class="text-center w-1/3">
|
|
<div class="text-8xl">🦸</div>
|
|
<h3 class="text-2xl font-bold text-cyan-400">Héros</h3>
|
|
<div class="w-full bg-gray-700 rounded-full h-6 mt-2 border-2 border-gray-600"><div id="combat-player-hp-bar" class="bg-green-500 h-full rounded-full text-center text-white font-bold transition-all duration-500"></div></div>
|
|
<div id="combat-player-hp-text" class="mt-1"></div>
|
|
</div>
|
|
<div class="text-4xl font-bold self-center">VS</div>
|
|
<div class="text-center w-1/3">
|
|
<div id="combat-opponent-icon" class="text-8xl"></div>
|
|
<h3 id="combat-opponent-name" class="text-2xl font-bold text-red-400"></h3>
|
|
<div class="w-full bg-gray-700 rounded-full h-6 mt-2 border-2 border-gray-600"><div id="combat-opponent-hp-bar" class="bg-red-500 h-full rounded-full text-center text-white font-bold transition-all duration-500"></div></div>
|
|
<div id="combat-opponent-hp-text" class="mt-1"></div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-12 flex flex-col items-center">
|
|
<button id="attack-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-4 px-8 rounded-lg text-2xl shadow-lg">⚔️ Attaquer</button>
|
|
<div id="combat-log" class="mt-4 h-24 text-center text-gray-300"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const loadingScreen = document.getElementById('loading');
|
|
|
|
const MAP_SIZE = 200;
|
|
const TILE_WIDTH = 64;
|
|
const TILE_HEIGHT = 32;
|
|
const ELEVATION_STEP = TILE_HEIGHT / 4;
|
|
|
|
const TIME_SCALE = 5;
|
|
const TICKS_PER_DAY = 1440;
|
|
const DAYS_PER_SEASON = 10;
|
|
const TICKS_PER_SEASON = TICKS_PER_DAY * DAYS_PER_SEASON;
|
|
const SEASONS = ['Printemps', 'Été', 'Automne', 'Hiver'];
|
|
let gameTicks = TICKS_PER_DAY * 0.25;
|
|
const BIOMES = {
|
|
WATER_DEEP: { name: 'Eau Profonde', color: '#3D5A80' }, WATER_SHALLOW: { name: 'Eau Peu Profonde', color: '#5A8AB8' },
|
|
SAND: { name: 'Sable', color: '#E9D9A1' }, GRASSLAND: { name: 'Plaine', color: '#98C159' },
|
|
FOREST: { name: 'Forêt', color: '#6A994E' }, ENCHANTED_FOREST: { name: 'Forêt Enchantée', color: '#7B6094' },
|
|
MOUNTAIN: { name: 'Montagne', color: '#A9A9A9' }, SNOW: { name: 'Toundra', color: '#F7F7F7' },
|
|
DESERT: { name: 'Désert', color: '#D4A373' },
|
|
RIVER: { name: 'Rivière', color: '#5A8AB8' }
|
|
};
|
|
const AFFINITIES = ['Lumière', 'Ténèbres', 'Feu', 'Roche', 'Eau', 'Air', 'Nature'];
|
|
|
|
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', icon: '🐺', hp: 20, strength: 4, xp: 15, loot: { 'Cuir': 1, 'Os': 1 } },
|
|
BOAR: { name: 'Sanglier', icon: '🐗', hp: 25, strength: 5, xp: 20, loot: { 'Cuir': 2 } },
|
|
};
|
|
const wolfSvgString = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="wolfFurGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#4A5568;" /><stop offset="100%" style="stop-color:#2D3748;" /></linearGradient><linearGradient id="wolfShadowGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#2D3748;" /><stop offset="100%" style="stop-color:#1A202C;" /></linearGradient></defs><g><path d="M 50 10 L 90 40 L 80 95 L 20 95 L 10 40 Z" fill="url(#wolfFurGradient)" /><path d="M 50 10 L 70 45 L 50 55 L 30 45 Z" fill="url(#wolfShadowGradient)" /><path d="M 20 95 Q 40 70 50 75 Q 60 70 80 95 L 75 98 L 25 98 Z" fill="#2D3748" /><path d="M 25 42 L 10 20 L 35 30 Z" fill="#4A5568" stroke="#1A202C" stroke-width="1"/><path d="M 75 42 L 90 20 L 65 30 Z" fill="#4A5568" stroke="#1A202C" stroke-width="1"/><path d="M 28 38 L 18 25 L 35 32 Z" fill="#2D3748"/><path d="M 72 38 L 82 25 L 65 32 Z" fill="#2D3748"/><path d="M 50 50 L 65 80 L 50 90 L 35 80 Z" fill="#4A5568" /><path d="M 50 85 L 55 95 L 45 95 Z" fill="black" /><path d="M 35 45 Q 40 42 45 45 Q 40 48 35 45 Z" fill="#FBBF24" /><path d="M 65 45 Q 60 42 55 45 Q 60 48 65 45 Z" fill="#FBBF24" /><circle cx="39" cy="45" r="1" fill="black" /><circle cx="61" cy="45" r="1" fill="black" /><path d="M 18 50 L 10 40 L 25 55 Z" fill="#4A5568" /><path d="M 82 50 L 90 40 L 75 55 Z" fill="#4A5568" /><path d="M 20 65 L 5 55 L 22 70 Z" fill="#4A5568" /><path d="M 80 65 L 95 55 L 78 70 Z" fill="#4A5568" /></g></svg>`;
|
|
|
|
const ITEMS = {
|
|
'Épée en Bois': { name: 'Épée en Bois', type: 'weapon', stats: { strength: 2 }, icon: '🗡️' },
|
|
'Armure de Cuir': { name: 'Armure de Cuir', type: 'armor', stats: { vitality: 2 }, icon: '🧥' },
|
|
};
|
|
|
|
const NAME_PREFIXES = ["Chêne", "Sombre", "Pierre", "Haut", "Val", "Mur", "Guet", "Clair"];
|
|
const NAME_SUFFIXES = ["bourg", "fort", "ville", "mont", "port", "bois", "rivage", "gard"];
|
|
|
|
let mapData = [];
|
|
const player = {
|
|
x: 0, y: 0, color: '#E53E3E', inventory: { 'Épée en Bois': 1, 'Armure de Cuir': 1 },
|
|
level: 1, xp: 0, xpToNextLevel: 100,
|
|
attributes: { strength: 5, agility: 5, intelligence: 5, vitality: 5 },
|
|
totalAttributes: {},
|
|
derivedStats: { maxHp: 0, currentHp: 0, inventoryCapacity: 0 },
|
|
equipment: { weapon: null, armor: null }
|
|
};
|
|
const camera = { x: 0, y: 0 };
|
|
let selectedTile = null;
|
|
let floatingTexts = [];
|
|
let isGameReady = false;
|
|
let gameState = 'exploring';
|
|
let currentOpponent = null;
|
|
let npcs = [];
|
|
let wolfImage = null;
|
|
const controls = { up: false, down: false, left: false, right: false };
|
|
let lastMoveTime = 0;
|
|
const MOVE_DELAY = 150;
|
|
|
|
// --- CORRECTION : Toutes les fonctions sont maintenant définies avant d'être appelées ---
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = canvas.parentElement.clientHeight;
|
|
if (isGameReady) centerCameraOnPlayer();
|
|
}
|
|
function getSeasonalColor(baseColor, biomeName, season) {
|
|
if (season === 'Automne' && (biomeName === 'Forêt' || biomeName === 'Forêt Enchantée')) {
|
|
return shadeColor(baseColor, -10, '#D95F12');
|
|
}
|
|
if (season === 'Printemps' && biomeName === 'Plaine') {
|
|
return '#A8D080';
|
|
}
|
|
return baseColor;
|
|
}
|
|
function seededRandom(seed) { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280.0; }; }
|
|
function initializeGame() {
|
|
resizeCanvas();
|
|
setupControls();
|
|
|
|
prepareSvgImages();
|
|
|
|
calculateTotalAttributes();
|
|
player.derivedStats.currentHp = player.derivedStats.maxHp;
|
|
|
|
setTimeout(() => {
|
|
generateMap();
|
|
spawnPlayerOnLand();
|
|
centerCameraOnPlayer();
|
|
updateAllInfoPanels();
|
|
loadingScreen.style.display = 'none';
|
|
isGameReady = true;
|
|
gameLoop();
|
|
}, 50);
|
|
|
|
|
|
}
|
|
function generateMap() {
|
|
console.time('Map Generation');
|
|
const simplex = new SimplexNoise();
|
|
let structureLocations = [];
|
|
mapData = Array(MAP_SIZE).fill(null).map((_, y) => Array(MAP_SIZE).fill(null).map((_, x) => {
|
|
const scale = 0.05;
|
|
const eRaw = (simplex.noise2D(x * scale, y * scale) + 1) / 2;
|
|
const tRaw = (simplex.noise2D(x * scale * 0.8, y * scale * 0.8) + 1) / 2;
|
|
const mRaw = (simplex.noise2D(x * scale * 1.5, y * scale * 1.5) + 1) / 2;
|
|
const rand = seededRandom(x * 13 + y * 59);
|
|
|
|
let biome, elevation = Math.floor(eRaw * 12);
|
|
if (eRaw < 0.25) { biome = BIOMES.WATER_DEEP; elevation = 0; }
|
|
else if (eRaw < 0.3) { biome = BIOMES.WATER_SHALLOW; elevation = 1; }
|
|
else if (eRaw < 0.35) { biome = BIOMES.SAND; elevation = 2; }
|
|
else if (eRaw > 0.85) biome = BIOMES.SNOW;
|
|
else if (eRaw > 0.75) biome = BIOMES.MOUNTAIN;
|
|
else {
|
|
if (tRaw < 0.3) biome = BIOMES.DESERT;
|
|
else if (tRaw > 0.6) biome = (mRaw > 0.7) ? BIOMES.ENCHANTED_FOREST : BIOMES.FOREST;
|
|
else biome = BIOMES.GRASSLAND;
|
|
}
|
|
|
|
let structure = null;
|
|
if (!['Eau Profonde', 'Eau Peu Profonde', 'Montagne'].includes(biome.name)) {
|
|
const structureChance = rand();
|
|
if (structureChance < 0.005) {
|
|
structure = { type: 'city', name: NAME_PREFIXES[Math.floor(rand() * NAME_PREFIXES.length)] + NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)], population: 100 + 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'] };
|
|
structureLocations.push({x, y, type: 'city'});
|
|
} else if (structureChance < 0.02) {
|
|
structure = { type: 'village', name: NAME_PREFIXES[Math.floor(rand() * NAME_PREFIXES.length)] + NAME_SUFFIXES[Math.floor(rand() * NAME_SUFFIXES.length)], population: 20 + Math.floor(rand() * 20), buildings: ['Maison du Chef', 'Marchand', 'Lieu de Culte', 'Ferme', 'Ferme', 'Plusieurs Maisons'] };
|
|
structureLocations.push({x, y, type: 'village'});
|
|
}
|
|
}
|
|
|
|
let resource = null;
|
|
const resChance = rand();
|
|
if (!structure) {
|
|
if (biome === BIOMES.FOREST || biome === BIOMES.ENCHANTED_FOREST) {
|
|
if (resChance < 0.05) resource = { type: 'Champignons', amount: 1 + Math.floor(rand() * 2) };
|
|
else if (resChance < 0.25) resource = { type: 'Herbes', amount: 1 + Math.floor(rand() * 3) };
|
|
else if (resChance < 0.55) resource = { type: 'Bois', amount: 3 + Math.floor(rand() * 3) };
|
|
} else if (biome === BIOMES.MOUNTAIN) {
|
|
if (resChance < 0.02) resource = { type: 'Cristaux Magiques', amount: 1 };
|
|
else if (resChance < 0.07) resource = { type: 'Or', amount: 1 + Math.floor(rand() * 2) };
|
|
else if (resChance < 0.22) resource = { type: 'Fer', amount: 2 + Math.floor(rand() * 2) };
|
|
else if (resChance < 0.62) resource = { type: 'Pierre', amount: 3 + Math.floor(rand() * 4) };
|
|
} else if (biome === BIOMES.GRASSLAND) {
|
|
if (resChance < 0.25) resource = { type: 'Herbes', amount: 1 + Math.floor(rand() * 3) };
|
|
}
|
|
}
|
|
|
|
let monsters = [];
|
|
if (!structure && !['Eau Profonde', 'Eau Peu Profonde'].includes(biome.name) && rand() < 0.02) {
|
|
const numMonsters = 1 + Math.floor(rand() * 3);
|
|
for (let i = 0; i < numMonsters; i++) {
|
|
const monsterType = rand() < 0.6 ? MONSTER_TYPES.GOBLIN : MONSTER_TYPES.ORC;
|
|
monsters.push({ ...monsterType, currentHp: monsterType.hp });
|
|
}
|
|
}
|
|
let animals = [];
|
|
if (!structure && monsters.length === 0 && !['Eau Profonde', 'Eau Peu Profonde', 'Désert', 'Toundra'].includes(biome.name) && rand() < 0.04) {
|
|
const animalType = rand() < 0.5 ? ANIMAL_TYPES.WOLF : ANIMAL_TYPES.BOAR;
|
|
animals.push({ ...animalType, currentHp: animalType.hp, isAnimal: true });
|
|
}
|
|
return {
|
|
x, y, biome, elevation, mana: Math.round(mRaw * 100),
|
|
affinity: AFFINITIES[Math.floor(rand() * AFFINITIES.length)],
|
|
structure, resource, monsters, isFarm: false, animals, isRoad: false, hasBridge: false
|
|
};
|
|
}));
|
|
|
|
structureLocations.forEach(loc => {
|
|
const radius = loc.type === 'city' ? 5 : 3;
|
|
for (let dy = -radius; dy <= radius; dy++) {
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
const nx = loc.x + dx;
|
|
const ny = loc.y + dy;
|
|
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE) {
|
|
const tile = mapData[ny][nx];
|
|
if (!tile.structure && tile.biome === BIOMES.GRASSLAND && Math.random() < 0.5) {
|
|
tile.isFarm = true;
|
|
tile.farmCrop = null;
|
|
tile.farmHarvested = false;
|
|
npcs.push({ x: nx, y: ny, homeX: loc.x, homeY: loc.y, workX: nx, workY: ny, type: 'farmer', icon: '🧑🌾' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
generateRivers();
|
|
generateRoads(structureLocations);
|
|
console.timeEnd('Map Generation');
|
|
}
|
|
|
|
function generateRivers() {
|
|
const numRivers = 10;
|
|
for (let i = 0; i < numRivers; i++) {
|
|
let currentX = Math.floor(Math.random() * MAP_SIZE);
|
|
let currentY = Math.floor(Math.random() * MAP_SIZE);
|
|
let sourceTile = mapData[currentY][currentX];
|
|
|
|
if (sourceTile.biome !== BIOMES.MOUNTAIN) continue;
|
|
|
|
let maxLength = 200;
|
|
while (maxLength > 0) {
|
|
maxLength--;
|
|
const currentTile = mapData[currentY][currentX];
|
|
if (currentTile.biome === BIOMES.WATER_DEEP || currentTile.biome === BIOMES.WATER_SHALLOW) break;
|
|
|
|
currentTile.biome = BIOMES.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 < MAP_SIZE && ny >= 0 && ny < MAP_SIZE) {
|
|
const neighborTile = mapData[ny][nx];
|
|
if (neighborTile.elevation < lowestElevation) {
|
|
lowestElevation = neighborTile.elevation;
|
|
lowestNeighbor = {x: nx, y: ny};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lowestNeighbor) {
|
|
currentX = lowestNeighbor.x;
|
|
currentY = lowestNeighbor.y;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function generateRoads(locations) {
|
|
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 < MAP_SIZE && currentY >= 0 && currentY < MAP_SIZE) {
|
|
const tile = mapData[currentY][currentX];
|
|
if (tile.biome === BIOMES.RIVER) {
|
|
tile.hasBridge = true;
|
|
} else if (tile.biome !== BIOMES.WATER_DEEP && tile.biome !== BIOMES.WATER_SHALLOW) {
|
|
tile.isRoad = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function spawnPlayerOnLand() {
|
|
let spawnX, spawnY;
|
|
do {
|
|
spawnX = Math.floor(Math.random() * MAP_SIZE);
|
|
spawnY = Math.floor(Math.random() * MAP_SIZE);
|
|
} while (!mapData[spawnY] || !mapData[spawnY][spawnX] || [BIOMES.WATER_DEEP.name, BIOMES.WATER_SHALLOW.name, BIOMES.RIVER.name].includes(mapData[spawnY][spawnX].biome.name));
|
|
player.x = spawnX; player.y = spawnY;
|
|
}
|
|
let lastSeason = -1;
|
|
function gameLoop(currentTime) {
|
|
const currentSeasonIndex = Math.floor((gameTicks / TICKS_PER_SEASON) % 4);
|
|
if (currentSeasonIndex !== lastSeason) {
|
|
updateFarmsForSeason(SEASONS[currentSeasonIndex]);
|
|
lastSeason = currentSeasonIndex;
|
|
}
|
|
|
|
if (gameState !== 'in_combat') {
|
|
gameTicks += TIME_SCALE;
|
|
}
|
|
if (gameState === 'camping') {
|
|
gameTicks += TIME_SCALE * 20; // Fast forward time
|
|
player.derivedStats.currentHp = Math.min(player.derivedStats.maxHp, player.derivedStats.currentHp + 1);
|
|
const timeOfDay = (gameTicks % TICKS_PER_DAY) / TICKS_PER_DAY;
|
|
if (timeOfDay >= 0.25 && timeOfDay < 0.3) { // Wake up at 6am
|
|
gameState = 'exploring';
|
|
}
|
|
}
|
|
|
|
if (gameState === 'exploring') {
|
|
handleMovement(currentTime);
|
|
}
|
|
updateNpcs();
|
|
draw(currentTime);
|
|
requestAnimationFrame(gameLoop);
|
|
|
|
}
|
|
function updateNpcs() {
|
|
if (gameState === 'camping') return;
|
|
const timeOfDay = (gameTicks % TICKS_PER_DAY) / TICKS_PER_DAY;
|
|
const isNight = timeOfDay > 0.83 || timeOfDay < 0.25;
|
|
|
|
npcs.forEach(npc => {
|
|
let targetX, targetY;
|
|
if (isNight) {
|
|
targetX = npc.homeX;
|
|
targetY = npc.homeY;
|
|
} else {
|
|
targetX = npc.workX;
|
|
targetY = npc.workY;
|
|
}
|
|
|
|
if (npc.x !== targetX || npc.y !== targetY) {
|
|
// Simple pathfinding
|
|
if (npc.x < targetX) npc.x++;
|
|
else if (npc.x > targetX) npc.x--;
|
|
if (npc.y < targetY) npc.y++;
|
|
else if (npc.y > targetY) npc.y--;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateFarmsForSeason(season) {
|
|
for (let y = 0; y < MAP_SIZE; y++) {
|
|
for (let x = 0; x < MAP_SIZE; x++) {
|
|
const tile = mapData[y][x];
|
|
if (tile.isFarm) {
|
|
tile.farmHarvested = false;
|
|
switch(season) {
|
|
case 'Printemps': tile.farmCrop = 'Blé (pousse)'; break;
|
|
case 'Été': tile.farmCrop = Math.random() < 0.5 ? 'Maïs' : 'Raisins'; break;
|
|
case 'Automne': tile.farmCrop = 'Blé'; break;
|
|
case 'Hiver': tile.farmCrop = null; break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function drawFarm(tile) {
|
|
const screenPos = cartToIso(tile.x, tile.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
ctx.save();
|
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
|
|
ctx.lineWidth = 1;
|
|
for (let i = -1; i <= 1; i += 2) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(-TILE_WIDTH / 4 * i, TILE_HEIGHT / 4 * 3);
|
|
ctx.lineTo(TILE_WIDTH / 4 * i, TILE_HEIGHT / 4);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
ctx.save();
|
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight + TILE_HEIGHT / 2);
|
|
ctx.font = "18px Arial"; ctx.textAlign = "center";
|
|
|
|
let icon = '';
|
|
if (!tile.farmHarvested) {
|
|
switch(tile.farmCrop) {
|
|
case 'Maïs': icon = '🌽'; break;
|
|
case 'Raisins': icon = '🍇'; break;
|
|
case 'Blé': icon = '🌾'; break;
|
|
}
|
|
}
|
|
ctx.fillText(icon, 10, -5);
|
|
ctx.restore();
|
|
}
|
|
function drawTimeOverlay() {
|
|
const timeOfDay = (gameTicks % TICKS_PER_DAY) / TICKS_PER_DAY;
|
|
let overlayColor = 'rgba(0,0,0,0)';
|
|
let opacity = 0;
|
|
|
|
if (timeOfDay > 0.83 || timeOfDay < 0.25) { // Nuit (20h -> 6h)
|
|
overlayColor = '#000033';
|
|
opacity = 0.5;
|
|
} else if (timeOfDay > 0.75) { // Soir (18h -> 20h)
|
|
overlayColor = '#FF8C00';
|
|
opacity = 0.3 * ( (timeOfDay - 0.75) / 0.08 );
|
|
} else if (timeOfDay < 0.33) { // Matin (6h -> 8h)
|
|
overlayColor = '#FFD700';
|
|
opacity = 0.3 * ( (0.33 - timeOfDay) / 0.08 );
|
|
}
|
|
|
|
ctx.fillStyle = overlayColor;
|
|
ctx.globalAlpha = opacity;
|
|
ctx.fillRect(camera.x * -1, camera.y * -1, canvas.width, canvas.height);
|
|
ctx.globalAlpha = 1.0;
|
|
}
|
|
function draw(currentTime) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.save();
|
|
ctx.translate(camera.x, camera.y);
|
|
|
|
for (let y = 0; y < MAP_SIZE; y++) for (let x = 0; x < MAP_SIZE; x++) drawTile(mapData[y][x]);
|
|
for (let y = 0; y < MAP_SIZE; y++) for (let x = 0; x < MAP_SIZE; x++) {
|
|
const tile = mapData[y][x];
|
|
if (tile.structure) drawStructure(tile);
|
|
if (tile.monsters.length > 0) drawCreature(tile, tile.monsters[0]);
|
|
if (tile.animals.length > 0) drawCreature(tile, tile.animals[0]);
|
|
if (tile.isFarm) drawFarm(tile);
|
|
if (tile.resource) drawResource(tile);
|
|
}
|
|
if (selectedTile) drawTile(selectedTile, true);
|
|
npcs.forEach(drawNpc);
|
|
drawPlayer();
|
|
drawTimeOverlay();
|
|
drawFloatingTexts(currentTime);
|
|
|
|
ctx.restore();
|
|
updateTimeUI();
|
|
}
|
|
|
|
|
|
function drawTile(tile, isSelected = false) {
|
|
const screenPos = cartToIso(tile.x, tile.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
const currentSeason = SEASONS[Math.floor((gameTicks / TICKS_PER_SEASON) % 4)];
|
|
|
|
ctx.save();
|
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
|
|
|
const baseColor = getSeasonalColor(tile.biome.color, tile.biome.name, currentSeason);
|
|
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();
|
|
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();
|
|
|
|
const rand = seededRandom(tile.x * 91 + tile.y * 67);
|
|
if (currentSeason === 'Hiver' && !['Sable', 'Désert', 'Eau Profonde', 'Eau Peu Profonde', 'Toundra'].includes(tile.biome.name)) {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
|
ctx.fill();
|
|
}
|
|
if (currentSeason === 'Printemps' && (tile.biome.name === 'Plaine' || tile.biome.name === 'Forêt') && rand() < 0.1) {
|
|
ctx.font = "14px Arial"; ctx.textAlign = "center";
|
|
ctx.fillText('🌸', (rand()-0.5) * 20, TILE_HEIGHT/2 + (rand()-0.5) * 10);
|
|
}
|
|
|
|
if (isSelected) { ctx.strokeStyle = '#FFFF00'; ctx.lineWidth = 2; ctx.stroke(); }
|
|
|
|
let textureId;
|
|
|
|
switch (tile.biome.name) {
|
|
case 'Plaine':
|
|
case 'Forêt':
|
|
case 'Forêt Enchantée':
|
|
textureId = 'grassTexture';
|
|
break;
|
|
case 'Sable':
|
|
case 'Désert':
|
|
textureId = 'sandTexture';
|
|
break;
|
|
case 'Montagne':
|
|
textureId = 'rockTexture';
|
|
break;
|
|
case 'Toundra':
|
|
textureId = 'snowTexture';
|
|
break;
|
|
default:
|
|
// Pour l'eau et les autres, on garde la couleur unie
|
|
ctx.fillStyle = tile.biome.color;
|
|
break;
|
|
}
|
|
|
|
|
|
// Si une texture a été définie, on l'utilise
|
|
if (textureId) {
|
|
ctx.fillStyle = `url(#${textureId})`;
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
// Déterminer quel motif utiliser
|
|
|
|
|
|
function drawResource(tile) {
|
|
const screenPos = cartToIso(tile.x, tile.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight + TILE_HEIGHT / 2);
|
|
ctx.font = "18px Arial"; ctx.textAlign = "center";
|
|
let icon = '';
|
|
switch(tile.resource.type) {
|
|
case 'Bois': icon = '🌲'; break; case 'Pierre': icon = '🗿'; break;
|
|
case 'Herbes': icon = '🌿'; break; case 'Champignons': icon = '🍄'; break;
|
|
case 'Cristaux Magiques': icon = '💎'; break; case 'Or': icon = '💰'; break;
|
|
case 'Fer': icon = '🔩'; break;
|
|
}
|
|
ctx.fillText(icon, 0, 0);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawStructure(tile) {
|
|
const screenPos = cartToIso(tile.x, tile.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight + TILE_HEIGHT/2);
|
|
if (tile.structure.type === 'village') {
|
|
ctx.fillStyle = '#D4A373'; ctx.fillRect(-8, -12, 16, 12); ctx.fillStyle = '#8B4513';
|
|
ctx.beginPath(); ctx.moveTo(0, -22); ctx.lineTo(-10, -12); ctx.lineTo(10, -12); ctx.closePath(); ctx.fill();
|
|
} else if (tile.structure.type === 'city') {
|
|
ctx.fillStyle = '#C0C0C0'; ctx.fillRect(-10, -20, 20, 20); ctx.fillStyle = '#808080';
|
|
ctx.beginPath(); ctx.moveTo(-10, -20); ctx.lineTo(-12, -22); ctx.lineTo(22, -22); ctx.lineTo(20, -20); ctx.closePath(); ctx.fill();
|
|
ctx.fillRect(-12, -2, 4, 4); ctx.fillRect(8, -2, 4, 4);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
function prepareSvgImages() {
|
|
return new Promise((resolve) => {
|
|
wolfImage = new Image(40, 40);
|
|
const wolfSvgBase64 = "data:image/svg+xml;base64," + btoa(wolfSvgString);
|
|
wolfImage.src = wolfSvgBase64;
|
|
wolfImage.onload = () => {
|
|
console.log("Image du loup SVG chargée.");
|
|
resolve();
|
|
};
|
|
wolfImage.onerror = () => {
|
|
console.error("Erreur lors du chargement de l'image SVG du loup.");
|
|
wolfImage = null;
|
|
resolve();
|
|
};
|
|
});
|
|
}
|
|
function drawCreature(tile, creature) {
|
|
const screenPos = cartToIso(tile.x, tile.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
ctx.save();
|
|
ctx.translate(screenPos.x, screenPos.y - elevationHeight + TILE_HEIGHT / 2);
|
|
|
|
if (creature.name === 'Loup' && wolfImage && wolfImage.complete) {
|
|
ctx.drawImage(wolfImage, -20, -35, 40, 40);
|
|
} else {
|
|
ctx.font = "24px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(creature.icon, 0, -10);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
function drawNpc(npc) {
|
|
const tile = mapData[npc.y][npc.x];
|
|
if (!tile) return;
|
|
const screenPos = cartToIso(npc.x, npc.y), elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight + TILE_HEIGHT / 2);
|
|
ctx.font = "18px Arial"; ctx.textAlign = "center";
|
|
ctx.fillText(npc.icon, 0, -5);
|
|
ctx.restore();
|
|
}
|
|
function drawPlayer() {
|
|
if (!isGameReady || !mapData[player.y] || !mapData[player.y][player.x]) return;
|
|
const tileData = mapData[player.y][player.x], screenPos = cartToIso(player.x, player.y), elevationHeight = tileData.elevation * ELEVATION_STEP;
|
|
ctx.save(); ctx.translate(screenPos.x, screenPos.y - elevationHeight);
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; ctx.beginPath(); ctx.ellipse(0, TILE_HEIGHT/2 + 2, 6, 3, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = player.color; ctx.beginPath(); ctx.arc(0, TILE_HEIGHT / 2 - 6, 8, 0, Math.PI * 2); ctx.fill();
|
|
ctx.strokeStyle = shadeColor(player.color, -30); ctx.lineWidth = 2; ctx.stroke();
|
|
|
|
if (player.equipment.weapon) {
|
|
ctx.strokeStyle = '#c0c0c0';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(5, -10);
|
|
ctx.lineTo(15, -20);
|
|
ctx.stroke();
|
|
}
|
|
if (gameState === 'camping') {
|
|
ctx.font = "24px Arial";
|
|
ctx.fillText('⛺', 0, -20);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawFloatingTexts(currentTime) {
|
|
floatingTexts = floatingTexts.filter(t => currentTime < t.endTime);
|
|
floatingTexts.forEach(text => {
|
|
const life = (text.endTime - currentTime) / text.duration;
|
|
ctx.save();
|
|
ctx.globalAlpha = Math.min(1, life * 2);
|
|
ctx.font = text.font || "bold 16px Arial";
|
|
ctx.fillStyle = text.color || "white";
|
|
ctx.strokeStyle = "black"; ctx.lineWidth = 4;
|
|
ctx.textAlign = "center";
|
|
const yOffset = (1 - life) * 40;
|
|
ctx.strokeText(text.content, text.x, text.y - yOffset);
|
|
ctx.fillText(text.content, text.x, text.y - yOffset);
|
|
ctx.restore();
|
|
});
|
|
}
|
|
|
|
function cartToIso(x, y) { return { x: (x - y) * (TILE_WIDTH / 2), y: (x + y) * (TILE_HEIGHT / 2) }; }
|
|
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 setupControls() {
|
|
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') interact();
|
|
else if (key === 'c') camp();
|
|
});
|
|
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', interact);
|
|
|
|
const mobilePanel = document.getElementById('mobile-info-panel');
|
|
document.getElementById('info-toggle-btn').addEventListener('click', () => {
|
|
mobilePanel.classList.toggle('hidden');
|
|
mobilePanel.classList.toggle('flex');
|
|
if (!mobilePanel.classList.contains('hidden')) {
|
|
updateAllInfoPanels();
|
|
}
|
|
});
|
|
mobilePanel.addEventListener('click', (e) => {
|
|
if (e.target === mobilePanel) {
|
|
mobilePanel.classList.add('hidden');
|
|
mobilePanel.classList.remove('flex');
|
|
}
|
|
});
|
|
document.getElementById('attack-btn').addEventListener('click', handleAttack);
|
|
|
|
document.body.addEventListener('click', (e) => {
|
|
if (e.target.matches('.equip-btn')) {
|
|
const itemName = e.target.dataset.item;
|
|
const action = e.target.dataset.action;
|
|
if (action === 'equip') {
|
|
equipItem(itemName);
|
|
} else if (action === 'unequip') {
|
|
unequipItem(itemName);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function calculateInventorySize() { return Object.values(player.inventory).reduce((sum, amount) => sum + amount, 0); }
|
|
|
|
function interact() {
|
|
if (!isGameReady || gameState !== 'exploring') return;
|
|
const tile = mapData[player.y][player.x];
|
|
|
|
if (tile.isFarm && !tile.farmHarvested && tile.farmCrop && !tile.farmCrop.includes('(pousse)')) {
|
|
if (calculateInventorySize() >= player.derivedStats.inventoryCapacity) {
|
|
floatingTexts.push({ content: `Inventaire plein !`, ...getFloatingTextParams() });
|
|
return;
|
|
}
|
|
const cropType = tile.farmCrop;
|
|
player.inventory[cropType] = (player.inventory[cropType] || 0) + 1;
|
|
tile.farmHarvested = true;
|
|
addXp(15);
|
|
floatingTexts.push({ content: `+1 ${cropType}`, ...getFloatingTextParams() });
|
|
updateAllInfoPanels();
|
|
return;
|
|
}
|
|
|
|
if (tile.resource && tile.resource.amount > 0) {
|
|
if (calculateInventorySize() >= player.derivedStats.inventoryCapacity) {
|
|
const playerScreenPos = cartToIso(player.x, player.y);
|
|
const elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
floatingTexts.push({ content: `Inventaire plein !`, x: playerScreenPos.x, y: playerScreenPos.y - elevationHeight - 10, endTime: performance.now() + 1500, duration: 1500 });
|
|
return;
|
|
}
|
|
|
|
const resourceType = tile.resource.type;
|
|
player.inventory[resourceType] = (player.inventory[resourceType] || 0) + 1;
|
|
tile.resource.amount--;
|
|
addXp(10);
|
|
|
|
const playerScreenPos = cartToIso(player.x, player.y);
|
|
const elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
floatingTexts.push({ content: `+1 ${resourceType}`, x: playerScreenPos.x, y: playerScreenPos.y - elevationHeight - 10, endTime: performance.now() + 1000, duration: 1000 });
|
|
|
|
if (tile.resource.amount <= 0) tile.resource = null;
|
|
updateAllInfoPanels();
|
|
}
|
|
}
|
|
|
|
function getFloatingTextParams() {
|
|
const tile = mapData[player.y][player.x];
|
|
const screenPos = cartToIso(player.x, player.y);
|
|
const elevationHeight = tile.elevation * ELEVATION_STEP;
|
|
return { x: screenPos.x, y: screenPos.y - elevationHeight - 10, endTime: performance.now() + 1000, duration: 1000 };
|
|
}
|
|
function handleMovement(currentTime) {
|
|
if (!isGameReady || currentTime - lastMoveTime < MOVE_DELAY) return;
|
|
let newX = player.x, newY = player.y;
|
|
if (controls.up) newY--; if (controls.down) newY++;
|
|
if (controls.left) newX--; if (controls.right) newX++;
|
|
if (newX !== player.x || newY !== player.y) {
|
|
lastMoveTime = currentTime;
|
|
if (newX >= 0 && newX < MAP_SIZE && newY >= 0 && newY < MAP_SIZE) {
|
|
const targetTile = mapData[newY][newX];
|
|
if (![BIOMES.WATER_DEEP.name, BIOMES.WATER_SHALLOW.name, BIOMES.RIVER.name].includes(targetTile.biome.name) || targetTile.hasBridge) {
|
|
player.x = newX; player.y = newY;
|
|
centerCameraOnPlayer();
|
|
selectedTile = null;
|
|
updateAllInfoPanels();
|
|
if (targetTile.monsters.length > 0) {
|
|
startCombat(targetTile.monsters[0]);
|
|
} else if (targetTile.animals.length > 0) {
|
|
startCombat(targetTile.animals[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function startCombat(opponent) {
|
|
gameState = 'in_combat';
|
|
currentOpponent = opponent;
|
|
document.getElementById('combat-screen').classList.remove('hidden');
|
|
document.getElementById('combat-screen').classList.add('flex');
|
|
updateCombatUI();
|
|
document.getElementById('combat-log').textContent = `Un ${opponent.name} sauvage apparaît !`;
|
|
}
|
|
|
|
function handleAttack() {
|
|
if (gameState !== 'in_combat') return;
|
|
|
|
const playerDamage = Math.max(1, player.totalAttributes.strength + Math.floor(Math.random() * 4) - 2);
|
|
currentOpponent.currentHp = Math.max(0, currentOpponent.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 (currentOpponent.currentHp <= 0) {
|
|
endCombat(true);
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const opponentDamage = Math.max(1, currentOpponent.strength + Math.floor(Math.random() * 4) - 2);
|
|
player.derivedStats.currentHp = Math.max(0, player.derivedStats.currentHp - opponentDamage);
|
|
document.getElementById('combat-log').textContent += `\nLe ${currentOpponent.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 (player.derivedStats.currentHp <= 0) {
|
|
endCombat(false);
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
function 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);
|
|
}
|
|
|
|
function updateCombatUI() {
|
|
document.getElementById('combat-player-hp-text').textContent = `${player.derivedStats.currentHp} / ${player.derivedStats.maxHp}`;
|
|
document.getElementById('combat-player-hp-bar').style.width = `${(player.derivedStats.currentHp / player.derivedStats.maxHp) * 100}%`;
|
|
if (currentOpponent) {
|
|
document.getElementById('combat-opponent-name').textContent = currentOpponent.name;
|
|
document.getElementById('combat-opponent-icon').textContent = currentOpponent.icon;
|
|
document.getElementById('combat-opponent-hp-text').textContent = `${currentOpponent.currentHp} / ${currentOpponent.hp}`;
|
|
document.getElementById('combat-opponent-hp-bar').style.width = `${(currentOpponent.currentHp / currentOpponent.hp) * 100}%`;
|
|
}
|
|
}
|
|
|
|
function calculateTotalAttributes() {
|
|
player.totalAttributes = {...player.attributes};
|
|
for (const slot in player.equipment) {
|
|
const item = player.equipment[slot];
|
|
if (item) {
|
|
for (const stat in item.stats) {
|
|
player.totalAttributes[stat] += item.stats[stat];
|
|
}
|
|
}
|
|
}
|
|
calculateDerivedStats();
|
|
}
|
|
|
|
function calculateDerivedStats() {
|
|
const oldMaxHp = player.derivedStats.maxHp;
|
|
player.derivedStats.maxHp = player.totalAttributes.vitality * 10;
|
|
player.derivedStats.inventoryCapacity = player.totalAttributes.strength * 5;
|
|
if (oldMaxHp > 0) {
|
|
const hpRatio = player.derivedStats.currentHp / oldMaxHp;
|
|
player.derivedStats.currentHp = Math.round(player.derivedStats.maxHp * hpRatio);
|
|
} else {
|
|
player.derivedStats.currentHp = player.derivedStats.maxHp;
|
|
}
|
|
}
|
|
|
|
function addXp(amount) {
|
|
player.xp += amount;
|
|
if (player.xp >= player.xpToNextLevel) levelUp();
|
|
updateAllInfoPanels();
|
|
}
|
|
|
|
function levelUp() {
|
|
player.level++;
|
|
player.xp -= player.xpToNextLevel;
|
|
player.xpToNextLevel = Math.floor(player.xpToNextLevel * 1.5);
|
|
|
|
Object.keys(player.attributes).forEach(attr => player.attributes[attr]++);
|
|
|
|
calculateTotalAttributes();
|
|
player.derivedStats.currentHp = player.derivedStats.maxHp;
|
|
|
|
const playerScreenPos = cartToIso(player.x, player.y);
|
|
const elevationHeight = mapData[player.y][player.x].elevation * ELEVATION_STEP;
|
|
floatingTexts.push({ content: `Niveau Supérieur !`, x: playerScreenPos.x, y: playerScreenPos.y - elevationHeight - 20, endTime: performance.now() + 2000, duration: 2000, font: 'bold 24px Arial', color: '#FFD700' });
|
|
}
|
|
|
|
function equipItem(itemName) {
|
|
const item = ITEMS[itemName];
|
|
if (!item) return;
|
|
player.inventory[itemName]--;
|
|
if (player.inventory[itemName] <= 0) {
|
|
delete player.inventory[itemName];
|
|
}
|
|
const oldItem = player.equipment[item.type];
|
|
if (oldItem) {
|
|
player.inventory[oldItem.name] = (player.inventory[oldItem.name] || 0) + 1;
|
|
}
|
|
player.equipment[item.type] = item;
|
|
calculateTotalAttributes();
|
|
updateAllInfoPanels();
|
|
}
|
|
|
|
function unequipItem(slot) {
|
|
const item = player.equipment[slot];
|
|
if (!item) return;
|
|
player.equipment[slot] = null;
|
|
player.inventory[item.name] = (player.inventory[item.name] || 0) + 1;
|
|
calculateTotalAttributes();
|
|
updateAllInfoPanels();
|
|
}
|
|
|
|
function camp() {
|
|
if (gameState !== 'exploring') return;
|
|
const tile = mapData[player.y][player.x];
|
|
const timeOfDay = (gameTicks % TICKS_PER_DAY) / TICKS_PER_DAY;
|
|
if (timeOfDay > 0.83 || timeOfDay < 0.25) { // Can only camp at night
|
|
if (!tile.structure && tile.monsters.length === 0 && tile.animals.length === 0) {
|
|
gameState = 'camping';
|
|
}
|
|
}
|
|
}
|
|
|
|
function centerCameraOnPlayer() {
|
|
if (!isGameReady || !mapData[player.y] || !mapData[player.y][player.x]) return;
|
|
const tileData = mapData[player.y][player.x];
|
|
const playerScreenPos = cartToIso(player.x, player.y);
|
|
const elevationHeight = tileData.elevation * ELEVATION_STEP;
|
|
camera.x = canvas.width / 2 - playerScreenPos.x;
|
|
camera.y = canvas.height / 2 - (playerScreenPos.y - elevationHeight);
|
|
}
|
|
|
|
function updateAllInfoPanels() {
|
|
if (!isGameReady) return;
|
|
|
|
const inventoryHTML = Object.keys(player.inventory).length > 0 ?
|
|
Object.entries(player.inventory).map(([item, amount]) => {
|
|
let button = '';
|
|
if (ITEMS[item]) {
|
|
button = `<button class="equip-btn" data-action="equip" data-item="${item}">Équiper</button>`;
|
|
}
|
|
return `<div>- ${item}: ${amount} ${button}</div>`;
|
|
}).join('') : "L'inventaire est vide.";
|
|
|
|
const playerInfoHTML = `
|
|
<div class="flex justify-between items-baseline">
|
|
<h3 class="font-bold text-lg">Héros</h3>
|
|
<span class="text-cyan-400 font-bold">Niv. ${player.level}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-medium text-red-400">PV</span>
|
|
<div class="w-full bg-gray-600 rounded-full h-2.5"><div class="bg-red-500 h-2.5 rounded-full" style="width: ${(player.derivedStats.currentHp / player.derivedStats.maxHp) * 100}%"></div></div>
|
|
<div class="text-center text-xs text-gray-300">${player.derivedStats.currentHp} / ${player.derivedStats.maxHp}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-medium text-yellow-400">XP</span>
|
|
<div class="w-full bg-gray-600 rounded-full h-2.5"><div class="bg-yellow-500 h-2.5 rounded-full" style="width: ${(player.xp / player.xpToNextLevel) * 100}%"></div></div>
|
|
<div class="text-center text-xs text-gray-300">${player.xp} / ${player.xpToNextLevel}</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-1 pt-2 text-gray-300">
|
|
<span>💪 Force: ${player.totalAttributes.strength}</span>
|
|
<span>🤸 Agilité: ${player.totalAttributes.agility}</span>
|
|
<span>❤️ Vitalité: ${player.totalAttributes.vitality}</span>
|
|
<span>🧠 Intel.: ${player.totalAttributes.intelligence}</span>
|
|
</div>
|
|
<hr class="border-gray-600">
|
|
<div>
|
|
<h3 class="font-bold">Équipement:</h3>
|
|
<div class="pl-2 text-gray-300">
|
|
<div>Arme: ${player.equipment.weapon ? player.equipment.weapon.name + ` <button class="equip-btn" data-action="unequip" data-item="weapon">Déséquiper</button>` : 'Aucune'}</div>
|
|
<div>Armure: ${player.equipment.armor ? player.equipment.armor.name + ` <button class="equip-btn" data-action="unequip" data-item="armor">Déséquiper</button>` : 'Aucune'}</div>
|
|
</div>
|
|
</div>
|
|
<hr class="border-gray-600">
|
|
<p><strong>Position:</strong> (${player.x}, ${player.y})</p>
|
|
<div>
|
|
<h3 class="font-bold">Inventaire (${calculateInventorySize()}/${player.derivedStats.inventoryCapacity}):</h3>
|
|
<div class="text-gray-300 pl-2">${inventoryHTML}</div>
|
|
</div>`;
|
|
|
|
const tileInfoHTML = selectedTile ?
|
|
`<p><strong>Coordonnées:</strong> (${selectedTile.x}, ${selectedTile.y})</p> <p><strong>Biome:</strong> <span style="color:${selectedTile.biome.color}; font-weight:bold;">${selectedTile.biome.name}</span></p> <p><strong>Élévation:</strong> ${selectedTile.elevation}</p>` +
|
|
(selectedTile.isFarm ? `<p><strong>Culture:</strong> ${selectedTile.farmHarvested ? 'Récolté' : selectedTile.farmCrop || 'En jachère'}</p>` : '') +
|
|
(selectedTile.resource ? `<p><strong>Ressource:</strong> ${selectedTile.resource.type} (${selectedTile.resource.amount})</p>` : '') +
|
|
(selectedTile.structure ? `<hr class="my-2 border-gray-600"><h3 class="font-bold text-lg text-yellow-300">${selectedTile.structure.name} (${selectedTile.structure.type})</h3><p><strong>Population:</strong> ${selectedTile.structure.population}</p><p><strong>Bâtiments:</strong></p><ul class="list-disc list-inside text-gray-300">${selectedTile.structure.buildings.map(b => `<li>${b}</li>`).join('')}</ul>` : '') +
|
|
(selectedTile.monsters.length > 0 ? `<p class="text-red-400 font-bold mt-2">Présence: ${selectedTile.monsters[0].name}</p>` : '') +
|
|
(selectedTile.animals.length > 0 ? `<p class="text-orange-400 font-bold mt-2">Présence: ${selectedTile.animals[0].name}</p>` : '')
|
|
: '<p>Aucune tuile sélectionnée.</p>';
|
|
|
|
const playerInfoDesktop = document.getElementById('player-info-desktop');
|
|
if (playerInfoDesktop) playerInfoDesktop.innerHTML = playerInfoHTML;
|
|
const tileInfoDesktop = document.getElementById('tile-info-desktop');
|
|
if (tileInfoDesktop) tileInfoDesktop.innerHTML = tileInfoHTML;
|
|
|
|
const playerInfoMobile = document.getElementById('player-info-mobile');
|
|
if (playerInfoMobile) playerInfoMobile.innerHTML = playerInfoHTML;
|
|
const tileInfoMobile = document.getElementById('tile-info-mobile');
|
|
if (tileInfoMobile) tileInfoMobile.innerHTML = tileInfoHTML;
|
|
}
|
|
|
|
function updateTimeUI() {
|
|
const day = Math.floor(gameTicks / TICKS_PER_DAY) + 1;
|
|
const season = SEASONS[Math.floor((gameTicks / TICKS_PER_SEASON) % 4)];
|
|
const timeOfDay = gameTicks % TICKS_PER_DAY;
|
|
const hours = Math.floor(timeOfDay / 60).toString().padStart(2, '0');
|
|
const minutes = Math.floor(timeOfDay % 60).toString().padStart(2, '0');
|
|
|
|
document.getElementById('time-display').textContent = `Jour ${day}, ${hours}:${minutes}`;
|
|
document.getElementById('season-display').textContent = season;
|
|
}
|
|
|
|
initializeGame();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|