vibes
This commit is contained in:
@@ -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{}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user