diff --git a/changes/vehicles-spec.md b/changes/vehicles-spec.md new file mode 100644 index 00000000..5eb34adf --- /dev/null +++ b/changes/vehicles-spec.md @@ -0,0 +1,8 @@ +bump: minor +type: feature +**`ludic.vehicles`: a kind's numbers are the game's.** The seat height, a horse's walk, gallop, +acceleration and turn, its stamina and hay, a boat's stroke, turn and the wind on it, and where a rider +gets off (the horse's flank, the boat's wade) were constants in the package; they are a `VehicleSpec` a +game hands each kind with `vehicles_spec(kind, spec)` before any vehicle of it is called, and +`vehicle_spec(kind)` asks them. A kind with no spec cannot be called, and a reset keeps the specs. +`VEHICLE_HAY_MIN` is gone: it is the horse spec's `hay_min`. diff --git a/packages/ludic.vehicles/README.md b/packages/ludic.vehicles/README.md index ff25aa91..632681a8 100644 --- a/packages/ludic.vehicles/README.md +++ b/packages/ludic.vehicles/README.md @@ -16,12 +16,17 @@ through the game). ## The rules it keeps +- **A kind's numbers are the game's data.** The package keeps the rules (a walk and a gallop on + stamina, a stroke and a turn, where a rider gets off) and no numbers: the game hands each kind a + `VehicleSpec` with `vehicles_spec(kind, spec)` - in Maroon Lake from its `VehicleKinds` table - + once, before any vehicle is made. A kind with no spec cannot be called; a reset keeps the specs. + - **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 + `safe_spot` - a wade of up to the spec's `off_depth` within its search from a boat, `off_side` out + by 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. - **A boat is a body in the game's physics** (`VehicleHull`, phase 16; a buoyant Jolt hull in Maroon Lake). The package rows it - the stick forward and back is a stroke along the bow, left and right @@ -66,7 +71,9 @@ export port VehicleRider { | | | | --- | --- | -| `VEHICLE_HORSE`, `VEHICLE_BOAT`, `VEHICLE_PLACES`, `VEHICLE_HAY_MIN` | the kinds, four places each, and the hay a horse needs to go | +| `VEHICLE_HORSE`, `VEHICLE_BOAT`, `VEHICLE_PLACES`, `VEHICLE_KINDS` | the kinds, four places each, and how many kinds may have a spec | +| `VehicleSpec { seat_h, walk, run, acc, turn, row, row_turn, wind, hay_per_m, hay_min, run_min, tire_at, tire, rest, off_side, off_depth }` | a kind's numbers: the seat, a horse's pace, stamina and hay, a boat's oars and wind, where one gets off | +| `vehicles_spec(kind, spec)`, `vehicle_spec(kind)` | set a kind's numbers (at boot), and ask them | | `Vehicle { kind, nid, x, y, z, yaw, speed, fuel, food, seat_h, 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 | @@ -85,7 +92,8 @@ export port VehicleRider { 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 +A fake shore with a pier head and a trough, and Maroon Lake's numbers as the specs: a kind with no +spec refused and a spec's walk and seat obeyed, 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; a real Jolt hull rowed out, losing its stroke to the swell, turned, stopped by the shore's bed and drifted by 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 index 91a4d583..0290cfbb 100644 --- a/packages/ludic.vehicles/call.ludic +++ b/packages/ludic.vehicles/call.ludic @@ -3,17 +3,17 @@ @alloc_ok("a player's own boat or horse, made the first time it is called and fetched after") function ve_add(vehicles_st: VehiclesState, kind: int, owner: int, x: float, z: float, yaw: float) -> Vehicle { if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } + let spec = vehicle_spec(vehicles_st, kind) + if spec == null { return null } let v = new Vehicle v.nid = VehicleWorld.next_id() v.kind = kind v.owner = owner v.yaw = yaw + v.seat_h = spec.seat_h 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) ve_file(vehicles_st.ve, v) diff --git a/packages/ludic.vehicles/horse.ludic b/packages/ludic.vehicles/horse.ludic index 16c7b7a6..e5d9b3f7 100644 --- a/packages/ludic.vehicles/horse.ludic +++ b/packages/ludic.vehicles/horse.ludic @@ -1,19 +1,16 @@ # horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it will # not step into water, and its legs (VehicleLegs) meet the ground and whatever stands in its way -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 - +# (its numbers are its kind's VehicleSpec: walk, run, acc, turn, the stamina and the hay) function ve_ride_horse(vehicles_st: mut VehiclesState, v: Vehicle, ix: float, iz: float, run: bool, dt: float) -> void { - var 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, -VE_HORSE_ACC * dt, VE_HORSE_ACC * dt) + let sp = vehicle_spec(vehicles_st, v.kind) + var top = sp.walk + if run and v.fuel > sp.run_min { top = sp.run } + v.speed = v.speed + Math.clamp(iz * top - v.speed, -sp.acc * dt, sp.acc * 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 * VE_HORSE_TURN * (dt * rate) + v.yaw = v.yaw - ix * sp.turn * (dt * rate) } let fx = -Math.sin(v.yaw) let fz = -Math.cos(v.yaw) @@ -24,7 +21,7 @@ function ve_ride_horse(vehicles_st: mut VehiclesState, v: Vehicle, ix: float, iz return } ve_legs_walk(vehicles_st, v, fx * v.speed, fz * v.speed, dt) - ve_stamina(v, d, dt) + ve_stamina(sp, v, d, dt) } # the legs carry it from where it is; what they covered along its heading is its speed now, never @@ -43,8 +40,8 @@ function ve_legs_walk(vehicles_st: mut VehiclesState, v: Vehicle, vx: float, vz: } # 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) +function ve_stamina(sp: VehicleSpec, v: Vehicle, d: float, dt: float) -> void { + if Math.abs(v.speed) > sp.tire_at { v.fuel = Math.max(v.fuel - sp.tire * dt, 0.0) } else { v.fuel = Math.min(v.fuel + sp.rest * dt, 100.0) } + v.food = Math.max(v.food - Math.abs(d) * sp.hay_per_m, 0.0) } diff --git a/packages/ludic.vehicles/index.ludic b/packages/ludic.vehicles/index.ludic index 6e641dda..3c4d2427 100644 --- a/packages/ludic.vehicles/index.ludic +++ b/packages/ludic.vehicles/index.ludic @@ -5,6 +5,7 @@ module ludic_vehicles uses ludic_base numbers float import "ludic.base" import "kinds.ludic" +import "spec.ludic" import "table.ludic" import "ports.ludic" import "fact_pool.ludic" diff --git a/packages/ludic.vehicles/mount.ludic b/packages/ludic.vehicles/mount.ludic index bac79cae..7203cc5b 100644 --- a/packages/ludic.vehicles/mount.ludic +++ b/packages/ludic.vehicles/mount.ludic @@ -1,12 +1,10 @@ # 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(vehicles_st: mut VehiclesState, v: Vehicle) -> bool { if v == null or v.owner != 0 or v.rider >= 0 or vehicles_st.ve_cur != null { return false } - if v.kind == VEHICLE_HORSE and v.food < VEHICLE_HAY_MIN { + if v.kind == VEHICLE_HORSE and v.food < vehicle_spec(vehicles_st, v.kind).hay_min { ve_say(vehicles_st, VEHICLE_HUNGRY, v, v.food) return false } @@ -17,21 +15,22 @@ export function vehicle_mount(vehicles_st: mut VehiclesState, v: Vehicle) -> boo 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. +# Off, onto the nearest spot a body could stand and walk off: from a boat a wade of up to its spec's +# off_depth, from a horse off_side out by its right flank. False (and said) when a boat is too far out. export function vehicle_dismount(vehicles_st: mut VehiclesState) -> bool { let v = vehicles_st.ve_cur if v == null { return false } + let sp = vehicle_spec(vehicles_st, v.kind) if v.kind == VEHICLE_BOAT { - if not VehicleRider.safe_spot(v.x, v.z, VehicleWorld.water(v.x, v.z), 1.25) { + if not VehicleRider.safe_spot(v.x, v.z, VehicleWorld.water(v.x, v.z), sp.off_depth) { ve_say(vehicles_st, VEHICLE_NO_LANDING, v, 0.0) return false } VehicleRider.step_off(VehicleRider.safe_x(), VehicleRider.safe_z()) ve_moor(v) } else { - let wx = v.x + Math.cos(v.yaw) * 1.2 - let wz = v.z - Math.sin(v.yaw) * 1.2 + let wx = v.x + Math.cos(v.yaw) * sp.off_side + let wz = v.z - Math.sin(v.yaw) * sp.off_side 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) } diff --git a/packages/ludic.vehicles/ports.ludic b/packages/ludic.vehicles/ports.ludic index b60f5419..f46fd94f 100644 --- a/packages/ludic.vehicles/ports.ludic +++ b/packages/ludic.vehicles/ports.ludic @@ -52,6 +52,7 @@ export state VehiclesState { ve_sz: float = 0.0 ve: VehicleTable = ve__new() ve_cur: Vehicle = null + @max(8) ve_specs: []VehicleSpec = ve_specs_new() # each kind's numbers (vehicles_spec), kept across a reset ve_dock_x: float = 0.0 ve_dock_z: float = 0.0 ve_dock_yaw: float = 0.0 diff --git a/packages/ludic.vehicles/ride.ludic b/packages/ludic.vehicles/ride.ludic index 3ea53bcd..3708f74a 100644 --- a/packages/ludic.vehicles/ride.ludic +++ b/packages/ludic.vehicles/ride.ludic @@ -1,9 +1,6 @@ # 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 is rowed - a stroke and a turn handed to its hull, # which the water floats, the wind drifts and the lake bed stops -const VE_ROW: float = 630.0 # N: a full stroke forward, about 3 m/s on Maroon Lake's hull -const VE_ROW_TURN: float = 270.0 # N.m: the oars pulled against each other, about 1.1 rad/s -const VE_WIND: float = 70.0 # N on a boat in a full wind export function vehicle_update(vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, dt: float) -> void { let v = vehicles_st.ve_cur @@ -18,7 +15,8 @@ export function vehicle_update(vehicles_st: mut VehiclesState, ix: float, iz: fl function ve_row(vehicles_st: mut VehiclesState, v: Vehicle, ix: float, iz: float, dt: float) -> void { ve_hull_read(v) let take = ve_swell(vehicles_st, v, iz, dt) - VehicleHull.row(v.nid, iz * VE_ROW * (1.0 - take), -ix * VE_ROW_TURN) + let sp = vehicle_spec(vehicles_st, v.kind) + VehicleHull.row(v.nid, iz * sp.row * (1.0 - take), -ix * sp.row_turn) let d = Math.abs(v.speed) * dt if d > 0.0 { ve_moved_add(vehicles_st, v, d) } } @@ -55,13 +53,14 @@ function ve_swell(vehicles_st: mut VehiclesState, v: Vehicle, iz: float, dt: flo # called or left, its hull read back and held there export function vehicles_tick(vehicles_st: VehiclesState, dt: float) -> void { let l = ve_all(vehicles_st) - let w = VehicleWorld.wind() * VE_WIND + let w = VehicleWorld.wind() let wy = VehicleWorld.wind_yaw() for i in 0 .. len(l) { let v = l[i] if v.kind != VEHICLE_BOAT { continue } if v.rider >= 0 { - if w > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * w, -Math.cos(wy) * w) } + let f = w * vehicle_spec(vehicles_st, v.kind).wind + if f > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * f, -Math.cos(wy) * f) } continue } if not VehicleWorld.active(v.x, v.z) { continue } # outside every zone: left where it lies diff --git a/packages/ludic.vehicles/spec.ludic b/packages/ludic.vehicles/spec.ludic new file mode 100644 index 00000000..a69141e0 --- /dev/null +++ b/packages/ludic.vehicles/spec.ludic @@ -0,0 +1,40 @@ +# spec.ludic - a kind's numbers, handed in by the game from its own table before any vehicle of the +# kind is made (vehicles_spec): where the rider sits, its pace, its oars, its hay, where one gets off +export property VehicleSpec { + seat_h: float = 0.0 # m: the rider sits this far above it + walk: float = 0.0 # m/s: a horse's walk + run: float = 0.0 # m/s: its gallop + acc: float = 0.0 # m/s2: how fast it gets there + turn: float = 0.0 # rad/s at a full stick + row: float = 0.0 # N: a full stroke forward + row_turn: float = 0.0 # N.m: the oars pulled against each other + wind: float = 0.0 # N on it in a full wind + hay_per_m: float = 0.0 # hay (0..100) every metre costs + hay_min: float = 0.0 # with less hay than this it will not go + run_min: float = 0.0 # stamina (0..100) a gallop needs + tire_at: float = 0.0 # m/s: faster than this is a gallop, and tires it + tire: float = 0.0 # stamina a second spent at a gallop + rest: float = 0.0 # and given back a second at a walk + off_side: float = 0.0 # m: a rider gets off this far out to its right + off_depth: float = 0.0 # m: the deepest wade a rider gets off into +} + +export const VEHICLE_KINDS: int = 8 # kinds a game may give numbers to, 0 .. 7 + +function ve_specs_new() -> []VehicleSpec { + let l = new []VehicleSpec + for i in 0 .. VEHICLE_KINDS { push(l, null) } + return l +} + +# kind's numbers from now on; a kind that has none cannot be called +export function vehicles_spec(vehicles_st: mut VehiclesState, kind: int, spec: VehicleSpec) -> void { + if kind < 0 or kind >= VEHICLE_KINDS { return } + vehicles_st.ve_specs[kind] = spec +} + +# a kind's numbers, or null when the game gave it none +export function vehicle_spec(vehicles_st: VehiclesState, kind: int) -> VehicleSpec { + if kind < 0 or kind >= VEHICLE_KINDS { return null } + return vehicles_st.ve_specs[kind] +} diff --git a/packages/ludic.vehicles/tests/vehicles_test.ludic b/packages/ludic.vehicles/tests/vehicles_test.ludic index b14dc079..9b7534cc 100644 --- a/packages/ludic.vehicles/tests/vehicles_test.ludic +++ b/packages/ludic.vehicles/tests/vehicles_test.ludic @@ -158,8 +158,37 @@ program VehiclesTest { const EAST: float = -1.5707963 + # Maroon Lake's numbers for its two kinds (its vehicle_kinds.lres), as a game hands them in + function horse_spec() -> VehicleSpec { + let s = new VehicleSpec + s.seat_h = 1.35 + s.walk = 4.5 + s.run = 11.0 + s.acc = 4.0 + s.turn = 1.6 + s.hay_per_m = 0.02 + s.hay_min = 10.0 + s.run_min = 5.0 + s.tire_at = 6.0 + s.tire = 6.0 + s.rest = 3.0 + s.off_side = 1.2 + return s + } + function boat_spec() -> VehicleSpec { + let s = new VehicleSpec + s.seat_h = 0.30 + s.row = 630.0 + s.row_turn = 270.0 + s.wind = 70.0 + s.off_depth = 1.25 + return s + } + function fresh(vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) -> void { vehicles_reset(vehicles_st) + vehicles_spec(vehicles_st, VEHICLE_HORSE, horse_spec()) + vehicles_spec(vehicles_st, VEHICLE_BOAT, boat_spec()) vehicles_dock_at(vehicles_st, 10.0, 0.0, EAST) vehicles_rail_at(vehicles_st, 0.0, 5.0, 0.0) vehicles_test_st.made = 0 @@ -189,6 +218,27 @@ program VehiclesTest { expect_near(vehicle_bay_z(vehicles_st, 2), 3.6, 0.01) } + test "a kind with no numbers cannot be called; its spec's numbers are the horse's pace and its seat" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { + fresh(vehicles_st, vehicles_test_st) + vehicles_spec(vehicles_st, VEHICLE_BOAT, null) + expect(not vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0)) + let slow = horse_spec() + slow.walk = 2.5 + slow.seat_h = 1.1 + vehicles_spec(vehicles_st, VEHICLE_HORSE, slow) + expect(vehicle_spec(vehicles_st, VEHICLE_HORSE) == slow) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + let h = vehicle_find(vehicles_st, VEHICLE_HORSE) + expect_near(h.seat_h, 1.1, 0.001) + vehicle_mount(vehicles_st, h) + ride(physics_st, vehicles_st, 0.0, 1.0, false, 3.0) + expect_near(h.speed, 2.5, 0.05) + expect_near(vehicles_test_st.ry - h.y, 1.1, 0.001) + vehicle_drop(vehicles_st) + vehicles_reset(vehicles_st) + expect(vehicle_spec(vehicles_st, VEHICLE_HORSE) == slow) # a reset keeps the numbers + } + test "the first call makes a vehicle, a later one fetches it, and a taken place refuses" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) { fresh(vehicles_st, vehicles_test_st) expect(vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0))