/
spawn.go
221 lines (193 loc) · 6.68 KB
/
spawn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// SPDX-FileCopyrightText: 2021 Softbear, Inc.
// SPDX-License-Identifier: AGPL-3.0-or-later
package server
import (
"github.com/SoftbearStudios/mk48/server/world"
"github.com/chewxy/math32"
"math/rand"
"runtime"
"sync"
"sync/atomic"
"time"
)
const (
// barrelRadius is the radius around an oil platform that barrels are counted.
barrelRadius = 125
// max amount of barrels around an oil platform
platformBarrelCount = 12
// platformBarrelSpawnRate is average seconds per barrel spawn.
// Cant be less than spawnPeriod.
platformBarrelSpawnRate = time.Second * 3
// platformBarrelSpawnProb is the probably that a barrel will spawn around an oil platform.
platformBarrelSpawnProb = float64(spawnPeriod) / float64(platformBarrelSpawnRate)
// hq is this many times better than platform
hqFactor = 2
)
// Spawn spawns non-boat/weapon entities such as collectibles and obstacles.
func (h *Hub) Spawn() {
defer h.timeFunction("spawn", time.Now())
// Outputs platforms that should spawn 1 barrel
barrelSpawnerOutput := make(chan world.Vec2f, runtime.NumCPU()*2)
barrelSpawnerPositions := make([]world.Vec2f, 0, 16)
var wait sync.WaitGroup
wait.Add(1)
go func() {
for position := range barrelSpawnerOutput {
barrelSpawnerPositions = append(barrelSpawnerPositions, position)
}
wait.Done()
}()
// Use int64s for atomic ops
currentCrateCount := int64(0)
currentBarrelSpawnerCount := int64(0)
h.world.SetParallel(true)
h.world.ForEntities(func(entity *world.Entity) (stop, remove bool) {
switch entity.Data().Kind {
case world.EntityKindCollectible:
atomic.AddInt64(¤tCrateCount, 1)
case world.EntityKindObstacle:
maxBarrels := 0
spawnProb := 0.0
switch entity.EntityType {
case world.EntityTypeHQ:
maxBarrels = platformBarrelCount * hqFactor
spawnProb = platformBarrelSpawnProb * hqFactor
case world.EntityTypeOilPlatform:
maxBarrels = platformBarrelCount
spawnProb = platformBarrelSpawnProb
}
if maxBarrels > 0 {
if rand.Float64() < spawnProb {
pos := entity.Position
barrelCount := 0
// Count current barrels
h.world.ForEntitiesInRadius(pos, barrelRadius, func(_ float32, entity *world.Entity) (_ bool) {
barrelCount++
return
})
if barrelCount < maxBarrels {
barrelSpawnerOutput <- pos
}
}
atomic.AddInt64(¤tBarrelSpawnerCount, 1)
}
}
return
})
h.world.SetParallel(false)
close(barrelSpawnerOutput)
wait.Wait()
// Spawn barrels
for _, data := range barrelSpawnerPositions {
barrelEntity := &world.Entity{
Transform: world.Transform{
Position: data,
Velocity: world.ToVelocity(rand.Float32()*10 + 10),
Direction: world.ToAngle(rand.Float32() * math32.Pi * 2),
},
EntityType: world.EntityTypeBarrel,
}
h.spawnEntity(barrelEntity, barrelRadius*0.9)
}
// Spawn crates
// Not all at once because then they decay all at once
targetCollectibleCount := world.CrateCountOf(h.clients.Len)
if maxCount := int(currentCrateCount) + 5 + targetCollectibleCount/60; targetCollectibleCount > maxCount {
targetCollectibleCount = maxCount
}
for i := int(currentCrateCount); i < targetCollectibleCount; i++ {
h.spawnEntity(&world.Entity{EntityType: world.EntityTypeCrate}, h.worldRadius)
}
// Spawn oil platforms
targetObstacleCount := world.ObstacleCountOf(h.clients.Len)
for i := int(currentBarrelSpawnerCount); i < targetObstacleCount; i++ {
entity := &world.Entity{EntityType: world.EntityTypeOilPlatform}
h.spawnEntity(entity, h.worldRadius)
}
}
// spawnEntity spawns an entity and sets its owners EntityID if applicable.
// Returns if non zero EntityID if spawned.
// TODO fix this mess
func (h *Hub) spawnEntity(entity *world.Entity, initialRadius float32) world.EntityID {
if initialRadius > 0 {
radius := max(initialRadius, 1)
center := entity.Position
threshold := float32(5.0)
governor := 0
// Always randomize on first iteration
for entity.Position == center || !h.canSpawn(entity, threshold) {
// Pick a new position
position := world.RandomAngle().Vec2f().Mul(math32.Sqrt(rand.Float32()) * radius)
entity.Position = center.Add(position)
entity.Direction = world.RandomAngle()
radius = min(radius*1.1, h.worldRadius*0.9)
threshold = 0.15 + threshold*0.85 // Approaches 1.0
governor++
if governor > 128 {
// Don't take down the server just because cannnot
// spawn an entity
break
}
}
entity.DirectionTarget = entity.Direction
}
if !h.canSpawn(entity, 1) {
return world.EntityIDInvalid
}
// Outside world
if entity.Position.LengthSquared() > h.worldRadius*h.worldRadius {
return world.EntityIDInvalid
}
h.world.AddEntity(entity)
entityID := entity.EntityID
if entity.Owner != nil && entity.Data().Kind == world.EntityKindBoat {
if entity.Owner.EntityID != world.EntityIDInvalid {
panic("owner already has EntityID")
}
if entity.Owner.Respawning() {
entity.Owner.ClearRespawn()
}
entity.Owner.EntityID = entityID
}
return entityID
}
// nearAny Returns if any entities are within a threshold for spawning (or if colliding with terrain)
func (h *Hub) canSpawn(entity *world.Entity, threshold float32) bool {
// Extra space between entities
radius := entity.Data().Radius
maxT := (radius + world.EntityRadiusMax) * threshold
switch entity.Data().Kind {
case world.EntityKindDecoy, world.EntityKindWeapon:
// Weapons spawn where the player shoots them regardless of entities,
// except if they would hit an obstacle (see #138)
if h.world.ForEntitiesInRadius(entity.Position, maxT, func(r float32, otherEntity *world.Entity) (stop bool) {
return otherEntity.Data().Kind == world.EntityKindObstacle && entity.Collides(otherEntity, 0)
}) {
// Colliding with an obstacle
return false
}
fallthrough
case world.EntityKindCollectible, world.EntityKindAircraft:
// Collectibles/aircraft don't care about colliding with entities while spawning
// Simply perform a terrain check against the current position (no slow conservative check)
return !h.terrain.Collides(entity, 0)
case world.EntityKindBoat:
// Be picky about spawning in appropriate depth water
// unless threshold is very low
// Ignore if owner has a team or enough points to upgrade to bigger ship
if threshold > 1.5 && (entity.Owner == nil || (entity.Owner.TeamID == world.TeamIDInvalid && entity.Owner.Score < world.LevelToScore(2))) {
belowKeel := entity.BelowKeel(h.terrain)
if belowKeel < 0 || belowKeel > 5 {
return false
}
}
}
// Slow, conservative check
if h.terrain.Collides(entity, -1) {
return false
}
return !h.world.ForEntitiesInRadius(entity.Position, maxT, func(r float32, otherEntity *world.Entity) (stop bool) {
t := (radius + otherEntity.Data().Radius) * threshold
return r < t*t
})
}