-- Gravity Force (Kingsoft 1989), DOGFIGHT: a pilot for either seat. -- -- Read from the game's own RAM (playbooks/gravityforce/battle.uss, measured -- with probe bots in solo runs; units are thousandths of a pixel): -- player records 0x37f30 (port 1, top view) and 0x37fd6 (port 0), 0xa6 apart: -- +0x00 long this player's view pointer into the cave bitmap -- +0x14 long x +0x18 long y (y grows downwards; the ship's -- centre is about (x+7, y+7) px) -- +0x1c long start x +0x20 long start y (its pad) -- +0x28 word heading, 0..71 in 5-degree steps clockwise from "right": -- 54 is straight up -- fuel 0x3b044 (port 1), 0x3b045 (port 0), 0..99 -- bullets 50 slots of 0x14 from 0x34c6a: +0 word frames to live -- (150 when fired), +2 x, +6 y, +0xa vx, +0xe vy (longs), -- +0x12 word: 0 a gunship's, 1 a player's (either: -- whose is told by where a new bullet appears) -- gunships records of 0x20 from 0x378f6 while the word at +0 is 4: -- +4 long x, +8 long y (pixels: the image is 46 x 29 -- from x+2, y+1; touching it is within ~30 across and -- ~22 up or down of its centre). They patrol between y 264 and 336 and shoot at -- whoever comes near; their images are blitted into the -- bitmap, so the map holds their starting pictures -- the cave one 336 x 801 bitmap, 5 planes one after another from -- 0x56000, 42 bytes a row, 33642 bytes a plane; world y is -- its row. Any set bit is rock (the ships are sprites). -- Controls: LEFT/RIGHT turn one step a frame, FIRE thrusts (about 50 a -- frame along the heading), UP shoots. Gravity is about 10.5 a frame -- downwards and drag takes about 1% of the speed a frame. -- -- The pilot: build a map of the cave, keep a distance field to the enemy -- on 8x8 cells, fly along it with a velocity controller that keeps the -- nose near up, and when the enemy is in sight and in range, turn to the -- intercept angle and shoot. Rock ahead on the coasting path, or fuel low, -- takes over from all of that. local REC = { [1] = 0x37f30, [0] = 0x37fd6 } local FUEL = { [1] = 0x3b044, [0] = 0x3b045 } local BULLETS, BULLET_SLOTS, BULLET_SIZE = 0x34c6a, 50, 0x14 local slot_was, slot_mine = {}, {} -- per bullet slot: last life, fired by us local MAP, ROW, PLANE, ROWS, COLS = 0x56000, 42, 33642, 801, 42 local CELL = 8 local NCR = math.ceil(ROWS / CELL) -- cell rows local THRUST, GRAVITY, DRAG = 50, 10.5, 0.0105 local MUZZLE = 2500 -- bullet speed relative to the ship local SQRT = math.sqrt local floor, abs, min, max = math.floor, math.abs, math.min, math.max local band, rshift = function(a, b) return a & b end, function(a, b) return a >> b end local function s32(v) if v >= 0x80000000 then return v - 0x100000000 end return v end local GUNSHIPS, GUNSHIP_SIZE = 0x378f6, 0x20 local GUNSHIP_REACH = 85 -- px from a gunship's centre local function gunships() local out = {} for i = 0, 5 do local a = GUNSHIPS + i * GUNSHIP_SIZE if ram.u16(a) ~= 4 then break end out[#out + 1] = { s32(ram.u32(a + 4)) + 25, s32(ram.u32(a + 8)) + 15 } end return out end local turrets = {} -- gunships when the field was made -- What the gunships cost a route, per cell, built up as they are seen: -- their patrol lanes end to end and every cell one has been -- in, with the cells around it (they turn between lanes at the ends). local swept, lanes, extra = {}, {}, {} local function remember(ships) local new = false for _, g in ipairs(ships) do local c = max(0, min(41, floor(g[1] / 8))) local r = max(0, min(100, floor(g[2] / 8))) local k = r * 42 + c if not swept[k] then swept[k] = true; new = true for dr = -3, 3 do for dc = -3, 3 do local r2, c2 = r + dr, c + dc if r2 >= 0 and r2 <= 100 and c2 >= 0 and c2 < 42 then local k2 = r2 * 42 + c2 extra[k2] = min(60, (extra[k2] or 0) + 6) end end end end local ly = floor(g[2] / 4 + 0.5) * 4 if not lanes[ly] then lanes[ly] = true; new = true -- a cell whose centre is within 22 of the lane's: a ship there -- touches a gunship passing; one row further is a gap it can use for r2 = max(0, floor((ly - 26) / 8)), min(100, floor((ly + 26) / 8)) do if abs(r2 * 8 + 4 - ly) < 25 then for c2 = 0, 41 do extra[r2 * 42 + c2] = (extra[r2 * 42 + c2] or 0) + 200 end end end end end return new end local port, me, them, fuel_at local mask = {} -- mask[row] = string of 42 bytes, rock bits local built = 0 -- rows built so far local solid = {} -- solid[cr*COLS + c] = true for rock cells local near = {} -- near[cell] = rock within one cell local clear = {} -- clear[cell] = cells to the nearest rock local dist = {} -- distance field to the current goal local goal_cell, field_age = -1, 0 local prev = {} -- last positions, for velocities local stats = { deaths = 0, shots = 0, kills = 0 } local shoot_cool = 0 local last_pos local function rock(px, py) py = floor(py); px = floor(px) if py < 0 or py >= ROWS or px < 0 or px >= COLS * 8 then return true end local row = mask[py] if not row then return false end local b = row:byte(rshift(px, 3) + 1) return band(b, rshift(0x80, px & 7)) ~= 0 end -- The cave map, a few dozen rows a frame: rows of all five planes OR'd. local function build_some(rows) local stop = min(ROWS, built + rows) for y = built, stop - 1 do local acc = {} for i = 1, ROW do acc[i] = 0 end for p = 0, 4 do local s = ram.bytes(MAP + p * PLANE + y * ROW, ROW) for i = 1, ROW do acc[i] = acc[i] | s:byte(i) end end mask[y] = string.char(table.unpack(acc)) local cr = y // CELL for c = 0, COLS - 1 do if acc[c + 1] ~= 0 then solid[cr * COLS + c] = true end end end built = stop end -- Once the rows are in: which cells are near rock, then every cell's -- distance to rock (a breadth-first pass from all the rock at once), a -- slice a frame, so each frame fits in the arena's cost cap. local NEAR_ROWS, CLEAR_STEPS = 20, 900 local finished, near_row, cq, chead = false, 0, nil, 1 local function finish_some() if near_row < NCR then for cr = near_row, min(NCR, near_row + NEAR_ROWS) - 1 do for c = 0, COLS - 1 do local k = cr * COLS + c local n = solid[k] for dr = -1, 1 do for dc = -1, 1 do local r2, c2 = cr + dr, c + dc if r2 < 0 or r2 >= NCR or c2 < 0 or c2 >= COLS or solid[r2 * COLS + c2] then n = true end end end near[k] = n end end near_row = min(NCR, near_row + NEAR_ROWS) return end if not cq then cq = {} for k = 0, NCR * COLS - 1 do if solid[k] then clear[k] = 0; cq[#cq + 1] = k end end return end local stop = chead + CLEAR_STEPS while chead <= #cq and chead < stop do local k = cq[chead]; chead = chead + 1 local r, c = k // COLS, k % COLS for dr = -1, 1 do for dc = -1, 1 do local r2, c2 = r + dr, c + dc if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then local k2 = r2 * COLS + c2 if clear[k2] == nil then clear[k2] = clear[k] + 1; cq[#cq + 1] = k2 end end end end end if chead > #cq then finished = true; cq = nil end end local function cell_of(px, py) local c = max(0, min(COLS - 1, floor(px / CELL))) local r = max(0, min(NCR - 1, floor(py / CELL))) return r * COLS + c end -- Breadth-first distances from the goal over cells clear of rock by a cell -- (the ship is two cells across); cells that are merely free are allowed -- at a cost, so a tight passage is still a way through. local function field(goal) local d = {} local queue, head = { goal }, 1 d[goal] = 0 while head <= #queue do local k = queue[head]; head = head + 1 local r, c = k // COLS, k % COLS local base = d[k] for i = 1, 4 do local r2, c2 = r, c if i == 1 then r2 = r - 1 elseif i == 2 then r2 = r + 1 elseif i == 3 then c2 = c - 1 else c2 = c + 1 end if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then local k2 = r2 * COLS + c2 if not solid[k2] and d[k2] == nil then local cl = clear[k2] or 9 local cost = cl <= 1 and 20 or cl == 2 and 8 or cl == 3 and 3 or cl == 4 and 1.5 or 1 local cx, cy = c2 * CELL + 4, r2 * CELL + 4 for _, t in ipairs(turrets) do local tx, ty = cx - t[1], cy - t[2] if tx * tx + ty * ty < GUNSHIP_REACH * GUNSHIP_REACH then cost = cost + 25 end end cost = cost + (extra[k2] or 0) d[k2] = base + cost queue[#queue + 1] = k2 end end end end return d end local function thin_line(x1, y1, x2, y2) local dx, dy = x2 - x1, y2 - y1 local n = floor(SQRT(dx * dx + dy * dy) / 3) for i = 1, n do local t = i / (n + 1) if rock(x1 + dx * t, y1 + dy * t) then return false end end return true end -- A line the ship itself can fly: the centre line and two at its sides. local function clear_line(x1, y1, x2, y2) local dx, dy = x2 - x1, y2 - y1 local l = SQRT(dx * dx + dy * dy) if l < 1 then return true end local ox, oy = -dy / l * 7, dx / l * 7 return thin_line(x1, y1, x2, y2) and thin_line(x1 + ox, y1 + oy, x2 + ox, y2 + oy) and thin_line(x1 - ox, y1 - oy, x2 - ox, y2 - oy) end local function ship(rec) return s32(ram.u32(rec + 0x14)) / 1000 + 7, s32(ram.u32(rec + 0x18)) / 1000 + 7, ram.u16(rec + 0x28) % 72 end -- Does the ship, coasting from here, meet rock within `frames`? local function coast_hits(x, y, vx, vy, frames) for t = 1, frames do vy = vy + GRAVITY / 1000; vx = vx * (1 - DRAG); vy = vy * (1 - DRAG) x = x + vx; y = y + vy if rock(x - 5, y - 5) or rock(x + 5, y - 5) or rock(x - 6, y + 6) or rock(x + 6, y + 6) or rock(x, y + 7) or rock(x, y - 7) or rock(x - 7, y) or rock(x + 7, y) then return t end end return nil end local function angle_err(h, want) local d = (want - h) % 72 if d > 36 then d = 72 - d end return d end local DIRX, DIRY = {}, {} for i = 0, 71 do DIRX[i] = math.cos(i * math.pi / 36); DIRY[i] = math.sin(i * math.pi / 36) end -- Frames until rock, thrusting along `heading` for `burn` frames (none for -- a nil heading) and coasting after, over `horizon` frames. local function clear_for(x, y, vx, vy, heading, burn, horizon) for t = 1, horizon do if heading and t <= burn then vx = vx + THRUST / 1000 * DIRX[heading]; vy = vy + THRUST / 1000 * DIRY[heading] end vy = vy + GRAVITY / 1000; vx = vx * (1 - DRAG); vy = vy * (1 - DRAG) x = x + vx; y = y + vy if rock(x - 5, y - 5) or rock(x + 5, y - 5) or rock(x - 6, y + 6) or rock(x + 6, y + 6) or rock(x, y + 7) or rock(x, y - 7) or rock(x - 7, y) or rock(x + 7, y) then return t end end return horizon + 1 end -- The way out when rock is coming: of the headings within reach, the one -- whose burn keeps the ship clear longest (turning costs a frame a step, -- so far-off headings start late). local function escape(x, y, vx, vy, h) local best, bt = nil, clear_for(x, y, vx, vy, nil, 0, 40) for off = -24, 24, 3 do local hd = (h + off) % 72 local late = abs(off) -- coast while turning, then burn local cx, cy, cvx, cvy = x, y, vx, vy local crashed = false for t = 1, late do cvy = cvy + GRAVITY / 1000; cvx = cvx * (1 - DRAG); cvy = cvy * (1 - DRAG) cx = cx + cvx; cy = cy + cvy if rock(cx, cy) then crashed = true; break end end if not crashed then local t = late + clear_for(cx, cy, cvx, cvy, hd, 12, 40 - late) -- prefer near-upright headings on ties: they can hover after if t > bt or (t == bt and best and angle_err(hd, 54) < angle_err(best, 54)) then best, bt = hd, t end end end return best, bt end local function heading_to(ax, ay) -- angle of a vector as a heading index local a = math.atan(ay, ax) -- y down: clockwise local h = floor(a / (math.pi / 36) + 0.5) % 72 return h end local function turn_toward(h, want) -- bits to turn from h to want local d = (want - h) % 72 if d == 0 then return 0 end if d <= 36 then return bits.RIGHT end return bits.LEFT end -- The velocity (px/frame) to fly at: along the distance field towards the -- goal, through the farthest cell in sight, slower near rock and near it. local function waypoint(x, y) local k = cell_of(x, y) local best, bd = k, dist[k] or 1e9 -- walk downhill a few cells, keep the farthest one still in sight local cur = k local wx, wy = nil, nil for step = 1, 12 do local r, c = cur // COLS, cur % COLS local nk, nd = nil, dist[cur] or 1e9 for dr = -1, 1 do for dc = -1, 1 do if dr ~= 0 or dc ~= 0 then local r2, c2 = r + dr, c + dc if r2 >= 0 and r2 < NCR and c2 >= 0 and c2 < COLS then local k2 = r2 * COLS + c2 local d2 = dist[k2] if d2 and d2 < nd then nk, nd = k2, d2 end end end end end if not nk then break end cur = nk local cx, cy = (cur % COLS) * CELL + CELL / 2, (cur // COLS) * CELL + CELL / 2 if clear_line(x, y, cx, cy) then wx, wy = cx, cy else break end end return wx, wy end -- A push away from rock nearby: along 16 rays, the closer the rock, the -- harder, in px/frame. local RAYS = {} for i = 0, 15 do RAYS[i] = { math.cos(i * math.pi / 8), math.sin(i * math.pi / 8) } end local function repulsion(x, y) local px, py = 0, 0 for i = 0, 15 do local dx, dy = RAYS[i][1], RAYS[i][2] for r = 8, 26, 3 do if rock(x + dx * r, y + dy * r) then local f = (26 - r) / 18 px = px - dx * f; py = py - dy * f break end end end -- the strongest single push, not the sum: a floor under the ship is many -- rays and still only one floor local m = SQRT(px * px + py * py) local cap = 0.7 if m > cap then px, py = px / m * cap, py / m * cap end return px, py end -- Thrust and turn to make the velocity `want` (px/frame), keeping the nose -- within reach of up unless told it may point anywhere. local function fly(h, vx, vy, wvx, wvy, upright) local tau = 12 local ax = (wvx - vx) / tau local ay = (wvy - vy) / tau - GRAVITY / 1000 -- thrust is all there is to push with: a thrust vector, gravity cancelled local mag = SQRT(ax * ax + ay * ay) local want = heading_to(ax, ay) if upright then -- never further than 60 degrees from up: a pilot who is upside down -- when it has to climb falls into the rock first local off = (want - 54 + 36) % 72 - 36 if off > 12 then want = (54 + 12) % 72 elseif off < -12 then want = (54 - 12) % 72 end end local b = turn_toward(h, want) -- thrust only where it helps: along the nose as it is now, the push must -- go the way the ship needs to accelerate (clamped upright, a ship that -- needs to go down mostly lets gravity do it) local along = DIRX[h] * ax + DIRY[h] * ay if angle_err(h, want) <= 6 and along > 0.01 then b = b | bits.FIRE end return b end -- Where to aim to meet the enemy, or nil when no shot can get there. local function intercept(x, y, vx, vy, ex, ey, evx, evy) local dx, dy = ex - x, ey - y local wx, wy = evx - vx, evy - vy local s = MUZZLE / 1000 local a = wx * wx + wy * wy - s * s local b = 2 * (dx * wx + dy * wy) local c = dx * dx + dy * dy local t if abs(a) < 1e-6 then t = -c / b else local disc = b * b - 4 * a * c if disc < 0 then return nil end local r = SQRT(disc) local t1, t2 = (-b - r) / (2 * a), (-b + r) / (2 * a) t = (t1 > 0 and t2 > 0) and min(t1, t2) or max(t1, t2) end if not t or t <= 0 or t > 120 then return nil end return heading_to(dx + wx * t, dy + wy * t), t end -- The nearest enemy bullet that will pass within a ship's width soon. -- Which bullets are ours: a slot that comes alive next to our ship. local function sort_bullets(x, y, ex, ey) for i = 0, BULLET_SLOTS - 1 do local a = BULLETS + i * BULLET_SIZE local life = ram.u16(a) if life > 0 and (slot_was[i] or 0) == 0 then local bx, by = s32(ram.u32(a + 2)) / 1000, s32(ram.u32(a + 6)) / 1000 local dm = (bx - x) ^ 2 + (by - y) ^ 2 local de = (bx - ex) ^ 2 + (by - ey) ^ 2 slot_mine[i] = ram.u16(a + 0x12) ~= 0 and dm < de end slot_was[i] = life end end local function threat(x, y, vx, vy) for i = 0, BULLET_SLOTS - 1 do local a = BULLETS + i * BULLET_SIZE local life = ram.u16(a) if life > 0 and life <= 200 and not slot_mine[i] then local bx, by = s32(ram.u32(a + 2)) / 1000, s32(ram.u32(a + 6)) / 1000 local bvx, bvy = s32(ram.u32(a + 0xa)) / 1000, s32(ram.u32(a + 0xe)) / 1000 -- our own bullets start at our nose going our way: skip those close -- behind us that are leaving local rx, ry = bx - x, by - y local rvx, rvy = bvx - vx, bvy - vy local v2 = rvx * rvx + rvy * rvy if v2 > 0.01 then local t = -(rx * rvx + ry * rvy) / v2 if t > 0 and t < 25 then local cx, cy = rx + rvx * t, ry + rvy * t if cx * cx + cy * cy < 11 * 11 then return rvx, rvy, t end end end end end return nil end function init(p, players) port = p me = REC[p] or REC[1] them = (me == REC[1]) and REC[0] or REC[1] fuel_at = FUEL[p] or FUEL[1] print("gravityforce bot on port " .. p .. " of " .. players) end function frame(n) -- the map, a part a frame (the arena caps what a frame may cost) if built < ROWS then build_some(40); return 0 end if not finished then finish_some(); return 0 end local x, y, h = ship(me) local ex, ey = ship(them) local p = prev local vx = p.x and (x - p.x) or 0 local vy = p.y and (y - p.y) or 0 local evx = p.ex and (ex - p.ex) or 0 local evy = p.ey and (ey - p.ey) or 0 -- a jump back to the pad is a new life: no velocity across it if abs(vx) > 8 or abs(vy) > 8 then vx, vy = 0, 0; stats.deaths = stats.deaths + 1 end if abs(evx) > 8 or abs(evy) > 8 then evx, evy = 0, 0; stats.kills = stats.kills + 1 end -- an enemy that has not moved for three seconds is out of the game (or -- between lives): nothing to shoot or chase local still = (p.ex == ex and p.ey == ey) and (p.still or 0) + 1 or 0 prev = { x = x, y = y, ex = ex, ey = ey, still = still, refuel = p.refuel } if n % 1000 == 0 then print(string.format("frame %d port %d deaths %d enemy-deaths %d shots %d fuel %d", n, port, stats.deaths, stats.kills, stats.shots, ram.u8(fuel_at))) end sort_bullets(x, y, ex, ey) local ships = gunships() if remember(ships) then field_age = min(field_age, 3) end local fuel = ram.u8(fuel_at) local home_x = s32(ram.u32(me + 0x1c)) / 1000 + 7 local home_y = s32(ram.u32(me + 0x20)) / 1000 + 7 local landed = abs(x - home_x) < 3 and abs(y - home_y) < 2 and abs(vy) < 0.05 -- On the pad with little fuel: sit until it is filled. if landed and fuel < 90 and (p.refuel or fuel < 30) then prev.refuel = true return turn_toward(h, 54) end -- Resting on rock (a pad, or after a soft landing): a ship cannot turn -- there, so it lifts off nose up first. -- (within a few pixels of the ground it cannot turn either) local resting = abs(vx) < 0.3 and abs(vy) < 0.3 and (rock(x, y + 14) or rock(x - 4, y + 14) or rock(x + 4, y + 14)) and angle_err(h, 54) <= 2 if resting then return bits.FIRE end -- Rock on the coasting path soon: everything below flies slower and -- pushes off the rock harder. local hit = coast_hits(x, y, vx, vy, 18) -- An enemy bullet coming our way: thrust across its path. local tvx, tvy = threat(x, y, vx, vy) if tvx then local side = heading_to(-tvy, tvx) if angle_err(side, 54) > 18 then side = heading_to(tvy, -tvx) end if angle_err(side, 54) <= 24 then local b = turn_toward(h, side) if angle_err(h, side) <= 8 then b = b | bits.FIRE end return b end end -- Fuel low: go home. local gx, gy local going_home = fuel < 22 if going_home then gx, gy = home_x, home_y - 18 else gx, gy = ex, ey end -- Enemy in sight and in range: shoot. local dx, dy = ex - x, ey - y local d = SQRT(dx * dx + dy * dy) shoot_cool = max(0, shoot_cool - 1) local function shot_line(x1, y1, x2, y2) local lx, ly = x2 - x1, y2 - y1 local l = max(1, SQRT(lx * lx + ly * ly)) local ox, oy = -ly / l * 3, lx / l * 3 return thin_line(x1, y1, x2, y2) and thin_line(x1 + ox, y1 + oy, x2 + ox, y2 + oy) and thin_line(x1 - ox, y1 - oy, x2 - ox, y2 - oy) end local ehx = s32(ram.u32(them + 0x1c)) / 1000 + 7 local ehy = s32(ram.u32(them + 0x20)) / 1000 + 7 local enemy_home = (abs(ex - ehx) < 4 and abs(ey - ehy) < 4 and abs(evx) < 0.05 and abs(evy) < 0.05) or still > 150 if not going_home and not enemy_home and d < 170 and shot_line(x, y, ex, ey) and vy < 1.2 and not hit and not coast_hits(x, y, vx, vy, 30) then local aim = intercept(x, y, vx, vy, ex, ey, evx, evy) if aim then local b = turn_toward(h, aim) if angle_err(h, aim) <= 1 and shoot_cool == 0 then b = b | bits.UP; shoot_cool = 6; stats.shots = stats.shots + 1 end -- falling while aiming: a little thrust if the nose is high enough if vy > 0.5 and (h >= 36 and h <= 72) and angle_err(h, 54) <= 30 then b = b | bits.FIRE end return b end end -- Fly there: the distance field towards the goal, refreshed now and then. local gk = cell_of(gx, gy) field_age = field_age - 1 if gk ~= goal_cell or field_age <= 0 then turrets = ships dist = field(gk); goal_cell = gk; field_age = 15 end local wx, wy if clear_line(x, y, gx, gy) and (clear[cell_of(gx, gy)] or 0) >= 2 then wx, wy = gx, gy else wx, wy = waypoint(x, y) end if not wx then wx, wy = x, y - 10 end local ddx, ddy = wx - x, wy - y local dd = SQRT(ddx * ddx + ddy * ddy) local here = clear[cell_of(x, y)] or 4 local speed = min(1.0, dd / 30, 0.15 + 0.17 * here) if going_home and dd < 30 then speed = min(speed, 0.4) end -- keep a standoff from the enemy when it is in sight if not going_home and d < 90 and thin_line(x, y, ex, ey) then speed = 0 end if hit then speed = min(speed, 0.4) end -- a gunship near the way ahead and not yet near us: wait for it to pass local function near_ship(px, py, r) for _, g in ipairs(ships) do local qx, qy = px - g[1], py - g[2] if qx * qx + qy * qy < r * r then return g end end end local g_here = near_ship(x, y, 55) local in_lane = false for ly in pairs(lanes) do if abs(y - ly) < 26 then in_lane = true end end if not g_here and not in_lane and near_ship(x + ddx * min(1, 40 / max(dd, 1)), y + ddy * min(1, 40 / max(dd, 1)), GUNSHIP_REACH) then speed = 0 end -- in one's reach: away from it, fast if g_here then local qx, qy = x - g_here[1], y - g_here[2] local ql = max(1, SQRT(qx * qx + qy * qy)) -- straight away from it, mostly up or down: they fly level ddx, ddy = qx / ql * 20, (qy >= 0 and 40 or -40) dd = SQRT(ddx * ddx + ddy * ddy) speed = max(speed, 0.9) end local wvx, wvy = 0, 0 if dd > 0.5 then wvx, wvy = ddx / dd * speed, ddy / dd * speed end local rx, ry = repulsion(x, y) local push = hit and 1.0 or 0.6 wvx, wvy = wvx + rx * push, wvy + ry * push local wm = SQRT(wvx * wvx + wvy * wvy) local top = max(speed, 0.5) if wm > top then wvx, wvy = wvx / wm * top, wvy / wm * top end -- the lanes are walls: below one, never rise within 30 of its centre; -- above one, never sink within 30 of it. Falling is slow (gravity only, -- unless the ship turns over), so the limits start early. for ly in pairs(lanes) do if y > ly and y < ly + 70 then local room = y - (ly + 28) -- rising, only as fast as gravity alone can stop in the room left if room < 0 then wvy = max(wvy, 0.5) elseif wvy < 0 then wvy = max(wvy, -0.6 * SQRT(2 * GRAVITY / 1000 * room)) end elseif y < ly and y > ly - 70 then local room = (ly - 28) - y if room < 0 then wvy = min(wvy, -0.5) elseif wvy > 0 then wvy = min(wvy, room / 25) end end end -- landing: straight down onto the pad, slowly if going_home and abs(x - home_x) < 6 and y < home_y and y > home_y - 40 then wvx, wvy = (home_x - x) / 20, 0.35 end return fly(h, vx, vy, wvx, wvy, true) end