Commit fd8f1d07 authored by talljosh's avatar talljosh
Browse files

Approving partial changes while reviewing upgrade system.

parent d9bb6c9f0f7d
......@@ -133,8 +133,6 @@ class BuyUpgradeMsg(AgentRequest):
player.items.server_approve(upgrade)
agent.messageToAgent(UpgradeApprovedMsg(upgrade.upgradeType))
player.onCoinsSpent(requiredCoins)
class PlayerHasUpgradeMsg(ClientCommand):
'''
......@@ -207,7 +205,6 @@ class BuyAmmoMsg(ClientCommand):
game.sendServerCommand(PlayerCoinsSpentMsg(player.id, cost))
self.player_id = player.id
game.sendServerCommand(self)
player.onCoinsSpent(cost)
def validate(self, player):
if player is None or not player.aggression:
......
......@@ -13,16 +13,18 @@ class IdManager:
self.players = BytesIDGenerator()
self.coins = BytesIDGenerator()
self.shots = IDGenerator()
self.upgrades = IDGenerator()
self.projectiles = IDGenerator()
world.onShotRemoved.addListener(self.shots.mark_id_as_unused)
world.onCollectableCoinRemoved.addListener(self.coins.mark_id_as_unused)
world.onPlayerRemoved.addListener(self.seen_player_removed)
world.on_projectile_removed.addListener(self.projectiles.mark_id_as_unused)
def stop(self):
self.world.onShotRemoved.removeListener(self.shots.mark_id_as_unused)
self.world.onCollectableCoinRemoved.removeListener(self.coins.mark_id_as_unused)
self.world.onPlayerRemoved.removeListener(self.seen_player_removed)
self.world.on_projectile_removed.removeListener(self.projectiles.mark_id_as_unused)
def seen_player_removed(self, player, old_id):
self.players.mark_id_as_unused(old_id)
......
......@@ -1608,40 +1608,6 @@ class Player(Unit):
and isNear(self.ghostThrust, msg.ghostThrust)
and msg.health == self.health)
def weaponDischarged(self):
'''
Updates the player's reload time because the player has just fired a
shot.
'''
world = self.world
zone = self.getZone()
if self.team is None:
reloadTime = world.physics.playerNeutralReloadTime
elif world.trosballManager.enabled:
if (
abs(self.pos[0] - world.trosballManager.getPosition()[0])
< 1e-5):
reloadTime = world.physics.playerNeutralReloadTime
# If self is on blue, and on the left of the trosball; or
# completely vice versa
elif self.isOnside(world.trosballManager.getPosition()):
reloadTime = world.physics.playerOwnDarkReloadTime
else:
reloadTime = world.physics.playerEnemyDarkReloadTime
elif zone and zone.owner == self.team and zone.dark:
reloadTime = world.physics.playerOwnDarkReloadTime
elif zone and not zone.dark:
reloadTime = world.physics.playerNeutralReloadTime
else:
reloadTime = world.physics.playerEnemyDarkReloadTime
self.guns.standard_reload_time = reloadTime
if self.shoxwave:
reloadTime *= world.physics.playerShoxwaveReloadRate
elif self.machineGunner:
reloadTime = self.machineGunner.weaponDischarged()
self.guns.reload_time = self.guns.reload_from = reloadTime
def createShot(self, gun_type=None, shotId=None, shotClass=Shot):
'''
Factory function for building a Shot object.
......
......@@ -6,7 +6,7 @@ from trosnoth.utils.collision import collideTrajectory
from trosnoth.utils.math import fadeValues, distance
try:
from typing import Protocol
from typing import Protocol, Tuple
except ImportError:
from typing_extensions import Protocol
......@@ -25,6 +25,25 @@ class Advanceable(Protocol):
...
class InteractingUnit(Protocol):
'''
For use by units that interact with players based on the player’s
position on the server, e.g., shots and mines.
Not for things like Trosball and coins which interact based on the
player’s client position.
'''
pos: Tuple[float, float]
oldPos: Tuple[float, float]
collision_check_buffer: float
def check_collision(self, player):
...
def collided_with(self, players):
...
class Unit(object):
def __init__(self, world, *args, **kwargs):
super(Unit, self).__init__(*args, **kwargs)
......
'''universe.py - defines anything that has to do with the running of the
universe. This includes players, shots, zones, and the level itself.'''
import asyncio
from collections import defaultdict
import cProfile
import heapq
import io
......@@ -11,12 +10,13 @@ from math import sin, cos
import pstats
import random
import types
from typing import Iterable
import simplejson
from twisted.internet import defer, reactor
from trosnoth.const import (
TROSBALL_DEATH_HIT, SHOXWAVE_HIT, SHOT_HIT, GRENADE_HIT,
TROSBALL_DEATH_HIT, SHOXWAVE_HIT, GRENADE_HIT,
COLLECTABLE_COIN_LIFETIME, TICK_PERIOD,
MAX_PLAYER_NAME_LENGTH, BOMBER_DEATH_HIT, COINS_PER_KILL,
FRONT_LINE_NORMAL, FRONT_LINE_TROSBALL, BOT_GOAL_NONE,
......@@ -28,13 +28,14 @@ from trosnoth.levels.base import LobbyLevel, Level
from trosnoth.model.idmanager import IdManager
from trosnoth.model.macguffins import MacGuffinManager, MacGuffin
from trosnoth.model.settingsmanager import SettingsSynchroniser
from trosnoth.model.upgrades import upgradeOfType, allUpgrades
from trosnoth.model.unit import InteractingUnit
from trosnoth.model.upgrades import upgradeOfType, allUpgrades, Shoxwave
from trosnoth.model.physics import WorldPhysics
from trosnoth.model.map import MapLayout, MapState
from trosnoth.model.player import Player
from trosnoth.model.coin import CollectableCoin
from trosnoth.model.shot import Shot, GrenadeShot
from trosnoth.model.shot import Shot, GrenadeShot, MineShot
from trosnoth.model.team import Team
from trosnoth.model.trosball import Trosball
from trosnoth.model.universe_base import NEUTRAL_TEAM_ID, NO_PLAYER
......@@ -800,6 +801,7 @@ class Universe(MessageConsumer):
self.onPlayerAdded = Event(['player'])
self.onPlayerRemoved = Event(['player', 'oldId'])
self.onShotRemoved = Event() # (shotId)
self.on_projectile_removed = Event(['projectile_id'])
self.onCollectableCoinSpawned = Event(['coin'])
self.onCollectableCoinRemoved = Event()
self.onStandardGameFinished = Event(['winner'])
......@@ -813,6 +815,7 @@ class Universe(MessageConsumer):
self.onGrenadeExplosion = Event() # (pos, radius)
self.onTrosballExplosion = Event() # (player)
self.onBomberExplosion = Event() # (player)
self.on_mine_explosion = Event(['pos'])
self.onReset = Event()
self.onServerTickComplete = Event()
self.onChangeVoiceChatRooms = Event() # (teams, players)
......@@ -827,6 +830,7 @@ class Universe(MessageConsumer):
self.playerWithId = {}
self.shotWithId = {}
self.teamWithId = {NEUTRAL_TEAM_ID: None}
self.projectile_by_id = {}
# Create Teams:
self.teams = (
......@@ -1018,6 +1022,7 @@ class Universe(MessageConsumer):
# Update the player and shot positions.
advancables = (
list(self.shotWithId.values()) + list(self.players) + list(self.grenades)
+ list(self.projectile_by_id.values())
+ list(self.collectableCoins.values()))
advancables.extend(self.trosballManager.getAdvancables())
for unit in advancables:
......@@ -1082,7 +1087,7 @@ class Universe(MessageConsumer):
firingPlayer = self.getPlayer(msg.playerId)
if not firingPlayer:
return
firingPlayer.weaponDischarged()
firingPlayer.guns.gun_was_fired(Shoxwave)
for shot in list(self.shotWithId.values()):
if (not shot.originatingPlayer.isFriendsWith(firingPlayer) and
......@@ -1128,6 +1133,7 @@ class Universe(MessageConsumer):
oldCoins = player.coins
player.coins -= msg.count
player.onCoinsChanged(oldCoins)
player.onCoinsSpent(msg.count)
@UpgradeChangedMsg.handler
def changeUpgrade(self, msg):
......@@ -1174,6 +1180,7 @@ class Universe(MessageConsumer):
self.deadCoins = set()
self.collectableCoins = {}
self.shotWithId = {}
self.projectile_by_id = {}
self.grenades = set()
def setTestMode(self):
......@@ -1249,6 +1256,7 @@ class Universe(MessageConsumer):
'kind': shot.kind,
} for shot in list(self.shotWithId.values()) if not shot.expired
],
'projectiles': [unit.dump() for unit in self.projectile_by_id.values()],
'coins': [
{
'id': coin.id,
......@@ -1340,6 +1348,14 @@ class Universe(MessageConsumer):
tuple(shotData['vel']), shotData['kind'], shotData['time'])
self.shotWithId[shot.id] = shot
self.projectile_by_id = {}
for projectile_data in data['projectiles']:
# For now, mines are the only thing in projectile_by_id, but
# in future this should include grenades, so we’ll probably
# want to call a classmethod on a base Projectile class.
unit = MineShot.rebuild(self, projectile_data)
self.projectile_by_id[unit.id] = unit
self.deadCoins = set()
self.collectableCoins = {}
for coinData in data['coins']:
......@@ -1407,6 +1423,10 @@ class Universe(MessageConsumer):
def trosballExploded(self, player):
self.onTrosballExplosion(player)
def remove_projectile(self, projectile):
del self.projectile_by_id[projectile.id]
self.on_projectile_removed(projectile.id)
class ScenarioManager:
def __init__(self, world):
......@@ -1544,6 +1564,7 @@ class ServerUniverse(Universe):
self.activeAchievementCategories = set()
self.jitterLogger = JitterLogger(20)
self.sentGraceTick = False
self.interaction_detector = InteractionDetector(self, 100)
if PROFILE_LONG_TICKS:
self.long_tick_profiler = LongTickProfiler()
else:
......@@ -1639,7 +1660,7 @@ class ServerUniverse(Universe):
def advanceEverything(self):
super(ServerUniverse, self).advanceEverything()
self.checkShotCollisions()
self.interaction_detector.process_collisions()
self.updateCollectableCoins()
self.onUnitsAllAdvanced()
self.bootTardyPlayers()
......@@ -1665,41 +1686,6 @@ class ServerUniverse(Universe):
error=True, text='You have been idle for too long!'.encode('utf-8')))
self.sendServerCommand(RemovePlayerMsg(player.id))
def checkShotCollisions(self, resolution=200):
'''
Performs collision checks of all shots with all nearby players.
'''
buckets = defaultdict(list)
for player in self.players:
if player.dead or player.is_invulnerable():
continue
x, y = player.pos
xBucket, yBucket = x // resolution, y // resolution
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
buckets[xBucket + dx, yBucket + dy].append(player)
for shot in list(self.shots):
x, y = shot.pos
xBucket, yBucket = x // resolution, y // resolution
hitPlayers = [
p for p in buckets[xBucket, yBucket]
if shot.checkCollision(p)]
if hitPlayers:
# If multiple players are hit in the same tick, the server
# randomly selects one to die.
hitPlayer = random.choice(hitPlayers)
hitpoints = 1
if not shot.originatingPlayer or shot.originatingPlayer.id == -1:
originating_player_id = NO_PLAYER
else:
originating_player_id = shot.originatingPlayer.id
self.sendServerCommand(PlayerHitMsg(
hitPlayer.id, SHOT_HIT, hitpoints,
originating_player_id, shotId=shot.id,
))
def updateCollectableCoins(self):
try:
for coin in list(self.collectableCoins.values()):
......@@ -1875,13 +1861,11 @@ class ServerUniverse(Universe):
# Account for possibility player has left game
oldKiller = killer
killer = killer if killer in self.players else None
xpos, ypos = pos
for player in self.players:
if player.isFriendsWith(oldKiller) or player.dead or player.is_invulnerable():
continue
dist = ((player.pos[0] - xpos) ** 2 +
(player.pos[1] - ypos) ** 2) ** 0.5
dist = distance(player.pos, pos)
if dist <= radius:
self.sendKill(
player, GRENADE_HIT, killer,
......@@ -1920,6 +1904,70 @@ class ServerUniverse(Universe):
super(ServerUniverse, self).bomberExploded(player)
class InteractionDetector:
'''
Calculates interactions between players and units such as shots or
mines.
This is not used for collectible coins, because they have special
magic that makes them collectible based on where the client thinks
the player is.
'''
def __init__(self, world: ServerUniverse, resolution=200):
self.world = world
self.resolution = resolution
def process_collisions(self):
# Divide the live players into buckets so each unit only needs
# to consider nearby players.
buckets = Buckets(self.resolution)
for player in self.world.players:
if not player.dead:
buckets.add(player.pos, player)
for unit in self.get_interacting_units():
affects = [
p for p in buckets.get(unit.oldPos, unit.pos, buffer=unit.collision_check_buffer)
if unit.check_collision(p)]
if affects:
unit.collided_with(affects)
def get_interacting_units(self) -> Iterable[InteractingUnit]:
yield from self.world.shots
yield from self.world.projectile_by_id.values()
class Buckets:
def __init__(self, resolution):
self.resolution = resolution
self.buckets = {}
def add(self, pos, thing):
x, y = pos
key = x // self.resolution, y // self.resolution
if key in self.buckets:
self.buckets[key].add(thing)
else:
self.buckets[key] = {thing}
def get(self, *points, buffer=0):
'''
Iterates through everything in the buckets bounded by the
smallest rectangle containing all the given points.
'''
min_x = min(p[0] for p in points) - buffer
max_x = max(p[0] for p in points) + buffer
min_y = min(p[1] for p in points) - buffer
max_y = max(p[1] for p in points) + buffer
for i in range(int(min_x / self.resolution), int(max_x // self.resolution) + 1):
for j in range(int(min_y // self.resolution), int(max_y // self.resolution) + 1):
key = i, j
if key in self.buckets:
yield from self.buckets[key]
class Region(object):
'''
Base class for regions for use as event triggers. Regions must be
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment