This commit is contained in:
root
2026-10-01 22:16:48 +02:00
parent b6d1909d7f
commit 9dfdf79fa7
39 changed files with 4560 additions and 181 deletions
+289
View File
@@ -0,0 +1,289 @@
package sim
import (
"halcyon/config"
"halcyon/fixed"
"halcyon/world"
)
// Ore travels in containers. A ship has no hold of its own: it tows containers
// behind it, and every kilogram of them (the container and the ore in it) adds
// to the mass its engine has to push. Containers belong to no one: any ship can
// tow any container that is adrift near it.
const (
ContainerMassKg = 500 // an empty container
ContainerCapKg = 2000 // ore one container holds
ContainerRangeKm = 10 // "near the ship": how close a container must be to be attached
MaxOrder = 10 // empty containers the station delivers per request
MaxContainersPerPlayer = 50 // the station will not deliver past this many of a player's ordered containers
ContainerLifeDays = 10 // a container adrift this long vanishes, so lost ones cannot pile up
)
// Container is a free-floating body. It has no owner.
type Container struct {
ID int64
// OrderedBy is the player who had the station deliver it. It is bookkeeping for
// the ordering allowance only (MaxContainersPerPlayer): it gives no right to the
// container, which anyone may tow, and a sale ends it.
OrderedBy int64
Pos fixed.Vec
Vel fixed.Vec
Ore [world.NumOre]int64 // kg of each ore inside
Towed int64 // the ship towing it, or 0 while it drifts on its own
// Dropped is the ship that last let it go: when the container is sold, that ship
// and its player are paid.
Dropped int64
// ExpiresAt is when a container that stays adrift vanishes (seconds since the
// world began); 0 while it is in tow.
ExpiresAt int64
}
func (c *Container) OreTotal() int64 {
var t int64
for _, v := range c.Ore {
t += v
}
return t
}
// Mass is the container plus the ore in it, in kg.
func (c *Container) Mass() int64 { return ContainerMassKg + c.OreTotal() }
// Room is how much more ore fits, in kg.
func (c *Container) Room() int64 { return ContainerCapKg - c.OreTotal() }
// Tow summarises the containers a ship is towing.
type Tow struct {
Count int
Ore [world.NumOre]int64
OreTotal int64
Room int64
Mass int64
}
func (s *State) towOf(shipID int64) Tow {
var t Tow
for _, c := range s.Containers {
if c.Towed != shipID {
continue
}
t.Count++
for o, kg := range c.Ore {
t.Ore[o] += kg
}
t.OreTotal += c.OreTotal()
t.Room += c.Room()
t.Mass += c.Mass()
}
return t
}
// shipMass is everything the engine pushes: hull, fuel and towed containers.
func (s *State) shipMass(sh *Ship) fixed.F {
return sh.baseMass() + fixed.FromInt(s.towOf(sh.ID).Mass)
}
func (s *State) newContainer(orderedBy int64, pos, vel fixed.Vec) *Container {
s.NextContainer++
c := &Container{ID: s.NextContainer, OrderedBy: orderedBy, Pos: pos, Vel: vel}
s.Containers = append(s.Containers, c) // IDs only grow, so the slice stays sorted
return c
}
// canAttach reports whether container c is adrift and near enough to take in tow.
func (s *State) canAttach(sh *Ship, c *Container) bool {
return c.Towed == 0 && s.canReach(sh, c.Pos, c.Vel, ContainerRangeKm)
}
// nearbyFree counts the drifting containers the ship could attach now.
func (s *State) nearbyFree(sh *Ship) int {
n := 0
for _, c := range s.Containers {
if s.canAttach(sh, c) {
n++
}
}
return n
}
// attach takes every drifting container near the ship in tow.
func (s *State) attach(sh *Ship, tc *tickCtx) {
n := 0
for _, c := range s.Containers {
if s.canAttach(sh, c) {
c.Towed, c.Pos, c.Vel, c.ExpiresAt = sh.ID, sh.Pos, sh.Vel, 0
n++
}
}
if n > 0 {
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "attach", itoa(int64(n)) + " containers"})
}
}
// detach lets go of everything the ship is towing. The containers keep drifting
// with the ship's velocity.
func (s *State) detach(sh *Ship, tc *tickCtx) {
n := 0
for _, c := range s.Containers {
if c.Towed == sh.ID {
s.letGo(c, sh.ID, tc.now)
n++
}
}
if n > 0 {
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "detach", itoa(int64(n)) + " containers"})
}
}
// order has the station deliver up to n empty containers to the ship's position,
// provided the ship is close to the station. It returns how many were delivered.
func (s *State) order(sh *Ship, n int, tc *tickCtx) int {
if n > MaxOrder {
n = MaxOrder
}
if n <= 0 || sh.Pos.Sub(tc.stPos).Len() > fixed.FromInt(DockRangeKm) {
return 0
}
owned := 0
for _, c := range s.Containers {
if c.OrderedBy == sh.Owner {
owned++
}
}
n = min(n, MaxContainersPerPlayer-owned)
for i := 0; i < n; i++ {
c := s.newContainer(sh.Owner, sh.Pos, sh.Vel)
c.ExpiresAt = tc.now + ContainerLifeDays*config.SecondsPerDay // delivered adrift
}
if n > 0 {
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "delivery", itoa(int64(n)) + " containers"})
}
return n
}
// fillTow pours mined ore into the ship's containers, first come first filled.
// The caller has checked that it all fits.
func (s *State) fillTow(shipID int64, taken [world.NumOre]int64) {
for o, kg := range taken {
for _, c := range s.Containers {
if kg == 0 {
break
}
if c.Towed != shipID {
continue
}
put := min(kg, c.Room())
c.Ore[o] += put
kg -= put
}
}
}
// syncTow keeps a ship's containers where the ship is.
func (s *State) syncTow(sh *Ship) {
for _, c := range s.Containers {
if c.Towed == sh.ID {
c.Pos, c.Vel = sh.Pos, sh.Vel
}
}
}
// releaseTow sets a lost ship's containers adrift.
func (s *State) releaseTow(sh *Ship, now int64) {
for _, c := range s.Containers {
if c.Towed == sh.ID {
s.letGo(c, sh.ID, now)
}
}
}
// letGo sets a towed container adrift and starts its ten days.
func (s *State) letGo(c *Container, ship, now int64) {
c.Towed, c.Dropped = 0, ship
c.ExpiresAt = now + ContainerLifeDays*config.SecondsPerDay
}
// expireContainers removes containers that have been adrift for their ten days,
// with whatever ore they hold. A container adrift with no timer at all (from a
// snapshot made before there was one) is given a full ten days from now.
func (s *State) expireContainers(tc *tickCtx) {
kept := s.Containers[:0]
for _, c := range s.Containers {
if c.Towed == 0 {
if c.ExpiresAt == 0 {
c.ExpiresAt = tc.now + ContainerLifeDays*config.SecondsPerDay
} else if tc.now >= c.ExpiresAt {
tc.res.Events = append(tc.res.Events, Event{tc.tick, c.Dropped, "expired", "container " + itoa(c.ID) + " adrift for " + itoa(ContainerLifeDays) + " days"})
continue
}
}
kept = append(kept, c)
}
s.Containers = kept
}
// sellNearStation buys every drifting container that has ore in it and is close
// to the station, at the tick's positions. The station takes the container along
// with the ore, so a sold container is gone. The ship that last let it go, and its
// player, are paid. Empty containers are not bought.
func (s *State) sellNearStation(tc *tickCtx) {
kept := s.Containers[:0]
for _, c := range s.Containers {
payee := s.ship(c.Dropped)
if c.Towed != 0 || c.OreTotal() == 0 || payee == nil || c.Pos.Sub(tc.stPos).Len() > fixed.FromInt(DockRangeKm) {
kept = append(kept, c)
continue
}
value := s.Market.Value(c.Ore)
s.Credits[payee.Owner] += value
payee.Earned += value
for o, kg := range c.Ore {
tc.sold[o] += kg
}
tc.res.Events = append(tc.res.Events, Event{tc.tick, payee.ID, "sold", itoa(value) + " credits (container " + itoa(c.ID) + " taken)"})
}
s.Containers = kept
}
// driftContainers moves the containers nobody is towing. The star pulls them
// exactly as it pulls ships; one that falls in is lost with its ore.
func (s *State) driftContainers(orbitSpeed fixed.F, dt int64, tick int, res *DayResult) {
dtF := fixed.FromInt(dt)
kept := s.Containers[:0]
for _, c := range s.Containers {
if c.Towed == 0 {
r := c.Pos.Len()
if r < fixed.FromInt(StarRadiusKm) {
res.Events = append(res.Events, Event{tick, c.Dropped, "lost", "container " + itoa(c.ID) + " fell into the star"})
continue
}
g := orbitSpeed.Mul(orbitSpeed).Mul(dtF).Div(r)
c.Vel = c.Vel.Sub(c.Pos.Unit().Scale(g))
c.Pos = c.Pos.Add(c.Vel.Scale(dtF))
}
kept = append(kept, c)
}
s.Containers = kept
}
// migrateLegacyCargo turns ore that a ship still carries in the cargo hold of
// the earlier design into towed containers, so nothing is lost across the change.
func (s *State) migrateLegacyCargo() {
for _, sh := range s.Ships {
var left int64
for _, kg := range sh.Cargo {
left += kg
}
for left > 0 {
c := s.newContainer(0, sh.Pos, sh.Vel) // not counted against anyone's ordering allowance
c.Towed = sh.ID
for o := range sh.Cargo {
put := min(sh.Cargo[o], c.Room())
c.Ore[o] += put
sh.Cargo[o] -= put
left -= put
}
}
sh.Cargo = [world.NumOre]int64{}
}
}