From 163577aab28df574e040bd0ddb8297b466a1ea9b Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 29 Jun 2025 13:58:46 +0200 Subject: [PATCH 1/2] Base structure works for track editing --- racetrack/editor/editor.js | 206 +++++++++++++++++++++++++++++++++++ racetrack/editor/index.html | 35 ++++++ racetrack/index.html | 20 ++++ racetrack/script.js | 124 +++++++++++++++++++++ racetrack/serve_app.sh | 3 + racetrack/style.css | 70 ++++++++++++ racetrack/tracks/oval.png | Bin 0 -> 238 bytes racetrack/tracks/s-track.png | Bin 0 -> 258 bytes racetrack/tracks/tracks.json | 4 + 9 files changed, 462 insertions(+) create mode 100644 racetrack/editor/editor.js create mode 100644 racetrack/editor/index.html create mode 100644 racetrack/index.html create mode 100644 racetrack/script.js create mode 100755 racetrack/serve_app.sh create mode 100644 racetrack/style.css create mode 100644 racetrack/tracks/oval.png create mode 100644 racetrack/tracks/s-track.png create mode 100644 racetrack/tracks/tracks.json diff --git a/racetrack/editor/editor.js b/racetrack/editor/editor.js new file mode 100644 index 0000000..2fba1ba --- /dev/null +++ b/racetrack/editor/editor.js @@ -0,0 +1,206 @@ +const canvas = document.getElementById('editorCanvas'); +const ctx = canvas.getContext('2d'); +const cellSize = 20; + +const COLORS = { + road: [255, 255, 255], + wall: [0, 0, 255], + start: [0, 255, 255], + finish: [0, 0, 0], + gravel: [255, 255, 0], + curb: [127, 127, 127] +}; + +let currentColor = 'road'; +let grid = []; +let cols = 40; +let rows = 30; + +function triggerFileInput() { + document.getElementById('fileInput').click(); +} +document.getElementById('fileInput').addEventListener('change', loadFromFile); + +function loadFromFile() { + const input = document.getElementById('fileInput'); + if (!input.files.length) return; + + const file = input.files[0]; + const reader = new FileReader(); + + reader.onload = function(e) { + const img = new Image(); + img.onload = function() { + const offCanvas = document.createElement('canvas'); + offCanvas.width = img.width; + offCanvas.height = img.height; + const offCtx = offCanvas.getContext('2d'); + offCtx.drawImage(img, 0, 0); + + const imageData = offCtx.getImageData(0, 0, img.width, img.height).data; + cols = img.width; + rows = img.height; + canvas.width = cols * cellSize; + canvas.height = rows * cellSize; + + grid = Array.from({length: rows}, () => Array(cols).fill('wall')); + + const rgbToKey = (r, g, b) => { + for (let key in COLORS) { + const [cr, cg, cb] = COLORS[key]; + if (r === cr && g === cg && b === cb) return key; + } + return 'wall'; // default fallback + }; + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const i = (y * cols + x) * 4; + const r = imageData[i]; + const g = imageData[i + 1]; + const b = imageData[i + 2]; + grid[y][x] = rgbToKey(r, g, b); + } + } + + drawPalette(); + drawGrid(); + }; + + img.src = e.target.result; + }; + + reader.readAsDataURL(file); +} + + +function resetGrid() { + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + grid[y][x] = 'wall'; + } + } + drawGrid(); +} + +function initGridEditor(width, height) { + cols = width; + rows = height; + canvas.width = cols * cellSize; + canvas.height = rows * cellSize; + + grid = Array.from({length: rows}, () => Array(cols).fill('wall')); + + drawPalette(); + drawGrid(); +} + +function drawPalette() { + const paletteDiv = document.getElementById('palette'); + paletteDiv.innerHTML = ''; // Clear existing + + for (let key in COLORS) { + const div = document.createElement('div'); + div.classList.add('palette-color'); + if (key === currentColor) div.classList.add('selected'); + div.dataset.color = key; + + const box = document.createElement('div'); + box.classList.add('palette-color-box'); + box.style.backgroundColor = `rgb(${COLORS[key].join(',')})`; + + div.appendChild(box); + div.appendChild(document.createTextNode(key)); + + div.onclick = () => { + document.querySelectorAll('.palette-color') + .forEach(el => el.classList.remove('selected')); + div.classList.add('selected'); + currentColor = key; + }; + + paletteDiv.appendChild(div); + } +} + +function goBackToGame() { + window.location.href = '../index.html'; +} + +function drawGrid() { + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const color = COLORS[grid[y][x]]; + ctx.fillStyle = `rgb(${color.join(',')})`; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + ctx.strokeStyle = '#ccc'; + ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } +} + +let isDrawing = false; + +function drawCellAtMouse(e) { + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + if (x >= 0 && x < cols && y >= 0 && y < rows) { + grid[y][x] = currentColor; + drawGrid(); + } +} + +canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + drawCellAtMouse(e); +}); + +canvas.addEventListener('mousemove', (e) => { + if (isDrawing) { + drawCellAtMouse(e); + } +}); + +canvas.addEventListener('mouseup', () => { + isDrawing = false; +}); + +canvas.addEventListener('mouseleave', () => { + isDrawing = false; +}); + +function exportImage() { + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = cols; + exportCanvas.height = rows; + const exportCtx = exportCanvas.getContext('2d'); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const [r, g, b] = COLORS[grid[y][x]]; + exportCtx.fillStyle = `rgb(${r},${g},${b})`; + exportCtx.fillRect(x, y, 1, 1); + } + } + + const link = document.createElement('a'); + link.download = 'track.png'; + link.href = exportCanvas.toDataURL(); + link.click(); +} + +// Form to set canvas/grid size +document.getElementById('sizeForm').addEventListener('submit', (e) => { + e.preventDefault(); + const width = parseInt(document.getElementById('colsInput').value); + const height = parseInt(document.getElementById('rowsInput').value); + if (width > 0 && height > 0) { + initGridEditor(width, height); + } +}); + +// Start default grid on load +window.onload = () => { + initGridEditor(40, 30); +}; diff --git a/racetrack/editor/index.html b/racetrack/editor/index.html new file mode 100644 index 0000000..58160fe --- /dev/null +++ b/racetrack/editor/index.html @@ -0,0 +1,35 @@ + + + + + Track Editor + + + +

Racetrack Track Editor

+ + + + + +
+ + + +
+
+ +
+ Width (cells): + Height (cells): + +
+ +
+ + + + + + + diff --git a/racetrack/index.html b/racetrack/index.html new file mode 100644 index 0000000..05d6db3 --- /dev/null +++ b/racetrack/index.html @@ -0,0 +1,20 @@ + + + + + Racetrack Game + + + +

Racetrack Game

+ + + + + + + + + + + diff --git a/racetrack/script.js b/racetrack/script.js new file mode 100644 index 0000000..6c076d8 --- /dev/null +++ b/racetrack/script.js @@ -0,0 +1,124 @@ +const canvas = document.getElementById('trackCanvas'); +const ctx = canvas.getContext('2d'); +const cellSize = 20; +let currentTrackData = null; + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + +function drawGrid(trackWidth, trackHeight) { + ctx.strokeStyle = '#eee'; + for (let x = 0; x <= trackWidth; x++) { + ctx.beginPath(); + ctx.moveTo(x * cellSize, 0); + ctx.lineTo(x * cellSize, trackHeight * cellSize); + ctx.stroke(); + } + + for (let y = 0; y <= trackHeight; y++) { + ctx.beginPath(); + ctx.moveTo(0, y * cellSize); + ctx.lineTo(trackWidth * cellSize, y * cellSize); + ctx.stroke(); + } +} + +function drawCellByColor(x, y, r, g, b) { + const colorHex = (r << 16) | (g << 8) | b; + + let fill = null; + switch (colorHex) { + case 0x00ffff: + fill = 'cyan'; + break; // Start + case 0xffffff: + fill = 'white'; + break; // Road + case 0x7f7f7f: + fill = 'gray'; + break; // Curb + case 0xffff00: + fill = 'yellow'; + break; // Gravel + case 0x0000ff: + fill = 'blue'; + break; // Wall + case 0x000000: + fill = 'black'; + break; // Finish + default: + return; + } + + ctx.fillStyle = fill; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); +} + +function loadTrack(trackName) { + const image = new Image(); + image.src = `tracks/${trackName}`; + image.onload = () => { + const width = image.width; + const height = image.height; + + canvas.width = width * cellSize; + canvas.height = height * cellSize; + + const offCanvas = document.createElement('canvas'); + offCanvas.width = width; + offCanvas.height = height; + const offCtx = offCanvas.getContext('2d'); + offCtx.drawImage(image, 0, 0); + + const imageData = offCtx.getImageData(0, 0, width, height).data; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const r = imageData[index]; + const g = imageData[index + 1]; + const b = imageData[index + 2]; + drawCellByColor(x, y, r, g, b); + } + } + + drawGrid(width, height); // draw only over track + }; +} + +// Load tracks from JSON +function populateTrackList() { + fetch('tracks/tracks.json').then(res => res.json()).then(tracks => { + const select = document.getElementById('trackSelect'); + select.innerHTML = ''; + tracks.forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name.replace('.png', ''); + select.appendChild(option); + }); + + // Load first track by default + if (tracks.length > 0) loadTrack(tracks[0]); + + select.addEventListener('change', e => { + loadTrack(e.target.value); + }); + }); +} + +function openEditor() { + window.location.href = 'editor/index.html'; +} + +// Initialize +window.onload = () => { + resizeCanvas(); + populateTrackList(); +}; + +window.addEventListener('resize', resizeCanvas); diff --git a/racetrack/serve_app.sh b/racetrack/serve_app.sh new file mode 100755 index 0000000..ce0d837 --- /dev/null +++ b/racetrack/serve_app.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python3 -m http.server 8000 \ No newline at end of file diff --git a/racetrack/style.css b/racetrack/style.css new file mode 100644 index 0000000..255374b --- /dev/null +++ b/racetrack/style.css @@ -0,0 +1,70 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + background: #f7f7f7; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; +} + +h1 { + margin-top: 20px; + text-align: center; +} + +form, #palette, #trackSelect, button { + margin: 10px; +} + +canvas { + border: 1px solid #333; + background: white; + display: block; + margin: 10px auto; + max-width: 100%; +} + +#palette { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.palette-color { + display: flex; + align-items: center; + border: 2px solid transparent; + margin: 5px; + cursor: pointer; + padding: 2px 6px; + background: #fff; + font-size: 14px; +} + +.palette-color-box { + width: 20px; + height: 20px; + margin-right: 6px; +} + +.palette-color.selected { + border-color: black; +} + +button, select, input[type="number"] { + padding: 6px 12px; + font-size: 14px; + border-radius: 4px; + border: 1px solid #ccc; +} + +button:hover { + background-color: #eee; + cursor: pointer; +} diff --git a/racetrack/tracks/oval.png b/racetrack/tracks/oval.png new file mode 100644 index 0000000000000000000000000000000000000000..94d699a599830b461845980615d7424404443ba5 GIT binary patch literal 238 zcmVX0002BNklI>M8HVz57x?6*ur-NEvtv=OGh_ zdT+iymsw5_9S02XN)HZz+KX3rF_RKImo72$4Q4U({HQ8Nzr{OH)G`owGf9MdH|F}! o)J{J7ngpsDW`dcZnt;0g0ZH6*X0002VNkl5<}vfU1DyCdh)feF$7`v$n1|APbj3CCGmPGNGuAt*L0LAiAk^PnQF> zLWP8Is)7!PSdhvDGnScSX7Sd5OnUPUf*gucka~pxeR~+Nr?0kJm#KoifUWXk77}S8 z&1w{`c6hpzXm->f8MtSDc~Regz*Aq;OsJ%>4mhf!EBJW@PgYrx Date: Sun, 29 Jun 2025 14:49:28 +0200 Subject: [PATCH 2/2] Game logic is here --- racetrack/editor/editor.js | 4 +- racetrack/editor/index.html | 13 +- racetrack/index.html | 12 +- racetrack/script.js | 374 +++++++++++++++++++++++++++++----- racetrack/style.css | 2 +- racetrack/tracks/ovalsand.png | Bin 0 -> 287 bytes racetrack/tracks/tracks.json | 1 + 7 files changed, 347 insertions(+), 59 deletions(-) create mode 100644 racetrack/tracks/ovalsand.png diff --git a/racetrack/editor/editor.js b/racetrack/editor/editor.js index 2fba1ba..1277d42 100644 --- a/racetrack/editor/editor.js +++ b/racetrack/editor/editor.js @@ -77,7 +77,7 @@ function loadFromFile() { function resetGrid() { for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { - grid[y][x] = 'wall'; + grid[y][x] = 'road'; } } drawGrid(); @@ -89,7 +89,7 @@ function initGridEditor(width, height) { canvas.width = cols * cellSize; canvas.height = rows * cellSize; - grid = Array.from({length: rows}, () => Array(cols).fill('wall')); + grid = Array.from({length: rows}, () => Array(cols).fill('road')); drawPalette(); drawGrid(); diff --git a/racetrack/editor/index.html b/racetrack/editor/index.html index 58160fe..661a577 100644 --- a/racetrack/editor/index.html +++ b/racetrack/editor/index.html @@ -2,32 +2,27 @@ - Track Editor + Racetrack Editor -

Racetrack Track Editor

+

Racetrack Editor

-
- - + -
-
Width (cells): Height (cells): - +
- diff --git a/racetrack/index.html b/racetrack/index.html index 05d6db3..d6fa491 100644 --- a/racetrack/index.html +++ b/racetrack/index.html @@ -10,10 +10,20 @@

Racetrack Game

+ + + + + - + diff --git a/racetrack/script.js b/racetrack/script.js index 6c076d8..e92a4d9 100644 --- a/racetrack/script.js +++ b/racetrack/script.js @@ -1,22 +1,140 @@ const canvas = document.getElementById('trackCanvas'); const ctx = canvas.getContext('2d'); const cellSize = 20; -let currentTrackData = null; -function resizeCanvas() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; +let trackImageData = null; +let trackWidth = 0; +let trackHeight = 0; + +let players = []; +let currentPlayerIndex = 0; +let moveTrails = []; +let hoverTarget = null; +let legalMoves = []; +let selectedMoveIndex = 0; +let previousMovesKey = ''; +const PLAYER_COLORS = ['red', 'blue', 'green', 'orange']; + +function setupPlayers() { + const count = parseInt(document.getElementById('playerCount').value); + players = []; + moveTrails = []; + currentPlayerIndex = 0; + + const starts = []; + + for (let y = 0; y < trackHeight; y++) { + for (let x = 0; x < trackWidth; x++) { + const color = getPixelColor(x, y); + if (color === 'start') starts.push({x, y}); + } + } + + if (starts.length < count) { + alert('Not enough start tiles!'); + return; + } + + for (let i = 0; i < count; i++) { + players.push({ + x: starts[i].x, + y: starts[i].y, + vx: 0, + vy: 0, + alive: true, + color: PLAYER_COLORS[i] + }); + moveTrails.push([]); + } + + redrawGame(); +} + +function getPixelColor(x, y) { + if (!trackImageData || x < 0 || y < 0 || x >= trackWidth || y >= trackHeight) + return 'wall'; + const i = (y * trackWidth + x) * 4; + const r = trackImageData[i], g = trackImageData[i + 1], + b = trackImageData[i + 2]; + const hex = (r << 16) | (g << 8) | b; + switch (hex) { + case 0x0000ff: + return 'wall'; + case 0x7f7f7f: + return 'curb'; + case 0xffff00: + return 'gravel'; + case 0x000000: + return 'finish'; + case 0x00ffff: + return 'start'; + default: + return 'road'; + } +} + +function getLegalMoves() { + const p = players[currentPlayerIndex]; + const moves = []; + const surface = getPixelColor(p.x, p.y); + + for (let ax = -1; ax <= 1; ax++) { + for (let ay = -1; ay <= 1; ay++) { + const vx = p.vx + ax, vy = p.vy + ay; + const x = p.x + vx, y = p.y + vy; + + if (!isInsideTrack(x, y)) continue; + + let allowed = true; + if (surface === 'curb') + allowed = (ax === 0 && ay === 0); + else if (surface === 'gravel') + allowed = Math.sqrt(vx * vx + vy * vy) <= 1.01; + + if (allowed && !checkCollision(p.x, p.y, x, y)) { + moves.push({x, y, vx, vy}); + } + } + } + + selectedMoveIndex = 0; + return moves; +} + +function isInsideTrack(x, y) { + return x >= 0 && y >= 0 && x < trackWidth && y < trackHeight; +} + +function checkCollision(x1, y1, x2, y2) { + const dx = x2 - x1, dy = y2 - y1; + const steps = Math.max(Math.abs(dx), Math.abs(dy)); + for (let i = 0; i <= steps; i++) { + const x = Math.round(x1 + dx * i / steps); + const y = Math.round(y1 + dy * i / steps); + if (getPixelColor(x, y) === 'wall') return true; + } + return false; } -function drawGrid(trackWidth, trackHeight) { - ctx.strokeStyle = '#eee'; +function checkFinishCrossed(x1, y1, x2, y2) { + const dx = x2 - x1, dy = y2 - y1; + const steps = Math.max(Math.abs(dx), Math.abs(dy)); + for (let i = 0; i <= steps; i++) { + const x = Math.round(x1 + dx * i / steps); + const y = Math.round(y1 + dy * i / steps); + if (getPixelColor(x, y) === 'finish') return true; + } + return false; +} + +function drawGrid() { + ctx.strokeStyle = '#ddd'; for (let x = 0; x <= trackWidth; x++) { ctx.beginPath(); ctx.moveTo(x * cellSize, 0); ctx.lineTo(x * cellSize, trackHeight * cellSize); ctx.stroke(); } - for (let y = 0; y <= trackHeight; y++) { ctx.beginPath(); ctx.moveTo(0, y * cellSize); @@ -25,72 +143,235 @@ function drawGrid(trackWidth, trackHeight) { } } -function drawCellByColor(x, y, r, g, b) { - const colorHex = (r << 16) | (g << 8) | b; +function drawTrack() { + for (let y = 0; y < trackHeight; y++) { + for (let x = 0; x < trackWidth; x++) { + const i = (y * trackWidth + x) * 4; + drawCellByColor( + x, y, trackImageData[i], trackImageData[i + 1], + trackImageData[i + 2]); + } + } +} +function drawCellByColor(x, y, r, g, b) { + const hex = (r << 16) | (g << 8) | b; let fill = null; - switch (colorHex) { + switch (hex) { case 0x00ffff: fill = 'cyan'; - break; // Start + break; case 0xffffff: fill = 'white'; - break; // Road + break; case 0x7f7f7f: fill = 'gray'; - break; // Curb + break; case 0xffff00: fill = 'yellow'; - break; // Gravel + break; case 0x0000ff: fill = 'blue'; - break; // Wall + break; case 0x000000: fill = 'black'; - break; // Finish + break; default: return; } - ctx.fillStyle = fill; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } -function loadTrack(trackName) { - const image = new Image(); - image.src = `tracks/${trackName}`; - image.onload = () => { - const width = image.width; - const height = image.height; +function drawTrail() { + players.forEach((p, i) => { + if (!p.alive) return; + const trail = moveTrails[i]; + if (trail.length === 0) return; + ctx.strokeStyle = p.color; + ctx.beginPath(); + ctx.moveTo((trail[0].x + 0.5) * cellSize, (trail[0].y + 0.5) * cellSize); + trail.forEach( + t => ctx.lineTo((t.x + 0.5) * cellSize, (t.y + 0.5) * cellSize)); + ctx.lineTo((p.x + 0.5) * cellSize, (p.y + 0.5) * cellSize); + ctx.stroke(); + }); +} - canvas.width = width * cellSize; - canvas.height = height * cellSize; +function drawPlayers() { + players.forEach((p, i) => { + if (!p.alive) return; + ctx.fillStyle = (i === currentPlayerIndex) ? + (hoverTarget ? rgba(p.color, 0.5) : p.color) : + rgba(p.color, 0.4); + const drawX = hoverTarget && i === currentPlayerIndex ? hoverTarget.x : p.x; + const drawY = hoverTarget && i === currentPlayerIndex ? hoverTarget.y : p.y; + ctx.beginPath(); + ctx.arc( + (drawX + 0.5) * cellSize, (drawY + 0.5) * cellSize, cellSize / 3, 0, + 2 * Math.PI); + ctx.fill(); + }); +} - const offCanvas = document.createElement('canvas'); - offCanvas.width = width; - offCanvas.height = height; - const offCtx = offCanvas.getContext('2d'); - offCtx.drawImage(image, 0, 0); +function drawLegalMoves() { + const p = players[currentPlayerIndex]; + const playerColor = p.color; + const validSet = new Set(legalMoves.map(m => `${m.x},${m.y}`)); - const imageData = offCtx.getImageData(0, 0, width, height).data; + for (let ax = -1; ax <= 1; ax++) { + for (let ay = -1; ay <= 1; ay++) { + const vx = p.vx + ax, vy = p.vy + ay; + const x = p.x + vx, y = p.y + vy; + if (!isInsideTrack(x, y)) continue; - ctx.clearRect(0, 0, canvas.width, canvas.height); + const key = `${x},${y}`; + const isValid = validSet.has(key); + const isSelected = legalMoves[selectedMoveIndex] && + legalMoves[selectedMoveIndex].x === x && + legalMoves[selectedMoveIndex].y === y; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const index = (y * width + x) * 4; - const r = imageData[index]; - const g = imageData[index + 1]; - const b = imageData[index + 2]; - drawCellByColor(x, y, r, g, b); - } + ctx.strokeStyle = isSelected ? playerColor : + isValid ? playerColor : + 'gray'; + + ctx.lineWidth = isSelected ? 3 : 1; + ctx.setLineDash(isValid ? [] : [3, 3]); + ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); + ctx.setLineDash([]); } + } +} + + +function drawHoverPreview() { + if (!hoverTarget) return; + + const p = players[currentPlayerIndex]; + ctx.setLineDash([4, 4]); + ctx.strokeStyle = p.color; + ctx.beginPath(); + ctx.moveTo((p.x + 0.5) * cellSize, (p.y + 0.5) * cellSize); + ctx.lineTo( + (hoverTarget.x + 0.5) * cellSize, (hoverTarget.y + 0.5) * cellSize); + ctx.stroke(); + ctx.setLineDash([]); +} + + +function rgba(color, alpha = 1) { + const COLOR_MAP = { + red: [255, 0, 0], + blue: [0, 0, 255], + green: [0, 128, 0], + orange: [255, 165, 0] + }; + + const rgb = COLOR_MAP[color] || [0, 0, 0]; + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`; +} + + +function redrawGame() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + legalMoves = getLegalMoves(); + drawTrack(); + drawGrid(); + drawTrail(); + drawHoverPreview(); + drawPlayers(); + drawLegalMoves(); + + if (players[currentPlayerIndex].alive && legalMoves.length === 0) { + alert(`Player ${currentPlayerIndex + 1} crashed!`); + players[currentPlayerIndex].alive = false; + advanceTurn(); + redrawGame(); + } +} + +function advanceTurn() { + const alive = players.filter(p => p.alive); + if (alive.length <= 1) return; // Don't rotate if solo or winner + + do { + currentPlayerIndex = (currentPlayerIndex + 1) % players.length; + } while (!players[currentPlayerIndex].alive); + hoverTarget = null; +} + + +canvas.addEventListener('mousemove', (e) => { + const p = players[currentPlayerIndex]; + if (!p || !p.alive) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + const match = legalMoves.find(m => m.x === x && m.y === y); + hoverTarget = match ? {x, y} : null; + redrawGame(); +}); + +canvas.addEventListener('click', (e) => { + const p = players[currentPlayerIndex]; + if (!p || !p.alive) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + const move = legalMoves.find(m => m.x === x && m.y === y); + if (move) makeMove(p, move); +}); + +document.addEventListener('keydown', (e) => { + if (legalMoves.length === 0) return; + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + selectedMoveIndex = (selectedMoveIndex + 1) % legalMoves.length; + redrawGame(); + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + selectedMoveIndex = + (selectedMoveIndex - 1 + legalMoves.length) % legalMoves.length; + redrawGame(); + } else if (e.key === 'Enter') { + const move = legalMoves[selectedMoveIndex]; + if (move) makeMove(players[currentPlayerIndex], move); + } +}); + +function makeMove(player, move) { + moveTrails[currentPlayerIndex].push({x: player.x, y: player.y}); + const prevX = player.x, prevY = player.y; + player.x = move.x; + player.y = move.y; + player.vx = move.vx; + player.vy = move.vy; + if (checkFinishCrossed(prevX, prevY, move.x, move.y)) { + alert(`Player ${currentPlayerIndex + 1} wins!`); + players.forEach(p => p.alive = false); + } else { + advanceTurn(); + } + redrawGame(); +} - drawGrid(width, height); // draw only over track +function loadTrack(trackName) { + const image = new Image(); + image.src = `tracks/${trackName}`; + image.onload = () => { + const width = image.width, height = image.height; + canvas.width = width * cellSize; + canvas.height = height * cellSize; + trackWidth = width; + trackHeight = height; + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.drawImage(image, 0, 0); + trackImageData = tempCtx.getImageData(0, 0, width, height).data; + setupPlayers(); // auto-start }; } -// Load tracks from JSON function populateTrackList() { fetch('tracks/tracks.json').then(res => res.json()).then(tracks => { const select = document.getElementById('trackSelect'); @@ -101,10 +382,7 @@ function populateTrackList() { option.textContent = name.replace('.png', ''); select.appendChild(option); }); - - // Load first track by default if (tracks.length > 0) loadTrack(tracks[0]); - select.addEventListener('change', e => { loadTrack(e.target.value); }); @@ -115,10 +393,14 @@ function openEditor() { window.location.href = 'editor/index.html'; } -// Initialize window.onload = () => { resizeCanvas(); populateTrackList(); }; +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + window.addEventListener('resize', resizeCanvas); diff --git a/racetrack/style.css b/racetrack/style.css index 255374b..66f45c8 100644 --- a/racetrack/style.css +++ b/racetrack/style.css @@ -14,7 +14,7 @@ body { } h1 { - margin-top: 20px; + margin-top: 10px; text-align: center; } diff --git a/racetrack/tracks/ovalsand.png b/racetrack/tracks/ovalsand.png new file mode 100644 index 0000000000000000000000000000000000000000..18e739399f736b1e6ec50b8ca1885f69c1bb293f GIT binary patch literal 287 zcmV+)0pR|LP)X0002yNkl0eH>%{2 z)#C3GQQMuZHUlOXks)_}2S5}qZ;Xco0lna4$PH-A%+_X5!Ly9mAf)IiIao>w208T< zV`!tV%@(Hx?ZtY8!OzP**|isY!(C0V&I*s7tOqsTW=autI<9)sBa*wc{^yN)wUW2q lBu00ph3|JUD(&MHd;tjbqvK4>FQNbd002ovPDHLkV1kGFd-MPR literal 0 HcmV?d00001 diff --git a/racetrack/tracks/tracks.json b/racetrack/tracks/tracks.json index b292442..f9bae1f 100644 --- a/racetrack/tracks/tracks.json +++ b/racetrack/tracks/tracks.json @@ -1,4 +1,5 @@ [ "oval.png", + "ovalsand.png", "s-track.png" ]