290 lines
8.6 KiB
Go
290 lines
8.6 KiB
Go
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{}
|
|
}
|
|
}
|