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{} } }