feat: progress
This commit is contained in:
+168
-60
@@ -403,6 +403,10 @@
|
||||
<span class="stat-label">Attack:</span>
|
||||
<span class="stat-value" id="statAttack">5</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Range:</span>
|
||||
<span class="stat-value" id="statRange">4</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Defense:</span>
|
||||
<span class="stat-value" id="statDefense">2</span>
|
||||
@@ -455,19 +459,19 @@
|
||||
<div class="camera-controls" id="cameraControls">
|
||||
<div class="cam-label">📷 CAMERA</div>
|
||||
<div class="cam-row">
|
||||
<button class="cam-btn" id="camUp" title="Tilt up">▲</button>
|
||||
<button type="button" class="cam-btn" id="camUp" title="Tilt up">▲</button>
|
||||
</div>
|
||||
<div class="cam-row">
|
||||
<button class="cam-btn" id="camLeft" title="Orbit left">◀</button>
|
||||
<button class="cam-btn" id="camCenter" title="Re-center on player">⊙</button>
|
||||
<button class="cam-btn" id="camRight" title="Orbit right">▶</button>
|
||||
<button type="button" class="cam-btn" id="camLeft" title="Orbit left">◀</button>
|
||||
<button type="button" class="cam-btn" id="camCenter" title="Re-center on player">⊙</button>
|
||||
<button type="button" class="cam-btn" id="camRight" title="Orbit right">▶</button>
|
||||
</div>
|
||||
<div class="cam-row">
|
||||
<button class="cam-btn" id="camDown" title="Tilt down">▼</button>
|
||||
<button type="button" class="cam-btn" id="camDown" title="Tilt down">▼</button>
|
||||
</div>
|
||||
<div class="cam-row" style="margin-top:4px; gap:4px;">
|
||||
<button class="cam-btn wide" id="camZoomIn" title="Zoom in">🔍 +</button>
|
||||
<button class="cam-btn wide" id="camZoomOut" title="Zoom out">🔍 −</button>
|
||||
<button type="button" class="cam-btn wide" id="camZoomIn" title="Zoom in">🔍 +</button>
|
||||
<button type="button" class="cam-btn wide" id="camZoomOut" title="Zoom out">🔍 −</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,19 +479,24 @@
|
||||
<div style="color: #667eea; font-weight: bold; margin-bottom: 10px;">Controls</div>
|
||||
<div class="controls-help">Each action costs 1 AP. Hover buttons to see details.</div>
|
||||
<div class="button-group">
|
||||
<button class="action-button" id="moveUpBtn" title="Move north by 1 tile (cost: 1 AP)">⬆ N</button>
|
||||
<button class="action-button" id="moveDownBtn" title="Move south by 1 tile (cost: 1 AP)">⬇ S</button>
|
||||
<button class="action-button" id="moveLeftBtn" title="Move west by 1 tile (cost: 1 AP)">⬅ W</button>
|
||||
<button class="action-button" id="moveRightBtn" title="Move east by 1 tile (cost: 1 AP)">➡ E</button>
|
||||
<button type="button" class="action-button" id="moveUpBtn" title="Move north by 1 tile (cost: 1 AP)">⬆ N</button>
|
||||
<button type="button" class="action-button" id="moveDownBtn" title="Move south by 1 tile (cost: 1 AP)">⬇ S</button>
|
||||
<button type="button" class="action-button" id="moveLeftBtn" title="Move west by 1 tile (cost: 1 AP)">⬅ W</button>
|
||||
<button type="button" class="action-button" id="moveRightBtn" title="Move east by 1 tile (cost: 1 AP)">➡ E</button>
|
||||
</div>
|
||||
<div class="button-group" style="margin-top: 10px;">
|
||||
<button class="action-button" id="gatherBtn" title="Gather nearby trees/mountains within radius 5 (cost: 1 AP)">🌳 Gather</button>
|
||||
<button class="action-button" id="attackBtn" title="Attack nearest selected enemy target (cost: 1 AP on success)">⚔️ Attack</button>
|
||||
<button type="button" class="action-button" id="gatherBtn" title="Gather nearby trees/mountains within radius 5 (cost: 1 AP)">🌳 Gather</button>
|
||||
<button type="button" class="action-button" id="attackBtn" title="Attack nearest selected enemy target (cost: 1 AP on success)">⚔️ Attack</button>
|
||||
</div>
|
||||
<div class="button-group" style="margin-top: 10px;">
|
||||
<button class="action-button" id="buildHouseBtn" title="Build House (20 wood, 10 stone)">🏠 House</button>
|
||||
<button class="action-button" id="buildFarmBtn" title="Build Farm (15 wood, 5 stone)">🌾 Farm</button>
|
||||
<button class="action-button" id="buildTowerBtn" title="Build Guard Tower (30 wood, 20 stone)">🛡️ Tower</button>
|
||||
<button type="button" class="action-button" id="buildHouseBtn" title="Build House (20 wood, 10 stone)">🏠 House</button>
|
||||
<button type="button" class="action-button" id="buildFarmBtn" title="Build Farm (15 wood, 5 stone)">🌾 Farm</button>
|
||||
<button type="button" class="action-button" id="buildTowerBtn" title="Build Guard Tower (30 wood, 20 stone)">🛡️ Tower</button>
|
||||
</div>
|
||||
<div class="button-group" style="margin-top: 10px;">
|
||||
<button type="button" class="action-button" id="buildBlockBtn" title="Build Block 1 tile north (cost: 2 wood)">🧱 Block</button>
|
||||
<button type="button" class="action-button" id="lootBtn" title="Loot nearest chest in range">🗝 Loot</button>
|
||||
<button type="button" class="action-button" id="equipBtn" title="Equip first available weapon slot">⚒ Equip</button>
|
||||
</div>
|
||||
<div class="status-message" id="statusMessage">Ready for adventure!</div>
|
||||
</div>
|
||||
@@ -512,6 +521,7 @@
|
||||
let gameActive = true;
|
||||
let ws = null;
|
||||
let cameraKeyBindingsAttached = false;
|
||||
let cameraAutoFollow = true;
|
||||
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
|
||||
@@ -529,7 +539,8 @@
|
||||
document.getElementById('healthBar').style.width = `${(currentPlayer.health / currentPlayer.max_health) * 100}%`;
|
||||
document.getElementById('statActionPoints').textContent = currentPlayer.action_points;
|
||||
document.getElementById('actionBar').style.width = `${(currentPlayer.action_points / currentPlayer.max_action_points) * 100}%`;
|
||||
document.getElementById('statAttack').textContent = currentPlayer.attack;
|
||||
document.getElementById('statAttack').textContent = currentPlayer.effective_attack ?? currentPlayer.attack;
|
||||
document.getElementById('statRange').textContent = (currentPlayer.attack_range ?? 4).toFixed(1);
|
||||
document.getElementById('statDefense').textContent = currentPlayer.defense;
|
||||
document.getElementById('statMove').textContent = currentPlayer.movement_capacity;
|
||||
|
||||
@@ -556,11 +567,25 @@
|
||||
let ossBossObject = null;
|
||||
const structureObjects = {};
|
||||
const resourceObjects = {};
|
||||
const chestObjects = {};
|
||||
|
||||
function initScene() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a0a0a);
|
||||
|
||||
// Sky-like vertical gradient background (top light blue -> horizon pale -> lower blue).
|
||||
const skyCanvas = document.createElement('canvas');
|
||||
skyCanvas.width = 8;
|
||||
skyCanvas.height = 512;
|
||||
const skyCtx = skyCanvas.getContext('2d');
|
||||
const grad = skyCtx.createLinearGradient(0, 0, 0, skyCanvas.height);
|
||||
grad.addColorStop(0.0, '#7ecbff');
|
||||
grad.addColorStop(0.45, '#bfe5ff');
|
||||
grad.addColorStop(1.0, '#5aa7df');
|
||||
skyCtx.fillStyle = grad;
|
||||
skyCtx.fillRect(0, 0, skyCanvas.width, skyCanvas.height);
|
||||
const skyTexture = new THREE.CanvasTexture(skyCanvas);
|
||||
scene.background = skyTexture;
|
||||
|
||||
camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 10000);
|
||||
camera.position.set(0, 50, 50);
|
||||
@@ -579,6 +604,8 @@
|
||||
controls.minDistance = 10;
|
||||
controls.maxDistance = 200;
|
||||
controls.enableKeys = false; // We handle arrow keys ourselves.
|
||||
// Keep follow mode only until the user takes manual camera control.
|
||||
controls.addEventListener('start', () => { cameraAutoFollow = false; });
|
||||
|
||||
// Lighting
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
@@ -599,8 +626,8 @@
|
||||
ground.receiveShadow = true;
|
||||
scene.add(ground);
|
||||
|
||||
// Grid helper
|
||||
const gridHelper = new THREE.GridHelper(500, 50, 0x444444, 0x222222);
|
||||
// Grid helper: smaller cells so 1-tile movement matches visible grid steps.
|
||||
const gridHelper = new THREE.GridHelper(500, 500, 0x4a5d66, 0x2b3b42);
|
||||
gridHelper.position.y = 0.1;
|
||||
scene.add(gridHelper);
|
||||
|
||||
@@ -721,13 +748,24 @@
|
||||
sprite.position.set(player.position.x, 2.5, player.position.y);
|
||||
scene.add(sprite);
|
||||
|
||||
playerSpheres[player.id] = { mesh: sphere, sprite };
|
||||
const northGeo = new THREE.ConeGeometry(0.18, 0.6, 8);
|
||||
const northMat = new THREE.MeshPhongMaterial({ color: player.color });
|
||||
const northArrow = new THREE.Mesh(northGeo, northMat);
|
||||
// Rotate so the tip points north (-Z direction in Three.js world space).
|
||||
northArrow.rotation.x = -Math.PI / 2;
|
||||
northArrow.position.set(player.position.x, 0.9, player.position.y - 0.8);
|
||||
scene.add(northArrow);
|
||||
|
||||
playerSpheres[player.id] = { mesh: sphere, sprite, northArrow };
|
||||
}
|
||||
|
||||
function updatePlayerSphere(player) {
|
||||
if (playerSpheres[player.id]) {
|
||||
playerSpheres[player.id].mesh.position.set(player.position.x, 0.5, player.position.y);
|
||||
playerSpheres[player.id].sprite.position.set(player.position.x, 2.5, player.position.y);
|
||||
if (playerSpheres[player.id].northArrow) {
|
||||
playerSpheres[player.id].northArrow.position.set(player.position.x, 0.9, player.position.y - 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,6 +800,9 @@
|
||||
} else if (structure.structure_type === 'guard_tower') {
|
||||
geometry = new THREE.ConeGeometry(0.35, 1.4, 4);
|
||||
color = 0x808080;
|
||||
} else if (structure.structure_type === 'block') {
|
||||
geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9);
|
||||
color = 0x7a5f3e;
|
||||
}
|
||||
const material = new THREE.MeshPhongMaterial({ color });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
@@ -771,36 +812,38 @@
|
||||
structureObjects[structure.id] = mesh;
|
||||
}
|
||||
|
||||
function createChestMesh(chest) {
|
||||
const baseGeo = new THREE.BoxGeometry(0.9, 0.55, 0.7);
|
||||
const baseMat = new THREE.MeshPhongMaterial({ color: 0x8b5a2b });
|
||||
const base = new THREE.Mesh(baseGeo, baseMat);
|
||||
base.position.y = 0.28;
|
||||
|
||||
const lidGeo = new THREE.BoxGeometry(0.9, 0.2, 0.7);
|
||||
const lidMat = new THREE.MeshPhongMaterial({ color: 0xd1a13a });
|
||||
const lid = new THREE.Mesh(lidGeo, lidMat);
|
||||
lid.position.y = 0.63;
|
||||
|
||||
const group = new THREE.Group();
|
||||
group.add(base);
|
||||
if (!chest.opened) group.add(lid);
|
||||
group.position.set(chest.position.x, 0, chest.position.y);
|
||||
scene.add(group);
|
||||
chestObjects[chest.id] = group;
|
||||
}
|
||||
|
||||
function createResourceMesh(resource) {
|
||||
let mesh;
|
||||
if (resource.resource_type === 'tree') {
|
||||
const trunkGeo = new THREE.CylinderGeometry(0.12, 0.18, 0.8, 8);
|
||||
const trunkMat = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
|
||||
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
|
||||
trunk.position.y = 0.4;
|
||||
const foliageMat = new THREE.MeshPhongMaterial({ color: 0x228b22 });
|
||||
const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.85, 1.3, 8), foliageMat);
|
||||
foliage1.position.y = 1.3;
|
||||
const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.6, 1.0, 8), foliageMat);
|
||||
foliage2.position.y = 2.1;
|
||||
mesh = new THREE.Group();
|
||||
mesh.add(trunk, foliage1, foliage2);
|
||||
} else {
|
||||
const rockMat = new THREE.MeshPhongMaterial({ color: 0x8a8a8a, flatShading: true });
|
||||
const main = new THREE.Mesh(new THREE.DodecahedronGeometry(0.65, 0), rockMat);
|
||||
main.position.y = 0.5;
|
||||
main.rotation.y = Math.random() * Math.PI;
|
||||
const side1 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.38, 0), rockMat);
|
||||
side1.position.set(0.6, 0.25, 0.12);
|
||||
side1.rotation.y = Math.random() * Math.PI;
|
||||
const side2 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.28, 0), rockMat);
|
||||
side2.position.set(-0.5, 0.2, 0.25);
|
||||
side2.rotation.y = Math.random() * Math.PI;
|
||||
mesh = new THREE.Group();
|
||||
mesh.add(main, side1, side2);
|
||||
}
|
||||
mesh.position.set(resource.position.x, 0, resource.position.y);
|
||||
const isTree = resource.resource_type === 'tree';
|
||||
const geometry = isTree
|
||||
? new THREE.ConeGeometry(0.45, 1.1, 8)
|
||||
: new THREE.BoxGeometry(0.9, 0.75, 0.9);
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: isTree ? 0x2e8b57 : 0x6f7b86,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(resource.position.x, isTree ? 0.55 : 0.38, resource.position.y);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
scene.add(mesh);
|
||||
resourceObjects[resource.id] = mesh;
|
||||
}
|
||||
@@ -822,6 +865,7 @@
|
||||
if (!(gameState.players || {})[pid]) {
|
||||
scene.remove(playerSpheres[pid].mesh);
|
||||
scene.remove(playerSpheres[pid].sprite);
|
||||
if (playerSpheres[pid].northArrow) scene.remove(playerSpheres[pid].northArrow);
|
||||
delete playerSpheres[pid];
|
||||
}
|
||||
}
|
||||
@@ -873,7 +917,20 @@
|
||||
if (!resources[rid]) { scene.remove(resourceObjects[rid]); delete resourceObjects[rid]; }
|
||||
}
|
||||
|
||||
focusCameraOnPlayer(false);
|
||||
const chests = gameState.chests || {};
|
||||
for (const chest of Object.values(chests)) {
|
||||
if (!chestObjects[chest.id]) createChestMesh(chest);
|
||||
}
|
||||
for (const cid in chestObjects) {
|
||||
if (!chests[cid]) {
|
||||
scene.remove(chestObjects[cid]);
|
||||
delete chestObjects[cid];
|
||||
}
|
||||
}
|
||||
|
||||
if (cameraAutoFollow) {
|
||||
focusCameraOnPlayer(false);
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
@@ -981,12 +1038,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function performAction(actionType, targetId = null, tx = null, ty = null, structureType = null) {
|
||||
async function performAction(actionType, targetId = null, tx = null, ty = null, structureType = null, slotIndex = null) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/player/${currentPlayerId}/action`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action_type: actionType, target_id: targetId, tx, ty, structure_type: structureType }),
|
||||
body: JSON.stringify({ action_type: actionType, target_id: targetId, tx, ty, structure_type: structureType, slot_index: slotIndex }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
@@ -1017,7 +1074,10 @@
|
||||
function bindCamBtn(id, az, pl, zm = 0) {
|
||||
const btn = document.getElementById(id);
|
||||
let interval = null;
|
||||
const fire = () => rotateCameraBy(az, pl, zm);
|
||||
const fire = () => {
|
||||
cameraAutoFollow = false;
|
||||
rotateCameraBy(az, pl, zm);
|
||||
};
|
||||
btn.addEventListener('mousedown', () => { fire(); interval = setInterval(fire, 80); });
|
||||
btn.addEventListener('touchstart', (e) => { e.preventDefault(); fire(); interval = setInterval(fire, 80); }, { passive: false });
|
||||
const stop = () => clearInterval(interval);
|
||||
@@ -1031,7 +1091,10 @@
|
||||
bindCamBtn('camDown', 0, PL);
|
||||
bindCamBtn('camZoomIn', 0, 0, -ZM);
|
||||
bindCamBtn('camZoomOut', 0, 0, ZM);
|
||||
document.getElementById('camCenter').onclick = () => focusCameraOnPlayer(true);
|
||||
document.getElementById('camCenter').onclick = () => {
|
||||
cameraAutoFollow = true;
|
||||
focusCameraOnPlayer(true);
|
||||
};
|
||||
|
||||
if (!cameraKeyBindingsAttached) {
|
||||
cameraKeyBindingsAttached = true;
|
||||
@@ -1045,6 +1108,7 @@
|
||||
|
||||
if (["arrowleft", "arrowright", "arrowup", "arrowdown", "z", "q", "s", "d"].includes(key)) {
|
||||
event.preventDefault();
|
||||
cameraAutoFollow = false;
|
||||
}
|
||||
|
||||
if (key === 'arrowleft') rotateCameraBy(rotationStep, 0, 0);
|
||||
@@ -1059,14 +1123,34 @@
|
||||
}
|
||||
|
||||
document.getElementById('attackBtn').onclick = () => {
|
||||
if (gameState && gameState.boss) {
|
||||
performAction('attack', gameState.boss.id);
|
||||
} else if (gameState && Object.keys(gameState.monsters).length > 0) {
|
||||
const targetId = Object.keys(gameState.monsters)[0];
|
||||
performAction('attack', targetId);
|
||||
} else {
|
||||
if (!gameState || !currentPlayer) {
|
||||
showMessage('No targets available');
|
||||
return;
|
||||
}
|
||||
|
||||
const range = currentPlayer.attack_range || 4;
|
||||
const candidates = [];
|
||||
if (gameState.boss) candidates.push(gameState.boss);
|
||||
for (const m of Object.values(gameState.monsters || {})) candidates.push(m);
|
||||
for (const s of Object.values(gameState.structures || {})) {
|
||||
if (s.structure_type === 'block') candidates.push(s);
|
||||
}
|
||||
|
||||
let nearest = null;
|
||||
let nearestDist = Infinity;
|
||||
for (const target of candidates) {
|
||||
const dist = Math.hypot(target.position.x - currentPlayer.position.x, target.position.y - currentPlayer.position.y);
|
||||
if (dist <= range && dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearest) {
|
||||
showMessage(`No target in range (${range.toFixed(1)})`);
|
||||
return;
|
||||
}
|
||||
performAction('attack', nearest.id);
|
||||
};
|
||||
|
||||
document.getElementById('gatherBtn').onclick = () => {
|
||||
@@ -1084,6 +1168,30 @@
|
||||
document.getElementById('buildTowerBtn').onclick = () => {
|
||||
performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'guard_tower');
|
||||
};
|
||||
|
||||
document.getElementById('buildBlockBtn').onclick = () => {
|
||||
performAction('build', null, null, null, 'block');
|
||||
};
|
||||
|
||||
document.getElementById('lootBtn').onclick = async () => {
|
||||
const data = await performAction('loot');
|
||||
if (data && data.success && typeof data.slot_index === 'number') {
|
||||
await performAction('equip', null, null, null, null, data.slot_index);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('equipBtn').onclick = () => {
|
||||
if (!currentPlayer || !Array.isArray(currentPlayer.weapon_slots)) {
|
||||
showMessage('No weapon slots available');
|
||||
return;
|
||||
}
|
||||
const slot = currentPlayer.weapon_slots.findIndex(w => w);
|
||||
if (slot < 0) {
|
||||
showMessage('No weapon to equip');
|
||||
return;
|
||||
}
|
||||
performAction('equip', null, null, null, null, slot);
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== LOGIN FORM ====================
|
||||
|
||||
Reference in New Issue
Block a user