<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>Mini Total War 0.2</title>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, Arial, sans-serif;
background: #182218;
color: white;
touch-action: none;
}
#gameWrap {
position: fixed;
inset: 0;
width: 100vw;
height: 100dvh;
}
#top {
position: absolute;
z-index: 10;
top: 0;
left: 0;
right: 0;
height: 58px;
background: #111811;
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
}
#title {
font-weight: 800;
font-size: 15px;
}
#status {
font-size: 11px;
opacity: 0.8;
margin-top: 2px;
}
.controls {
display: flex;
gap: 5px;
}
button {
border: 0;
border-radius: 8px;
padding: 8px 10px;
font-weight: 700;
font-size: 12px;
}
#pause {
background: #e8c75c;
}
#restart {
background: #ddd;
}
canvas {
position: absolute;
left: 0;
top: 58px;
width: 100%;
height: calc(100% - 58px);
display: block;
background: #587346;
}
/* МЕНЮ АРМИИ */
#menu {
position: absolute;
z-index: 30;
inset: 0;
background: #172117;
display: flex;
align-items: center;
justify-content: center;
padding: 18px;
overflow: auto;
}
.panel {
width: min(440px, 100%);
background: #263526;
border: 1px solid #516451;
border-radius: 16px;
padding: 18px;
}
.panel h1 {
margin: 0 0 4px;
font-size: 25px;
}
.subtitle {
font-size: 13px;
opacity: 0.75;
margin-bottom: 16px;
}
.budget {
background: #111811;
padding: 10px;
border-radius: 10px;
margin-bottom: 12px;
font-weight: 700;
}
.row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
background: #344734;
border-radius: 11px;
padding: 10px;
margin: 8px 0;
}
.unitName {
font-weight: 800;
}
.unitInfo {
font-size: 11px;
opacity: 0.7;
margin-top: 3px;
}
.counter {
display: flex;
align-items: center;
gap: 9px;
}
.counter button {
width: 36px;
height: 36px;
font-size: 20px;
padding: 0;
}
.count {
min-width: 22px;
text-align: center;
font-size: 18px;
font-weight: 800;
}
#battleButton {
width: 100%;
margin-top: 14px;
padding: 13px;
background: #d5b14c;
color: #161616;
font-size: 16px;
}
.hint {
font-size: 11px;
opacity: 0.65;
margin-top: 12px;
line-height: 1.4;
}
</style>
</head>
<body>
<div id="gameWrap">
<div id="top">
<div>
<div id="title">⚔️ MINI TOTAL WAR 0.2</div>
<div id="status">Подготовка</div>
</div>
<div class="controls">
<button id="pause">⏸ Пауза</button>
<button id="restart">Новый бой</button>
</div>
</div>
<canvas id="game"></canvas>
<div id="menu">
<div class="panel">
<h1>⚔️ Собери армию</h1>
<div class="subtitle">У тебя и противника одинаковый бюджет.</div>
<div class="budget">Бюджет: <span id="money">1000</span> / 1000</div>
<div class="row">
<div>
<div class="unitName">🛡 Пехота</div>
<div class="unitInfo">100 очков • сильна в ближнем бою</div>
</div>
<div class="counter">
<button onclick="changeCount('infantry', -1)">−</button>
<span class="count" id="infantryCount">0</span>
<button onclick="changeCount('infantry', 1)">+</button>
</div>
</div>
<div class="row">
<div>
<div class="unitName">🏹 Лучники</div>
<div class="unitInfo">150 очков • дальняя атака</div>
</div>
<div class="counter">
<button onclick="changeCount('archer', -1)">−</button>
<span class="count" id="archerCount">0</span>
<button onclick="changeCount('archer', 1)">+</button>
</div>
</div>
<div class="row">
<div>
<div class="unitName">🐎 Кавалерия</div>
<div class="unitInfo">250 очков • скорость и сильная атака</div>
</div>
<div class="counter">
<button onclick="changeCount('cavalry', -1)">−</button>
<span class="count" id="cavalryCount">0</span>
<button onclick="changeCount('cavalry', 1)">+</button>
</div>
</div>
<button id="battleButton">В БОЙ</button>
<div class="hint">
После начала боя нажми на синий отряд, затем на место на поле. Можно выбрать сразу
несколько отрядов.
</div>
</div>
</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const pauseBtn = document.getElementById("pause");
const restartBtn = document.getElementById("restart");
const menu = document.getElementById("menu");
const moneyEl = document.getElementById("money");
let W = 0,
H = 0;
let units = [];
let selected = [];
let last = performance.now();
let battleStarted = false;
let paused = false;
let finished = false;
const BUDGET = 1000;
const prices = {
infantry: 100,
archer: 150,
cavalry: 250,
};
let army = {
infantry: 3,
archer: 2,
cavalry: 1,
};
const stats = {
infantry: {
hp: 210,
speed: 42,
range: 30,
damage: 17,
rate: 0.68,
morale: 100,
},
archer: {
hp: 115,
speed: 36,
range: 165,
damage: 11,
rate: 0.9,
morale: 90,
},
cavalry: {
hp: 165,
speed: 82,
range: 32,
damage: 25,
rate: 0.7,
morale: 105,
},
};
function resize() {
const r = canvas.getBoundingClientRect();
W = r.width;
H = r.height;
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function cost() {
return (
army.infantry * prices.infantry +
army.archer * prices.archer +
army.cavalry * prices.cavalry
);
}
function updateMenu() {
document.getElementById("infantryCount").textContent = army.infantry;
document.getElementById("archerCount").textContent = army.archer;
document.getElementById("cavalryCount").textContent = army.cavalry;
moneyEl.textContent = BUDGET - cost();
}
function changeCount(type, delta) {
const next = Math.max(0, army[type] + delta);
const old = army[type];
army[type] = next;
if (cost() > BUDGET) army[type] = old;
updateMenu();
}
function makeUnit(team, type, x, y) {
const s = stats[type];
return {
team,
type,
x,
y,
tx: x,
ty: y,
hp: s.hp,
maxHp: s.hp,
morale: s.morale,
maxMorale: s.morale,
speed: s.speed,
range: s.range,
damage: s.damage,
rate: s.rate,
cooldown: 0,
alive: true,
fleeing: false,
ordered: false,
moving: false,
};
}
function buildArmy(team, composition) {
const result = [];
const types = [];
for (const type of ["infantry", "archer", "cavalry"]) {
for (let i = 0; i < composition[type]; i++) types.push(type);
}
const x = team === "blue" ? 55 : W - 55;
const spacing = Math.min(70, H / (types.length + 1));
types.forEach((type, i) => {
const total = (types.length - 1) * spacing;
const y = H / 2 - total / 2 + i * spacing;
result.push(makeUnit(team, type, x, y));
});
return result;
}
/* ИИ получает тот же состав и тот же бюджет */
function startBattle() {
if (cost() === 0) {
statusEl.textContent = "Сначала выбери армию";
return;
}
resize();
units = [...buildArmy("blue", army), ...buildArmy("red", army)];
selected = [];
paused = false;
finished = false;
battleStarted = true;
pauseBtn.textContent = "⏸ Пауза";
statusEl.textContent = "Бой начался";
menu.style.display = "none";
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function nearestEnemy(u) {
let target = null,
best = Infinity;
for (const e of units) {
if (!e.alive || e.team === u.team) continue;
const d = distance(u, e);
if (d < best) {
best = d;
target = e;
}
}
return [target, best];
}
function moveToward(u, x, y, dt, mult = 1) {
const dx = x - u.x,
dy = y - u.y;
const d = Math.hypot(dx, dy);
if (d < 2) return;
const step = Math.min(d, u.speed * mult * dt);
u.x += (dx / d) * step;
u.y += (dy / d) * step;
}
function attack(a, e) {
if (a.cooldown > 0) return;
let dmg = a.damage;
if (a.type === "cavalry" && a.moving) {
dmg *= 1.45;
e.morale -= 7;
}
e.hp -= dmg;
e.morale -= dmg * 0.16;
a.cooldown = a.rate;
if (e.hp <= 0) {
e.alive = false;
selected = selected.filter((x) => x !== e);
for (const ally of units) {
if (ally.alive && ally.team === e.team && distance(ally, e) < 110) {
ally.morale -= 9;
}
}
}
}
function update(dt) {
if (!battleStarted || paused || finished) return;
for (const u of units) {
if (!u.alive) continue;
u.cooldown = Math.max(0, u.cooldown - dt);
if (u.morale <= 15 && !u.fleeing) {
u.fleeing = true;
selected = selected.filter((x) => x !== u);
}
if (u.fleeing) {
const exit = u.team === "blue" ? -100 : W + 100;
moveToward(u, exit, u.y, dt, 1.4);
if (u.x < -60 || u.x > W + 60) u.alive = false;
continue;
}
const [enemy, d] = nearestEnemy(u);
if (!enemy) continue;
/* Красный ИИ */
if (u.team === "red") {
if (d > u.range) {
moveToward(u, enemy.x, enemy.y, dt);
u.moving = true;
} else {
u.moving = false;
attack(u, enemy);
}
continue;
}
/* Синие выполняют приказ игрока */
if (u.ordered) {
const orderD = Math.hypot(u.tx - u.x, u.ty - u.y);
if (orderD > 7) {
moveToward(u, u.tx, u.ty, dt);
u.moving = true;
} else {
u.ordered = false;
u.moving = false;
}
} else {
u.moving = false;
}
const current = distance(u, enemy);
/* Отряд защищается, если враг подошёл */
if (current <= u.range) {
attack(u, enemy);
}
}
const blue = units.some((u) => u.alive && u.team === "blue");
const red = units.some((u) => u.alive && u.team === "red");
if (!blue || !red) {
finished = true;
statusEl.textContent = blue ? "🏆 Победа!" : "💀 Поражение";
}
}
function drawBackground() {
ctx.fillStyle = "#587346";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(255,255,255,.06)";
ctx.lineWidth = 1;
for (let x = 0; x < W; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 0; y < H; y += 50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
ctx.strokeStyle = "rgba(255,255,255,.17)";
ctx.setLineDash([8, 8]);
ctx.beginPath();
ctx.moveTo(W / 2, 0);
ctx.lineTo(W / 2, H);
ctx.stroke();
ctx.setLineDash([]);
}
function drawUnit(u) {
if (!u.alive) return;
const chosen = selected.includes(u);
let color = u.team === "blue" ? "#4da3ff" : "#e64f4f";
if (u.type === "archer") color = u.team === "blue" ? "#6ed5ff" : "#ff8b72";
if (u.type === "cavalry") color = u.team === "blue" ? "#b6e6ff" : "#ffc0b8";
ctx.save();
ctx.translate(u.x, u.y);
ctx.fillStyle = color;
ctx.strokeStyle = chosen ? "#ffe75b" : "#152015";
ctx.lineWidth = chosen ? 5 : 2;
if (u.type === "cavalry") {
ctx.beginPath();
ctx.moveTo(0, -18);
ctx.lineTo(22, 16);
ctx.lineTo(-22, 16);
ctx.closePath();
ctx.fill();
ctx.stroke();
} else {
ctx.fillRect(-22, -15, 44, 30);
ctx.strokeRect(-22, -15, 44, 30);
}
ctx.fillStyle = "#fff";
ctx.font = "bold 10px Arial";
ctx.textAlign = "center";
const label = u.type === "infantry" ? "INF" : u.type === "archer" ? "ARC" : "CAV";
ctx.fillText(label, 0, 4);
/* HP */
ctx.fillStyle = "#222";
ctx.fillRect(-22, -27, 44, 5);
ctx.fillStyle = "#4bea69";
ctx.fillRect(-22, -27, 44 * Math.max(0, u.hp / u.maxHp), 5);
/* Мораль */
ctx.fillStyle = "#222";
ctx.fillRect(-22, 20, 44, 4);
ctx.fillStyle = "#ffd84d";
ctx.fillRect(-22, 20, 44 * Math.max(0, u.morale / u.maxMorale), 4);
if (u.fleeing) {
ctx.fillStyle = "white";
ctx.font = "bold 15px Arial";
ctx.fillText("!", 0, -36);
}
ctx.restore();
}
function draw() {
drawBackground();
units.forEach(drawUnit);
if (paused && battleStarted && !finished) {
ctx.fillStyle = "rgba(0,0,0,.38)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "white";
ctx.font = "bold 30px Arial";
ctx.textAlign = "center";
ctx.fillText("ПАУЗА", W / 2, H / 2);
}
}
function loop(now) {
const dt = Math.min(0.04, (now - last) / 1000);
last = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
/* УПРАВЛЕНИЕ */
canvas.addEventListener("pointerdown", (e) => {
if (!battleStarted || paused || finished) return;
e.preventDefault();
const r = canvas.getBoundingClientRect();
const x = e.clientX - r.left;
const y = e.clientY - r.top;
let hit = null;
for (const u of units) {
if (u.alive && u.team === "blue" && Math.hypot(u.x - x, u.y - y) < 38) {
hit = u;
break;
}
}
if (hit) {
if (selected.includes(hit)) {
selected = selected.filter((u) => u !== hit);
} else {
selected.push(hit);
}
statusEl.textContent = "Выбрано: " + selected.length;
return;
}
if (selected.length) {
const gap = 48;
selected.forEach((u, i) => {
u.tx = Math.max(30, Math.min(W - 30, x));
u.ty = Math.max(30, Math.min(H - 30, y + (i - (selected.length - 1) / 2) * gap));
u.ordered = true;
});
statusEl.textContent = "⚔️ Приказ отдан";
}
});
pauseBtn.addEventListener("click", () => {
if (!battleStarted || finished) return;
paused = !paused;
pauseBtn.textContent = paused ? "▶ Продолжить" : "⏸ Пауза";
statusEl.textContent = paused ? "Бой остановлен" : "Бой продолжается";
});
restartBtn.addEventListener("click", () => {
battleStarted = false;
paused = false;
finished = false;
units = [];
selected = [];
menu.style.display = "flex";
statusEl.textContent = "Подготовка";
pauseBtn.textContent = "⏸ Пауза";
});
document.getElementById("battleButton").addEventListener("click", startBattle);
window.addEventListener("resize", resize);
resize();
updateMenu();
requestAnimationFrame(loop);
</script>
</body>
</html>