コード例 #1
0
ファイル: ability.go プロジェクト: runningwild/jota
func (m UseAbility) Apply(_g interface{}) {
	g := _g.(*Game)
	ent, ok := g.Ents[m.Gid]
	if !ok || ent == nil {
		base.Error().Printf("Got a use ability that made no sense: %v", m)
		return
	}
	abilities := ent.Abilities()
	if m.Index < 0 || m.Index >= len(abilities) {
		base.Error().Printf("Got a UseAbility on index %d with only %d abilities.", m.Index, len(abilities))
		return
	}

	// Don't use the ability if any other abilities are active
	anyActive := false
	for i, ability := range abilities {
		if i == m.Index {
			// It's ok to send input to the active ability, so if the ability this
			// command is trying to use is active that's ok.
			continue
		}
		if ability.IsActive() {
			anyActive = true
			break
		}
	}
	if anyActive {
		return
	}
	abilities[m.Index].Input(ent, g, m.Button, m.Trigger)
}
コード例 #2
0
ファイル: main.go プロジェクト: runningwild/jota
func main() {
	defer base.StackCatcher()
	sys.Startup()
	err := gl.Init()
	if err != nil {
		base.Error().Fatalf("%v", err)
	}

	render.Init()
	render.Queue(func() {
		sys.CreateWindow(10, 10, wdx, wdy)
		sys.EnableVSync(true)
	})
	base.InitShaders()
	runtime.GOMAXPROCS(10)
	sys.Think()

	base.LoadAllDictionaries()

	if Version() != "standard" {
		engine := debugHookup(Version())
		mainLoop(engine, "standard")
	} else {
		// TODO: Reimplement standard hookup
	}
}
コード例 #3
0
ファイル: scripts.go プロジェクト: runningwild/jota
func (jm *JotaModule) Param(vs ...runtime.Val) runtime.Val {
	jm.dieOnTerminated()
	jm.paramsMutex.Lock()
	defer jm.paramsMutex.Unlock()
	paramName := vs[0].String()
	value, ok := jm.params[paramName]
	if !ok {
		return runtime.Nil
	}
	switch t := value.(type) {
	case string:
		return runtime.String(t)
	case bool:
		return runtime.Bool(t)
	case int:
		return runtime.Number(t)
	case float64:
		return runtime.Number(t)
	case linear.Vec2:
		return jm.newVec(t.X, t.Y)
	case game.Gid:
		return jm.newEnt(t)
	default:
		base.Error().Printf("Requested parameter of unexpected type: %T", t)
		return runtime.Nil
	}
}
コード例 #4
0
ファイル: game.go プロジェクト: runningwild/jota
func (m Move) Apply(_g interface{}) {
	g := _g.(*Game)
	ent := g.Ents[m.Gid]
	if ent == nil {
		return
	}
	if ent.Id() != m.Gid {
		base.Error().Printf("Move.Apply(): %v %v")
	}
	ent.Move(m.Angle, m.Magnitude)
}
コード例 #5
0
ファイル: main.go プロジェクト: runningwild/jota
func debugHookup(version string) *cgf.Engine {
	var err error
	for false && len(sys.GetActiveDevices()[gin.DeviceTypeController]) < 2 {
		time.Sleep(time.Millisecond * 100)
		sys.Think()
	}

	var engine *cgf.Engine
	if version != "host" {
		engine, err = cgf.NewClientEngine(17, "thunderingvictory.dyndns.org", 20007, base.EmailCrashReport, base.Log())
		if err != nil {
			base.Log().Printf("Unable to connect: %v", err)
			base.Error().Fatalf("%v", err.Error())
		}
	} else {
		sys.Think()
		g := game.MakeGame()
		if version == "host" {
			engine, err = cgf.NewHostEngine(g, 17, "", 20007, base.EmailCrashReport, base.Log())
			if err != nil {
				panic(err)
			}
			err = cgf.Host(20007, "thunderball")
			if err != nil {
				panic(err)
			}
		} else {
			engine, err = cgf.NewLocalEngine(g, 17, base.EmailCrashReport, base.Log())
		}
		if err != nil {
			base.Error().Fatalf("%v", err.Error())
		}
	}
	engine.Pause()
	engine.GetState().(*game.Game).SetSystem(sys)
	engine.Unpause()

	base.Log().Printf("Engine Id: %v", engine.Id())
	base.Log().Printf("All Ids: %v", engine.Ids())
	return engine
}
コード例 #6
0
ファイル: base_ent.go プロジェクト: runningwild/jota
func (b *BaseEnt) BindAi(name string, engine *cgf.Engine) {
	if b.ai != nil {
		base.Warn().Printf("Can't bind an Ai when there is already one bound.")
		return
	}
	if b.Gid == "" {
		base.Error().Printf("Can't bind an Ai on an ent before setting its Gid.")
		return
	}
	b.ai = ai_maker(name, engine, b.Gid)
	b.ai.Start()
}
コード例 #7
0
ファイル: game_graphics.go プロジェクト: runningwild/jota
// Draws everything that is relevant to the players on a computer, but not the
// players across the network.  Any ui used to determine how to place an object
// or use an ability, for example.
func (g *Game) RenderLocal(region g2.Region) {
	switch {
	case g.Setup != nil:
		g.RenderLocalSetup(region)
	case !g.editor.Active():
		g.RenderLocalGame(region)
	case g.editor.Active():
		g.RenderLocalEditor(region)
	default:
		base.Error().Printf("Unexpected case.")
	}
}
コード例 #8
0
ファイル: editor_graphics.go プロジェクト: runningwild/jota
func (g *Game) RenderLocalEditor(region g2.Region) {
	g.editor.Lock()
	defer g.editor.Unlock()
	g.editor.region = region
	g.editor.camera.regionDims = linear.Vec2{float64(region.Dims.Dx), float64(region.Dims.Dy)}
	levelDims := linear.Vec2{float64(g.Level.Room.Dx), float64(g.Level.Room.Dy)}
	g.editor.camera.StandardRegion(levelDims.Scale(0.5), levelDims)
	g.editor.camera.approachTarget()

	gl.MatrixMode(gl.PROJECTION)
	gl.PushMatrix()
	gl.LoadIdentity()
	defer gl.PopMatrix()

	gl.PushAttrib(gl.VIEWPORT_BIT)
	gl.Viewport(gl.Int(region.X), gl.Int(region.Y), gl.Sizei(region.Dx), gl.Sizei(region.Dy))
	defer gl.PopAttrib()

	current := &g.editor.camera.current
	gl.Ortho(
		gl.Double(current.mid.X-current.dims.X/2),
		gl.Double(current.mid.X+current.dims.X/2),
		gl.Double(current.mid.Y+current.dims.Y/2),
		gl.Double(current.mid.Y-current.dims.Y/2),
		gl.Double(1000),
		gl.Double(-1000),
	)
	defer func() {
		gl.MatrixMode(gl.PROJECTION)
		gl.PopMatrix()
		gl.MatrixMode(gl.MODELVIEW)
	}()
	gl.MatrixMode(gl.MODELVIEW)

	gl.Enable(gl.BLEND)
	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

	g.renderWalls()
	g.renderEdges()
	g.renderBases()
	g.renderEntsAndAbilities()
	g.renderProcesses()

	g.editor.renderPathing(&g.Level.Room, g.local.pathingData)

	switch g.editor.action {
	case editorActionNone:
	case editorActionPlaceBlock:
		g.editor.renderPlaceBlock(g)
	default:
		base.Error().Printf("Unexpected editorAction: %v", g.editor.action)
	}
}
コード例 #9
0
ファイル: mana_source.go プロジェクト: runningwild/jota
func (ms *ManaSource) Init(options *ManaSourceOptions) {
	ms.options = *options
	if options.NumNodeCols < 2 || options.NumNodeRows < 2 {
		base.Error().Fatalf(fmt.Sprintf("Invalid options: %v", options))
	}

	r := rand.New(options.Rng)

	seeds := make([]nodeSeed, options.NumSeeds)
	for i := range seeds {
		seed := &seeds[i]
		seed.x = options.BoardLeft + r.Float64()*(options.BoardRight-options.BoardLeft)
		seed.y = options.BoardTop + r.Float64()*(options.BoardBottom-options.BoardTop)
		seed.color = r.Intn(3)
	}

	ms.rawNodes = newNodes(options.NumNodeCols * options.NumNodeRows)
	// ms.rawNodes = make([]node, options.NumNodeCols*options.NumNodeRows)
	ms.nodes = make([][]node, options.NumNodeCols)
	for col := 0; col < options.NumNodeCols; col++ {
		ms.nodes[col] = ms.rawNodes[col*options.NumNodeRows : (col+1)*options.NumNodeRows]
		for row := 0; row < options.NumNodeRows; row++ {
			x := options.BoardLeft + float64(col)/float64(options.NumNodeCols-1)*(options.BoardRight-options.BoardLeft)
			y := options.BoardTop + float64(row)/float64(options.NumNodeRows-1)*(options.BoardBottom-options.BoardTop)

			maxWeightByColor := [3]float64{0.0, 0.0, 0.0}
			for _, seed := range seeds {
				c := seed.color
				dx := x - seed.x
				dy := y - seed.y
				distSquared := dx*dx + dy*dy
				weight := 1 / (distSquared + 1.0)
				if weight > maxWeightByColor[c] {
					maxWeightByColor[c] = weight
				}
			}

			normalizeWeights(options.NodeMagnitude, maxWeightByColor[:])
			var weightsCopy [3]float64
			copy(weightsCopy[:], maxWeightByColor[:])

			ms.nodes[col][row] = node{
				X:             x,
				Y:             y,
				RegenPerFrame: options.RegenPerFrame,
				Mana:          maxWeightByColor,
				MaxMana:       weightsCopy,
			}
		}
	}
}
コード例 #10
0
ファイル: scripts.go プロジェクト: runningwild/jota
func (ai *GameAi) Start() {
	if ai.jm == nil {
		return
	}
	ctx := runtime.NewCtx(getGlobalJotaResolver(), new(compiler.Compiler))
	ctx.RegisterNativeModule(new(stdlib.TimeMod))
	ctx.RegisterNativeModule(&LogModule{})
	ctx.RegisterNativeModule(ai.jm)
	mod, err := ctx.Load(ai.jm.name)
	if err != nil {
		base.Error().Printf("Error compiling script: %v", err)
		return
	}
	go func() {
		for {
			_, err := mod.Run()
			if err.Error() == "module terminated" {
				return
			}
			base.Error().Printf("Error running script: %v", err)
		}
	}()
}
コード例 #11
0
ファイル: editor_graphics.go プロジェクト: runningwild/jota
func (editor *editorData) saveAction(room *Room) {
	data, err := json.MarshalIndent(room, "", "  ")
	if err != nil {
		base.Error().Printf("Unable to encode room to json: %v", err)
		return
	}
	name := fmt.Sprintf("save-%v.json", time.Now())
	fullPath := filepath.Join(base.GetDataDir(), name)
	err = ioutil.WriteFile(fullPath, data, 0664)
	if err != nil {
		base.Warn().Printf("Unable to write output json file: %v", err)
		return
	}
}
コード例 #12
0
ファイル: game_graphics.go プロジェクト: runningwild/jota
func (g *Game) HandleEventGroup(group gin.EventGroup) {
	g.local.Engine.Pause()
	defer g.local.Engine.Unpause()
	switch {
	case g.Setup != nil:
		g.HandleEventGroupSetup(group)
	case g.editor.Active():
		g.HandleEventGroupEditor(group)
	case !g.editor.Active():
		g.HandleEventGroupGame(group)
	default:
		base.Error().Printf("Unexpected case in HandleEventGroup()")
	}
}
コード例 #13
0
ファイル: mana_source.go プロジェクト: runningwild/jota
func (ms *ManaSource) regenerateMana() {
	for i := range ms.rawNodes {
		node := &ms.rawNodes[i]
		for c := range node.Mana {
			if node.MaxMana[c] == 0 {
				continue
			}
			maxRecovery := node.MaxMana[c] * node.RegenPerFrame
			scale := (node.MaxMana[c] - node.Mana[c]) / node.MaxMana[c]
			node.Mana[c] += scale * maxRecovery
			if scale != scale || maxRecovery != maxRecovery {
				base.Error().Fatalf("NaN showed up somewhere!")
			}
		}
	}
}
コード例 #14
0
ファイル: scripts.go プロジェクト: runningwild/jota
func (jm *JotaModule) setParam(name string, value interface{}) {
	jm.paramsMutex.Lock()
	defer jm.paramsMutex.Unlock()

	// NOTE: The list of supported types here should match the list in
	// JotaModule.Param()
	switch value.(type) {
	case string:
	case bool:
	case int:
	case float64:
	case linear.Vec2:
	case game.Gid:
	default:
		base.Error().Printf("Tried to specify a parameter with an unexpected type: %T", value)
		return
	}
	jm.params[name] = value
}
コード例 #15
0
ファイル: thunder_menu.go プロジェクト: runningwild/jota
func setupSound() {
	soundInit.Do(func() {
		var err error
		// fmodSys, err = fmod.CreateSystem()
		if err != nil {
			// base.Error().Fatalf("Unable to initialize fmod: %v", err)
		}
		// err = fmodSys.Init(2, 0, nil)
		if err != nil {
			// base.Error().Fatalf("Unable to initialize fmod: %v", err)
		}
		target := filepath.Join(base.GetDataDir(), "sound/ping.wav")
		base.Log().Printf("Trying to load ", target)
		// sound, err = fmodSys.CreateSound_FromFilename(target, fmod.MODE_DEFAULT)
		if err != nil {
			base.Error().Fatalf("Unable to load sound: %v", err)
		}
	})
}
コード例 #16
0
ファイル: creep.go プロジェクト: runningwild/jota
func (g *Game) AddCreeps(pos linear.Vec2, count, side int, params map[string]interface{}) {
	if side < 0 || side >= len(g.Level.Room.SideData) {
		base.Error().Fatalf("Got side %d, but this level only supports sides from 0 to %d.", side, len(g.Level.Room.SideData)-1)
		return
	}
	for i := 0; i < count; i++ {
		var c CreepEnt
		c.StatsInst = stats.Make(stats.Base{
			Health: 100,
			Mass:   250,
			Acc:    50.0,
			Rate:   0.0,
			Size:   8,
			Vision: 400,
		})

		// Evenly space the players on a circle around the starting position.
		randAngle := rand.New(g.Rng).Float64() * math.Pi
		rot := (linear.Vec2{15, 0}).Rotate(randAngle + float64(i)*2*3.1415926535/float64(count))
		c.Position = pos.Add(rot)

		c.Side_ = side
		c.Gid = g.NextGid()

		c.Abilities_ = append(
			c.Abilities_,
			ability_makers["asplode"](map[string]float64{"startRadius": 40, "endRadius": 70, "durationThinks": 50, "dps": 5}))

		// if playerData.gid[0:2] == "Ai" {
		//  c.BindAi("simple", g.local.Engine)
		// }

		g.AddEnt(&c)
		c.BindAi("creep", g.local.Engine)
		for name, value := range params {
			c.ai.SetParam(name, value)
		}
	}
}
コード例 #17
0
ファイル: player.go プロジェクト: runningwild/jota
func (g *Game) addPlayersToSide(playerDatas []addPlayerData, side int) {
	if side < 0 || side >= len(g.Level.Room.SideData) {
		base.Error().Fatalf("Got side %d, but this level only supports sides from 0 to %d.", len(g.Level.Room.SideData)-1)
	}
	for i, playerData := range playerDatas {
		var p PlayerEnt
		p.StatsInst = stats.Make(stats.Base{
			Health: 1000,
			Mass:   750,
			Acc:    150.0,
			Turn:   0.07,
			Rate:   0.5,
			Size:   12,
			Vision: 500,
		})

		// Evenly space the players on a circle around the starting position.
		rot := (linear.Vec2{25, 0}).Rotate(float64(i) * 2 * 3.1415926535 / float64(len(playerDatas)))
		p.Position = g.Level.Room.SideData[side].Base.Add(rot)

		p.Side_ = side
		p.Gid = playerData.gid
		p.Processes = make(map[int]Process)

		for _, ability := range g.Champs[playerData.champ].Abilities {
			p.Abilities_ = append(
				p.Abilities_,
				ability_makers[ability.Name](ability.Params))
		}

		if playerData.gid[0:2] == "Ai" {
			// p.BindAi("simple", g.local.Engine)
		}

		g.AddEnt(&p)
	}
}
コード例 #18
0
ファイル: main.go プロジェクト: runningwild/jota
func mainLoop(engine *cgf.Engine, mode string) {
	defer engine.Kill()
	var profile_output *os.File
	var contention_output *os.File
	var num_mem_profiles int
	// ui.AddChild(base.MakeConsole())

	ticker := time.Tick(time.Millisecond * 17)
	ui := g2.Make(0, 0, wdx, wdy)
	ui.AddChild(&game.GameWindow{Engine: engine, Dims: g2.Dims{wdx - 50, wdy - 50}}, g2.AnchorDeadCenter)
	ui.AddChild(g2.MakeConsole(wdx-50, wdy-50), g2.AnchorDeadCenter)
	// side0Index := gin.In().BindDerivedKeyFamily("Side0", gin.In().MakeBindingFamily(gin.Key1, []gin.KeyIndex{gin.EitherControl}, []bool{true}))
	// side1Index := gin.In().BindDerivedKeyFamily("Side1", gin.In().MakeBindingFamily(gin.Key2, []gin.KeyIndex{gin.EitherControl}, []bool{true}))
	// side2Index := gin.In().BindDerivedKeyFamily("Side2", gin.In().MakeBindingFamily(gin.Key3, []gin.KeyIndex{gin.EitherControl}, []bool{true}))
	// side0Key := gin.In().GetKeyFlat(side0Index, gin.DeviceTypeAny, gin.DeviceIndexAny)
	// side1Key := gin.In().GetKeyFlat(side1Index, gin.DeviceTypeAny, gin.DeviceIndexAny)
	// side2Key := gin.In().GetKeyFlat(side2Index, gin.DeviceTypeAny, gin.DeviceIndexAny)
	defer ui.StopEventListening()
	for {
		<-ticker
		if gin.In().GetKey(gin.AnyEscape).FramePressCount() != 0 {
			return
		}
		start := time.Now()
		sys.Think()
		start = time.Now()
		render.Queue(func() {
			ui.Draw()
		})
		render.Queue(func() {
			start = time.Now()
			sys.SwapBuffers()
		})
		render.Purge()
		start = time.Now()
		// TODO: Replace the 'P' key with an appropriate keybind
		var err error
		if gin.In().GetKey(gin.AnyKeyP).FramePressCount() > 0 {
			if profile_output == nil {
				profile_output, err = os.Create(filepath.Join(datadir, "cpu.prof"))
				if err == nil {
					err = pprof.StartCPUProfile(profile_output)
					if err != nil {
						base.Log().Printf("Unable to start CPU profile: %v\n", err)
						profile_output.Close()
						profile_output = nil
					}
					base.Log().Printf("cpu prof: %v\n", profile_output)
				} else {
					base.Log().Printf("Unable to start CPU profile: %v\n", err)
				}
			} else {
				pprof.StopCPUProfile()
				profile_output.Close()
				profile_output = nil
			}
		}

		if gin.In().GetKey(gin.AnyKeyL).FramePressCount() > 0 {
			if contention_output == nil {
				contention_output, err = os.Create(filepath.Join(datadir, "contention.prof"))
				if err == nil {
					runtime.SetBlockProfileRate(1)
					base.Log().Printf("contention prof: %v\n", contention_output)
				} else {
					base.Log().Printf("Unable to start contention profile: %v\n", err)
				}
			} else {
				pprof.Lookup("block").WriteTo(contention_output, 0)
				contention_output.Close()
				contention_output = nil
			}
		}

		// TODO: Replace the 'M' key with an appropriate keybind
		if gin.In().GetKey(gin.AnyKeyM).FramePressCount() > 0 {
			f, err := os.Create(filepath.Join(datadir, fmt.Sprintf("mem.%d.prof", num_mem_profiles)))
			if err != nil {
				base.Error().Printf("Unable to write mem profile: %v", err)
			}
			pprof.WriteHeapProfile(f)
			f.Close()
			num_mem_profiles++
		}
	}
}
コード例 #19
0
ファイル: game.go プロジェクト: runningwild/jota
func (u SetupComplete) Apply(_g interface{}) {
	g := _g.(*Game)
	if g.Setup == nil {
		return
	}
	sideCount := make(map[int]int)
	// Must have at least two sides
	sideCount[0] = 0
	sideCount[1] = 0
	for _, spd := range g.Setup.Players {
		sideCount[spd.Side]++
	}
	g.Engines = make(map[int64]*PlayerData)
	for id, player := range g.Setup.Players {
		var gid Gid
		if id < 0 {
			gid = Gid(fmt.Sprintf("Ai:%d", -id))
		} else {
			gid = Gid(fmt.Sprintf("Engine:%d", id))
		}
		g.Engines[id] = &PlayerData{
			PlayerGid:  Gid(gid),
			Side:       player.Side,
			ChampIndex: player.ChampIndex,
		}
	}

	// Now that we have the information we can set up a lot of the local data for
	// this engine's player.

	if g.IsPlaying() {
		g.local.Side = g.Engines[g.local.Engine.Id()].Side
		g.local.Gid = g.Engines[g.local.Engine.Id()].PlayerGid
		g.local.Data = g.Engines[g.local.Engine.Id()]
	}

	var room Room
	err := base.LoadJson(filepath.Join(base.GetDataDir(), "rooms/basic.json"), &room)
	if err != nil {
		base.Error().Fatalf("%v", err)
	}
	errs := room.Validate()
	for _, err := range errs {
		base.Error().Printf("%v", err)
	}
	if len(errs) > 0 {
		base.Error().Fatalf("Errors with the level, bailing...")
	}
	g.Level = &Level{}
	g.Level.Room = room
	g.Rng = cmwc.MakeGoodCmwc()
	g.Rng.Seed(u.Seed)
	g.Ents = make(map[Gid]Ent)
	g.Friction = 0.97
	g.losCache = makeLosCache(g.Level.Room.Dx, g.Level.Room.Dy)
	sides := make(map[int][]int64)
	var playerDatas []*PlayerData
	base.DoOrdered(g.Engines, func(a, b int64) bool { return a < b }, func(id int64, data *PlayerData) {
		sides[data.Side] = append(sides[data.Side], id)
		playerDatas = append(playerDatas, data)
	})
	for id, ed := range g.Engines {
		base.Log().Printf("%v -> %v", id, *ed)
	}
	g.AddPlayers(playerDatas)

	g.MakeControlPoints()
	g.Init()
	base.Log().Printf("Nillifying g.Setup()")
	g.Setup = nil
}
コード例 #20
0
ファイル: game.go プロジェクト: runningwild/jota
func (g *Game) ThinkGame() {
	// cache wall data
	if g.local.temp.AllWalls == nil || g.local.temp.AllWallsDirty {
		g.local.temp.AllWallsDirty = false
		g.local.temp.AllWalls = nil
		g.local.temp.WallCache = nil
		g.local.temp.VisibleWallCache = nil

		// Can't use a nil slice, otherwise we'll run this block every Think for levels
		// with no walls.
		allWalls := make([]linear.Seg2, 0)
		base.DoOrdered(g.Level.Room.Walls, func(a, b string) bool { return a < b }, func(_ string, walls linear.Poly) {
			for i := range walls {
				allWalls = append(allWalls, walls.Seg(i))
			}
		})
		g.local.temp.AllWalls = allWalls
		g.local.temp.WallCache = &wallCache{}
		g.local.temp.WallCache.SetWalls(g.Level.Room.Dx, g.Level.Room.Dy, allWalls, 100)
		g.local.temp.VisibleWallCache = &wallCache{}
		g.local.temp.VisibleWallCache.SetWalls(g.Level.Room.Dx, g.Level.Room.Dy, allWalls, stats.LosPlayerHorizon)
		g.local.pathingData = makePathingData(&g.Level.Room)
	}

	// cache ent data
	for _, ent := range g.local.temp.AllEnts {
		if ent.Dead() {
			if _, ok := ent.(*PlayerEnt); ok {
				var id int64
				_, err := fmt.Sscanf(string(ent.Id()), "Engine:%d", &id)
				if err != nil {
					_, err = fmt.Sscanf(string(ent.Id()), "Ai:%d", &id)
					id = -id // Ai's engine ids are negative
				}
				if err != nil {
					base.Error().Printf("Unable to parse player id '%v'", ent.Id())
				} else {
					if engineData, ok := g.Engines[id]; ok {
						if !ok {
							base.Error().Printf("Unable to find engine %d for player %v", id, ent.Id())
						} else {
							engineData.CountdownFrames = 60 * 10
							base.Log().Printf("%v died, counting down....", *engineData)
						}
					}
				}
			}
			ent.OnDeath(g)
			g.RemoveEnt(ent.Id())
		}
	}

	// Death countdown
	base.DoOrdered(g.Engines, func(a, b int64) bool { return a < b }, func(_ int64, engineData *PlayerData) {
		if engineData.CountdownFrames > 0 {
			engineData.CountdownFrames--
			if engineData.CountdownFrames == 0 {
				g.AddPlayers([]*PlayerData{engineData})
			}
		}
	})

	if g.local.temp.AllEnts == nil || g.local.temp.AllEntsDirty {
		g.local.temp.AllEnts = g.local.temp.AllEnts[0:0]
		g.DoForEnts(func(gid Gid, ent Ent) {
			g.local.temp.AllEnts = append(g.local.temp.AllEnts, ent)
		})
		g.local.temp.AllEntsDirty = false
	}
	if g.local.temp.EntGrid == nil {
		g.local.temp.EntGrid = MakeEntCache(g.Level.Room.Dx, g.Level.Room.Dy)
	}
	g.local.temp.EntGrid.SetEnts(g.local.temp.AllEnts)

	for _, proc := range g.Processes {
		proc.Think(g)
	}
	algorithm.Choose(&g.Processes, func(proc Process) bool { return !proc.Dead() })

	// Advance players, check for collisions, add segments
	eps := 1.0e-3
	for _, ent := range g.local.temp.AllEnts {
		ent.Think(g)
		for _, ab := range ent.Abilities() {
			ab.Think(ent, g)
		}
		pos := ent.Pos()
		pos.X = clamp(pos.X, eps, float64(g.Level.Room.Dx)-eps)
		pos.Y = clamp(pos.Y, eps, float64(g.Level.Room.Dy)-eps)
		ent.SetPos(pos)
	}

	var nearby []Ent
	for i := 0; i < len(g.local.temp.AllEnts); i++ {
		outerEnt := g.local.temp.AllEnts[i]
		outerSize := outerEnt.Stats().Size()
		if outerSize == 0 {
			continue
		}
		g.local.temp.EntGrid.EntsInRange(outerEnt.Pos(), 100, &nearby)
		for _, innerEnt := range nearby {
			innerSize := innerEnt.Stats().Size()
			if innerSize == 0 {
				continue
			}
			distSq := outerEnt.Pos().Sub(innerEnt.Pos()).Mag2()
			colDist := innerSize + outerSize
			if distSq > colDist*colDist {
				continue
			}
			if distSq < 0.015625 { // this means that dist < 0.125
				continue
			}
			dist := math.Sqrt(distSq)
			force := 50.0 * (colDist - dist)
			innerEnt.ApplyForce(innerEnt.Pos().Sub(outerEnt.Pos()).Scale(force / dist))
		}
	}

	g.Level.ManaSource.Think(g.Ents)
}