diff --git a/packages/README.md b/packages/README.md index 67ee8b27..29f19126 100644 --- a/packages/README.md +++ b/packages/README.md @@ -32,6 +32,7 @@ section. The rules are in [ludic.base](ludic.base/README.md). | [ludic.things](ludic.things/README.md) | the placed things of a world and their kinds (an open registry), queries, facts, a save | | [ludic.tracks](ludic.tracks/README.md) | prints and sign animals leave, their age, and reading them | | [ludic.update](ludic.update/README.md) | a Velopack-installed game updating itself a frame at a time | +| [ludic.vehicles](ludic.vehicles/README.md) | each player's boat and horse: called to berths and bays, owner and rider, riding, getting off onto a safe spot | | [ludic.wallet](ludic.wallet/README.md) | money carried: earned, spent, lost, every change a fact | | [ludic.weather](ludic.weather/README.md) | spells of weather described as data: odds, wetness, wind, storms, a forecast | | [ludic.wildlife](ludic.wildlife/README.md) | a valley's animals: ranges, their day, senses and alert, feeding, lures, spawning | diff --git a/packages/ludic.vehicles/README.md b/packages/ludic.vehicles/README.md new file mode 100644 index 00000000..888da964 --- /dev/null +++ b/packages/ludic.vehicles/README.md @@ -0,0 +1,75 @@ +# ludic.vehicles + +Each player's boat and horse: called to one of four berths off a pier head or four bays at a trough, +made there the first time and fetched there after, owned by one player and ridden by one at a time, +ridden over water or land, and got off onto a spot the rider's own safe-spot rule found. Uses +[`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.vehicles" +``` + +The package never draws, prices or words anything. A game draws a vehicle when `VehicleWorld.made` +says one exists and moves its model on `placed`; it charges for a hire and says the words; and it +binds the rider to its walking body (in Maroon Lake, [ludic.character](../ludic.character/README.md) +through the game). + +## The rules it keeps + +- **Nothing is parked for anyone.** A vehicle has an `owner` and a `rider`; `vehicle_find(kind)` is + this player's own and nothing else may pick "the" boat. A place is taken while a boat lies in it + or a horse is tied at it (`home_x`, `home_z`), within half a place's spacing. +- **Where a body is put down is one question with one answer.** Getting off asks the rider's + `safe_spot` - a wade of up to 1.25 m within its search from a boat, beside the right flank from a + horse - and a boat too far out refuses and says so (`VEHICLE_NO_LANDING`) rather than dropping the + rider in the lake. +- **Steering follows signed speed**: backing up with the stick left swings the nose right. +- A stale rider locks a vehicle for ever, so getting off is `vehicle_dismount` or `vehicle_drop`. + +## The ports + +```ludic +export port VehicleWorld { + ground: fn(float, float) -> float # required + water: fn(float, float) -> float # the surface (unbound: never wet) + blocked: fn(float, float, float) -> bool # (x, z, r): a collider in a horse's way + swell: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost + wind: fn() -> float # 0..1 + wind_yaw: fn() -> float # the yaw the wind pushes toward + next_id: fn() -> int # an id across machines + made: fn(Vehicle) -> void # draw it + placed: fn(Vehicle) -> void # move its model + gone: fn(Vehicle) -> void # its owner left +} +export port VehicleRider { + board(x, z, yaw), ride_at(x, y, z, yaw, speed), step_off(x, z), + safe_spot(x, z, from_y, max_depth) -> bool, safe_x(), safe_z() +} +``` + +## API + +| | | +| --- | --- | +| `VEHICLE_HORSE`, `VEHICLE_BOAT`, `VEHICLE_PLACES`, `VEHICLE_HAY_MIN` | the kinds, four places each, and the hay a horse needs to go | +| `Vehicle { kind, nid, x, y, z, yaw, speed, fuel, food, seat_h, bob, owned, owner, rider, home_x, home_z }` | a vehicle (change one through the verbs) | +| `vehicles_dock_at(x, z, yaw)`, `vehicles_rail_at(x, z, yaw)`, `vehicles_dock_yaw()` | where the places are measured from | +| `vehicle_berth_x/z(k)`, `vehicle_post_x/z(k)`, `vehicle_bay_x/z(k)`, `vehicle_place_x/z(kind, k)` | the places | +| `vehicle_call_for(pid, kind, k) -> bool`, `vehicle_in_place(kind, k)`, `vehicle_free_place(kind)`, `vehicle_mine_in_place(kind)` | calling one to a place | +| `vehicle_of(pid, kind)`, `vehicle_find(kind)`, `vehicle_nearest(x, z, reach)`, `vehicles_all()`, `vehicle_current()`, `vehicle_ride_kind()` | questions | +| `vehicle_mount(v) -> bool`, `vehicle_dismount() -> bool`, `vehicle_drop()` | on and off | +| `vehicle_update(ix, iz, run, dt)` | a frame of riding from the rider's stick (bind it where the body hands its stick over) | +| `vehicle_own(v)`, `vehicle_feed(v, hay)`, `vehicle_follow(v, x, y, z, yaw)`, `vehicles_release(pid)`, `vehicle_restore(kind, owned, food)` | hiring, hay, a tied horse's wandering, a player leaving, a saved word | +| `vehicles_tick(dt)` | the idle boats' bob (the game calls it where the world is run) | +| `vehicle_facts() -> Queue` | `{ what, nid, kind, owner, x, z, v }`: `VEHICLE_MOUNTED`, `_DISMOUNTED`, `_NO_LANDING`, `_MOVED` (v: metres), `_SWELL` (every 14 s), `_HUNGRY`, `_FED` | +| `vehicles_reset()`, `vehicles_system() -> System` | `"vehicles"`: this player's own saved by kind (hired, hay) - where they lie is not saved | + +## Tests + +```bash +ludic test packages/ludic.vehicles +``` + +A fake shore with a pier head and a trough: berths and bays, calling and fetching, a taken place, a +player leaving, a hungry horse, riding a horse into a trunk and to the water's edge, rowing to the +shore, the swell and the wind, getting off near a shore and far from one, and the save. diff --git a/packages/ludic.vehicles/call.ludic b/packages/ludic.vehicles/call.ludic new file mode 100644 index 00000000..ff789133 --- /dev/null +++ b/packages/ludic.vehicles/call.ludic @@ -0,0 +1,104 @@ +# call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there +# after; gone when its player leaves +function ve_add(kind: int, x: float, z: float, yaw: float) -> Vehicle { + if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } + let v = new Vehicle + v.nid = VehicleWorld.next_id() + v.kind = kind + v.yaw = yaw + if kind == VEHICLE_HORSE { + v.seat_h = 1.35 + v.fuel = 100.0 + v.food = 40.0 + } else { + v.seat_h = 0.30 + } + ve_put(v, x, z) + push(ve_all(), v) + VehicleWorld.made(v) + return v +} + +# at rest at (x, z): a boat on the water, a horse on the ground and tied there +function ve_put(v: Vehicle, x: float, z: float) -> void { + v.x = x + v.z = z + v.speed = 0.0 + if v.kind == VEHICLE_BOAT { v.y = VehicleWorld.water(x, z) + 0.02 } else { v.y = VehicleWorld.ground(x, z) } + v.home_x = x + v.home_z = z +} + +# Bring player pid's boat or horse to place k. False when the place is somebody else's, or it is +# being ridden. A horse faces the trough: the bay lies on its near side. +export function vehicle_call_for(pid: int, kind: int, k: int) -> bool { + if not ve_have_dock or k < 0 or k >= VEHICLE_PLACES { return false } + let here = vehicle_in_place(kind, k) + let v = vehicle_of(pid, kind) + if here != null { return v != null and here.nid == v.nid } + var yaw = ve_dock_yaw + if kind == VEHICLE_HORSE { yaw = ve_rail_yaw } + if v == null { + let made = ve_add(kind, vehicle_place_x(kind, k), vehicle_place_z(kind, k), yaw) + if made == null { return false } + made.owner = pid + if pid == 0 { ve_apply_kept(made) } + return true + } + if v.rider >= 0 { return false } + ve_put(v, vehicle_place_x(kind, k), vehicle_place_z(kind, k)) + v.yaw = yaw + VehicleWorld.placed(v) + return true +} + +# a player left: their boat and horse go with them, and their places are free +export function vehicles_release(pid: int) -> void { + if pid == 0 { return } + let keep = new []Vehicle + let l = ve_all() + for i in 0 .. len(l) { + if l[i].owner != pid { push(keep, l[i]) } else { VehicleWorld.gone(l[i]) } + } + ve_list = keep +} + +# hired or rented for the trip +export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } } + +# hay for a horse, up to a full belly +export function vehicle_feed(v: Vehicle, amount: float) -> void { + if v == null { return } + v.food = Math.min(v.food + amount, 100.0) + ve_say(VEHICLE_FED, v, amount) +} + +# a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z) +export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: float) -> void { + if v == null or v.rider >= 0 { return } + v.x = x + v.y = y + v.z = z + v.yaw = yaw +} + +# a saved trip's word on this player's own: hired, and how fed +export function vehicle_restore(kind: int, owned: bool, food: float) -> void { + if kind == VEHICLE_HORSE { + ve_kept_horse = owned + ve_kept_food = food + } else { ve_kept_boat = owned } + let v = vehicle_find(kind) + if v != null { ve_apply_kept(v) } +} + +var ve_kept_horse: bool = false +var ve_kept_boat: bool = false +var ve_kept_food: float = 40.0 + +function ve_apply_kept(v: Vehicle) -> void { + if v.kind == VEHICLE_HORSE { + v.owned = ve_kept_horse + v.food = ve_kept_food + } else { v.owned = ve_kept_boat } +} diff --git a/packages/ludic.vehicles/index.ludic b/packages/ludic.vehicles/index.ludic new file mode 100644 index 00000000..b3ca745b --- /dev/null +++ b/packages/ludic.vehicles/index.ludic @@ -0,0 +1,14 @@ +# ludic.vehicles - a player's boat and horse: made the first time they are called to a place and +# fetched there after, one rider at a time, ridden over water or land, and put down again through +# the rider's own safe spot. The game draws them, prices them and words them. +module ludic_vehicles uses ludic_base +numbers float +import "ludic.base" +import "kinds.ludic" +import "ports.ludic" +import "state.ludic" +import "places.ludic" +import "call.ludic" +import "mount.ludic" +import "ride.ludic" +import "system.ludic" diff --git a/packages/ludic.vehicles/kinds.ludic b/packages/ludic.vehicles/kinds.ludic new file mode 100644 index 00000000..fa604780 --- /dev/null +++ b/packages/ludic.vehicles/kinds.ludic @@ -0,0 +1,41 @@ +# kinds.ludic - what can be ridden, a vehicle, and what happened to one +export const VEHICLE_HORSE: int = 1 # land only: stamina for a gallop, hay to go at all +export const VEHICLE_BOAT: int = 3 # water only (2 was a machine the wilderness has no place for) +export const VEHICLE_PLACES: int = 4 # berths on the pier head, bays at the rail + +export property Vehicle { + kind: int = 0 + nid: int = 0 # its id across machines + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + yaw: float = 0.0 + speed: float = 0.0 + fuel: float = 0.0 # a horse's stamina, 0..100 + food: float = 0.0 # a horse's hay, 0..100 + seat_h: float = 0.0 # the rider sits this far above it + bob: float = 0.0 + owned: bool = false # hired or rented for the trip + owner: int = 0 # the player it belongs to (0: this machine's) + rider: int = -1 # the player on it, -1 nobody + home_x: float = 0.0 # a horse is tied where its home is + home_z: float = 0.0 +} + +export const VEHICLE_MOUNTED: int = 0 +export const VEHICLE_DISMOUNTED: int = 1 +export const VEHICLE_NO_LANDING: int = 2 # no shore near enough to step onto +export const VEHICLE_MOVED: int = 3 # v: metres covered this frame by the rider +export const VEHICLE_SWELL: int = 4 # the edge's swell is taking the stroke; every 14 s +export const VEHICLE_HUNGRY: int = 5 # a horse without hay will not go +export const VEHICLE_FED: int = 6 + +export property VehicleFact { + what: int = 0 + nid: int = 0 + kind: int = 0 + owner: int = 0 + x: float = 0.0 + z: float = 0.0 + v: float = 0.0 +} diff --git a/packages/ludic.vehicles/mount.ludic b/packages/ludic.vehicles/mount.ludic new file mode 100644 index 00000000..9577cd33 --- /dev/null +++ b/packages/ludic.vehicles/mount.ludic @@ -0,0 +1,54 @@ +# mount.ludic - getting on, and getting off onto a spot the rider's own safe-spot rule found: the +# same one answer a seat and a stand-up use, so a landing is never inside a boulder or on a bank +# the body cannot climb off +export const VEHICLE_HAY_MIN: float = 10.0 # a horse with less hay than this will not go + +# this machine's player onto v; false when it is somebody else's, taken, or a hungry horse +export function vehicle_mount(v: Vehicle) -> bool { + if v == null or v.owner != 0 or v.rider >= 0 or ve_cur != null { return false } + if v.kind == VEHICLE_HORSE and v.food < VEHICLE_HAY_MIN { + ve_say(VEHICLE_HUNGRY, v, v.food) + return false + } + ve_cur = v + v.rider = 0 + VehicleRider.board(v.x, v.z, v.yaw) + ve_say(VEHICLE_MOUNTED, v, 0.0) + return true +} + +# Off, onto the nearest spot a body could stand and walk off: from a boat a wade of up to 1.25 m +# within ten metres, from a horse beside its right flank. False (and said) when a boat is too far out. +export function vehicle_dismount() -> bool { + let v = ve_cur + if v == null { return false } + if v.kind == VEHICLE_BOAT { + if not VehicleRider.safe_spot(v.x, v.z, VehicleWorld.water(v.x, v.z), 1.25) { + ve_say(VEHICLE_NO_LANDING, v, 0.0) + return false + } + VehicleRider.step_off(VehicleRider.safe_x(), VehicleRider.safe_z()) + } else { + let wx = v.x + Math.cos(v.yaw) * 1.2 + let wz = v.z - Math.sin(v.yaw) * 1.2 + if VehicleRider.safe_spot(wx, wz, VehicleWorld.ground(v.x, v.z), 0.0) { + VehicleRider.step_off(VehicleRider.safe_x(), VehicleRider.safe_z()) + } else { VehicleRider.step_off(v.x, v.z) } + v.home_x = v.x + v.home_z = v.z + } + ve_cur = null + v.rider = -1 + v.speed = 0.0 + ve_say(VEHICLE_DISMOUNTED, v, 0.0) + return true +} + +# off whatever this player is on, wherever it is (a load, a collapse, a map change) +export function vehicle_drop() -> void { + if ve_cur != null { + ve_cur.rider = -1 + ve_cur.speed = 0.0 + } + ve_cur = null +} diff --git a/packages/ludic.vehicles/package.ludic b/packages/ludic.vehicles/package.ludic new file mode 100644 index 00000000..66a4d394 --- /dev/null +++ b/packages/ludic.vehicles/package.ludic @@ -0,0 +1,5 @@ +# ludic.vehicles - boats and horses each player owns: called to a berth or a bay, mounted and got +# off onto a safe spot, ridden with a stroke or a gallop. Uses ludic.base and nothing else. +package "ludic.vehicles" +version "0.1.0" +kind source diff --git a/packages/ludic.vehicles/places.ludic b/packages/ludic.vehicles/places.ludic new file mode 100644 index 00000000..3a0e3a2d --- /dev/null +++ b/packages/ludic.vehicles/places.ludic @@ -0,0 +1,81 @@ +# places.ludic - four berths off the pier head and four bays at the trough. Nobody's boat waits in +# one: a player calls theirs into a place that is empty. A place is taken while a boat lies in it or +# a horse is tied at it, and frees itself the moment it is rowed or ridden away. +export function vehicles_dock_at(x: float, z: float, yaw: float) -> void { + ve_dock_x = x + ve_dock_z = z + ve_dock_yaw = yaw + ve_have_dock = true +} + +export function vehicles_rail_at(x: float, z: float, yaw: float) -> void { + ve_rail_x = x + ve_rail_z = z + ve_rail_yaw = yaw +} + +# which way the pier head faces, for the posts a game stands on it +export function vehicles_dock_yaw() -> float { return ve_dock_yaw } + +function ve_across(k: int) -> float { return -3.3 + float(k) * 2.2 } + +# a berth: 90 cm off the pier head's outer edge, bow out; its post on the deck above it +export function vehicle_berth_x(k: int) -> float { return ve_dock_x - Math.sin(ve_dock_yaw) * 7.25 + Math.cos(ve_dock_yaw) * ve_across(k) } +export function vehicle_berth_z(k: int) -> float { return ve_dock_z - Math.cos(ve_dock_yaw) * 7.25 - Math.sin(ve_dock_yaw) * ve_across(k) } +export function vehicle_post_x(k: int) -> float { return ve_dock_x - Math.sin(ve_dock_yaw) * 6.15 + Math.cos(ve_dock_yaw) * ve_across(k) } +export function vehicle_post_z(k: int) -> float { return ve_dock_z - Math.cos(ve_dock_yaw) * 6.15 - Math.sin(ve_dock_yaw) * ve_across(k) } + +# a bay: off the trough's long side, a trough's length apart - four horses at a feeding trough +# stand shoulder to shoulder along it +function ve_bay_across(k: int) -> float { return -1.44 + float(k) * 0.96 } +export function vehicle_bay_x(k: int) -> float { return ve_rail_x - Math.sin(ve_rail_yaw) * 1.4 + Math.cos(ve_rail_yaw) * ve_bay_across(k) } +export function vehicle_bay_z(k: int) -> float { return ve_rail_z - Math.cos(ve_rail_yaw) * 1.4 - Math.sin(ve_rail_yaw) * ve_bay_across(k) } + +export function vehicle_place_x(kind: int, k: int) -> float { + if kind == VEHICLE_HORSE { return vehicle_bay_x(k) } + return vehicle_berth_x(k) +} + +export function vehicle_place_z(kind: int, k: int) -> float { + if kind == VEHICLE_HORSE { return vehicle_bay_z(k) } + return vehicle_berth_z(k) +} + +# who is in place k: the boat lying in the berth or the horse tied in the bay, or null +export function vehicle_in_place(kind: int, k: int) -> Vehicle { + let px = vehicle_place_x(kind, k) + let pz = vehicle_place_z(kind, k) + let l = ve_all() + for i in 0 .. len(l) { + let v = l[i] + if v.kind != kind or v.rider >= 0 { continue } + # within half a place's spacing: the bays are 0.96 m apart, and a looser test put a horse in two + var vx = v.x + var vz = v.z + var near = 1.2 + if kind == VEHICLE_HORSE { + vx = v.home_x + vz = v.home_z + near = 0.2 + } + if (vx - px) * (vx - px) + (vz - pz) * (vz - pz) < near { return v } + } + return null +} + +# the first free place, or -1 +export function vehicle_free_place(kind: int) -> int { + for k in 0 .. VEHICLE_PLACES { if vehicle_in_place(kind, k) == null { return k } } + return -1 +} + +# this player's own is lying in one of the places +export function vehicle_mine_in_place(kind: int) -> bool { + let mine = vehicle_find(kind) + if mine == null { return false } + for k in 0 .. VEHICLE_PLACES { + let h = vehicle_in_place(kind, k) + if h != null and h.nid == mine.nid { return true } + } + return false +} diff --git a/packages/ludic.vehicles/ports.ludic b/packages/ludic.vehicles/ports.ludic new file mode 100644 index 00000000..ab64bab7 --- /dev/null +++ b/packages/ludic.vehicles/ports.ludic @@ -0,0 +1,47 @@ +# ports.ludic - what a vehicle asks the world and its rider. Unbound, the world is dry flat ground +# with nothing in the way and no wind, and the rider is put exactly where asked. +export port VehicleWorld { + ground: fn(float, float) -> float + water: fn(float, float) -> float = fn ve__dry # the surface over a point + blocked: fn(float, float, float) -> bool = fn ve__open # (x, z, r): a collider in a horse's way + swell: fn(float, float, float, float) -> float = fn ve__calm # (x, z, fx, fz): 0..1 of a stroke lost + wind: fn() -> float = fn ve__zero # 0..1 + wind_yaw: fn() -> float = fn ve__zero # the yaw the wind pushes toward + next_id: fn() -> int = fn ve__next + made: fn(Vehicle) -> void = fn ve__nothing # a new vehicle: draw it + placed: fn(Vehicle) -> void = fn ve__nothing # it moved: put its model there + gone: fn(Vehicle) -> void = fn ve__nothing # its owner left: take it away +} + +# the body on board: the game's walking body (ludic.character, through the game) +export port VehicleRider { + board: fn(float, float, float) -> void = fn ve__board # (x, z, yaw) + ride_at: fn(float, float, float, float, float) -> void = fn ve__ride # (x, y, z, yaw, speed) + step_off: fn(float, float) -> void = fn ve__off # (x, z) + safe_spot: fn(float, float, float, float) -> bool = fn ve__safe # (x, z, from_y, max_depth) + safe_x: fn() -> float = fn ve__safe_x + safe_z: fn() -> float = fn ve__safe_z +} + +var ve_ids: int = 0 +var ve_sx: float = 0.0 +var ve_sz: float = 0.0 +function ve__dry(x: float, z: float) -> float { return -10000.0 } +function ve__open(x: float, z: float, r: float) -> bool { return false } +function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 } +function ve__zero() -> float { return 0.0 } +function ve__next() -> int { + ve_ids += 1 + return ve_ids +} +function ve__nothing(v: Vehicle) -> void { } +function ve__board(x: float, z: float, yaw: float) -> void { } +function ve__ride(x: float, y: float, z: float, yaw: float, speed: float) -> void { } +function ve__off(x: float, z: float) -> void { } +function ve__safe(x: float, z: float, from_y: float, max_depth: float) -> bool { + ve_sx = x + ve_sz = z + return true +} +function ve__safe_x() -> float { return ve_sx } +function ve__safe_z() -> float { return ve_sz } diff --git a/packages/ludic.vehicles/ride.ludic b/packages/ludic.vehicles/ride.ludic new file mode 100644 index 00000000..554cdae5 --- /dev/null +++ b/packages/ludic.vehicles/ride.ludic @@ -0,0 +1,106 @@ +# ride.ludic - the rider's stick steers what they are on: a horse walks, canters on stamina and +# stops at water, a slope or a trunk; a boat rows, drifts on the wind and stops at the shore +const VE_HORSE_WALK: float = 4.5 +const VE_HORSE_GALLOP: float = 11.0 +const VE_HORSE_ACC: float = 4.0 +const VE_HORSE_TURN: float = 1.6 +const VE_BOAT_TOP: float = 3.2 +const VE_BOAT_ACC: float = 1.2 +const VE_BOAT_TURN: float = 1.1 + +export function vehicle_update(ix: float, iz: float, run: bool, dt: float) -> void { + let v = ve_cur + if v == null { return } + var top = VE_BOAT_TOP + var acc = VE_BOAT_ACC + var turn = VE_BOAT_TURN + if v.kind == VEHICLE_HORSE { + acc = VE_HORSE_ACC + turn = VE_HORSE_TURN + top = VE_HORSE_WALK + if run and v.fuel > 5.0 { top = VE_HORSE_GALLOP } + } + v.speed = v.speed + Math.clamp(iz * top - v.speed, -acc * dt, acc * dt) + if v.kind == VEHICLE_BOAT { ve_swell(v, dt) } + # Steering follows SIGNED speed: backing up with the stick left swings the nose right + if Math.abs(v.speed) > 0.3 { + var rate = Math.clamp(Math.abs(v.speed) / 3.0, 0.4, 1.0) + if v.speed < 0.0 { rate = -rate } + v.yaw = v.yaw - ix * turn * (dt * rate) + } + let d = v.speed * dt + ve_travel(v, v.x - Math.sin(v.yaw) * d, v.z - Math.cos(v.yaw) * d, d) + if v.kind == VEHICLE_HORSE { ve_stamina(v, d, dt) } else { ve_float(v, d, dt) } + VehicleWorld.placed(v) + VehicleRider.ride_at(v.x, v.y + v.seat_h, v.z, v.yaw, Math.abs(v.speed)) +} + +# a boat only where there is water under it; a horse only on dry ground, up a walkable rise, clear +function ve_travel(v: Vehicle, nx: float, nz: float, d: float) -> void { + let nh = VehicleWorld.ground(nx, nz) + var ok = true + if v.kind == VEHICLE_BOAT { ok = nh < VehicleWorld.water(nx, nz) - 0.35 } else { + if nh < VehicleWorld.water(nx, nz) + 0.4 { ok = false } + if nh - v.y > Math.abs(d) * 0.9 { ok = false } + if VehicleWorld.blocked(nx, nz, 0.8) { ok = false } + } + if not ok { + v.speed = 0.0 + return + } + v.x = nx + v.z = nz + if v.kind != VEHICLE_BOAT { v.y = nh } + if Math.abs(d) > 0.0 { ve_say(VEHICLE_MOVED, v, Math.abs(d)) } +} + +# a boat gets a longer leash than a swimmer and the same current at the end of it: the outward +# part of the stroke only, so a boat at the limit can always be put about +function ve_swell(v: Vehicle, dt: float) -> void { + var fx = -Math.sin(v.yaw) + var fz = -Math.cos(v.yaw) + if v.speed < 0.0 { + fx = -fx + fz = -fz + } + let take = Math.clamp(VehicleWorld.swell(v.x, v.z, fx, fz), 0.0, 1.0) + v.speed = v.speed * (1.0 - take) + if take > 0.55 and ve_swell_said < 0.0 { + ve_swell_said = 14.0 + ve_say(VEHICLE_SWELL, v, take) + } + ve_swell_said = ve_swell_said - dt +} + +# a gallop spends stamina and a walk gives it back; every metre costs hay +function ve_stamina(v: Vehicle, d: float, dt: float) -> void { + if Math.abs(v.speed) > 6.0 { v.fuel = Math.max(v.fuel - 6.0 * dt, 0.0) } else { v.fuel = Math.min(v.fuel + 3.0 * dt, 100.0) } + v.food = Math.max(v.food - Math.abs(d) * 0.02, 0.0) +} + +# the bob on the water, higher in a wind, and the wind pushing the boat across the lake +function ve_float(v: Vehicle, d: float, dt: float) -> void { + v.bob = v.bob + dt + let w = VehicleWorld.wind() + v.y = VehicleWorld.water(v.x, v.z) + (0.02 + (0.05 + 0.12 * w) * Math.sin(v.bob * 1.4)) + let push = w * 0.6 * dt + let wx = v.x - Math.sin(VehicleWorld.wind_yaw()) * push + let wz = v.z - Math.cos(VehicleWorld.wind_yaw()) * push + if VehicleWorld.ground(wx, wz) < VehicleWorld.water(wx, wz) - 0.35 { + v.x = wx + v.z = wz + } +} + +# the boats nobody is in ride the swell where they lie +export function vehicles_tick(dt: float) -> void { + let l = ve_all() + for i in 0 .. len(l) { + let v = l[i] + if v.kind == VEHICLE_BOAT and v.rider < 0 { + v.bob = v.bob + dt + v.y = VehicleWorld.water(v.x, v.z) + (0.02 + 0.04 * Math.sin(v.bob * 1.1)) + VehicleWorld.placed(v) + } + } +} diff --git a/packages/ludic.vehicles/state.ludic b/packages/ludic.vehicles/state.ludic new file mode 100644 index 00000000..e01418ab --- /dev/null +++ b/packages/ludic.vehicles/state.ludic @@ -0,0 +1,74 @@ +# state.ludic - every vehicle, the one this machine's player is on, where the places are measured +# from, and the facts +var ve_list: []Vehicle = null +var ve_cur: Vehicle = null +var ve_dock_x: float = 0.0 +var ve_dock_z: float = 0.0 +var ve_dock_yaw: float = 0.0 +var ve_rail_x: float = 0.0 +var ve_rail_z: float = 0.0 +var ve_rail_yaw: float = 0.0 +var ve_have_dock: bool = false +var ve_swell_said: float = 0.0 +var ve_fact_q: Queue = null +const ve_pi: float = 3.14159265 + +export function vehicle_facts() -> Queue { + if ve_fact_q == null { ve_fact_q = queue_new("vehicles.facts") } + return ve_fact_q +} + +function ve_say(what: int, v: Vehicle, n: float) -> void { + let f = new VehicleFact + f.what = what + f.nid = v.nid + f.kind = v.kind + f.owner = v.owner + f.x = v.x + f.z = v.z + f.v = n + q_push(vehicle_facts(), f) +} + +function ve_all() -> []Vehicle { + if ve_list == null { ve_list = new []Vehicle } + return ve_list +} + +# every vehicle in the valley, to draw and to aim at (change one only through the verbs) +export function vehicles_all() -> []Vehicle { return ve_all() } + +# the kind this machine's player is riding, 0 none +export function vehicle_ride_kind() -> int { + if ve_cur == null { return 0 } + return ve_cur.kind +} + +export function vehicle_current() -> Vehicle { return ve_cur } + +export function vehicle_of(pid: int, kind: int) -> Vehicle { + let l = ve_all() + for i in 0 .. len(l) { if l[i].kind == kind and l[i].owner == pid { return l[i] } } + return null +} + +# this player's own boat or horse +export function vehicle_find(kind: int) -> Vehicle { return vehicle_of(0, kind) } + +# the nearest vehicle nobody is on within `reach` of (x, z); nothing while riding +export function vehicle_nearest(x: float, z: float, reach: float) -> Vehicle { + if ve_cur != null { return null } + var best: Vehicle = null + var bd = reach + let l = ve_all() + for i in 0 .. len(l) { + let v = l[i] + if v.rider >= 0 { continue } + let d = Math.sqrt((v.x - x) * (v.x - x) + (v.z - z) * (v.z - z)) + if d < bd { + bd = d + best = v + } + } + return best +} diff --git a/packages/ludic.vehicles/system.ludic b/packages/ludic.vehicles/system.ludic new file mode 100644 index 00000000..c09bed89 --- /dev/null +++ b/packages/ludic.vehicles/system.ludic @@ -0,0 +1,38 @@ +# system.ludic - vehicles as a system: a reset, and this player's own saved by kind (hired or not, +# and the horse's hay); where they lie is not saved - they are called again. The idle boats' bob is +# vehicles_tick, which the game calls where the world is run +export function vehicles_reset() -> void { + ve_list = new []Vehicle + ve_cur = null + ve_have_dock = false + ve_swell_said = 0.0 + ve_kept_horse = false + ve_kept_boat = false + ve_kept_food = 40.0 + q_clear(vehicle_facts()) +} + +export function vehicles_save() -> Val { + let v = Value.object() + let h = vehicle_find(VEHICLE_HORSE) + let b = vehicle_find(VEHICLE_BOAT) + if h != null { + sv_put_bool(v, "horse_own", h.owned) + sv_put_float(v, "horse_food", h.food) + } + if b != null { sv_put_bool(v, "boat_own", b.owned) } + return v +} + +export function vehicles_load(v: Val, version: int) -> void { + vehicle_drop() + vehicle_restore(VEHICLE_HORSE, sv_bool(v, "horse_own", false), sv_float(v, "horse_food", 40.0)) + vehicle_restore(VEHICLE_BOAT, sv_bool(v, "boat_own", false), 0.0) +} + +export function vehicles_system() -> System { + let s = system_new("vehicles", PH_SIMULATE) + s.save = fn vehicles_save + s.load = fn vehicles_load + return s +} diff --git a/packages/ludic.vehicles/tests/vehicles_test.ludic b/packages/ludic.vehicles/tests/vehicles_test.ludic new file mode 100644 index 00000000..3a9f7927 --- /dev/null +++ b/packages/ludic.vehicles/tests/vehicles_test.ludic @@ -0,0 +1,237 @@ +# vehicles_test.ludic - a fake shore: land at x < 10, a lake past it deepening a metre a metre, a +# pier head at the shore facing out and a trough on the meadow, a trunk in a horse's way. Calling to +# berths and bays, the owner and the rider, mounting a hungry horse, riding both, the swell, the wind, +# getting off near a shore and far from one, a player leaving, and the save. +import "ludic.vehicles" +import "ludic.base" +program VehiclesTest { + numbers float + + function fk_ground(x: float, z: float) -> float { + if x > 10.0 { return -(x - 10.0) } + return 0.0 + } + function fk_water(x: float, z: float) -> float { + if x > 9.5 { return 0.0 } + return -10000.0 + } + function fk_blocked(x: float, z: float, r: float) -> bool { return Math.abs(x) < 1.0 + r and Math.abs(z + 60.0) < 1.0 + r } + var swell: float = 0.0 + function fk_swell(x: float, z: float, fx: float, fz: float) -> float { + if fx > 0.0 { return swell } + return 0.0 + } + var wind: float = 0.0 + function fk_wind() -> float { return wind } + var made: int = 0 + var gone: int = 0 + var placed: int = 0 + function fk_made(v: Vehicle) -> void { made += 1 } + function fk_gone(v: Vehicle) -> void { gone += 1 } + function fk_placed(v: Vehicle) -> void { placed += 1 } + + # the rider: where it was put, and a safe spot that walks back toward the shore + var rx: float = 0.0 + var ry: float = 0.0 + var rz: float = 0.0 + var boarded: int = 0 + var sx: float = 0.0 + var sz: float = 0.0 + function fk_board(x: float, z: float, yaw: float) -> void { boarded += 1 } + function fk_ride(x: float, y: float, z: float, yaw: float, speed: float) -> void { + rx = x + ry = y + rz = z + } + function fk_off(x: float, z: float) -> void { + rx = x + rz = z + ry = fk_ground(x, z) + } + function fk_safe(x: float, z: float, from_y: float, max_depth: float) -> bool { + for s in 0 .. 11 { + let px = x - float(s) + let depth = fk_water(px, z) - fk_ground(px, z) + if not (depth > max_depth) { + sx = px + sz = z + return true + } + } + return false + } + function fk_sx() -> float { return sx } + function fk_sz() -> float { return sz } + + bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, blocked: fn fk_blocked, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed } + bind VehicleRider { board: fn fk_board, ride_at: fn fk_ride, step_off: fn fk_off, safe_spot: fn fk_safe, safe_x: fn fk_sx, safe_z: fn fk_sz } + + const EAST: float = -1.5707963 + + function fresh() -> void { + vehicles_reset() + vehicles_dock_at(10.0, 0.0, EAST) + vehicles_rail_at(0.0, 5.0, 0.0) + made = 0 + gone = 0 + swell = 0.0 + wind = 0.0 + } + function facts() -> []VehicleFact { return q_drain(vehicle_facts()) } + function count(fs: []VehicleFact, what: int) -> int { + var n = 0 + for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } + return n + } + function ride(ix: float, iz: float, run: bool, secs: float) -> void { + for i in 0 .. int(secs * 60.0) { vehicle_update(ix, iz, run, 1.0 / 60.0) } + } + + test "a berth is off the pier head in the water, a bay on the meadow" { + fresh() + expect_near(vehicle_berth_x(0), 17.25, 0.01) + expect_near(vehicle_berth_z(0), -3.3, 0.01) + expect_near(vehicle_post_x(3), 16.15, 0.01) + expect_near(vehicle_bay_z(2), 3.6, 0.01) + } + + test "the first call makes a vehicle, a later one fetches it, and a taken place refuses" { + fresh() + expect(vehicle_call_for(0, VEHICLE_BOAT, 0)) + expect_eq(made, 1) + let b = vehicle_find(VEHICLE_BOAT) + expect(b != null) + expect_near(b.y, 0.02, 0.001) + expect(vehicle_call_for(0, VEHICLE_BOAT, 0)) + expect_eq(made, 1) + expect(not vehicle_call_for(2, VEHICLE_BOAT, 0)) + expect(vehicle_call_for(2, VEHICLE_BOAT, 1)) + expect_eq(made, 2) + expect(vehicle_call_for(0, VEHICLE_BOAT, 2)) + expect(vehicle_in_place(VEHICLE_BOAT, 0) == null) + expect_eq(vehicle_free_place(VEHICLE_BOAT), 0) + expect(vehicle_mine_in_place(VEHICLE_BOAT)) + } + + test "a player leaving takes theirs away; this machine's player never leaves" { + fresh() + vehicle_call_for(0, VEHICLE_HORSE, 0) + vehicle_call_for(3, VEHICLE_HORSE, 1) + vehicles_release(0) + expect_eq(len(vehicles_all()), 2) + vehicles_release(3) + expect_eq(len(vehicles_all()), 1) + expect_eq(gone, 1) + expect(vehicle_in_place(VEHICLE_HORSE, 1) == null) + } + + test "a hungry horse will not go; a fed one is mounted, and only by its owner" { + fresh() + vehicle_call_for(0, VEHICLE_HORSE, 0) + vehicle_call_for(4, VEHICLE_HORSE, 1) + let h = vehicle_find(VEHICLE_HORSE) + h.food = 5.0 + expect(not vehicle_mount(h)) + expect_eq(count(facts(), VEHICLE_HUNGRY), 1) + vehicle_feed(h, 50.0) + expect_near(h.food, 55.0, 0.001) + expect(not vehicle_mount(vehicle_of(4, VEHICLE_HORSE))) + boarded = 0 + expect(vehicle_mount(h)) + expect_eq(boarded, 1) + expect_eq(vehicle_ride_kind(), VEHICLE_HORSE) + expect_eq(h.rider, 0) + expect(vehicle_nearest(h.x, h.z, 5.0) == null) + expect(vehicle_in_place(VEHICLE_HORSE, 0) == null) + } + + test "a horse walks, gallops on stamina, stops at the water and at a trunk" { + fresh() + vehicle_call_for(0, VEHICLE_HORSE, 0) + let h = vehicle_find(VEHICLE_HORSE) + vehicle_mount(h) + ride(0.0, 1.0, false, 3.0) + expect_near(h.speed, 4.5, 0.05) + expect(count(facts(), VEHICLE_MOVED) > 100) + ride(0.0, 1.0, true, 3.0) + expect_near(h.speed, 11.0, 0.1) + expect(h.fuel < 100.0) + expect(h.z < -15.0) + expect_near(rz, h.z, 0.001) + expect_near(ry, 1.35, 0.001) + # the trunk at z = -60 stops it + ride(0.0, 1.0, true, 6.0) + expect(h.z > -60.0 + 1.7) + expect_near(h.speed, 0.0, 0.001) + # and the lake to the east does too + vehicle_drop() + h.x = 7.0 + h.z = 0.0 + h.yaw = EAST + vehicle_mount(h) + ride(0.0, 1.0, false, 4.0) + expect(h.x < 9.6) + } + + test "a boat rows on water, stops at the shore, loses its stroke to the swell, drifts on the wind" { + fresh() + vehicle_call_for(0, VEHICLE_BOAT, 0) + let b = vehicle_find(VEHICLE_BOAT) + vehicle_mount(b) + ride(0.0, 1.0, false, 2.0) + let x2 = b.x + expect(x2 > 18.0) + swell = 1.0 + ride(0.0, 1.0, false, 1.0) + expect_near(b.x, x2, 0.2) + expect_eq(count(facts(), VEHICLE_SWELL), 1) + swell = 0.0 + ride(0.0, -1.0, false, 12.0) + expect(b.x > 10.3) + wind = 1.0 + let z0 = b.z + ride(0.0, 0.0, false, 1.0) + expect(b.z < z0) + } + + test "getting off a boat needs a shore near enough; a horse is left tied where it stands" { + fresh() + vehicle_call_for(0, VEHICLE_BOAT, 0) + let b = vehicle_find(VEHICLE_BOAT) + vehicle_mount(b) + b.x = 40.0 + expect(not vehicle_dismount()) + expect_eq(count(facts(), VEHICLE_NO_LANDING), 1) + expect_eq(vehicle_ride_kind(), VEHICLE_BOAT) + b.x = 12.0 + expect(vehicle_dismount()) + expect_near(rx, 11.0, 0.001) + expect_eq(vehicle_ride_kind(), 0) + expect_eq(b.rider, -1) + vehicle_call_for(0, VEHICLE_HORSE, 0) + let h = vehicle_find(VEHICLE_HORSE) + vehicle_mount(h) + h.x = -5.0 + h.z = -5.0 + expect(vehicle_dismount()) + expect_near(h.home_x, -5.0, 0.001) + expect(vehicle_in_place(VEHICLE_HORSE, 0) == null) + expect_near(rx, -5.0 + 1.2, 0.001) + } + + test "this player's own are saved by kind, and a save read before they are made still applies" { + fresh() + vehicle_call_for(0, VEHICLE_HORSE, 0) + vehicle_call_for(0, VEHICLE_BOAT, 0) + vehicle_own(vehicle_find(VEHICLE_HORSE)) + vehicle_find(VEHICLE_HORSE).food = 77.0 + let saved = vehicles_save() + fresh() + vehicles_load(saved, 1) + vehicle_call_for(0, VEHICLE_HORSE, 0) + vehicle_call_for(0, VEHICLE_BOAT, 0) + expect(vehicle_find(VEHICLE_HORSE).owned) + expect_near(vehicle_find(VEHICLE_HORSE).food, 77.0, 0.01) + expect(not vehicle_find(VEHICLE_BOAT).owned) + } +}