跑外卖 · 进阶版
by CyberFox46820 lines24.3 KB
<!DOCTYPE html>
<html lang="zh-CN">
<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>跑外卖 · 进阶版</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#e6edf2;
font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}
#game{display:block;width:100%;height:100%;touch-action:none}
#hud{position:fixed;inset:0;pointer-events:none;padding:max(12px,env(safe-area-inset-top)) 14px 0;z-index:4}
.topbar{display:flex;gap:8px;justify-content:space-between}
.pill{background:rgba(255,255,255,.9);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
border-radius:999px;padding:8px 15px;font-size:15px;font-weight:700;color:#334155;
box-shadow:0 2px 10px rgba(0,0,0,.06);white-space:nowrap}
#money{color:#ea580c}
#hearts{font-size:13px;letter-spacing:1px}
#orders{position:fixed;left:50%;transform:translateX(-50%);
bottom:max(16px,env(safe-area-inset-bottom));width:min(92%,420px);
display:flex;flex-direction:column;gap:8px;pointer-events:none;z-index:5}
.order-card{background:rgba(255,255,255,.95);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
border-radius:14px;padding:10px 14px;box-shadow:0 4px 16px rgba(0,0,0,.08);
pointer-events:auto;cursor:pointer;transition:transform .1s}
.order-card:active{transform:scale(.98)}
.order-head{font-size:11px;color:#94a3b8;font-weight:700;margin-bottom:3px}
.order-phase{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:6px}
.order-bar{height:5px;border-radius:3px;background:#e2e8f0;overflow:hidden}
.order-bar>div{height:100%;border-radius:3px;transition:width .15s linear}
.order-time{font-size:11px;color:#94a3b8;text-align:right;margin-top:3px}
.overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(230,237,242,.95);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
z-index:10;padding:24px}
.card{background:#fff;border-radius:24px;padding:30px 24px;text-align:center;
max-width:340px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,.12)}
.card h1{margin:0 0 12px;font-size:24px;color:#1e293b}
.card p{margin:6px 0;color:#64748b;font-size:14px;line-height:1.7}
.tip{background:#f1f5f9;border-radius:12px;padding:12px;margin:14px 0!important;font-size:13px;text-align:left}
.stat{font-size:15px;color:#475569;margin:4px 0}
.stat b{color:#ea580c;font-size:20px}
button{border:0;background:#ff6b35;color:#fff;font-size:17px;font-weight:700;
padding:14px 44px;border-radius:16px;cursor:pointer;margin-top:20px;
box-shadow:0 8px 20px rgba(255,107,53,.35);transition:transform .1s}
button:active{transform:scale(.97)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
<div class="topbar">
<div class="pill">收入 <span id="money">¥0</span></div>
<div class="pill" id="hearts">❤️❤️❤️</div>
<div class="pill"><span id="done">0</span> 单</div>
</div>
</div>
<div id="orders"></div>
<div id="start" class="overlay">
<div class="card">
<h1>🛵 跑外卖 · 进阶版</h1>
<p>点击地图移动,自动寻路</p>
<p class="tip">
🟡 黄点:商家 · 取餐<br>
🟣 紫点:便利店 · 帮买<br>
🔵 蓝点:顾客 · 送达<br>
🟨 黄门:小区唯一入口<br>
<br>点击底部订单卡片可自动导航
</p>
<p style="font-size:13px;color:#94a3b8">超时 3 次收工</p>
<button id="btnStart">开始接单</button>
</div>
</div>
<div id="over" class="overlay" style="display:none">
<div class="card">
<h1>收工了</h1>
<div id="overBody"></div>
<button id="btnAgain">再来一单</button>
</div>
</div>
<script>
(function () {
'use strict';
// ===== 常量 =====
var GRID = 26;
var TILE = 40;
var WORLD = GRID * TILE;
var SPEED = 4.5;
var T_ROAD = 0, T_BUILDING = 1, T_WALL = 2, T_GATE = 3, T_YARD = 4;
var SHOP_NAMES = ['兰州拉面','沙县小吃','黄焖鸡','蜜雪冰城','华莱士','麻辣烫',
'煲仔饭','猪脚饭','螺蛳粉','牛肉面','炸鸡汉堡','小碗菜'];
var ERRAND_ITEMS = ['一包烟','一瓶水','一袋盐','两瓶啤酒','一包纸巾','一盒鸡蛋'];
// ===== DOM =====
var cvs = document.getElementById('game');
var ctx = cvs.getContext('2d');
var $money = document.getElementById('money');
var $hearts = document.getElementById('hearts');
var $done = document.getElementById('done');
var $orders = document.getElementById('orders');
var $start = document.getElementById('start');
var $over = document.getElementById('over');
var $overBody = document.getElementById('overBody');
var $btnStart = document.getElementById('btnStart');
var $btnAgain = document.getElementById('btnAgain');
// ===== 尺寸 =====
var W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
cvs.width = Math.round(W * DPR);
cvs.height = Math.round(H * DPR);
cvs.style.width = W + 'px';
cvs.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', function(){
resize();
if (state === 'playing') updateCamera(1);
});
// ===== 状态 =====
var map = [];
var compounds = [];
var shops = [];
var player = { x: 1, y: 1, path: [] };
var cam = { x: 0, y: 0 };
var orders = [];
var money = 0, hearts = 3, done = 0, combo = 0;
var state = 'ready';
var floaters = [];
var orderIdCounter = 0;
var needNewOrder = false;
var lastUIUpdate = 0;
var lastT = 0;
// ===== 工具 =====
function randInt(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
function shuffle(a) {
for (var i = a.length - 1; i > 0; i--) {
var j = (Math.random() * (i + 1)) | 0;
var t = a[i]; a[i] = a[j]; a[j] = t;
}
return a;
}
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); }
function isWalk(x, y) {
if (x < 0 || y < 0 || x >= GRID || y >= GRID) return false;
var t = map[y][x];
return t === T_ROAD || t === T_GATE || t === T_YARD;
}
function nearestWalkable(x, y) {
if (isWalk(x, y)) return { x: x, y: y };
for (var r = 1; r <= 10; r++) {
for (var dy = -r; dy <= r; dy++) {
for (var dx = -r; dx <= r; dx++) {
if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue;
var nx = x + dx, ny = y + dy;
if (isWalk(nx, ny)) return { x: nx, y: ny };
}
}
}
return null;
}
function nearCompound(x, y) {
for (var i = 0; i < compounds.length; i++) {
var c = compounds[i];
if (x >= c.x0 - 1 && x <= c.x0 + 4 && y >= c.y0 - 1 && y <= c.y0 + 4) return true;
}
return false;
}
// ===== A* 寻路 =====
function findPath(sx, sy, tx, ty) {
var start = nearestWalkable(Math.round(sx), Math.round(sy));
var goal = nearestWalkable(Math.round(tx), Math.round(ty));
if (!start || !goal) return [];
if (start.x === goal.x && start.y === goal.y) return [goal];
var open = [{ x: start.x, y: start.y, g: 0, f: 0, parent: null }];
var closed = {};
var best = {};
var sk = start.y * GRID + start.x;
best[sk] = open[0];
var iter = 0;
while (open.length > 0 && iter++ < 8000) {
var bi = 0;
for (var i = 1; i < open.length; i++) {
if (open[i].f < open[bi].f) bi = i;
}
var node = open.splice(bi, 1)[0];
var k = node.y * GRID + node.x;
if (closed[k]) continue;
closed[k] = true;
if (node.x === goal.x && node.y === goal.y) {
var path = [];
var cur = node;
while (cur) { path.push({ x: cur.x, y: cur.y }); cur = cur.parent; }
path.reverse();
return path;
}
var dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (var d = 0; d < 4; d++) {
var nx = node.x + dirs[d][0];
var ny = node.y + dirs[d][1];
if (!isWalk(nx, ny)) continue;
var nk = ny * GRID + nx;
if (closed[nk]) continue;
var g = node.g + 1;
var h = Math.abs(nx - goal.x) + Math.abs(ny - goal.y);
var old = best[nk];
if (old && old.g <= g) continue;
var n = { x: nx, y: ny, g: g, f: g + h, parent: node };
best[nk] = n;
open.push(n);
}
}
return [];
}
function pathDist(a, b) {
var p = findPath(a.x, a.y, b.x, b.y);
if (p.length > 1) return p.length - 1;
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
// ===== 地图生成 =====
function generateMap() {
map = [];
for (var y = 0; y < GRID; y++) {
map[y] = [];
for (var x = 0; x < GRID; x++) map[y][x] = T_BUILDING;
}
// 道路网格
for (var y2 = 0; y2 < GRID; y2++) {
for (var x2 = 0; x2 < GRID; x2++) {
if (x2 % 5 === 0 || y2 % 5 === 0) map[y2][x2] = T_ROAD;
}
}
// 小区
compounds = [];
var blocks = [];
for (var by = 0; by < 5; by++) {
for (var bx = 0; bx < 5; bx++) blocks.push({ bx: bx, by: by });
}
shuffle(blocks);
for (var i = 0; i < 4; i++) {
var b = blocks[i];
var x0 = b.bx * 5 + 1, y0 = b.by * 5 + 1;
for (var yy = y0; yy < y0 + 4; yy++) {
for (var xx = x0; xx < x0 + 4; xx++) {
var border = (xx === x0 || xx === x0 + 3 || yy === y0 || yy === y0 + 3);
map[yy][xx] = border ? T_WALL : T_YARD;
}
}
// 门
var candidates = [];
for (var gx = x0 + 1; gx <= x0 + 2; gx++) {
candidates.push({ x: gx, y: y0 });
candidates.push({ x: gx, y: y0 + 3 });
}
for (var gy = y0 + 1; gy <= y0 + 2; gy++) {
candidates.push({ x: x0, y: gy });
candidates.push({ x: x0 + 3, y: gy });
}
shuffle(candidates);
var gates = candidates.slice(0, 2);
for (var gi = 0; gi < gates.length; gi++) {
map[gates[gi].y][gates[gi].x] = T_GATE;
}
compounds.push({ x0: x0, y0: y0, gates: gates });
}
// 便利店
shops = [];
for (var si = 0; si < 5; si++) {
var sx, sy, tries = 0;
do {
sx = randInt(1, GRID - 2);
sy = randInt(1, GRID - 2);
tries++;
} while ((map[sy][sx] !== T_ROAD || nearCompound(sx, sy)) && tries < 300);
shops.push({ x: sx, y: sy });
}
}
// ===== 订单 =====
function randomMerchantPos() {
var x, y, tries = 0;
do {
x = randInt(1, GRID - 2);
y = randInt(1, GRID - 2);
tries++;
} while ((map[y][x] !== T_ROAD || nearCompound(x, y)) && tries < 300);
return { x: x, y: y };
}
function randomCustomerPos() {
var yards = [];
for (var y = 0; y < GRID; y++) {
for (var x = 0; x < GRID; x++) {
if (map[y][x] === T_YARD) yards.push({ x: x, y: y });
}
}
if (yards.length > 0 && Math.random() < 0.75) {
return yards[randInt(0, yards.length - 1)];
}
return randomMerchantPos();
}
function calcOrderTime(o) {
var d = 0;
var from = { x: Math.round(player.x), y: Math.round(player.y) };
if (o.phase === 'pickup') {
d += pathDist(from, o.merchant);
if (o.errandShop) d += pathDist(o.merchant, o.errandShop);
var last = o.errandShop || o.merchant;
d += pathDist(last, o.customer);
} else if (o.phase === 'errand') {
d += pathDist(from, o.errandShop);
d += pathDist(o.errandShop, o.customer);
} else {
d += pathDist(from, o.customer);
}
o.totalDist = d;
o.maxTime = Math.max(12, d / SPEED * 1.8 + 8);
o.timeLeft = o.maxTime;
}
function makeOrder(type) {
var merchant = randomMerchantPos();
merchant.name = SHOP_NAMES[randInt(0, SHOP_NAMES.length - 1)];
var customer = randomCustomerPos();
var errandShop = null, errandItem = null;
if (type === 'errand' && shops.length > 0) {
errandShop = shops[randInt(0, shops.length - 1)];
errandItem = ERRAND_ITEMS[randInt(0, ERRAND_ITEMS.length - 1)];
}
var o = {
id: ++orderIdCounter,
type: type,
merchant: merchant,
merchantName: merchant.name,
customer: customer,
errandShop: errandShop,
errandItem: errandItem,
phase: 'pickup',
timeLeft: 0,
maxTime: 0,
totalDist: 0
};
calcOrderTime(o);
return o;
}
function spawnNextOrder() {
if (state !== 'playing') return;
if (orders.length > 0) return;
var isCombo = done >= 2 && Math.random() < 0.35;
var count = isCombo ? 2 : 1;
for (var i = 0; i < count; i++) {
var type = 'normal';
if (done >= 1 && Math.random() < 0.4) type = 'errand';
orders.push(makeOrder(type));
}
}
// ===== 完成 / 超时 =====
function completeOrder(o, idx) {
var ratio = o.timeLeft / o.maxTime;
var base = 8 + Math.round(o.totalDist * 0.3);
var speedBonus = Math.round(ratio * 10);
combo++;
var comboBonus = Math.min(combo, 5) * 2;
var earn = base + speedBonus + comboBonus;
money += earn;
done++;
$money.textContent = money;
$done.textContent = done;
addFloater('+¥' + earn, o.customer.x, o.customer.y, '#16a34a');
orders.splice(idx, 1);
if (orders.length === 0) needNewOrder = true;
}
function updateOrders(dt) {
for (var i = orders.length - 1; i >= 0; i--) {
var o = orders[i];
o.timeLeft -= dt;
if (o.timeLeft <= 0) {
hearts--;
combo = 0;
money = Math.max(0, money - 5);
$money.textContent = money;
updateHearts();
addFloater('超时 -¥5', player.x, player.y, '#dc2626');
orders.splice(i, 1);
if (orders.length === 0) needNewOrder = true;
if (hearts <= 0) {
gameOver();
return;
}
}
}
}
// ===== 交互判定 =====
function adjacent(x1, y1, x2, y2) {
return Math.abs(x1 - x2) <= 1 && Math.abs(y1 - y2) <= 1;
}
function checkInteractions() {
var px = Math.round(player.x);
var py = Math.round(player.y);
for (var i = orders.length - 1; i >= 0; i--) {
var o = orders[i];
if (o.phase === 'pickup' && adjacent(px, py, o.merchant.x, o.merchant.y)) {
o.phase = o.errandShop ? 'errand' : 'deliver';
addFloater('取餐成功', o.merchant.x, o.merchant.y, '#f59e0b');
calcOrderTime(o);
} else if (o.phase === 'errand' && o.errandShop && adjacent(px, py, o.errandShop.x, o.errandShop.y)) {
o.phase = 'deliver';
addFloater('买好了:' + o.errandItem, o.errandShop.x, o.errandShop.y, '#8b5cf6');
calcOrderTime(o);
} else if (o.phase === 'deliver' && adjacent(px, py, o.customer.x, o.customer.y)) {
completeOrder(o, i);
}
}
}
// ===== 玩家移动 =====
function updatePlayer(dt) {
if (player.path.length === 0) return;
var next = player.path[0];
var dx = next.x - player.x;
var dy = next.y - player.y;
var d = Math.sqrt(dx * dx + dy * dy);
var step = SPEED * dt;
if (d <= step) {
player.x = next.x;
player.y = next.y;
player.path.shift();
} else {
player.x += dx / d * step;
player.y += dy / d * step;
}
}
// ===== 相机 =====
function updateCamera(lerp) {
var px = player.x * TILE + TILE / 2;
var py = player.y * TILE + TILE / 2;
var targetX = px - W / 2;
var targetY = py - H / 2;
targetX = clamp(targetX, 0, Math.max(0, WORLD - W));
targetY = clamp(targetY, 0, Math.max(0, WORLD - H));
cam.x += (targetX - cam.x) * lerp;
cam.y += (targetY - cam.y) * lerp;
}
// ===== 浮动文字 =====
function addFloater(text, gx, gy, color) {
floaters.push({ x: gx, y: gy, text: text, color: color, life: 1.2 });
}
function updateFloaters(dt) {
for (var i = floaters.length - 1; i >= 0; i--) {
floaters[i].life -= dt;
if (floaters[i].life <= 0) floaters.splice(i, 1);
}
}
// ===== 渲染 =====
function drawTile(x, y, sx, sy) {
var t = map[y][x];
var color;
if (t === T_ROAD) {
color = '#dae3ee';
} else if (t === T_BUILDING) {
var shade = ((x * 7 + y * 13) % 5) * 5;
color = 'rgb(' + (176 - shade) + ',' + (190 - shade) + ',' + (210 - shade) + ')';
} else if (t === T_WALL) {
color = '#8794a8';
} else if (t === T_GATE) {
color = '#fbbf24';
} else if (t === T_YARD) {
color = '#b5e0a8';
} else {
color = '#fff';
}
ctx.fillStyle = color;
ctx.fillRect(sx, sy, TILE, TILE);
if (t === T_BUILDING) {
ctx.fillStyle = 'rgba(255,255,255,0.08)';
ctx.fillRect(sx + 5, sy + 5, TILE - 10, TILE - 10);
}
if (t === T_GATE) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 14px -apple-system,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('门', sx + TILE / 2, sy + TILE / 2 + 1);
}
ctx.strokeStyle = 'rgba(0,0,0,0.04)';
ctx.lineWidth = 1;
ctx.strokeRect(sx + 0.5, sy + 0.5, TILE - 1, TILE - 1);
}
function drawMarker(p, color, label, active) {
var sx = p.x * TILE - cam.x + TILE / 2;
var sy = p.y * TILE - cam.y + TILE / 2;
if (sx < -60 || sx > W + 60 || sy < -60 || sy > H + 60) return;
if (active) {
var pulse = 0.5 + 0.5 * Math.sin(Date.now() / 250);
ctx.save();
ctx.globalAlpha = 0.18 + pulse * 0.2;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(sx, sy, 22 + pulse * 10, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.beginPath();
ctx.arc(sx, sy, 13, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px -apple-system,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, sx, sy + 1);
}
function drawOrderMarkers() {
for (var i = 0; i < orders.length; i++) {
var o = orders[i];
drawMarker(o.merchant, '#f59e0b', '店', o.phase === 'pickup');
if (o.errandShop) drawMarker(o.errandShop, '#8b5cf6', '买', o.phase === 'errand');
drawMarker(o.customer, '#3b82f6', '家', o.phase === 'deliver');
}
}
function drawPlayer() {
var sx = player.x * TILE - cam.x + TILE / 2;
var sy = player.y * TILE - cam.y + TILE / 2;
if (player.path.length > 0) {
ctx.beginPath();
ctx.moveTo(sx, sy);
for (var i = 0; i < player.path.length; i++) {
var px = player.path[i].x * TILE - cam.x + TILE / 2;
var py = player.path[i].y * TILE - cam.y + TILE / 2;
ctx.lineTo(px, py);
}
ctx.strokeStyle = 'rgba(255,107,53,0.35)';
ctx.lineWidth = 3;
ctx.setLineDash([6, 6]);
ctx.stroke();
ctx.setLineDash([]);
}
ctx.beginPath();
ctx.ellipse(sx, sy + 9, 11, 5, 0, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fill();
ctx.beginPath();
ctx.arc(sx, sy, 12, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b35';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2.5;
ctx.stroke();
ctx.beginPath();
ctx.arc(sx, sy - 2, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.fill();
ctx.fillStyle = '#334155';
ctx.fillRect(sx - 6, sy + 5, 12, 8);
}
function render() {
ctx.fillStyle = '#e6edf2';
ctx.fillRect(0, 0, W, H);
var startX = Math.floor(cam.x / TILE);
var startY = Math.floor(cam.y / TILE);
var endX = Math.ceil((cam.x + W) / TILE);
var endY = Math.ceil((cam.y + H) / TILE);
startX = clamp(startX, 0, GRID - 1);
startY = clamp(startY, 0, GRID - 1);
endX = clamp(endX, 0, GRID);
endY = clamp(endY, 0, GRID);
for (var y = startY; y < endY; y++) {
for (var x = startX; x < endX; x++) {
var sx = x * TILE - cam.x;
var sy = y * TILE - cam.y;
drawTile(x, y, sx, sy);
}
}
drawOrderMarkers();
drawPlayer();
for (var i = 0; i < floaters.length; i++) {
var f = floaters[i];
var fx = f.x * TILE - cam.x + TILE / 2;
var fy = f.y * TILE - cam.y + TILE / 2 - (1.2 - f.life) * 40;
ctx.save();
ctx.globalAlpha = Math.max(0, Math.min(1, f.life));
ctx.fillStyle = f.color;
ctx.font = 'bold 15px -apple-system,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(f.text, fx, fy);
ctx.restore();
}
}
// ===== UI 更新 =====
function updateHearts() {
var s = '';
for (var i = 0; i < 3; i++) s += i < hearts ? '❤️' : '🖤';
$hearts.textContent = s;
}
function updateOrderUI() {
var now = Date.now();
if (now - lastUIUpdate < 150) return;
lastUIUpdate = now;
var html = '';
for (var i = 0; i < orders.length; i++) {
var o = orders[i];
var pct = Math.max(0, o.timeLeft / o.maxTime) * 100;
var barColor = pct > 50 ? '#22c55e' : (pct > 25 ? '#f59e0b' : '#ef4444');
var phaseText = '';
if (o.phase === 'pickup') phaseText = '去「' + o.merchantName + '」取餐';
else if (o.phase === 'errand') phaseText = '帮买:' + o.errandItem;
else phaseText = '送到顾客';
html += '<div class="order-card" data-id="' + o.id + '">' +
'<div class="order-head">' + (o.type === 'errand' ? '帮买单' : '外卖单') + '</div>' +
'<div class="order-phase">' + phaseText + '</div>' +
'<div class="order-bar"><div style="width:' + pct + '%;background:' + barColor + '"></div></div>' +
'<div class="order-time">' + Math.ceil(o.timeLeft) + 's</div>' +
'</div>';
}
$orders.innerHTML = html;
}
// ===== 开始 / 结束 =====
function reset() {
resize();
state = 'playing';
money = 0; hearts = 3; done = 0; combo = 0;
orders = [];
floaters = [];
orderIdCounter = 0;
needNewOrder = false;
$money.textContent = '0';
$done.textContent = '0';
updateHearts();
$start.style.display = 'none';
$over.style.display = 'none';
generateMap();
var spawn = nearestWalkable(1, 1) || { x: 1, y: 1 };
player.x = spawn.x;
player.y = spawn.y;
player.path = [];
cam.x = clamp(player.x * TILE + TILE / 2 - W / 2, 0, Math.max(0, WORLD - W));
cam.y = clamp(player.y * TILE + TILE / 2 - H / 2, 0, Math.max(0, WORLD - H));
spawnNextOrder();
updateOrderUI();
}
function gameOver() {
state = 'over';
orders = [];
$orders.innerHTML = '';
$overBody.innerHTML =
'<p class="stat">总收入 <b>¥' + money + '</b></p>' +
'<p class="stat">完成 <b>' + done + '</b> 单</p>';
$over.style.display = 'flex';
}
// ===== 主循环 =====
function loop(t) {
if (!lastT) lastT = t;
var dt = (t - lastT) / 1000;
lastT = t;
if (dt > 0.05) dt = 0.05;
if (state === 'playing') {
updatePlayer(dt);
checkInteractions();
updateOrders(dt);
updateFloaters(dt);
updateCamera(Math.min(1, dt * 8));
if (needNewOrder) {
needNewOrder = false;
spawnNextOrder();
}
updateOrderUI();
}
render();
requestAnimationFrame(loop);
}
// ===== 事件绑定 =====
$btnStart.addEventListener('click', function (e) {
e.stopPropagation();
reset();
});
$btnAgain.addEventListener('click', function (e) {
e.stopPropagation();
reset();
});
cvs.addEventListener('pointerdown', function (e) {
if (state !== 'playing') return;
e.preventDefault();
var wx = (e.clientX + cam.x) / TILE;
var wy = (e.clientY + cam.y) / TILE;
var path = findPath(player.x, player.y, wx, wy);
if (path.length > 1) {
player.path = path.slice(1);
}
});
// 点击订单卡片自动导航
$orders.addEventListener('click', function (e) {
var card = e.target.closest('.order-card');
if (!card || state !== 'playing') return;
var id = parseInt(card.getAttribute('data-id'), 10);
var o = null;
for (var i = 0; i < orders.length; i++) {
if (orders[i].id === id) { o = orders[i]; break; }
}
if (!o) return;
var target = o.merchant;
if (o.phase === 'errand') target = o.errandShop;
else if (o.phase === 'deliver') target = o.customer;
if (target) {
var path = findPath(player.x, player.y, target.x, target.y);
if (path.length > 1) {
player.path = path.slice(1);
}
}
});
// 初始渲染
resize();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 跑外卖 · 进阶版
Creator: CyberFox46
Libraries: none
Complexity: complex (820 lines, 24.3 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-cyberfox46" to link back to the original. Then publish at arcadelab.ai/publish.