Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Push — main ( 30fe2e...c58695 )
by Dan
04:45
created

AbstractSmrPlayer::moveDestinationButton()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
c 0
b 0
f 0
nc 3
nop 3
dl 0
loc 22
rs 9.2222
1
<?php declare(strict_types=1);
2
require_once('missions.inc.php');
3
4
use Smr\BountyType;
5
use Smr\Database;
6
use Smr\DatabaseRecord;
7
use Smr\DisplayNameValidator;
8
use Smr\Epoch;
9
use Smr\Exceptions\AccountNotFound;
10
use Smr\Exceptions\PlayerNotFound;
11
use Smr\Exceptions\UserError;
12
use Smr\Messages;
13
use Smr\Pages\Player\ExamineTrader;
14
use Smr\Pages\Player\NewbieLeaveProcessor;
15
use Smr\Pages\Player\Planet\KickProcessor;
16
use Smr\Pages\Player\SearchForTraderResult;
17
use Smr\Pages\Player\WeaponDisplayToggleProcessor;
18
use Smr\Path;
19
use Smr\Race;
20
use Smr\ScoutMessageGroupType;
21
use Smr\StoredDestination;
22
use Smr\TurnsLevel;
23
24
abstract class AbstractSmrPlayer {
25
26
	use Traits\RaceID;
27
28
	protected const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
29
	protected const TIME_FOR_ALLIANCE_SWITCH = 0;
30
31
	protected const SHIP_INSURANCE_FRACTION = 0.25; // ship value regained on death
32
33
	protected const HOF_CHANGED = 1;
34
	protected const HOF_NEW = 2;
35
36
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
37
	protected static array $CACHE_SECTOR_PLAYERS = [];
38
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
39
	protected static array $CACHE_PLANET_PLAYERS = [];
40
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
41
	protected static array $CACHE_ALLIANCE_PLAYERS = [];
42
	/** @var array<int, array<int, SmrPlayer>> */
43
	protected static array $CACHE_PLAYERS = [];
44
45
	protected Database $db;
46
	protected readonly string $SQL;
47
48
	protected string $playerName;
49
	protected int $playerID;
50
	protected int $sectorID;
51
	protected int $lastSectorID;
52
	protected int $newbieTurns;
53
	protected bool $dead;
54
	protected bool $npc = false; // initialized for legacy combat logs
55
	protected bool $newbieStatus;
56
	protected bool $newbieWarning;
57
	protected bool $landedOnPlanet;
58
	protected int $lastActive;
59
	protected int $credits;
60
	protected int $alignment;
61
	protected int $experience;
62
	protected ?int $level;
63
	protected int $allianceID;
64
	protected int $shipID;
65
	protected int $kills;
66
	protected int $deaths;
67
	protected int $assists;
68
	/** @var array<int, int> */
69
	protected array $personalRelations;
70
	/** @var array<int, int> */
71
	protected array $relations;
72
	protected int $militaryPayment;
73
	/** @var array<int, array<string, mixed>> */
74
	protected array $bounties;
75
	protected int $turns;
76
	protected int $lastCPLAction;
77
	/** @var array<int, array<string, mixed>> */
78
	protected array $missions;
79
80
	/** @var array<string, array<string, mixed>> */
81
	protected array $tickers;
82
	protected int $lastTurnUpdate;
83
	protected int $lastNewsUpdate;
84
	protected string $attackColour;
85
	protected int $allianceJoinable;
86
	protected int $lastPort;
87
	protected int $bank;
88
	protected int $zoom;
89
	protected bool $displayMissions;
90
	protected bool $displayWeapons;
91
	protected bool $forceDropMessages;
92
	protected ScoutMessageGroupType $scoutMessageGroupType;
93
	protected bool $ignoreGlobals;
94
	protected ?Path $plottedCourse;
95
	protected bool $nameChanged;
96
	protected bool $raceChanged;
97
	protected bool $combatDronesKamikazeOnMines;
98
	protected string|false $customShipName;
99
	/** @var array<int, StoredDestination> */
100
	protected array $storedDestinations;
101
	/** @var array<int, bool> */
102
	protected array $canFed;
103
	protected bool $underAttack;
104
105
	/** @var array<int> */
106
	protected array $unvisitedSectors;
107
	/** @var array<int, int> */
108
	protected array $allianceRoles = [
109
		0 => 0,
110
	];
111
112
	protected bool $draftLeader;
113
	protected string|false $gpWriter;
114
	/** @var array<string, float> */
115
	protected array $HOF;
116
	/** @var array<string, string> */
117
	protected static array $HOFVis;
118
119
	protected bool $hasChanged = false;
120
	/** @var array<string, int> */
121
	protected array $hasHOFChanged = [];
122
	/** @var array<string, int> */
123
	protected static array $hasHOFVisChanged = [];
124
	/** @var array<int, bool> */
125
	protected array $hasBountyChanged = [];
126
127
	public static function clearCache(): void {
128
		self::$CACHE_PLAYERS = [];
129
		self::$CACHE_SECTOR_PLAYERS = [];
130
		self::$CACHE_PLANET_PLAYERS = [];
131
		self::$CACHE_ALLIANCE_PLAYERS = [];
132
	}
133
134
	public static function savePlayers(): void {
135
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
136
			foreach ($gamePlayers as $player) {
137
				$player->save();
138
			}
139
		}
140
	}
141
142
	/**
143
	 * @param array<int> $allianceIDs
144
	 * @return array<int, SmrPlayer>
145
	 */
146
	public static function getSectorPlayersByAlliances(int $gameID, int $sectorID, array $allianceIDs, bool $forceUpdate = false): array {
147
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
148
		foreach ($players as $accountID => $player) {
149
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
150
				unset($players[$accountID]);
151
			}
152
		}
153
		return $players;
154
	}
155
156
	/**
157
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
158
	 * but for an entire galaxy rather than a single sector. This is useful
159
	 * for reducing the number of queries in galaxy-wide processing.
160
	 *
161
	 * @return array<int, array<int, SmrPlayer>>
162
	 */
163
	public static function getGalaxyPlayers(int $gameID, int $galaxyID, bool $forceUpdate = false): array {
164
		$db = Database::getInstance();
165
		$dbResult = $db->read('SELECT player.* FROM player LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
166
		$galaxyPlayers = [];
167
		foreach ($dbResult->records() as $dbRecord) {
168
			$sectorID = $dbRecord->getInt('sector_id');
169
			$accountID = $dbRecord->getInt('account_id');
170
			$player = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
171
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
172
			$galaxyPlayers[$sectorID][$accountID] = $player;
173
		}
174
		return $galaxyPlayers;
175
	}
176
177
	/**
178
	 * @return array<int, SmrPlayer>
179
	 */
180
	public static function getSectorPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
181
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
182
			$db = Database::getInstance();
183
			$dbResult = $db->read('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
184
			$players = [];
185
			foreach ($dbResult->records() as $dbRecord) {
186
				$accountID = $dbRecord->getInt('account_id');
187
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
188
			}
189
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
190
		}
191
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
192
	}
193
194
	/**
195
	 * @return array<int, SmrPlayer>
196
	 */
197
	public static function getPlanetPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
198
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
199
			$db = Database::getInstance();
200
			$dbResult = $db->read('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(true) . ' AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
201
			$players = [];
202
			foreach ($dbResult->records() as $dbRecord) {
203
				$accountID = $dbRecord->getInt('account_id');
204
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
205
			}
206
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
207
		}
208
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
209
	}
210
211
	/**
212
	 * @return array<int, SmrPlayer>
213
	 */
214
	public static function getAlliancePlayers(int $gameID, int $allianceID, bool $forceUpdate = false): array {
215
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
216
			$db = Database::getInstance();
217
			$dbResult = $db->read('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
218
			$players = [];
219
			foreach ($dbResult->records() as $dbRecord) {
220
				$accountID = $dbRecord->getInt('account_id');
221
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
222
			}
223
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
224
		}
225
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
226
	}
227
228
	public static function getPlayer(int $accountID, int $gameID, bool $forceUpdate = false, DatabaseRecord $dbRecord = null): SmrPlayer {
229
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
230
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $dbRecord);
231
		}
232
		return self::$CACHE_PLAYERS[$gameID][$accountID];
233
	}
234
235
	public static function getPlayerByPlayerID(int $playerID, int $gameID, bool $forceUpdate = false): SmrPlayer {
236
		$db = Database::getInstance();
237
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID));
238
		if ($dbResult->hasRecord()) {
239
			$dbRecord = $dbResult->record();
240
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
241
		}
242
		throw new PlayerNotFound('Player ID not found.');
243
	}
244
245
	public static function getPlayerByPlayerName(string $playerName, int $gameID, bool $forceUpdate = false): SmrPlayer {
246
		$db = Database::getInstance();
247
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName));
248
		if ($dbResult->hasRecord()) {
249
			$dbRecord = $dbResult->record();
250
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
251
		}
252
		throw new PlayerNotFound('Player Name not found.');
253
	}
254
255
	protected function __construct(
256
		protected readonly int $gameID,
257
		protected readonly int $accountID,
258
		DatabaseRecord $dbRecord = null
259
	) {
260
		$this->db = Database::getInstance();
261
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
0 ignored issues
show
Bug introduced by
The property SQL is declared read-only in AbstractSmrPlayer.
Loading history...
262
263
		if ($dbRecord === null) {
264
			$dbResult = $this->db->read('SELECT * FROM player WHERE ' . $this->SQL);
265
			if ($dbResult->hasRecord()) {
266
				$dbRecord = $dbResult->record();
267
			}
268
		}
269
		if ($dbRecord === null) {
270
			throw new PlayerNotFound('Invalid accountID: ' . $accountID . ' OR gameID: ' . $gameID);
271
		}
272
273
		$this->playerName = $dbRecord->getString('player_name');
274
		$this->playerID = $dbRecord->getInt('player_id');
275
		$this->sectorID = $dbRecord->getInt('sector_id');
276
		$this->lastSectorID = $dbRecord->getInt('last_sector_id');
277
		$this->turns = $dbRecord->getInt('turns');
278
		$this->lastTurnUpdate = $dbRecord->getInt('last_turn_update');
279
		$this->newbieTurns = $dbRecord->getInt('newbie_turns');
280
		$this->lastNewsUpdate = $dbRecord->getInt('last_news_update');
281
		$this->attackColour = $dbRecord->getString('attack_warning');
282
		$this->dead = $dbRecord->getBoolean('dead');
283
		$this->npc = $dbRecord->getBoolean('npc');
284
		$this->newbieStatus = $dbRecord->getBoolean('newbie_status');
285
		$this->landedOnPlanet = $dbRecord->getBoolean('land_on_planet');
286
		$this->lastActive = $dbRecord->getInt('last_active');
287
		$this->lastCPLAction = $dbRecord->getInt('last_cpl_action');
288
		$this->raceID = $dbRecord->getInt('race_id');
289
		$this->credits = $dbRecord->getInt('credits');
290
		$this->experience = $dbRecord->getInt('experience');
291
		$this->alignment = $dbRecord->getInt('alignment');
292
		$this->militaryPayment = $dbRecord->getInt('military_payment');
293
		$this->allianceID = $dbRecord->getInt('alliance_id');
294
		$this->allianceJoinable = $dbRecord->getInt('alliance_join');
295
		$this->shipID = $dbRecord->getInt('ship_type_id');
296
		$this->kills = $dbRecord->getInt('kills');
297
		$this->deaths = $dbRecord->getInt('deaths');
298
		$this->assists = $dbRecord->getInt('assists');
299
		$this->lastPort = $dbRecord->getInt('last_port');
300
		$this->bank = $dbRecord->getInt('bank');
301
		$this->zoom = $dbRecord->getInt('zoom');
302
		$this->displayMissions = $dbRecord->getBoolean('display_missions');
303
		$this->displayWeapons = $dbRecord->getBoolean('display_weapons');
304
		$this->forceDropMessages = $dbRecord->getBoolean('force_drop_messages');
305
		$this->scoutMessageGroupType = ScoutMessageGroupType::from($dbRecord->getString('group_scout_messages'));
306
		$this->ignoreGlobals = $dbRecord->getBoolean('ignore_globals');
307
		$this->newbieWarning = $dbRecord->getBoolean('newbie_warning');
308
		$this->nameChanged = $dbRecord->getBoolean('name_changed');
309
		$this->raceChanged = $dbRecord->getBoolean('race_changed');
310
		$this->combatDronesKamikazeOnMines = $dbRecord->getBoolean('combat_drones_kamikaze_on_mines');
311
		$this->underAttack = $dbRecord->getBoolean('under_attack');
312
	}
313
314
	/**
315
	 * Insert a new player into the database. Returns the new player object.
316
	 */
317
	public static function createPlayer(int $accountID, int $gameID, string $playerName, int $raceID, bool $isNewbie, bool $npc = false): self {
318
		$time = Epoch::time();
319
		$db = Database::getInstance();
320
		$db->lockTable('player');
321
322
		// Player names must be unique within each game
323
		try {
324
			self::getPlayerByPlayerName($playerName, $gameID);
325
			$db->unlock();
326
			throw new UserError('That player name already exists.');
327
		} catch (PlayerNotFound) {
328
			// Player name does not yet exist, we may proceed
329
		}
330
331
		// Get the next available player ID (start at 1 if no players yet)
332
		$dbResult = $db->read('SELECT IFNULL(MAX(player_id), 0) AS player_id FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
333
		$playerID = $dbResult->record()->getInt('player_id') + 1;
334
335
		$startSectorID = 0; // Temporarily put player into non-existent sector
336
		$db->insert('player', [
337
			'account_id' => $db->escapeNumber($accountID),
338
			'game_id' => $db->escapeNumber($gameID),
339
			'player_id' => $db->escapeNumber($playerID),
340
			'player_name' => $db->escapeString($playerName),
341
			'race_id' => $db->escapeNumber($raceID),
342
			'sector_id' => $db->escapeNumber($startSectorID),
343
			'last_cpl_action' => $db->escapeNumber($time),
344
			'last_active' => $db->escapeNumber($time),
345
			'npc' => $db->escapeBoolean($npc),
346
			'newbie_status' => $db->escapeBoolean($isNewbie),
347
		]);
348
		$db->unlock();
349
350
		$player = self::getPlayer($accountID, $gameID);
351
		$player->setSectorID($player->getHome());
352
		return $player;
353
	}
354
355
	/**
356
	 * Get array of players whose info can be accessed by this player.
357
	 * Skips players who are not in the same alliance as this player.
358
	 *
359
	 * @return array<AbstractSmrPlayer>
360
	 */
361
	public function getSharingPlayers(bool $forceUpdate = false): array {
362
		$results = [$this];
363
364
		// Only return this player if not in an alliance
365
		if (!$this->hasAlliance()) {
366
			return $results;
367
		}
368
369
		// Get other players who are sharing info for this game.
370
		// NOTE: game_id=0 means that player shares info for all games.
371
		$dbResult = $this->db->read('SELECT from_account_id FROM account_shares_info WHERE to_account_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND (game_id=0 OR game_id=' . $this->db->escapeNumber($this->getGameID()) . ')');
372
		foreach ($dbResult->records() as $dbRecord) {
373
			try {
374
				$otherPlayer = self::getPlayer($dbRecord->getInt('from_account_id'), $this->getGameID(), $forceUpdate);
375
			} catch (PlayerNotFound) {
376
				// Skip players that have not joined this game
377
				continue;
378
			}
379
380
			// players must be in the same alliance
381
			if ($this->sameAlliance($otherPlayer)) {
382
				$results[] = $otherPlayer;
383
			}
384
		}
385
		return $results;
386
	}
387
388
	public function getSQL(): string {
389
		return $this->SQL;
390
	}
391
392
	public function getZoom(): int {
393
		return $this->zoom;
394
	}
395
396
	protected function setZoom(int $zoom): void {
397
		// Set the zoom level between [1, 9]
398
		$zoom = max(1, min(9, $zoom));
399
		if ($this->zoom == $zoom) {
400
			return;
401
		}
402
		$this->zoom = $zoom;
403
		$this->hasChanged = true;
404
	}
405
406
	public function increaseZoom(int $zoom): void {
407
		if ($zoom < 0) {
408
			throw new Exception('Trying to increase negative zoom.');
409
		}
410
		$this->setZoom($this->getZoom() + $zoom);
411
	}
412
413
	public function decreaseZoom(int $zoom): void {
414
		if ($zoom < 0) {
415
			throw new Exception('Trying to decrease negative zoom.');
416
		}
417
		$this->setZoom($this->getZoom() - $zoom);
418
	}
419
420
	public function getAttackColour(): string {
421
		return $this->attackColour;
422
	}
423
424
	public function setAttackColour(string $colour): void {
425
		if ($this->attackColour == $colour) {
426
			return;
427
		}
428
		$this->attackColour = $colour;
429
		$this->hasChanged = true;
430
	}
431
432
	public function isIgnoreGlobals(): bool {
433
		return $this->ignoreGlobals;
434
	}
435
436
	public function setIgnoreGlobals(bool $bool): void {
437
		if ($this->ignoreGlobals == $bool) {
438
			return;
439
		}
440
		$this->ignoreGlobals = $bool;
441
		$this->hasChanged = true;
442
	}
443
444
	public function getAccount(): SmrAccount {
445
		return SmrAccount::getAccount($this->getAccountID());
446
	}
447
448
	public function getAccountID(): int {
449
		return $this->accountID;
450
	}
451
452
	public function getGameID(): int {
453
		return $this->gameID;
454
	}
455
456
	public function getGame(bool $forceUpdate = false): SmrGame {
457
		return SmrGame::getGame($this->gameID, $forceUpdate);
458
	}
459
460
	public function getNewbieTurns(): int {
461
		return $this->newbieTurns;
462
	}
463
464
	public function hasNewbieTurns(): bool {
465
		return $this->getNewbieTurns() > 0;
466
	}
467
468
	public function setNewbieTurns(int $newbieTurns): void {
469
		if ($this->newbieTurns == $newbieTurns) {
470
			return;
471
		}
472
		$this->newbieTurns = $newbieTurns;
473
		$this->hasChanged = true;
474
	}
475
476
	public function getShip(bool $forceUpdate = false): AbstractSmrShip {
477
		return SmrShip::getShip($this, $forceUpdate);
478
	}
479
480
	public function getShipTypeID(): int {
481
		return $this->shipID;
482
	}
483
484
	/**
485
	 * Do not call directly. Use SmrShip::setTypeID instead.
486
	 */
487
	public function setShipTypeID(int $shipID): void {
488
		if ($this->shipID == $shipID) {
489
			return;
490
		}
491
		$this->shipID = $shipID;
492
		$this->hasChanged = true;
493
	}
494
495
	public function hasCustomShipName(): bool {
496
		return $this->getCustomShipName() !== false;
497
	}
498
499
	public function getCustomShipName(): string|false {
500
		if (!isset($this->customShipName)) {
501
			$dbResult = $this->db->read('SELECT * FROM ship_has_name WHERE ' . $this->SQL);
502
			if ($dbResult->hasRecord()) {
503
				$this->customShipName = $dbResult->record()->getString('ship_name');
504
			} else {
505
				$this->customShipName = false;
506
			}
507
		}
508
		return $this->customShipName;
509
	}
510
511
	public function setCustomShipName(string $name): void {
512
		$this->db->replace('ship_has_name', [
513
			'game_id' => $this->db->escapeNumber($this->getGameID()),
514
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
515
			'ship_name' => $this->db->escapeString($name),
516
		]);
517
	}
518
519
	/**
520
	 * Get planet owned by this player.
521
	 * Returns null if this player does not own a planet.
522
	 */
523
	public function getPlanet(): ?SmrPlanet {
524
		$dbResult = $this->db->read('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
525
		if ($dbResult->hasRecord()) {
526
			$dbRecord = $dbResult->record();
527
			return SmrPlanet::getPlanet($this->getGameID(), $dbRecord->getInt('sector_id'), false, $dbRecord);
528
		}
529
		return null;
530
	}
531
532
	public function getSectorPlanet(): SmrPlanet {
533
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
534
	}
535
536
	public function getSectorPort(): AbstractSmrPort {
537
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
538
	}
539
540
	public function getSectorID(): int {
541
		return $this->sectorID;
542
	}
543
544
	public function getSector(): SmrSector {
545
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
546
	}
547
548
	public function setSectorID(int $sectorID): void {
549
		if ($this->sectorID == $sectorID) {
550
			return;
551
		}
552
553
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
554
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
555
556
		$this->setLastSectorID($this->getSectorID());
557
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
558
		$this->sectorID = $sectorID;
559
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
560
		$this->hasChanged = true;
561
562
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
563
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
564
	}
565
566
	public function getLastSectorID(): int {
567
		return $this->lastSectorID;
568
	}
569
570
	public function setLastSectorID(int $lastSectorID): void {
571
		if ($this->lastSectorID == $lastSectorID) {
572
			return;
573
		}
574
		$this->lastSectorID = $lastSectorID;
575
		$this->hasChanged = true;
576
	}
577
578
	public function getHome(): int {
579
		// Draft games may have customized home sectors
580
		if ($this->getGame()->isGameType(SmrGame::GAME_TYPE_DRAFT) && $this->hasAlliance()) {
581
			$leaderID = $this->getAlliance()->getLeaderID();
582
			$dbResult = $this->db->read('SELECT home_sector_id FROM draft_leaders WHERE account_id = ' . $this->db->escapeNumber($leaderID) . ' AND game_id = ' . $this->db->escapeNumber($this->getGameID()));
583
			if ($dbResult->hasRecord()) {
584
				return $dbResult->record()->getInt('home_sector_id');
585
			}
586
		}
587
588
		// get his home sector
589
		$hq_id = GOVERNMENT + $this->getRaceID();
590
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
591
		if (empty($raceHqSectors)) {
592
			// No HQ, default to sector 1
593
			return 1;
594
		}
595
		// If race has multiple HQ's for some reason, use the first one
596
		return key($raceHqSectors);
597
	}
598
599
	public function isDead(): bool {
600
		return $this->dead;
601
	}
602
603
	public function isNPC(): bool {
604
		return $this->npc;
605
	}
606
607
	/**
608
	 * Does the player have Newbie status?
609
	 */
610
	public function hasNewbieStatus(): bool {
611
		return $this->newbieStatus;
612
	}
613
614
	/**
615
	 * Update the player's newbie status if it has changed.
616
	 * This function queries the account, so use sparingly.
617
	 */
618
	public function updateNewbieStatus(): void {
619
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
620
		if ($this->newbieStatus != $accountNewbieStatus) {
621
			$this->newbieStatus = $accountNewbieStatus;
622
			$this->hasChanged = true;
623
		}
624
	}
625
626
	/**
627
	 * Has this player been designated as the alliance flagship?
628
	 */
629
	public function isFlagship(): bool {
630
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
631
	}
632
633
	public function isPresident(): bool {
634
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
635
	}
636
637
	public function isOnCouncil(): bool {
638
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
639
	}
640
641
	public function isDraftLeader(): bool {
642
		if (!isset($this->draftLeader)) {
643
			$dbResult = $this->db->read('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL);
644
			$this->draftLeader = $dbResult->hasRecord();
645
		}
646
		return $this->draftLeader;
647
	}
648
649
	public function getGPWriter(): string|false {
650
		if (!isset($this->gpWriter)) {
651
			$this->gpWriter = false;
652
			$dbResult = $this->db->read('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
653
			if ($dbResult->hasRecord()) {
654
				$this->gpWriter = $dbResult->record()->getString('position');
655
			}
656
		}
657
		return $this->gpWriter;
658
	}
659
660
	public function isGPEditor(): bool {
661
		return $this->getGPWriter() == 'editor';
662
	}
663
664
	public function isForceDropMessages(): bool {
665
		return $this->forceDropMessages;
666
	}
667
668
	public function setForceDropMessages(bool $bool): void {
669
		if ($this->forceDropMessages == $bool) {
670
			return;
671
		}
672
		$this->forceDropMessages = $bool;
673
		$this->hasChanged = true;
674
	}
675
676
	public function getScoutMessageGroupLimit(): int {
677
		return match ($this->scoutMessageGroupType) {
678
			ScoutMessageGroupType::Always => 0,
679
			ScoutMessageGroupType::Auto => MESSAGES_PER_PAGE,
680
			ScoutMessageGroupType::Never => PHP_INT_MAX,
681
		};
682
	}
683
684
	public function getScoutMessageGroupType(): ScoutMessageGroupType {
685
		return $this->scoutMessageGroupType;
686
	}
687
688
	public function setScoutMessageGroupType(ScoutMessageGroupType $setting): void {
689
		if ($this->scoutMessageGroupType === $setting) {
690
			return;
691
		}
692
		$this->scoutMessageGroupType = $setting;
693
		$this->hasChanged = true;
694
	}
695
696
	/**
697
	 * @return int Message ID
698
	 */
699
	protected static function doMessageSending(int $senderID, int $receiverID, int $gameID, int $messageTypeID, string $message, int $expires, bool $senderDelete = false, bool $unread = true): int {
700
		$message = trim($message);
701
		$db = Database::getInstance();
702
		// Keep track of the message_id so it can be returned
703
		$insertID = $db->insert('message', [
704
			'account_id' => $db->escapeNumber($receiverID),
705
			'game_id' => $db->escapeNumber($gameID),
706
			'message_type_id' => $db->escapeNumber($messageTypeID),
707
			'message_text' => $db->escapeString($message),
708
			'sender_id' => $db->escapeNumber($senderID),
709
			'send_time' => $db->escapeNumber(Epoch::time()),
710
			'expire_time' => $db->escapeNumber($expires),
711
			'sender_delete' => $db->escapeBoolean($senderDelete),
712
		]);
713
714
		if ($unread === true) {
715
			// give him the message icon
716
			$db->replace('player_has_unread_messages', [
717
				'game_id' => $db->escapeNumber($gameID),
718
				'account_id' => $db->escapeNumber($receiverID),
719
				'message_type_id' => $db->escapeNumber($messageTypeID),
720
			]);
721
		}
722
723
		switch ($messageTypeID) {
724
			case MSG_PLAYER:
725
				$receiverAccount = SmrAccount::getAccount($receiverID);
726
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isActive()) {
727
					$sender = Messages::getMessagePlayer($senderID, $gameID, $messageTypeID);
728
					if ($sender instanceof self) {
0 ignored issues
show
introduced by
$sender is never a sub-type of self.
Loading history...
729
						$sender = $sender->getDisplayName();
730
					}
731
					$mail = setupMailer();
0 ignored issues
show
Bug introduced by
The function setupMailer was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

731
					$mail = /** @scrutinizer ignore-call */ setupMailer();
Loading history...
732
					$mail->Subject = 'Message Notification';
733
					$mail->setFrom('[email protected]', 'SMR Notifications');
734
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getDateTimeFormat(), Epoch::time()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, $gameID, true);
735
					$mail->msgHTML($bbifiedMessage);
736
					$mail->AltBody = strip_tags($bbifiedMessage);
737
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
738
					$mail->send();
739
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
740
					$receiverAccount->update();
741
				}
742
				break;
743
		}
744
745
		return $insertID;
746
	}
747
748
	public function sendMessageToBox(int $boxTypeID, string $message): void {
749
		// send him the message
750
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
751
	}
752
753
	public function sendGlobalMessage(string $message, bool $canBeIgnored = true): void {
754
		if ($canBeIgnored) {
755
			if ($this->getAccount()->isMailBanned()) {
756
				throw new UserError('You are currently banned from sending messages');
757
			}
758
		}
759
		$this->sendMessageToBox(BOX_GLOBALS, $message);
760
761
		// send to all online player
762
		$db = Database::getInstance();
763
		$dbResult = $db->read('SELECT account_id
764
					FROM active_session
765
					JOIN player USING (game_id, account_id)
766
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_INACTIVE) . '
767
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
768
						AND ignore_globals = \'FALSE\'
769
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
770
771
		foreach ($dbResult->records() as $dbRecord) {
772
			$this->sendMessage($dbRecord->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
773
		}
774
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
775
	}
776
777
	/**
778
	 * @return ($canBeIgnored is true ? int|false : int) Message ID
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($canBeIgnored at position 1 could not be parsed: Unknown type name '$canBeIgnored' at position 1 in ($canBeIgnored.
Loading history...
779
	 */
780
	public function sendMessage(int $receiverID, int $messageTypeID, string $message, bool $canBeIgnored = true, bool $unread = true, int $expires = null, bool $senderDelete = false): int|false {
781
		//get expire time
782
		if ($canBeIgnored) {
783
			if ($this->getAccount()->isMailBanned()) {
784
				throw new UserError('You are currently banned from sending messages');
785
			}
786
			// Don't send messages to players ignoring us
787
			$dbResult = $this->db->read('SELECT 1 FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
788
			if ($dbResult->hasRecord()) {
789
				return false;
790
			}
791
		}
792
793
		$message = word_filter($message);
794
795
		// If expires not specified, use default based on message type
796
		if ($expires === null) {
797
			$expires = match ($messageTypeID) {
798
				MSG_GLOBAL => 3600, // 1h
799
				MSG_PLAYER => 86400 * 31, // 1 month
800
				MSG_PLANET => 86400 * 7, // 1 week
801
				MSG_SCOUT => 86400 * 3, // 3 days
802
				MSG_POLITICAL => 86400 * 31, // 1 month
803
				MSG_ALLIANCE => 86400 * 31, // 1 month
804
				MSG_ADMIN => 86400 * 365, // 1 year
805
				MSG_CASINO => 86400 * 31, // 1 month
806
				default => 86400 * 7, // 1 week
807
			};
808
			$expires += Epoch::time();
809
		}
810
811
		// Do not put scout messages in the sender's sent box
812
		if ($messageTypeID == MSG_SCOUT) {
813
			$senderDelete = true;
814
		}
815
816
		// send him the message and return the message_id
817
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
818
	}
819
820
	public function sendMessageFromOpAnnounce(int $receiverID, string $message, int $expires = null): void {
821
		// get expire time if not set
822
		if ($expires === null) {
823
			$expires = Epoch::time() + 86400 * 14;
824
		}
825
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
826
	}
827
828
	public function sendMessageFromAllianceCommand(int $receiverID, string $message): void {
829
		$expires = Epoch::time() + 86400 * 365;
830
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
831
	}
832
833
	public static function sendMessageFromPlanet(int $gameID, int $receiverID, string $message): void {
834
		//get expire time
835
		$expires = Epoch::time() + 86400 * 31;
836
		// send him the message
837
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
838
	}
839
840
	public static function sendMessageFromPort(int $gameID, int $receiverID, string $message): void {
841
		//get expire time
842
		$expires = Epoch::time() + 86400 * 31;
843
		// send him the message
844
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
845
	}
846
847
	public static function sendMessageFromFedClerk(int $gameID, int $receiverID, string $message): void {
848
		$expires = Epoch::time() + 86400 * 365;
849
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
850
	}
851
852
	public static function sendMessageFromAdmin(int $gameID, int $receiverID, string $message, int $expires = null): void {
853
		//get expire time
854
		if ($expires === null) {
855
			$expires = Epoch::time() + 86400 * 365;
856
		}
857
		// send him the message
858
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
859
	}
860
861
	public static function sendMessageFromAllianceAmbassador(int $gameID, int $receiverID, string $message, int $expires = null): void {
862
		//get expire time
863
		if ($expires === null) {
864
			$expires = Epoch::time() + 86400 * 31;
865
		}
866
		// send him the message
867
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
868
	}
869
870
	public static function sendMessageFromCasino(int $gameID, int $receiverID, string $message, int $expires = null): void {
871
		//get expire time
872
		if ($expires === null) {
873
			$expires = Epoch::time() + 86400 * 7;
874
		}
875
		// send him the message
876
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
877
	}
878
879
	public static function sendMessageFromRace(int $raceID, int $gameID, int $receiverID, string $message, int $expires = null): void {
880
		//get expire time
881
		if ($expires === null) {
882
			$expires = Epoch::time() + 86400 * 5;
883
		}
884
		// send him the message
885
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
886
	}
887
888
	public function setMessagesRead(int $messageTypeID): void {
889
		$this->db->write('DELETE FROM player_has_unread_messages
890
							WHERE ' . $this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
891
		$this->db->write('UPDATE message SET msg_read = ' . $this->db->escapeBoolean(true) . '
892
				WHERE message_type_id = ' . $this->db->escapeNumber($messageTypeID) . ' AND ' . $this->SQL);
893
	}
894
895
	public function getSafeAttackRating(): int {
896
		return max(0, min(8, IFloor($this->getAlignment() / 150) + 4));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

896
		return max(0, min(8, /** @scrutinizer ignore-call */ IFloor($this->getAlignment() / 150) + 4));
Loading history...
897
	}
898
899
	public function hasFederalProtection(): bool {
900
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
901
		if (!$sector->offersFederalProtection()) {
902
			return false;
903
		}
904
905
		$ship = $this->getShip();
906
		if ($ship->hasIllegalGoods()) {
907
			return false;
908
		}
909
910
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
911
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
912
				if ($this->canBeProtectedByRace($fedRaceID)) {
913
					return true;
914
				}
915
			}
916
		}
917
918
		return false;
919
	}
920
921
	public function canBeProtectedByRace(int $raceID): bool {
922
		if (!isset($this->canFed)) {
923
			$this->canFed = [];
924
			foreach (Race::getAllIDs() as $raceID2) {
925
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
926
			}
927
			$dbResult = $this->db->read('SELECT race_id, allowed FROM player_can_fed
928
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(Epoch::time()));
929
			foreach ($dbResult->records() as $dbRecord) {
930
				$this->canFed[$dbRecord->getInt('race_id')] = $dbRecord->getBoolean('allowed');
931
			}
932
		}
933
		return $this->canFed[$raceID];
934
	}
935
936
	/**
937
	 * Returns a boolean identifying if the player can currently
938
	 * participate in battles.
939
	 */
940
	public function canFight(): bool {
941
		return !($this->hasNewbieTurns() ||
942
		         $this->isDead() ||
943
		         $this->isLandedOnPlanet() ||
944
		         $this->hasFederalProtection());
945
	}
946
947
	public function setDead(bool $bool): void {
948
		if ($this->dead == $bool) {
949
			return;
950
		}
951
		$this->dead = $bool;
952
		$this->hasChanged = true;
953
	}
954
955
	public function getKills(): int {
956
		return $this->kills;
957
	}
958
959
	public function increaseKills(int $kills): void {
960
		if ($kills < 0) {
961
			throw new Exception('Trying to increase negative kills.');
962
		}
963
		$this->setKills($this->kills + $kills);
964
	}
965
966
	public function setKills(int $kills): void {
967
		if ($this->kills == $kills) {
968
			return;
969
		}
970
		$this->kills = $kills;
971
		$this->hasChanged = true;
972
	}
973
974
	public function getDeaths(): int {
975
		return $this->deaths;
976
	}
977
978
	public function increaseDeaths(int $deaths): void {
979
		if ($deaths < 0) {
980
			throw new Exception('Trying to increase negative deaths.');
981
		}
982
		$this->setDeaths($this->getDeaths() + $deaths);
983
	}
984
985
	public function setDeaths(int $deaths): void {
986
		if ($this->deaths == $deaths) {
987
			return;
988
		}
989
		$this->deaths = $deaths;
990
		$this->hasChanged = true;
991
	}
992
993
	public function getAssists(): int {
994
		return $this->assists;
995
	}
996
997
	public function increaseAssists(int $assists): void {
998
		if ($assists < 1) {
999
			throw new Exception('Must increase by a positive number.');
1000
		}
1001
		$this->assists += $assists;
1002
		$this->hasChanged = true;
1003
	}
1004
1005
	public function hasGoodAlignment(): bool {
1006
		return $this->alignment >= ALIGNMENT_GOOD;
1007
	}
1008
1009
	public function hasEvilAlignment(): bool {
1010
		return $this->alignment <= ALIGNMENT_EVIL;
1011
	}
1012
1013
	public function hasNeutralAlignment(): bool {
1014
		return !$this->hasGoodAlignment() && !$this->hasEvilAlignment();
1015
	}
1016
1017
	public function getAlignment(): int {
1018
		return $this->alignment;
1019
	}
1020
1021
	public function increaseAlignment(int $align): void {
1022
		if ($align < 0) {
1023
			throw new Exception('Trying to increase negative align.');
1024
		}
1025
		if ($align == 0) {
1026
			return;
1027
		}
1028
		$align += $this->alignment;
1029
		$this->setAlignment($align);
1030
	}
1031
1032
	public function decreaseAlignment(int $align): void {
1033
		if ($align < 0) {
1034
			throw new Exception('Trying to decrease negative align.');
1035
		}
1036
		if ($align == 0) {
1037
			return;
1038
		}
1039
		$align = $this->alignment - $align;
1040
		$this->setAlignment($align);
1041
	}
1042
1043
	public function setAlignment(int $align): void {
1044
		if ($this->alignment == $align) {
1045
			return;
1046
		}
1047
		$this->alignment = $align;
1048
		$this->hasChanged = true;
1049
	}
1050
1051
	public function getCredits(): int {
1052
		return $this->credits;
1053
	}
1054
1055
	public function getBank(): int {
1056
		return $this->bank;
1057
	}
1058
1059
	/**
1060
	 * Increases personal bank account up to the maximum allowed credits.
1061
	 * Returns the amount that was actually added to handle overflow.
1062
	 */
1063
	public function increaseBank(int $credits): int {
1064
		if ($credits == 0) {
1065
			return 0;
1066
		}
1067
		if ($credits < 0) {
1068
			throw new Exception('Trying to increase negative credits.');
1069
		}
1070
		$newTotal = min($this->bank + $credits, MAX_MONEY);
1071
		$actualAdded = $newTotal - $this->bank;
1072
		$this->setBank($newTotal);
1073
		return $actualAdded;
1074
	}
1075
1076
	public function decreaseBank(int $credits): void {
1077
		if ($credits == 0) {
1078
			return;
1079
		}
1080
		if ($credits < 0) {
1081
			throw new Exception('Trying to decrease negative credits.');
1082
		}
1083
		$newTotal = $this->bank - $credits;
1084
		$this->setBank($newTotal);
1085
	}
1086
1087
	public function setBank(int $credits): void {
1088
		if ($this->bank == $credits) {
1089
			return;
1090
		}
1091
		if ($credits < 0) {
1092
			throw new Exception('Trying to set negative credits.');
1093
		}
1094
		if ($credits > MAX_MONEY) {
1095
			throw new Exception('Trying to set more than max credits.');
1096
		}
1097
		$this->bank = $credits;
1098
		$this->hasChanged = true;
1099
	}
1100
1101
	public function getExperience(): int {
1102
		return $this->experience;
1103
	}
1104
1105
	/**
1106
	 * Returns the percent progress towards the next level.
1107
	 * This value is rounded because it is used primarily in HTML img widths.
1108
	 */
1109
	public function getNextLevelPercentAcquired(): int {
1110
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1111
			return 100;
1112
		}
1113
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1113
		return max(0, min(100, /** @scrutinizer ignore-call */ IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
Loading history...
1114
	}
1115
1116
	public function getNextLevelPercentRemaining(): int {
1117
		return 100 - $this->getNextLevelPercentAcquired();
1118
	}
1119
1120
	/**
1121
	 * @return array<string, string|int>
1122
	 */
1123
	public function getNextLevel(): array {
1124
		$LEVELS = Globals::getLevelRequirements();
1125
		if (!isset($LEVELS[$this->getLevelID() + 1])) {
1126
			return $LEVELS[$this->getLevelID()]; //Return current level experience if on last level.
1127
		}
1128
		return $LEVELS[$this->getLevelID() + 1];
1129
	}
1130
1131
	public function getNextLevelExperience(): int {
1132
		return $this->getNextLevel()['Requirement'];
1133
	}
1134
1135
	public function getThisLevelExperience(): int {
1136
		$LEVELS = Globals::getLevelRequirements();
1137
		return $LEVELS[$this->getLevelID()]['Requirement'];
1138
	}
1139
1140
	public function setExperience(int $experience): void {
1141
		if ($this->experience == $experience) {
1142
			return;
1143
		}
1144
		if ($experience < MIN_EXPERIENCE) {
1145
			$experience = MIN_EXPERIENCE;
1146
		}
1147
		if ($experience > MAX_EXPERIENCE) {
1148
			$experience = MAX_EXPERIENCE;
1149
		}
1150
		$this->experience = $experience;
1151
		$this->hasChanged = true;
1152
1153
		// Since exp has changed, invalidate the player level so that it can
1154
		// be recomputed next time it is queried (in case it has changed).
1155
		$this->level = null;
1156
	}
1157
1158
	/**
1159
	 * Increases onboard credits up to the maximum allowed credits.
1160
	 * Returns the amount that was actually added to handle overflow.
1161
	 */
1162
	public function increaseCredits(int $credits): int {
1163
		if ($credits == 0) {
1164
			return 0;
1165
		}
1166
		if ($credits < 0) {
1167
			throw new Exception('Trying to increase negative credits.');
1168
		}
1169
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1170
		$actualAdded = $newTotal - $this->credits;
1171
		$this->setCredits($newTotal);
1172
		return $actualAdded;
1173
	}
1174
1175
	public function decreaseCredits(int $credits): void {
1176
		if ($credits == 0) {
1177
			return;
1178
		}
1179
		if ($credits < 0) {
1180
			throw new Exception('Trying to decrease negative credits.');
1181
		}
1182
		$newTotal = $this->credits - $credits;
1183
		$this->setCredits($newTotal);
1184
	}
1185
1186
	public function setCredits(int $credits): void {
1187
		if ($this->credits == $credits) {
1188
			return;
1189
		}
1190
		if ($credits < 0) {
1191
			throw new Exception('Trying to set negative credits.');
1192
		}
1193
		if ($credits > MAX_MONEY) {
1194
			throw new Exception('Trying to set more than max credits.');
1195
		}
1196
		$this->credits = $credits;
1197
		$this->hasChanged = true;
1198
	}
1199
1200
	public function increaseExperience(int $experience): void {
1201
		if ($experience < 0) {
1202
			throw new Exception('Trying to increase negative experience.');
1203
		}
1204
		if ($experience == 0) {
1205
			return;
1206
		}
1207
		$newExperience = $this->experience + $experience;
1208
		$this->setExperience($newExperience);
1209
		$this->increaseHOF($experience, ['Experience', 'Total', 'Gain'], HOF_PUBLIC);
1210
	}
1211
	public function decreaseExperience(int $experience): void {
1212
		if ($experience < 0) {
1213
			throw new Exception('Trying to decrease negative experience.');
1214
		}
1215
		if ($experience == 0) {
1216
			return;
1217
		}
1218
		$newExperience = $this->experience - $experience;
1219
		$this->setExperience($newExperience);
1220
		$this->increaseHOF($experience, ['Experience', 'Total', 'Loss'], HOF_PUBLIC);
1221
	}
1222
1223
	public function isLandedOnPlanet(): bool {
1224
		return $this->landedOnPlanet;
1225
	}
1226
1227
	public function setLandedOnPlanet(bool $bool): void {
1228
		if ($this->landedOnPlanet == $bool) {
1229
			return;
1230
		}
1231
		$this->landedOnPlanet = $bool;
1232
		$this->hasChanged = true;
1233
	}
1234
1235
	/**
1236
	 * Returns the numerical level of the player (e.g. 1-50).
1237
	 */
1238
	public function getLevelID(): int {
1239
		// The level is cached for performance reasons unless `setExperience`
1240
		// is called and the player's experience changes.
1241
		if (!isset($this->level)) {
1242
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1243
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1244
				if ($this->getExperience() >= $require['Requirement']) {
1245
					continue;
1246
				}
1247
				$this->level = $level_id - 1;
1248
				return $this->level;
1249
			}
1250
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1251
		}
1252
		return $this->level;
1253
	}
1254
1255
	public function getLevelName(): string {
1256
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1257
		if ($this->isPresident()) {
1258
			$level_name = '<img src="images/council_president.png" title="' . Race::getName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1259
		}
1260
		return $level_name;
1261
	}
1262
1263
	public function getMaxLevel(): int {
1264
		return max(array_keys(Globals::getLevelRequirements()));
1265
	}
1266
1267
	public function getPlayerID(): int {
1268
		return $this->playerID;
1269
	}
1270
1271
	/**
1272
	 * Returns the player name.
1273
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1274
	 */
1275
	public function getPlayerName(): string {
1276
		return $this->playerName;
1277
	}
1278
1279
	public function setPlayerName(string $name): void {
1280
		$this->playerName = $name;
1281
		$this->hasChanged = true;
1282
	}
1283
1284
	/**
1285
	 * Returns the decorated player name, suitable for HTML display.
1286
	 */
1287
	public function getDisplayName(bool $includeAlliance = false): string {
1288
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1289
		$return = get_colored_text($this->getAlignment(), $name);
1290
		if ($this->isNPC()) {
1291
			$return .= ' <span class="npcColour">[NPC]</span>';
1292
		}
1293
		if ($includeAlliance) {
1294
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1295
		}
1296
		return $return;
1297
	}
1298
1299
	public function getBBLink(): string {
1300
			return '[player=' . $this->getPlayerID() . ']';
1301
	}
1302
1303
	public function getLinkedDisplayName(bool $includeAlliance = true): string {
1304
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1305
		if ($includeAlliance) {
1306
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1307
		}
1308
		return $return;
1309
	}
1310
1311
	/**
1312
	 * Change a player's name, with name validation.
1313
	 *
1314
	 * @throws Smr\Exceptions\UserError When the new name is not permitted.
1315
	 */
1316
	public function changePlayerName(string $name): void {
1317
		// Check if the player already has this name (case-sensitive)
1318
		if ($this->getPlayerName() == $name) {
1319
			throw new UserError('Your player already has that name!');
1320
		}
1321
1322
		// Make sure the name passes some basic character requirements
1323
		DisplayNameValidator::validate($name);
1324
1325
		// Check if name is in use by any other player.
1326
		try {
1327
			$other = self::getPlayerByPlayerName($name, $this->getGameID());
1328
			// The player_name field has case-insensitive collation, so if we
1329
			// find our own player, then it is because the new name has a
1330
			// different case (since we did a case-sensitive identity check
1331
			// above), and we allow it to be changed.
1332
			if ($this->equals($other)) {
1333
				throw new PlayerNotFound();
1334
			}
1335
			throw new UserError('That name is already being used in this game!');
1336
		} catch (PlayerNotFound) {
1337
			// Name is not in use, continue.
1338
		}
1339
1340
		$this->setPlayerName($name);
1341
	}
1342
1343
	/**
1344
	 * Use this method when the player is changing their own name.
1345
	 * This will flag the player as having used their free name change.
1346
	 */
1347
	public function changePlayerNameByPlayer(string $playerName): void {
1348
		$this->changePlayerName($playerName);
1349
		$this->setNameChanged(true);
1350
	}
1351
1352
	public function isNameChanged(): bool {
1353
		return $this->nameChanged;
1354
	}
1355
1356
	public function setNameChanged(bool $bool): void {
1357
		$this->nameChanged = $bool;
1358
		$this->hasChanged = true;
1359
	}
1360
1361
	public function isRaceChanged(): bool {
1362
		return $this->raceChanged;
1363
	}
1364
1365
	public function setRaceChanged(bool $raceChanged): void {
1366
		$this->raceChanged = $raceChanged;
1367
		$this->hasChanged = true;
1368
	}
1369
1370
	public function canChangeRace(): bool {
1371
		return !$this->isRaceChanged() && (Epoch::time() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1372
	}
1373
1374
	public static function getColouredRaceNameOrDefault(int $otherRaceID, self $player = null, bool $linked = false): string {
1375
		$relations = 0;
1376
		if ($player !== null) {
1377
			$relations = $player->getRelation($otherRaceID);
1378
		}
1379
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1380
	}
1381
1382
	public function getColouredRaceName(int $otherRaceID, bool $linked = false): string {
1383
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1384
	}
1385
1386
	public function setRaceID(int $raceID): void {
1387
		if ($this->raceID == $raceID) {
1388
			return;
1389
		}
1390
		$this->raceID = $raceID;
1391
		$this->hasChanged = true;
1392
	}
1393
1394
	public function isAllianceLeader(bool $forceUpdate = false): bool {
1395
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1396
	}
1397
1398
	public function getAlliance(bool $forceUpdate = false): SmrAlliance {
1399
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1400
	}
1401
1402
	public function getAllianceID(): int {
1403
		return $this->allianceID;
1404
	}
1405
1406
	public function hasAlliance(): bool {
1407
		return $this->getAllianceID() != 0;
1408
	}
1409
1410
	protected function setAllianceID(int $ID): void {
1411
		if ($this->allianceID == $ID) {
1412
			return;
1413
		}
1414
		$this->allianceID = $ID;
1415
		if ($this->allianceID != 0) {
1416
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1417
			$this->db->write('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1418
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1419
		}
1420
		$this->hasChanged = true;
1421
	}
1422
1423
	public function getAllianceBBLink(): string {
1424
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1425
	}
1426
1427
	public function getAllianceDisplayName(bool $linked = false, bool $includeAllianceID = false): string {
1428
		if (!$this->hasAlliance()) {
1429
			return 'No Alliance';
1430
		}
1431
		return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1432
	}
1433
1434
	public function getAllianceRole(int $allianceID = null): int {
1435
		if ($allianceID === null) {
1436
			$allianceID = $this->getAllianceID();
1437
		}
1438
		if (!isset($this->allianceRoles[$allianceID])) {
1439
			$this->allianceRoles[$allianceID] = 0;
1440
			$dbResult = $this->db->read('SELECT role_id
1441
						FROM player_has_alliance_role
1442
						WHERE ' . $this->SQL . '
1443
						AND alliance_id=' . $this->db->escapeNumber($allianceID));
1444
			if ($dbResult->hasRecord()) {
1445
				$this->allianceRoles[$allianceID] = $dbResult->record()->getInt('role_id');
1446
			}
1447
		}
1448
		return $this->allianceRoles[$allianceID];
1449
	}
1450
1451
	public function leaveAlliance(self $kickedBy = null): void {
1452
		$alliance = $this->getAlliance();
1453
		if ($kickedBy !== null) {
1454
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1455
			$this->actionTaken('PlayerKicked', ['Alliance' => $alliance, 'Player' => $kickedBy]);
1456
			$kickedBy->actionTaken('KickPlayer', ['Alliance' => $alliance, 'Player' => $this]);
1457
		} elseif ($this->isAllianceLeader()) {
1458
			$this->actionTaken('DisbandAlliance', ['Alliance' => $alliance]);
1459
		} else {
1460
			$this->actionTaken('LeaveAlliance', ['Alliance' => $alliance]);
1461
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1462
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1463
			}
1464
		}
1465
1466
		// Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1467
		if (!$this->isAllianceLeader() && !$alliance->isNHA()) {
1468
			$this->setAllianceJoinable(Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH);
1469
			$alliance->getLeader()->setAllianceJoinable(Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH); //We set the joinable time for leader here, that way a single player alliance won't cause a player to wait before switching.
1470
		}
1471
1472
		$this->setAllianceID(0);
1473
		$this->db->write('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1474
1475
		// Update the alliance cache
1476
		unset(self::$CACHE_ALLIANCE_PLAYERS[$this->gameID][$alliance->getAllianceID()][$this->accountID]);
1477
	}
1478
1479
	/**
1480
	 * Join an alliance (used for both Leader and New Member roles)
1481
	 */
1482
	public function joinAlliance(int $allianceID): void {
1483
		$this->setAllianceID($allianceID);
1484
		$alliance = $this->getAlliance();
1485
1486
		if (!$this->isAllianceLeader()) {
1487
			// Do not throw an exception if the NHL account doesn't exist.
1488
			try {
1489
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1490
			} catch (AccountNotFound $e) {
1491
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1492
					throw $e;
1493
				}
1494
			}
1495
1496
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1497
		} else {
1498
			$roleID = ALLIANCE_ROLE_LEADER;
1499
		}
1500
		$this->db->insert('player_has_alliance_role', [
1501
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1502
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1503
			'role_id' => $this->db->escapeNumber($roleID),
1504
			'alliance_id' => $this->db->escapeNumber($this->getAllianceID()),
1505
		]);
1506
1507
		$this->actionTaken('JoinAlliance', ['Alliance' => $alliance]);
1508
	}
1509
1510
	public function getAllianceJoinable(): int {
1511
		return $this->allianceJoinable;
1512
	}
1513
1514
	private function setAllianceJoinable(int $time): void {
1515
		if ($this->allianceJoinable == $time) {
1516
			return;
1517
		}
1518
		$this->allianceJoinable = $time;
1519
		$this->hasChanged = true;
1520
	}
1521
1522
	/**
1523
	 * Invites player with $accountID to this player's alliance.
1524
	 */
1525
	public function sendAllianceInvitation(int $accountID, string $message, int $expires): void {
1526
		if (!$this->hasAlliance()) {
1527
			throw new Exception('Must be in an alliance to send alliance invitations');
1528
		}
1529
		// Send message to invited player
1530
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1531
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1532
	}
1533
1534
	public function isCombatDronesKamikazeOnMines(): bool {
1535
		return $this->combatDronesKamikazeOnMines;
1536
	}
1537
1538
	public function setCombatDronesKamikazeOnMines(bool $bool): void {
1539
		if ($this->combatDronesKamikazeOnMines == $bool) {
1540
			return;
1541
		}
1542
		$this->combatDronesKamikazeOnMines = $bool;
1543
		$this->hasChanged = true;
1544
	}
1545
1546
	protected function getPersonalRelationsData(): void {
1547
		if (!isset($this->personalRelations)) {
1548
			//get relations
1549
			$this->personalRelations = [];
1550
			foreach (Race::getAllIDs() as $raceID) {
1551
				$this->personalRelations[$raceID] = 0;
1552
			}
1553
			$dbResult = $this->db->read('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL);
1554
			foreach ($dbResult->records() as $dbRecord) {
1555
				$this->personalRelations[$dbRecord->getInt('race_id')] = $dbRecord->getInt('relation');
1556
			}
1557
		}
1558
	}
1559
1560
	/**
1561
	 * @return array<int, int>
1562
	 */
1563
	public function getPersonalRelations(): array {
1564
		$this->getPersonalRelationsData();
1565
		return $this->personalRelations;
1566
	}
1567
1568
	/**
1569
	 * Get personal relations with a race
1570
	 */
1571
	public function getPersonalRelation(int $raceID): int {
1572
		$rels = $this->getPersonalRelations();
1573
		return $rels[$raceID];
1574
	}
1575
1576
	/**
1577
	 * Get total relations with all races (personal + political)
1578
	 *
1579
	 * @return array<int, int>
1580
	 */
1581
	public function getRelations(): array {
1582
		if (!isset($this->relations)) {
1583
			//get relations
1584
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1585
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1586
			$this->relations = [];
1587
			foreach (Race::getAllIDs() as $raceID) {
1588
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1589
			}
1590
		}
1591
		return $this->relations;
1592
	}
1593
1594
	/**
1595
	 * Get total relations with a race (personal + political)
1596
	 */
1597
	public function getRelation(int $raceID): int {
1598
		$rels = $this->getRelations();
1599
		return $rels[$raceID];
1600
	}
1601
1602
	/**
1603
	 * Increases personal relations from trading $numGoods units with the race
1604
	 * of the port given by $raceID.
1605
	 */
1606
	public function increaseRelationsByTrade(int $numGoods, int $raceID): void {
1607
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1607
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1608
		//Cap relations to a max of 1 after 500 have been reached
1609
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1610
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1611
		}
1612
		$this->increaseRelations($relations, $raceID);
1613
	}
1614
1615
	/**
1616
	 * Decreases personal relations from trading failures, e.g. rejected
1617
	 * bargaining and getting caught stealing.
1618
	 */
1619
	public function decreaseRelationsByTrade(int $numGoods, int $raceID): void {
1620
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1620
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1621
		$this->decreaseRelations($relations, $raceID);
1622
	}
1623
1624
	/**
1625
	 * Increase personal relations.
1626
	 */
1627
	public function increaseRelations(int $relations, int $raceID): void {
1628
		if ($relations < 0) {
1629
			throw new Exception('Trying to increase negative relations.');
1630
		}
1631
		if ($relations == 0) {
1632
			return;
1633
		}
1634
		$relations += $this->getPersonalRelation($raceID);
1635
		$this->setRelations($relations, $raceID);
1636
	}
1637
1638
	/**
1639
	 * Decrease personal relations.
1640
	 */
1641
	public function decreaseRelations(int $relations, int $raceID): void {
1642
		if ($relations < 0) {
1643
			throw new Exception('Trying to decrease negative relations.');
1644
		}
1645
		if ($relations == 0) {
1646
			return;
1647
		}
1648
		$relations = $this->getPersonalRelation($raceID) - $relations;
1649
		$this->setRelations($relations, $raceID);
1650
	}
1651
1652
	/**
1653
	 * Set personal relations.
1654
	 */
1655
	public function setRelations(int $relations, int $raceID): void {
1656
		$this->getRelations();
1657
		if ($this->personalRelations[$raceID] == $relations) {
1658
			return;
1659
		}
1660
		if ($relations < MIN_RELATIONS) {
1661
			$relations = MIN_RELATIONS;
1662
		}
1663
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1663
		$relationsDiff = /** @scrutinizer ignore-call */ IRound($relations - $this->personalRelations[$raceID]);
Loading history...
1664
		$this->personalRelations[$raceID] = $relations;
1665
		$this->relations[$raceID] += $relationsDiff;
1666
		$this->db->replace('player_has_relation', [
1667
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1668
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1669
			'race_id' => $this->db->escapeNumber($raceID),
1670
			'relation' => $this->db->escapeNumber($this->personalRelations[$raceID]),
1671
		]);
1672
	}
1673
1674
	/**
1675
	 * Set any starting personal relations bonuses or penalties.
1676
	 */
1677
	public function giveStartingRelations(): void {
1678
		if ($this->getRaceID() === RACE_ALSKANT) {
1679
			// Give Alskants bonus personal relations to start.
1680
			foreach (Race::getAllIDs() as $raceID) {
1681
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1682
			}
1683
		}
1684
	}
1685
1686
	public function getLastNewsUpdate(): int {
1687
		return $this->lastNewsUpdate;
1688
	}
1689
1690
	private function setLastNewsUpdate(int $time): void {
1691
		if ($this->lastNewsUpdate == $time) {
1692
			return;
1693
		}
1694
		$this->lastNewsUpdate = $time;
1695
		$this->hasChanged = true;
1696
	}
1697
1698
	public function updateLastNewsUpdate(): void {
1699
		$this->setLastNewsUpdate(Epoch::time());
1700
	}
1701
1702
	public function getLastPort(): int {
1703
		return $this->lastPort;
1704
	}
1705
1706
	public function setLastPort(int $lastPort): void {
1707
		if ($this->lastPort == $lastPort) {
1708
			return;
1709
		}
1710
		$this->lastPort = $lastPort;
1711
		$this->hasChanged = true;
1712
	}
1713
1714
	public function getPlottedCourse(): ?Path {
1715
		if (!isset($this->plottedCourse)) {
1716
			// check if we have a course plotted
1717
			$dbResult = $this->db->read('SELECT course FROM player_plotted_course WHERE ' . $this->SQL);
1718
1719
			if ($dbResult->hasRecord()) {
1720
				// get the course back
1721
				$this->plottedCourse = $dbResult->record()->getObject('course');
1722
			} else {
1723
				$this->plottedCourse = null;
1724
			}
1725
		}
1726
1727
		// Update the plotted course if we have moved since the last query
1728
		if ($this->plottedCourse !== null && $this->plottedCourse->getStartSectorID() != $this->getSectorID()) {
1729
			if ($this->plottedCourse->getEndSectorID() == $this->getSectorID()) {
1730
				// We have reached our destination
1731
				$this->deletePlottedCourse();
1732
			} elseif ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1733
				// We have walked into the next sector of the course
1734
				$this->plottedCourse->followPath();
1735
				$this->setPlottedCourse($this->plottedCourse);
1736
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1737
				// We have skipped to some later sector in the course
1738
				$this->plottedCourse->skipToSector($this->getSectorID());
1739
				$this->setPlottedCourse($this->plottedCourse);
1740
			}
1741
		}
1742
		return $this->plottedCourse;
1743
	}
1744
1745
	public function setPlottedCourse(Path $plottedCourse): void {
1746
		$this->plottedCourse = $plottedCourse;
1747
		$this->db->replace('player_plotted_course', [
1748
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1749
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1750
			'course' => $this->db->escapeObject($this->plottedCourse),
1751
		]);
1752
	}
1753
1754
	public function hasPlottedCourse(): bool {
1755
		return $this->getPlottedCourse() !== null;
1756
	}
1757
1758
	public function isPartOfCourse(SmrSector $sector): bool {
1759
		return $this->getPlottedCourse()?->isInPath($sector->getSectorID()) === true;
1760
	}
1761
1762
	public function deletePlottedCourse(): void {
1763
		$this->plottedCourse = null;
1764
		$this->db->write('DELETE FROM player_plotted_course WHERE ' . $this->SQL);
1765
	}
1766
1767
	/**
1768
	 * Computes the turn cost and max misjump between current and target sector
1769
	 *
1770
	 * @return array<string, int>
1771
	 */
1772
	public function getJumpInfo(SmrSector $targetSector): array {
1773
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1774
		if ($path === false) {
1775
			throw new UserError('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1776
		}
1777
		$distance = $path->getDistance();
1778
1779
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1779
		$turnCost = max(TURNS_JUMP_MINIMUM, /** @scrutinizer ignore-call */ IRound($distance * TURNS_PER_JUMP_DISTANCE));
Loading history...
1780
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1781
		return ['turn_cost' => $turnCost, 'max_misjump' => $maxMisjump];
1782
	}
1783
1784
	public function __sleep() {
1785
		return ['accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName', 'npc'];
1786
	}
1787
1788
	/**
1789
	 * @return array<int, StoredDestination>
1790
	 */
1791
	public function getStoredDestinations(): array {
1792
		if (!isset($this->storedDestinations)) {
1793
			$this->storedDestinations = [];
1794
			$dbResult = $this->db->read('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1795
			foreach ($dbResult->records() as $dbRecord) {
1796
				$sectorID = $dbRecord->getInt('sector_id');
1797
				$this->storedDestinations[$sectorID] = new StoredDestination(
1798
					sectorID: $sectorID,
1799
					label: $dbRecord->getString('label'),
1800
					offsetTop: $dbRecord->getInt('offset_top'),
1801
					offsetLeft: $dbRecord->getInt('offset_left'),
1802
				);
1803
			}
1804
		}
1805
		return $this->storedDestinations;
1806
	}
1807
1808
	public function moveDestinationButton(int $sectorID, int $offsetTop, int $offsetLeft): void {
1809
		$this->getStoredDestinations(); // make sure property is initialized
1810
1811
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1812
			throw new UserError('The saved sector must be in the box!');
1813
		}
1814
1815
		if (!isset($this->storedDestinations[$sectorID])) {
1816
			throw new UserError('You do not have a saved sector for #' . $sectorID);
1817
		}
1818
1819
		// Replace destination with updated offsets
1820
		$this->storedDestinations[$sectorID] = new StoredDestination(
1821
			sectorID: $sectorID,
1822
			label: $this->storedDestinations[$sectorID]->label,
1823
			offsetTop: $offsetTop,
1824
			offsetLeft: $offsetLeft,
1825
		);
1826
		$this->db->write('
1827
			UPDATE player_stored_sector
1828
				SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1829
			WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID));
1830
	}
1831
1832
	public function addDestinationButton(int $sectorID, string $label): void {
1833
		$this->getStoredDestinations(); // make sure property is initialized
1834
1835
		if (!SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1836
			throw new UserError('You want to add a non-existent sector?');
1837
		}
1838
1839
		// sector already stored ?
1840
		if (isset($this->storedDestinations[$sectorID])) {
1841
			throw new UserError('Sector already stored!');
1842
		}
1843
1844
		$this->storedDestinations[$sectorID] = new StoredDestination(
1845
			label: $label,
1846
			sectorID: $sectorID,
1847
			offsetTop: 1,
1848
			offsetLeft: 1,
1849
		);
1850
1851
		$this->db->insert('player_stored_sector', [
1852
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1853
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1854
			'sector_id' => $this->db->escapeNumber($sectorID),
1855
			'label' => $this->db->escapeString($label),
1856
			'offset_top' => 1,
1857
			'offset_left' => 1,
1858
		]);
1859
	}
1860
1861
	public function deleteDestinationButton(int $sectorID): void {
1862
		$this->getStoredDestinations(); // make sure property is initialized
1863
1864
		if (!isset($this->storedDestinations[$sectorID])) {
1865
			throw new Exception('Could not find stored destination');
1866
		}
1867
1868
		$this->db->write('
1869
			DELETE FROM player_stored_sector
1870
			WHERE ' . $this->SQL . '
1871
			AND sector_id = ' . $this->db->escapeNumber($sectorID));
1872
		unset($this->storedDestinations[$sectorID]);
1873
	}
1874
1875
	/**
1876
	 * @return array<string, array<string, mixed>>
1877
	 */
1878
	public function getTickers(): array {
1879
		if (!isset($this->tickers)) {
1880
			$this->tickers = [];
1881
			//get ticker info
1882
			$dbResult = $this->db->read('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(Epoch::time()));
1883
			foreach ($dbResult->records() as $dbRecord) {
1884
				$this->tickers[$dbRecord->getString('type')] = [
1885
					'Type' => $dbRecord->getString('type'),
1886
					'Time' => $dbRecord->getInt('time'),
1887
					'Expires' => $dbRecord->getInt('expires'),
1888
					'Recent' => $dbRecord->getString('recent'),
1889
				];
1890
			}
1891
		}
1892
		return $this->tickers;
1893
	}
1894
1895
	public function hasTickers(): bool {
1896
		return count($this->getTickers()) > 0;
1897
	}
1898
1899
	/**
1900
	 * @return array<string, mixed>|false
1901
	 */
1902
	public function getTicker(string $tickerType): array|false {
1903
		$tickers = $this->getTickers();
1904
		if (isset($tickers[$tickerType])) {
1905
			return $tickers[$tickerType];
1906
		}
1907
		return false;
1908
	}
1909
1910
	public function hasTicker(string $tickerType): bool {
1911
		return $this->getTicker($tickerType) !== false;
1912
	}
1913
1914
	/**
1915
	 * @return array<string, mixed>
1916
	 */
1917
	public function shootForces(SmrForce $forces): array {
1918
		return $this->getShip()->shootForces($forces);
1919
	}
1920
1921
	/**
1922
	 * @return array<string, mixed>
1923
	 */
1924
	public function shootPort(SmrPort $port): array {
1925
		return $this->getShip()->shootPort($port);
1926
	}
1927
1928
	/**
1929
	 * @return array<string, mixed>
1930
	 */
1931
	public function shootPlanet(SmrPlanet $planet): array {
1932
		return $this->getShip()->shootPlanet($planet);
1933
	}
1934
1935
	/**
1936
	 * @param array<AbstractSmrPlayer> $targetPlayers
1937
	 * @return array<string, mixed>
1938
	 */
1939
	public function shootPlayers(array $targetPlayers): array {
1940
		return $this->getShip()->shootPlayers($targetPlayers);
1941
	}
1942
1943
	public function getMilitaryPayment(): int {
1944
		return $this->militaryPayment;
1945
	}
1946
1947
	public function hasMilitaryPayment(): bool {
1948
		return $this->getMilitaryPayment() > 0;
1949
	}
1950
1951
	public function setMilitaryPayment(int $amount): void {
1952
		if ($this->militaryPayment == $amount) {
1953
			return;
1954
		}
1955
		$this->militaryPayment = $amount;
1956
		$this->hasChanged = true;
1957
	}
1958
1959
	public function increaseMilitaryPayment(int $amount): void {
1960
		if ($amount < 0) {
1961
			throw new Exception('Trying to increase negative military payment.');
1962
		}
1963
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1964
	}
1965
1966
	public function decreaseMilitaryPayment(int $amount): void {
1967
		if ($amount < 0) {
1968
			throw new Exception('Trying to decrease negative military payment.');
1969
		}
1970
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1971
	}
1972
1973
	protected function getBountiesData(): void {
1974
		if (!isset($this->bounties)) {
1975
			$this->bounties = [];
1976
			$dbResult = $this->db->read('SELECT * FROM bounty WHERE ' . $this->SQL);
1977
			foreach ($dbResult->records() as $dbRecord) {
1978
				$this->bounties[$dbRecord->getInt('bounty_id')] = [
1979
							'Amount' => $dbRecord->getInt('amount'),
1980
							'SmrCredits' => $dbRecord->getInt('smr_credits'),
1981
							'Type' => BountyType::from($dbRecord->getString('type')),
1982
							'Claimer' => $dbRecord->getInt('claimer_id'),
1983
							'Time' => $dbRecord->getInt('time'),
1984
							'ID' => $dbRecord->getInt('bounty_id'),
1985
							'New' => false];
1986
			}
1987
		}
1988
	}
1989
1990
	/**
1991
	 * Get bounties that can be claimed by this player.
1992
	 *
1993
	 * @return array<array<string, mixed>>
1994
	 */
1995
	public function getClaimableBounties(?BountyType $type = null): array {
1996
		$bounties = [];
1997
		$query = 'SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID());
1998
		$query .= match ($type) {
1999
			null => '',
2000
			default => ' AND type=' . $this->db->escapeString($type->value),
2001
		};
2002
		$dbResult = $this->db->read($query);
2003
		foreach ($dbResult->records() as $dbRecord) {
2004
			$bounties[] = [
2005
				'player' => self::getPlayer($dbRecord->getInt('account_id'), $this->getGameID()),
2006
				'bounty_id' => $dbRecord->getInt('bounty_id'),
2007
				'credits' => $dbRecord->getInt('amount'),
2008
				'smr_credits' => $dbRecord->getInt('smr_credits'),
2009
			];
2010
		}
2011
		return $bounties;
2012
	}
2013
2014
	/**
2015
	 * @return array<int, array<string, mixed>>
2016
	 */
2017
	public function getBounties(): array {
2018
		$this->getBountiesData();
2019
		return $this->bounties;
2020
	}
2021
2022
	public function hasBounties(): bool {
2023
		return count($this->getBounties()) > 0;
2024
	}
2025
2026
	/**
2027
	 * @return array<string, mixed>
2028
	 */
2029
	protected function getBounty(int $bountyID): array {
2030
		if (!$this->hasBounty($bountyID)) {
2031
			throw new Exception('BountyID does not exist: ' . $bountyID);
2032
		}
2033
		return $this->bounties[$bountyID];
2034
	}
2035
2036
	public function hasBounty(int $bountyID): bool {
2037
		$bounties = $this->getBounties();
2038
		return isset($bounties[$bountyID]);
2039
	}
2040
2041
	/**
2042
	 * @return array<string, mixed>
2043
	 */
2044
	protected function createBounty(BountyType $type): array {
2045
		$bounty = [
2046
			'Amount' => 0,
2047
			'SmrCredits' => 0,
2048
			'Type' => $type,
2049
			'Claimer' => 0,
2050
			'Time' => Epoch::time(),
2051
			'ID' => $this->getNextBountyID(),
2052
			'New' => true,
2053
		];
2054
		$this->setBounty($bounty);
2055
		return $bounty;
2056
	}
2057
2058
	protected function getNextBountyID(): int {
2059
		if (!$this->hasBounties()) {
2060
			return 0;
2061
		}
2062
		return max(array_keys($this->getBounties())) + 1;
2063
	}
2064
2065
	/**
2066
	 * @param array<string, mixed> $bounty
2067
	 */
2068
	protected function setBounty(array $bounty): void {
2069
		$this->bounties[$bounty['ID']] = $bounty;
2070
		$this->hasBountyChanged[$bounty['ID']] = true;
2071
	}
2072
2073
	/**
2074
	 * @return array<string, mixed>
2075
	 */
2076
	public function getCurrentBounty(BountyType $type): array {
2077
		$bounties = $this->getBounties();
2078
		foreach ($bounties as $bounty) {
2079
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
2080
				return $bounty;
2081
			}
2082
		}
2083
		return $this->createBounty($type);
2084
	}
2085
2086
	public function hasCurrentBounty(BountyType $type): bool {
2087
		$bounties = $this->getBounties();
2088
		foreach ($bounties as $bounty) {
2089
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
2090
				return true;
2091
			}
2092
		}
2093
		return false;
2094
	}
2095
2096
	public function getCurrentBountyAmount(BountyType $type): int {
2097
		$bounty = $this->getCurrentBounty($type);
2098
		return $bounty['Amount'];
2099
	}
2100
2101
	protected function setCurrentBountyAmount(BountyType $type, int $amount): void {
2102
		$bounty = $this->getCurrentBounty($type);
2103
		if ($bounty['Amount'] == $amount) {
2104
			return;
2105
		}
2106
		$bounty['Amount'] = $amount;
2107
		$this->setBounty($bounty);
2108
	}
2109
2110
	public function increaseCurrentBountyAmount(BountyType $type, int $amount): void {
2111
		if ($amount < 0) {
2112
			throw new Exception('Trying to increase negative current bounty.');
2113
		}
2114
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
2115
	}
2116
2117
	public function decreaseCurrentBountyAmount(BountyType $type, int $amount): void {
2118
		if ($amount < 0) {
2119
			throw new Exception('Trying to decrease negative current bounty.');
2120
		}
2121
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
2122
	}
2123
2124
	protected function getCurrentBountySmrCredits(BountyType $type): int {
2125
		$bounty = $this->getCurrentBounty($type);
2126
		return $bounty['SmrCredits'];
2127
	}
2128
2129
	protected function setCurrentBountySmrCredits(BountyType $type, int $credits): void {
2130
		$bounty = $this->getCurrentBounty($type);
2131
		if ($bounty['SmrCredits'] == $credits) {
2132
			return;
2133
		}
2134
		$bounty['SmrCredits'] = $credits;
2135
		$this->setBounty($bounty);
2136
	}
2137
2138
	public function increaseCurrentBountySmrCredits(BountyType $type, int $credits): void {
2139
		if ($credits < 0) {
2140
			throw new Exception('Trying to increase negative current bounty.');
2141
		}
2142
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2143
	}
2144
2145
	public function decreaseCurrentBountySmrCredits(BountyType $type, int $credits): void {
2146
		if ($credits < 0) {
2147
			throw new Exception('Trying to decrease negative current bounty.');
2148
		}
2149
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2150
	}
2151
2152
	public function setBountiesClaimable(self $claimer): void {
2153
		foreach ($this->getBounties() as $bounty) {
2154
			if ($bounty['Claimer'] == 0) {
2155
				$bounty['Claimer'] = $claimer->getAccountID();
2156
				$this->setBounty($bounty);
2157
			}
2158
		}
2159
	}
2160
2161
	protected function getHOFData(): void {
2162
		if (!isset($this->HOF)) {
2163
			//Get Player HOF
2164
			$dbResult = $this->db->read('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2165
			$this->HOF = [];
2166
			foreach ($dbResult->records() as $dbRecord) {
2167
				$this->HOF[$dbRecord->getString('type')] = $dbRecord->getFloat('amount');
2168
			}
2169
			self::getHOFVis();
2170
		}
2171
	}
2172
2173
	/**
2174
	 * @return array<string, string>
2175
	 */
2176
	public static function getHOFVis(): array {
2177
		if (!isset(self::$HOFVis)) {
2178
			//Get Player HOF Vis
2179
			$db = Database::getInstance();
2180
			$dbResult = $db->read('SELECT type,visibility FROM hof_visibility');
2181
			self::$HOFVis = [];
2182
			foreach ($dbResult->records() as $dbRecord) {
2183
				self::$HOFVis[$dbRecord->getString('type')] = $dbRecord->getString('visibility');
2184
			}
2185
			// Add non-database types
2186
			self::$HOFVis[HOF_TYPE_DONATION] = HOF_PUBLIC;
2187
			self::$HOFVis[HOF_TYPE_USER_SCORE] = HOF_PUBLIC;
2188
		}
2189
		return self::$HOFVis;
2190
	}
2191
2192
	/**
2193
	 * @param array<string> $typeList
2194
	 */
2195
	public function getHOF(array $typeList): float {
2196
		$this->getHOFData();
2197
		return $this->HOF[implode(':', $typeList)] ?? 0;
2198
	}
2199
2200
	/**
2201
	 * @param array<string> $typeList
2202
	 */
2203
	public function increaseHOF(float $amount, array $typeList, string $visibility): void {
2204
		if ($amount < 0) {
2205
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2206
		}
2207
		if ($amount == 0) {
2208
			return;
2209
		}
2210
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2211
	}
2212
2213
	/**
2214
	 * @param array<string> $typeList
2215
	 */
2216
	public function decreaseHOF(float $amount, array $typeList, string $visibility): void {
2217
		if ($amount < 0) {
2218
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2219
		}
2220
		if ($amount == 0) {
2221
			return;
2222
		}
2223
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2224
	}
2225
2226
	/**
2227
	 * @param array<string> $typeList
2228
	 */
2229
	public function setHOF(float $amount, array $typeList, string $visibility): void {
2230
		if ($this->isNPC()) {
2231
			// Don't store HOF for NPCs.
2232
			return;
2233
		}
2234
		if ($amount < 0) {
2235
			$amount = 0;
2236
		}
2237
		if ($this->getHOF($typeList) == $amount) {
2238
			return;
2239
		}
2240
2241
		$hofType = implode(':', $typeList);
2242
		if (!isset(self::$HOFVis[$hofType])) {
2243
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2244
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2245
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2246
		}
2247
		self::$HOFVis[$hofType] = $visibility;
2248
2249
		if (!isset($this->HOF[$hofType])) {
2250
			$this->hasHOFChanged[$hofType] = self::HOF_NEW;
2251
		} else {
2252
			$this->hasHOFChanged[$hofType] = self::HOF_CHANGED;
2253
		}
2254
		$this->HOF[$hofType] = $amount;
2255
	}
2256
2257
	public function isUnderAttack(): bool {
2258
		return $this->underAttack;
2259
	}
2260
2261
	public function setUnderAttack(bool $value): void {
2262
		if ($this->underAttack === $value) {
2263
			return;
2264
		}
2265
		$this->underAttack = $value;
2266
		$this->hasChanged = true;
2267
	}
2268
2269
	public function killPlayer(int $sectorID): void {
2270
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2271
		//msg taken care of in trader_att_proc.php
2272
		// forget plotted course
2273
		$this->deletePlottedCourse();
2274
2275
		$sector->diedHere($this);
2276
2277
		// if we are in an alliance we increase their deaths
2278
		if ($this->hasAlliance()) {
2279
			$this->db->write('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2280
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()));
2281
		}
2282
2283
		// record death stat
2284
		$this->increaseHOF(1, ['Dying', 'Deaths'], HOF_PUBLIC);
2285
		//record cost of ship lost
2286
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2287
		// reset turns since last death
2288
		$this->setHOF(0, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2289
2290
		// Reset credits to starting amount + ship insurance
2291
		$credits = $this->getGame()->getStartingCredits();
2292
		$credits += IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2292
		$credits += /** @scrutinizer ignore-call */ IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
Loading history...
2293
		$this->setCredits($credits);
2294
2295
		$this->setSectorID($this->getHome());
2296
		$this->increaseDeaths(1);
2297
		$this->setLandedOnPlanet(false);
2298
		$this->setDead(true);
2299
		$this->setNewbieWarning(true);
2300
		$this->getShip()->getPod($this->hasNewbieStatus());
2301
		$this->setNewbieTurns(NEWBIE_TURNS_ON_DEATH);
2302
		$this->setUnderAttack(false);
2303
	}
2304
2305
	/**
2306
	 * @return array<string, mixed>
2307
	 */
2308
	public function killPlayerByPlayer(self $killer): array {
2309
		$return = [];
2310
		$msg = $this->getBBLink();
2311
2312
		if ($this->hasCustomShipName()) {
2313
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2314
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2315
		}
2316
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2317
		if ($killer->hasCustomShipName()) {
2318
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2319
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2320
		}
2321
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2322
		$this->getSector()->increaseBattles(1);
2323
		$this->db->insert('news', [
2324
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2325
			'time' => $this->db->escapeNumber(Epoch::time()),
2326
			'news_message' => $this->db->escapeString($msg),
2327
			'killer_id' => $this->db->escapeNumber($killer->getAccountID()),
2328
			'killer_alliance' => $this->db->escapeNumber($killer->getAllianceID()),
2329
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2330
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2331
		]);
2332
2333
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2334
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2335
2336
		// Dead player loses between 5% and 25% experience
2337
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2338
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2338
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2339
		$expBeforeDeath = $this->getExperience();
2340
		$this->decreaseExperience($return['DeadExp']);
2341
2342
		// Killer gains 50% of the lost exp
2343
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2343
		$return['KillerExp'] = max(0, /** @scrutinizer ignore-call */ ICeil(0.5 * $return['DeadExp']));
Loading history...
2344
		$killer->increaseExperience($return['KillerExp']);
2345
2346
		$return['KillerCredits'] = $this->getCredits();
2347
		$killer->increaseCredits($return['KillerCredits']);
2348
2349
		// The killer may change alignment
2350
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2351
		$relation = $relations[$killer->getRaceID()];
2352
2353
		$alignChangePerRelation = 0.1;
2354
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2355
			$alignChangePerRelation = 0.04;
2356
		}
2357
2358
		$killerAlignChange = IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2358
		$killerAlignChange = /** @scrutinizer ignore-call */ IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
Loading history...
2359
		if ($killerAlignChange > 0) {
2360
			$killer->increaseAlignment($killerAlignChange);
2361
		} else {
2362
			$killer->decreaseAlignment(-$killerAlignChange);
2363
		}
2364
		// War setting gives them military pay
2365
		if ($relation <= RELATIONS_WAR) {
2366
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2367
		}
2368
2369
		//check for federal bounty being offered for current port raiders;
2370
		$this->db->write('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(Epoch::time() - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2371
		$query = 'SELECT 1
2372
					FROM player_attacks_port
2373
					JOIN port USING(game_id, sector_id)
2374
					JOIN player USING(game_id, account_id)
2375
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2376
		$dbResult = $this->db->read($query);
2377
		if ($dbResult->hasRecord()) {
2378
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2379
			$this->increaseCurrentBountyAmount(BountyType::HQ, $bounty);
2380
		}
2381
2382
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2383
		$this->setBountiesClaimable($killer);
2384
2385
		// If the alignment difference is greater than 200 then a bounty may be set
2386
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2387
		$return['BountyGained'] = [
2388
			'Type' => 'None',
2389
			'Amount' => 0,
2390
		];
2391
		if ($alignmentDiff >= 200) {
2392
			// If the podded players alignment makes them deputy or member then set bounty
2393
			if ($this->hasGoodAlignment()) {
2394
				$return['BountyGained']['Type'] = BountyType::HQ;
2395
			} elseif ($this->hasEvilAlignment()) {
2396
				$return['BountyGained']['Type'] = BountyType::UG;
2397
			}
2398
2399
			if ($return['BountyGained']['Type'] != 'None') {
2400
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2401
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2402
			}
2403
		}
2404
2405
		$killingHof = ['Killing'];
2406
		if ($this->isNPC()) {
2407
			$killingHof[] = 'NPC';
2408
		}
2409
		$killer->increaseHOF($return['KillerExp'], [...$killingHof, 'Experience', 'Gained'], HOF_PUBLIC);
2410
		$killer->increaseHOF($expBeforeDeath, [...$killingHof, 'Experience', 'Of Traders Killed'], HOF_PUBLIC);
2411
		$killer->increaseHOF($return['DeadExp'], [...$killingHof, 'Experience', 'Lost By Traders Killed'], HOF_PUBLIC);
2412
2413
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Lost By Traders Killed'], HOF_PUBLIC);
2414
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Gain'], HOF_PUBLIC);
2415
		$killer->increaseHOF($this->getShip()->getCost(), [...$killingHof, 'Money', 'Cost Of Ships Killed'], HOF_PUBLIC);
2416
		$killer->increaseHOF($return['BountyGained']['Amount'], [...$killingHof, 'Money', 'Bounty Gained'], HOF_PUBLIC);
2417
2418
		if ($killerAlignChange > 0) {
2419
			$killer->increaseHOF($killerAlignChange, [...$killingHof, 'Alignment', 'Gain'], HOF_PUBLIC);
2420
		} else {
2421
			$killer->increaseHOF(-$killerAlignChange, [...$killingHof, 'Alignment', 'Loss'], HOF_PUBLIC);
2422
		}
2423
2424
		if ($this->isNPC()) {
2425
			$killer->increaseHOF(1, ['Killing', 'NPC Kills'], HOF_PUBLIC);
2426
		} elseif ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2427
			$killer->increaseHOF(1, ['Killing', 'Newbie Kills'], HOF_PUBLIC);
2428
		} else {
2429
			$killer->increaseKills(1);
2430
			$killer->increaseHOF(1, ['Killing', 'Kills'], HOF_PUBLIC);
2431
2432
			if ($killer->hasAlliance()) {
2433
				$this->db->write('UPDATE alliance SET alliance_kills=alliance_kills+1 WHERE alliance_id=' . $this->db->escapeNumber($killer->getAllianceID()) . ' AND game_id=' . $this->db->escapeNumber($killer->getGameID()));
2434
			}
2435
2436
			// alliance vs. alliance stats
2437
			$this->incrementAllianceVsDeaths($killer->getAllianceID());
2438
		}
2439
2440
		$dyingHof = ['Dying', 'Players'];
2441
		if ($killer->isNPC()) {
2442
			$dyingHof[] = 'NPC';
2443
		}
2444
		$this->increaseHOF($return['BountyGained']['Amount'], [...$dyingHof, 'Money', 'Bounty Gained By Killer'], HOF_PUBLIC);
2445
		$this->increaseHOF($return['KillerExp'], [...$dyingHof, 'Experience', 'Gained By Killer'], HOF_PUBLIC);
2446
		$this->increaseHOF($return['DeadExp'], [...$dyingHof, 'Experience', 'Lost'], HOF_PUBLIC);
2447
		$this->increaseHOF($return['KillerCredits'], [...$dyingHof, 'Money Lost'], HOF_PUBLIC);
2448
		$this->increaseHOF($this->getShip()->getCost(), [...$dyingHof, 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2449
		$this->increaseHOF(1, [...$dyingHof, 'Deaths'], HOF_PUBLIC);
2450
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2451
2452
		$this->killPlayer($this->getSectorID());
2453
		return $return;
2454
	}
2455
2456
	/**
2457
	 * @return array<string, mixed>
2458
	 */
2459
	public function killPlayerByForces(SmrForce $forces): array {
2460
		$return = [];
2461
		$owner = $forces->getOwner();
2462
		// send a message to the person who died
2463
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2464
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2465
2466
		$news_message = $this->getBBLink();
2467
		if ($this->hasCustomShipName()) {
2468
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2469
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2470
		}
2471
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2472
		// insert the news entry
2473
		$this->db->insert('news', [
2474
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2475
			'time' => $this->db->escapeNumber(Epoch::time()),
2476
			'news_message' => $this->db->escapeString($news_message),
2477
			'killer_id' => $this->db->escapeNumber($owner->getAccountID()),
2478
			'killer_alliance' => $this->db->escapeNumber($owner->getAllianceID()),
2479
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2480
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2481
		]);
2482
2483
		// Player loses 15% experience
2484
		$expLossPercentage = .15;
2485
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2485
		$return['DeadExp'] = /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage);
Loading history...
2486
		$this->decreaseExperience($return['DeadExp']);
2487
2488
		$return['LostCredits'] = $this->getCredits();
2489
2490
		// alliance vs. alliance stats
2491
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2492
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2493
2494
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2495
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Forces', 'Experience Lost'], HOF_PUBLIC);
2496
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Forces', 'Money Lost'], HOF_PUBLIC);
2497
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Forces', 'Cost Of Ships Lost'], HOF_PUBLIC);
2498
		$this->increaseHOF(1, ['Dying', 'Forces', 'Deaths'], HOF_PUBLIC);
2499
2500
		$this->killPlayer($forces->getSectorID());
2501
		return $return;
2502
	}
2503
2504
	/**
2505
	 * @return array<string, mixed>
2506
	 */
2507
	public function killPlayerByPort(AbstractSmrPort $port): array {
2508
		$return = [];
2509
		// send a message to the person who died
2510
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2511
2512
		$news_message = $this->getBBLink();
2513
		if ($this->hasCustomShipName()) {
2514
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2515
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2516
		}
2517
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2518
		// insert the news entry
2519
		$this->db->insert('news', [
2520
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2521
			'time' => $this->db->escapeNumber(Epoch::time()),
2522
			'news_message' => $this->db->escapeString($news_message),
2523
			'killer_id' => $this->db->escapeNumber(ACCOUNT_ID_PORT),
2524
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2525
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2526
		]);
2527
2528
		// Player loses between 15% and 20% experience
2529
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2530
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2530
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2531
		$this->decreaseExperience($return['DeadExp']);
2532
2533
		$return['LostCredits'] = $this->getCredits();
2534
2535
		// alliance vs. alliance stats
2536
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2537
2538
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2539
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Ports', 'Experience Lost'], HOF_PUBLIC);
2540
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Ports', 'Money Lost'], HOF_PUBLIC);
2541
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Ports', 'Cost Of Ships Lost'], HOF_PUBLIC);
2542
		$this->increaseHOF(1, ['Dying', 'Ports', 'Deaths'], HOF_PUBLIC);
2543
2544
		$this->killPlayer($port->getSectorID());
2545
		return $return;
2546
	}
2547
2548
	/**
2549
	 * @return array<string, mixed>
2550
	 */
2551
	public function killPlayerByPlanet(SmrPlanet $planet): array {
2552
		$return = [];
2553
		// send a message to the person who died
2554
		$planetOwner = $planet->getOwner();
2555
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2556
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2557
2558
		$news_message = $this->getBBLink();
2559
		if ($this->hasCustomShipName()) {
2560
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2561
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2562
		}
2563
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2564
		// insert the news entry
2565
		$this->db->insert('news', [
2566
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2567
			'time' => $this->db->escapeNumber(Epoch::time()),
2568
			'news_message' => $this->db->escapeString($news_message),
2569
			'killer_id' => $this->db->escapeNumber($planetOwner->getAccountID()),
2570
			'killer_alliance' => $this->db->escapeNumber($planetOwner->getAllianceID()),
2571
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2572
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2573
		]);
2574
2575
		// Player loses between 15% and 20% experience
2576
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2577
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2577
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2578
		$this->decreaseExperience($return['DeadExp']);
2579
2580
		$return['LostCredits'] = $this->getCredits();
2581
2582
		// alliance vs. alliance stats
2583
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2584
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2585
2586
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2587
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Planets', 'Experience Lost'], HOF_PUBLIC);
2588
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Planets', 'Money Lost'], HOF_PUBLIC);
2589
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Planets', 'Cost Of Ships Lost'], HOF_PUBLIC);
2590
		$this->increaseHOF(1, ['Dying', 'Planets', 'Deaths'], HOF_PUBLIC);
2591
2592
		$this->killPlayer($planet->getSectorID());
2593
		return $return;
2594
	}
2595
2596
	public function incrementAllianceVsKills(int $otherID): void {
2597
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2598
		$this->db->write('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2599
	}
2600
2601
	public function incrementAllianceVsDeaths(int $otherID): void {
2602
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2603
		$this->db->write('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2604
	}
2605
2606
	public function getTurnsLevel(): TurnsLevel {
2607
		return match (true) {
2608
			$this->getTurns() === 0 => TurnsLevel::None,
2609
			$this->getTurns() <= 25 => TurnsLevel::Low,
2610
			$this->getTurns() <= 75 => TurnsLevel::Medium,
2611
			default => TurnsLevel::High,
2612
		};
2613
	}
2614
2615
	public function getTurns(): int {
2616
		return $this->turns;
2617
	}
2618
2619
	public function hasTurns(): bool {
2620
		return $this->turns > 0;
2621
	}
2622
2623
	public function getMaxTurns(): int {
2624
		return $this->getGame()->getMaxTurns();
2625
	}
2626
2627
	public function setTurns(int $turns): void {
2628
		if ($this->turns == $turns) {
2629
			return;
2630
		}
2631
		// Make sure turns are in range [0, MaxTurns]
2632
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2633
		$this->hasChanged = true;
2634
	}
2635
2636
	public function takeTurns(int $take, int $takeNewbie = 0): void {
2637
		if ($take < 0 || $takeNewbie < 0) {
2638
			throw new Exception('Trying to take negative turns.');
2639
		}
2640
		$take = ICeil($take);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2640
		$take = /** @scrutinizer ignore-call */ ICeil($take);
Loading history...
2641
		// Only take up to as many newbie turns as we have remaining
2642
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2643
2644
		$this->setTurns($this->getTurns() - $take);
2645
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2646
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2647
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Total'], HOF_ALLIANCE);
2648
		$this->increaseHOF($takeNewbie, ['Movement', 'Turns Used', 'Newbie'], HOF_ALLIANCE);
2649
2650
		// Player has taken an action
2651
		$this->setLastActive(Epoch::time());
2652
		$this->updateLastCPLAction();
2653
	}
2654
2655
	public function giveTurns(int $give, int $giveNewbie = 0): void {
2656
		if ($give < 0 || $giveNewbie < 0) {
2657
			throw new Exception('Trying to give negative turns.');
2658
		}
2659
		$this->setTurns($this->getTurns() + $give);
2660
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2661
	}
2662
2663
	/**
2664
	 * Calculate the time in seconds between the given time and when the
2665
	 * player will be at max turns.
2666
	 */
2667
	public function getTimeUntilMaxTurns(int $time, bool $forceUpdate = false): int {
2668
		$timeDiff = $time - $this->getLastTurnUpdate();
2669
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2670
		$ship = $this->getShip($forceUpdate);
2671
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2671
		$maxTurnsTime = /** @scrutinizer ignore-call */ ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
Loading history...
2672
		// If already at max turns, return 0
2673
		return max(0, $maxTurnsTime);
2674
	}
2675
2676
	/**
2677
	 * Calculate the time in seconds until the next turn is awarded.
2678
	 */
2679
	public function getTimeUntilNextTurn(): int {
2680
		$secondsSinceUpdate = Epoch::time() - $this->getLastTurnUpdate();
2681
		$secondsPerTurn = 3600 / $this->getShip()->getRealSpeed();
2682
		return ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2682
		return /** @scrutinizer ignore-call */ ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
Loading history...
2683
	}
2684
2685
	/**
2686
	 * Grant the player their starting turns.
2687
	 */
2688
	public function giveStartingTurns(): void {
2689
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2689
		$startTurns = /** @scrutinizer ignore-call */ IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
Loading history...
2690
		$this->giveTurns($startTurns);
2691
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2692
	}
2693
2694
	// Turns only update when player is active.
2695
	// Calculate turns gained between given time and the last turn update
2696
	public function getTurnsGained(int $time, bool $forceUpdate = false): int {
2697
		$timeDiff = $time - $this->getLastTurnUpdate();
2698
		$ship = $this->getShip($forceUpdate);
2699
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2699
		$extraTurns = /** @scrutinizer ignore-call */ IFloor($timeDiff * $ship->getRealSpeed() / 3600);
Loading history...
2700
		return $extraTurns;
2701
	}
2702
2703
	public function updateTurns(): void {
2704
		// is account validated?
2705
		if (!$this->getAccount()->isValidated()) {
2706
			return;
2707
		}
2708
2709
		// how many turns would he get right now?
2710
		$extraTurns = $this->getTurnsGained(Epoch::time());
2711
2712
		// do we have at least one turn to give?
2713
		if ($extraTurns > 0) {
2714
			// recalc the time to avoid rounding errors
2715
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2715
			$newLastTurnUpdate = $this->getLastTurnUpdate() + /** @scrutinizer ignore-call */ ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
Loading history...
2716
			$this->setLastTurnUpdate($newLastTurnUpdate);
2717
			$this->giveTurns($extraTurns);
2718
		}
2719
	}
2720
2721
	public function getLastTurnUpdate(): int {
2722
		return $this->lastTurnUpdate;
2723
	}
2724
2725
	public function setLastTurnUpdate(int $time): void {
2726
		if ($this->lastTurnUpdate == $time) {
2727
			return;
2728
		}
2729
		$this->lastTurnUpdate = $time;
2730
		$this->hasChanged = true;
2731
	}
2732
2733
	public function getLastActive(): int {
2734
		return $this->lastActive;
2735
	}
2736
2737
	public function setLastActive(int $lastActive): void {
2738
		if ($this->lastActive == $lastActive) {
2739
			return;
2740
		}
2741
		$this->lastActive = $lastActive;
2742
		$this->hasChanged = true;
2743
	}
2744
2745
	public function getLastCPLAction(): int {
2746
		return $this->lastCPLAction;
2747
	}
2748
2749
	public function setLastCPLAction(int $time): void {
2750
		if ($this->lastCPLAction == $time) {
2751
			return;
2752
		}
2753
		$this->lastCPLAction = $time;
2754
		$this->hasChanged = true;
2755
	}
2756
2757
	public function updateLastCPLAction(): void {
2758
		$this->setLastCPLAction(Epoch::time());
2759
	}
2760
2761
	public function setNewbieWarning(bool $bool): void {
2762
		if ($this->newbieWarning == $bool) {
2763
			return;
2764
		}
2765
		$this->newbieWarning = $bool;
2766
		$this->hasChanged = true;
2767
	}
2768
2769
	public function getNewbieWarning(): bool {
2770
		return $this->newbieWarning;
2771
	}
2772
2773
	public function isDisplayMissions(): bool {
2774
		return $this->displayMissions;
2775
	}
2776
2777
	public function setDisplayMissions(bool $bool): void {
2778
		if ($this->displayMissions == $bool) {
2779
			return;
2780
		}
2781
		$this->displayMissions = $bool;
2782
		$this->hasChanged = true;
2783
	}
2784
2785
	/**
2786
	 * @return array<int, array<string, mixed>>
2787
	 */
2788
	public function getMissions(): array {
2789
		if (!isset($this->missions)) {
2790
			$dbResult = $this->db->read('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2791
			$this->missions = [];
2792
			foreach ($dbResult->records() as $dbRecord) {
2793
				$missionID = $dbRecord->getInt('mission_id');
2794
				$this->missions[$missionID] = [
2795
					'On Step' => $dbRecord->getInt('on_step'),
2796
					'Progress' => $dbRecord->getInt('progress'),
2797
					'Unread' => $dbRecord->getBoolean('unread'),
2798
					'Expires' => $dbRecord->getInt('step_fails'),
2799
					'Sector' => $dbRecord->getInt('mission_sector'),
2800
					'Starting Sector' => $dbRecord->getInt('starting_sector'),
2801
				];
2802
				$this->rebuildMission($missionID);
2803
			}
2804
		}
2805
		return $this->missions;
2806
	}
2807
2808
	/**
2809
	 * @return array<int, array<string, mixed>>
2810
	 */
2811
	public function getActiveMissions(): array {
2812
		$missions = $this->getMissions();
2813
		foreach ($missions as $missionID => $mission) {
2814
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2815
				unset($missions[$missionID]);
2816
			}
2817
		}
2818
		return $missions;
2819
	}
2820
2821
	/**
2822
	 * @return array<string, mixed>|false
2823
	 */
2824
	protected function getMission(int $missionID): array|false {
2825
		$missions = $this->getMissions();
2826
		if (isset($missions[$missionID])) {
2827
			return $missions[$missionID];
2828
		}
2829
		return false;
2830
	}
2831
2832
	protected function hasMission(int $missionID): bool {
2833
		return $this->getMission($missionID) !== false;
2834
	}
2835
2836
	protected function updateMission(int $missionID): bool {
2837
		$this->getMissions();
2838
		if (isset($this->missions[$missionID])) {
2839
			$mission = $this->missions[$missionID];
2840
			$this->db->write('
2841
				UPDATE player_has_mission
2842
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2843
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2844
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2845
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2846
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2847
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2848
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2849
			return true;
2850
		}
2851
		return false;
2852
	}
2853
2854
	private function setupMissionStep(int $missionID): void {
2855
		$stepID = $this->missions[$missionID]['On Step'];
2856
		if ($stepID >= count(MISSIONS[$missionID]['Steps'])) {
2857
			// Nothing to do if this mission is already completed
2858
			return;
2859
		}
2860
		$step = MISSIONS[$missionID]['Steps'][$stepID];
2861
		if (isset($step['PickSector'])) {
2862
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2863
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2864
			if ($path === false) {
2865
				// Abandon the mission if it cannot be completed due to a
2866
				// sector that does not exist or cannot be reached.
2867
				// (Probably shouldn't bestow this mission in the first place)
2868
				$this->deleteMission($missionID);
2869
				throw new UserError('Cannot find a path to the destination!');
2870
			}
2871
			$this->missions[$missionID]['Sector'] = $path->getEndSectorID();
2872
		}
2873
	}
2874
2875
	/**
2876
	 * Declining a mission will permanently hide it from the player
2877
	 * by adding it in its completed state.
2878
	 */
2879
	public function declineMission(int $missionID): void {
2880
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2881
		$this->addMission($missionID, $finishedStep);
2882
	}
2883
2884
	public function addMission(int $missionID, int $step = 0): void {
2885
		if ($this->hasMission($missionID)) {
2886
			throw new Exception('Mission ID already added: ' . $missionID);
2887
		}
2888
2889
		$mission = [
2890
			'On Step' => $step,
2891
			'Progress' => 0,
2892
			'Unread' => true,
2893
			'Expires' => (Epoch::time() + 86400),
2894
			'Sector' => 0,
2895
			'Starting Sector' => $this->getSectorID(),
2896
		];
2897
2898
		$this->missions[$missionID] =& $mission;
2899
		$this->setupMissionStep($missionID);
2900
		$this->rebuildMission($missionID);
2901
2902
		$this->db->replace('player_has_mission', [
2903
			'game_id' => $this->db->escapeNumber($this->gameID),
2904
			'account_id' => $this->db->escapeNumber($this->accountID),
2905
			'mission_id' => $this->db->escapeNumber($missionID),
2906
			'on_step' => $this->db->escapeNumber($mission['On Step']),
2907
			'progress' => $this->db->escapeNumber($mission['Progress']),
2908
			'unread' => $this->db->escapeBoolean($mission['Unread']),
2909
			'starting_sector' => $this->db->escapeNumber($mission['Starting Sector']),
2910
			'mission_sector' => $this->db->escapeNumber($mission['Sector']),
2911
			'step_fails' => $this->db->escapeNumber($mission['Expires']),
2912
		]);
2913
	}
2914
2915
	private function rebuildMission(int $missionID): void {
2916
		$mission = $this->missions[$missionID];
2917
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2918
2919
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2920
			// If we have completed this mission just use false to indicate no current task.
2921
			$currentStep = false;
2922
		} else {
2923
			$data = ['player' => $this, 'mission' => $mission];
2924
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2925
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2926
		}
2927
		$this->missions[$missionID]['Task'] = $currentStep;
2928
	}
2929
2930
	public function deleteMission(int $missionID): void {
2931
		$this->getMissions();
2932
		if (isset($this->missions[$missionID])) {
2933
			unset($this->missions[$missionID]);
2934
			$this->db->write('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2935
			return;
2936
		}
2937
		throw new Exception('Mission with ID not found: ' . $missionID);
2938
	}
2939
2940
	/**
2941
	 * @return array<int>
2942
	 */
2943
	public function markMissionsRead(): array {
2944
		$this->getMissions();
2945
		$unreadMissions = [];
2946
		foreach ($this->missions as $missionID => &$mission) {
2947
			if ($mission['Unread']) {
2948
				$unreadMissions[] = $missionID;
2949
				$mission['Unread'] = false;
2950
				$this->updateMission($missionID);
2951
			}
2952
		}
2953
		return $unreadMissions;
2954
	}
2955
2956
	public function claimMissionReward(int $missionID): string {
2957
		if (!$this->hasMission($missionID)) {
2958
			throw new Exception('Unknown mission: ' . $missionID);
2959
		}
2960
		$mission =& $this->missions[$missionID];
2961
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2962
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2963
		}
2964
		$mission['On Step']++;
2965
		$mission['Unread'] = true;
2966
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2967
			switch ($rewardItem) {
2968
				case 'Credits':
2969
					$this->increaseCredits($amount);
2970
					break;
2971
				case 'Experience':
2972
					$this->increaseExperience($amount);
2973
					break;
2974
			}
2975
		}
2976
		$rewardText = $mission['Task']['Rewards']['Text'];
2977
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2978
			// If we haven't finished the mission yet then
2979
			$this->setupMissionStep($missionID);
2980
		}
2981
		$this->rebuildMission($missionID);
2982
		$this->updateMission($missionID);
2983
		return $rewardText;
2984
	}
2985
2986
	/**
2987
	 * @return array<int, array<string, mixed>>
2988
	 */
2989
	public function getAvailableMissions(): array {
2990
		$availableMissions = [];
2991
		foreach (MISSIONS as $missionID => $mission) {
2992
			if ($this->hasMission($missionID)) {
2993
				continue;
2994
			}
2995
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2996
			if ($this->getSector()->hasX($realX)) {
2997
				$availableMissions[$missionID] = $mission;
2998
			}
2999
		}
3000
		return $availableMissions;
3001
	}
3002
3003
	/**
3004
	 * Log a player action in the current sector to the admin log console.
3005
	 */
3006
	public function log(int $log_type_id, string $msg): void {
3007
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
3008
	}
3009
3010
	/**
3011
	 * @param array<string, mixed> $values
3012
	 */
3013
	public function actionTaken(string $actionID, array $values): void {
3014
		if (!in_array($actionID, MISSION_ACTIONS)) {
3015
			throw new Exception('Unknown action: ' . $actionID);
3016
		}
3017
		// TODO: Reenable this once tested.     if($this->getAccount()->isLoggingEnabled())
3018
		switch ($actionID) {
3019
			case 'WalkSector':
3020
				$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
3021
				break;
3022
			case 'JoinAlliance':
3023
				$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
3024
				break;
3025
			case 'LeaveAlliance':
3026
				$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
3027
				break;
3028
			case 'DisbandAlliance':
3029
				$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
3030
				break;
3031
			case 'KickPlayer':
3032
				$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
3033
				break;
3034
			case 'PlayerKicked':
3035
				$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
3036
				break;
3037
		}
3038
3039
		$this->getMissions();
3040
		foreach ($this->missions as $missionID => &$mission) {
3041
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
3042
				$requirements = $mission['Task']['Detail'];
3043
				if (checkMissionRequirements($values, $requirements) === true) {
3044
					$mission['On Step']++;
3045
					$mission['Unread'] = true;
3046
					$this->setupMissionStep($missionID);
3047
					$this->rebuildMission($missionID);
3048
					$this->updateMission($missionID);
3049
				}
3050
			}
3051
		}
3052
	}
3053
3054
	/**
3055
	 * @param array<SmrPlayer> $otherPlayerArray
3056
	 */
3057
	public function canSeeAny(array $otherPlayerArray): bool {
3058
		foreach ($otherPlayerArray as $otherPlayer) {
3059
			if ($this->canSee($otherPlayer)) {
3060
				return true;
3061
			}
3062
		}
3063
		return false;
3064
	}
3065
3066
	public function canSee(self $otherPlayer): bool {
3067
		if (!$otherPlayer->getShip()->isCloaked()) {
3068
			return true;
3069
		}
3070
		if ($this->sameAlliance($otherPlayer)) {
3071
			return true;
3072
		}
3073
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
3074
			return true;
3075
		}
3076
		return false;
3077
	}
3078
3079
	public function equals(self $otherPlayer = null): bool {
3080
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
3081
	}
3082
3083
	public function sameAlliance(self $otherPlayer = null): bool {
3084
		return $this->equals($otherPlayer) || ($otherPlayer !== null && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
3085
	}
3086
3087
	public function sharedForceAlliance(self $otherPlayer = null): bool {
3088
		return $this->sameAlliance($otherPlayer);
3089
	}
3090
3091
	public function forceNAPAlliance(self $otherPlayer = null): bool {
3092
		return $this->sameAlliance($otherPlayer);
3093
	}
3094
3095
	public function planetNAPAlliance(self $otherPlayer = null): bool {
3096
		return $this->sameAlliance($otherPlayer);
3097
	}
3098
3099
	public function traderNAPAlliance(self $otherPlayer = null): bool {
3100
		return $this->sameAlliance($otherPlayer);
3101
	}
3102
3103
	public function traderMAPAlliance(self $otherPlayer = null): bool {
3104
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
3105
	}
3106
3107
	public function traderAttackTraderAlliance(self $otherPlayer = null): bool {
3108
		return $this->sameAlliance($otherPlayer);
3109
	}
3110
3111
	public function traderDefendTraderAlliance(self $otherPlayer = null): bool {
3112
		return $this->sameAlliance($otherPlayer);
3113
	}
3114
3115
	public function traderAttackForceAlliance(self $otherPlayer = null): bool {
3116
		return $this->sameAlliance($otherPlayer);
3117
	}
3118
3119
	public function traderAttackPortAlliance(self $otherPlayer = null): bool {
3120
		return $this->sameAlliance($otherPlayer);
3121
	}
3122
3123
	public function traderAttackPlanetAlliance(self $otherPlayer = null): bool {
3124
		return $this->sameAlliance($otherPlayer);
3125
	}
3126
3127
	public function meetsAlignmentRestriction(int $restriction): bool {
3128
		if ($restriction < 0) {
3129
			return $this->getAlignment() <= $restriction;
3130
		}
3131
		if ($restriction > 0) {
3132
			return $this->getAlignment() >= $restriction;
3133
		}
3134
		return true;
3135
	}
3136
3137
	/**
3138
	 * Get an array of goods that are visible to the player
3139
	 *
3140
	 * @return array<int, array<string, string|int>>
3141
	 */
3142
	public function getVisibleGoods(): array {
3143
		$goods = Globals::getGoods();
3144
		$visibleGoods = [];
3145
		foreach ($goods as $key => $good) {
3146
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3147
				$visibleGoods[$key] = $good;
3148
			}
3149
		}
3150
		return $visibleGoods;
3151
	}
3152
3153
	/**
3154
	 * Returns an array of all unvisited sectors.
3155
	 *
3156
	 * @return array<int>
3157
	 */
3158
	public function getUnvisitedSectors(): array {
3159
		if (!isset($this->unvisitedSectors)) {
3160
			$this->unvisitedSectors = [];
3161
			// Note that this table actually has entries for the *unvisited* sectors!
3162
			$dbResult = $this->db->read('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3163
			foreach ($dbResult->records() as $dbRecord) {
3164
				$this->unvisitedSectors[] = $dbRecord->getInt('sector_id');
3165
			}
3166
		}
3167
		return $this->unvisitedSectors;
3168
	}
3169
3170
	/**
3171
	 * Check if player has visited the input sector.
3172
	 * Note that this populates the list of *all* unvisited sectors!
3173
	 */
3174
	public function hasVisitedSector(int $sectorID): bool {
3175
		return !in_array($sectorID, $this->getUnvisitedSectors());
3176
	}
3177
3178
	public function getLeaveNewbieProtectionHREF(): string {
3179
		return (new NewbieLeaveProcessor())->href();
3180
	}
3181
3182
	public function getExamineTraderHREF(): string {
3183
		$container = new ExamineTrader($this->getAccountID());
3184
		return $container->href();
3185
	}
3186
3187
	public function getAttackTraderHREF(): string {
3188
		return Globals::getAttackTraderHREF($this->getAccountID());
3189
	}
3190
3191
	public function getPlanetKickHREF(): string {
3192
		$container = new KickProcessor($this->getAccountID());
3193
		return $container->href();
3194
	}
3195
3196
	public function getTraderSearchHREF(): string {
3197
		$container = new SearchForTraderResult($this->getPlayerID());
3198
		return $container->href();
3199
	}
3200
3201
	public function getAllianceRosterHREF(): string {
3202
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3203
	}
3204
3205
	public function getToggleWeaponHidingHREF(bool $ajax = false): string {
3206
		$container = new WeaponDisplayToggleProcessor();
3207
		$container->allowAjax = $ajax;
3208
		return $container->href();
3209
	}
3210
3211
	public function isDisplayWeapons(): bool {
3212
		return $this->displayWeapons;
3213
	}
3214
3215
	/**
3216
	 * Should weapons be displayed in the right panel?
3217
	 * This updates the player database directly because it is used with AJAX,
3218
	 * which does not acquire a sector lock.
3219
	 */
3220
	public function setDisplayWeapons(bool $bool): void {
3221
		if ($this->displayWeapons == $bool) {
3222
			return;
3223
		}
3224
		$this->displayWeapons = $bool;
3225
		$this->db->write('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3226
	}
3227
3228
	public function update(): void {
3229
		$this->save();
3230
	}
3231
3232
	public function save(): void {
3233
		if ($this->hasChanged === true) {
3234
			$this->db->write('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3235
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3236
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3237
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3238
				', turns=' . $this->db->escapeNumber($this->turns) .
3239
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3240
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3241
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3242
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3243
				', dead=' . $this->db->escapeBoolean($this->dead) .
3244
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3245
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3246
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3247
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3248
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3249
				', credits=' . $this->db->escapeNumber($this->credits) .
3250
				', experience=' . $this->db->escapeNumber($this->experience) .
3251
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3252
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3253
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3254
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3255
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3256
				', kills=' . $this->db->escapeNumber($this->kills) .
3257
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3258
				', assists=' . $this->db->escapeNumber($this->assists) .
3259
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3260
				', bank=' . $this->db->escapeNumber($this->bank) .
3261
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3262
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3263
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3264
				', group_scout_messages=' . $this->db->escapeString($this->scoutMessageGroupType->value) .
3265
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3266
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3267
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3268
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3269
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3270
				', under_attack = ' . $this->db->escapeBoolean($this->underAttack) .
3271
				' WHERE ' . $this->SQL);
3272
			$this->hasChanged = false;
3273
		}
3274
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3275
			if ($bountyChanged === true) {
3276
				$bountyChanged = false;
3277
				$bounty = $this->getBounty($key);
3278
				if ($bounty['New'] === true) {
3279
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3280
						$this->db->insert('bounty', [
3281
							'account_id' => $this->db->escapeNumber($this->getAccountID()),
3282
							'game_id' => $this->db->escapeNumber($this->getGameID()),
3283
							'type' => $this->db->escapeString($bounty['Type']->value),
3284
							'amount' => $this->db->escapeNumber($bounty['Amount']),
3285
							'smr_credits' => $this->db->escapeNumber($bounty['SmrCredits']),
3286
							'claimer_id' => $this->db->escapeNumber($bounty['Claimer']),
3287
							'time' => $this->db->escapeNumber($bounty['Time']),
3288
						]);
3289
					}
3290
				} else {
3291
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3292
						$this->db->write('UPDATE bounty
3293
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3294
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3295
							type=' . $this->db->escapeString($bounty['Type']->value) . ',
3296
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3297
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3298
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL);
3299
					} else {
3300
						$this->db->write('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL);
3301
					}
3302
				}
3303
			}
3304
		}
3305
		$this->saveHOF();
3306
	}
3307
3308
	public function saveHOF(): void {
3309
		foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3310
			if ($changeType == self::HOF_NEW) {
3311
				$this->db->insert('hof_visibility', [
3312
					'type' => $this->db->escapeString($hofType),
3313
					'visibility' => $this->db->escapeString(self::$HOFVis[$hofType]),
3314
				]);
3315
			} else {
3316
				$this->db->write('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType));
3317
			}
3318
			unset(self::$hasHOFVisChanged[$hofType]);
3319
		}
3320
3321
		foreach ($this->hasHOFChanged as $hofType => $changeType) {
3322
			$amount = $this->HOF[$hofType];
3323
			if ($changeType === self::HOF_NEW) {
3324
				if ($amount > 0) {
3325
					$this->db->insert('player_hof', [
3326
						'account_id' => $this->db->escapeNumber($this->getAccountID()),
3327
						'game_id' => $this->db->escapeNumber($this->getGameID()),
3328
						'type' => $this->db->escapeString($hofType),
3329
						'amount' => $this->db->escapeNumber($amount),
3330
					]);
3331
				}
3332
			} elseif ($changeType === self::HOF_CHANGED) {
3333
				$this->db->write('UPDATE player_hof
3334
					SET amount=' . $this->db->escapeNumber($amount) . '
3335
					WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeString($hofType));
3336
			}
3337
			unset($this->hasHOFChanged[$hofType]);
3338
		}
3339
	}
3340
3341
}
3342