Skip to content

Commit

Permalink
gameplay: optional permanent port destruction (#1866)
Browse files Browse the repository at this point in the history
* The `Game` class has a new property `destroyPorts` to track whether or
  not permanent destruction of ports is enabled. This setting can be
  modified when creating or editing a game.

* After a level 1 port is busted, it can optionally be destroyed if the
  feature is enabled for the game. No credits are gained, alignment is
  lost, and a news entry is created.

* Claiming a port for your race has been made mutually exclusive with
  all other post-bust options. It can only be done once, and you only
  get 50% of the credits.

* Changed terminology from "destroyed" to "busted" in Port/Planet
  interfaces to disambiguate between breached defenses and permanent
  destruction.

* Removed some hard-coded alignment change numbers in favor of named
  constants defined in `config.php`.
  • Loading branch information
hemberger authored Jun 28, 2024
1 parent a8794b5 commit 786ba90
Show file tree
Hide file tree
Showing 26 changed files with 164 additions and 69 deletions.
3 changes: 3 additions & 0 deletions db/patches/V1_6_69_01__destroy_ports.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add boolean property `game.destroy_ports`
ALTER TABLE `game`
ADD COLUMN `destroy_ports` enum('TRUE','FALSE') NOT NULL DEFAULT 'FALSE';
6 changes: 6 additions & 0 deletions src/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
const ALIGNMENT_EVIL = -100;
const ALIGNMENT_PRESIDENT = 150;

const ALIGNMENT_LOSS_PORT_DESTROY = 5;
const ALIGNMENT_LOSS_PORT_DAMAGE = 1;
const ALIGNMENT_GAIN_PORT_DAMAGE = 1;
const ALIGNMENT_LOSS_ILLEGAL_SEARCH = 5;
const ALIGNMENT_GAIN_ILLEGAL_SEARCH = 1;

/*
* Log types
*/
Expand Down
8 changes: 4 additions & 4 deletions src/lib/Smr/AbstractShip.php
Original file line number Diff line number Diff line change
Expand Up @@ -879,11 +879,11 @@ public function shootPort(Port $port): array {
if ($results['TotalDamage'] >= Port::DAMAGE_NEEDED_FOR_ALIGNMENT_CHANGE) {
$relations = Globals::getRaceRelations($thisPlayer->getGameID(), $thisPlayer->getRaceID());
if ($relations[$port->getRaceID()] <= RELATIONS_WAR) {
$thisPlayer->increaseAlignment(1);
$thisPlayer->increaseHOF(1, ['Combat', 'Port', 'Alignment', 'Gain'], HOF_PUBLIC);
$thisPlayer->increaseAlignment(ALIGNMENT_GAIN_PORT_DAMAGE);
$thisPlayer->increaseHOF(ALIGNMENT_GAIN_PORT_DAMAGE, ['Combat', 'Port', 'Alignment', 'Gain'], HOF_PUBLIC);
} else {
$thisPlayer->decreaseAlignment(1);
$thisPlayer->increaseHOF(1, ['Combat', 'Port', 'Alignment', 'Loss'], HOF_PUBLIC);
$thisPlayer->decreaseAlignment(ALIGNMENT_LOSS_PORT_DAMAGE);
$thisPlayer->increaseHOF(ALIGNMENT_LOSS_PORT_DAMAGE, ['Combat', 'Port', 'Alignment', 'Loss'], HOF_PUBLIC);
}
}
return $results;
Expand Down
16 changes: 16 additions & 0 deletions src/lib/Smr/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Game {
protected int $allianceMaxPlayers;
protected int $allianceMaxVets;
protected int $startingCredits;
protected bool $destroyPorts;

protected int $totalPlayers;
/** @var array<int> */
Expand Down Expand Up @@ -112,6 +113,7 @@ protected function __construct(
$this->allianceMaxPlayers = $dbRecord->getInt('alliance_max_players');
$this->allianceMaxVets = $dbRecord->getInt('alliance_max_vets');
$this->startingCredits = $dbRecord->getInt('starting_credits');
$this->destroyPorts = $dbRecord->getBoolean('destroy_ports');
} elseif ($create === true) {
$this->isNew = true;
} else {
Expand Down Expand Up @@ -140,6 +142,7 @@ public function save(): void {
'alliance_max_players' => $this->getAllianceMaxPlayers(),
'alliance_max_vets' => $this->getAllianceMaxVets(),
'starting_credits' => $this->getStartingCredits(),
'destroy_ports' => $db->escapeBoolean($this->canDestroyPorts()),
]);
} elseif ($this->hasChanged) {
$db->update(
Expand All @@ -161,6 +164,7 @@ public function save(): void {
'alliance_max_players' => $this->getAllianceMaxPlayers(),
'alliance_max_vets' => $this->getAllianceMaxVets(),
'starting_credits' => $this->getStartingCredits(),
'destroy_ports' => $db->escapeBoolean($this->canDestroyPorts()),
],
['game_id' => $this->getGameID()],
);
Expand Down Expand Up @@ -387,6 +391,18 @@ public function setStartingCredits(int $int): void {
$this->hasChanged = true;
}

public function canDestroyPorts(): bool {
return $this->destroyPorts;
}

public function setDestroyPorts(bool $destroyPorts): void {
if (!$this->isNew && $this->destroyPorts === $destroyPorts) {
return;
}
$this->destroyPorts = $destroyPorts;
$this->hasChanged = true;
}

public function getTotalPlayers(): int {
if (!isset($this->totalPlayers)) {
$db = Database::getInstance();
Expand Down
8 changes: 4 additions & 4 deletions src/lib/Smr/Planet.php
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ public function moveMountedWeaponDown(int $orderID): void {
$this->swapMountedWeapons($orderID + 1, $orderID);
}

public function isDestroyed(): bool {
public function isBusted(): bool {
return !$this->hasCDs() && !$this->hasShields() && !$this->hasArmour();
}

Expand Down Expand Up @@ -1244,7 +1244,7 @@ public function shootPlayers(array $targetPlayers): array {
foreach ($targetPlayers as $targetPlayer) {
$results['TotalDamagePerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
}
if ($this->isDestroyed()) {
if ($this->isBusted()) {
$results['DeadBeforeShot'] = true;
return $results;
}
Expand Down Expand Up @@ -1311,7 +1311,7 @@ public function checkForDowngrade(int $damage): array {
* @return TakenDamageData
*/
public function takeDamage(array $damage): array {
$alreadyDead = $this->isDestroyed();
$alreadyDead = $this->isBusted();
$shieldDamage = 0;
$cdDamage = 0;
$armourDamage = 0;
Expand All @@ -1331,7 +1331,7 @@ public function takeDamage(array $damage): array {
}

return [
'KillingShot' => !$alreadyDead && $this->isDestroyed(),
'KillingShot' => !$alreadyDead && $this->isBusted(),
'TargetAlreadyDead' => $alreadyDead,
'Shield' => $shieldDamage,
'CDs' => $cdDamage,
Expand Down
83 changes: 58 additions & 25 deletions src/lib/Smr/Port.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
use Smr\Combat\Weapon\Weapon;
use Smr\Exceptions\CachedPortNotFound;
use Smr\Exceptions\PathNotFound;
use Smr\Page\Page;
use Smr\Pages\Player\AttackPortClaimProcessor;
use Smr\Pages\Player\AttackPortConfirm;
use Smr\Pages\Player\AttackPortLootProcessor;
use Smr\Pages\Player\AttackPortPayoutProcessor;
Expand Down Expand Up @@ -46,6 +44,7 @@ class Port {
protected const GOODS_TRADED_MONEY_MULTIPLIER = 50;
protected const BASE_PAYOUT = 0.85; // fraction of credits for looting
public const RAZE_PAYOUT = 0.75; // fraction of base payout for razing
public const CLAIM_PAYOUT = 0.5; // fraction of base payout for claiming
public const KILLER_RELATIONS_LOSS = 45; // relations lost by killer in PR

public const SQL = 'sector_id = :sector_id AND game_id = :game_id';
Expand Down Expand Up @@ -1008,7 +1007,7 @@ public function isUnderAttack(): bool {
return ($this->getReinforceTime() >= Epoch::time());
}

public function isDestroyed(): bool {
public function isBusted(): bool {
return $this->getArmour() < 1;
}

Expand Down Expand Up @@ -1111,28 +1110,13 @@ public function getAttackHREF(): string {
return (new AttackPortProcessor())->href();
}

public function getClaimHREF(): string {
return (new AttackPortClaimProcessor())->href();
}

/**
* @return ($justContainer is false ? string : Page)
*/
public function getRazeHREF(bool $justContainer = false): string|Page {
$container = new AttackPortPayoutProcessor(PortPayoutType::Raze);
return $justContainer === false ? $container->href() : $container;
}

/**
* @return ($justContainer is false ? string : Page)
*/
public function getLootHREF(bool $justContainer = false): string|Page {
public function getPayoutHREF(PortPayoutType $payoutType): string {
if ($this->getCredits() > 0) {
$container = new AttackPortPayoutProcessor(PortPayoutType::Loot);
$container = new AttackPortPayoutProcessor($payoutType);
} else {
$container = new CurrentSector(message: 'This port has already been looted.');
$container = new CurrentSector(message: 'The fate of this port has already been decided.');
}
return $justContainer === false ? $container->href() : $container;
return $container->href();
}

public function getLootGoodHREF(int $boughtGoodID): string {
Expand Down Expand Up @@ -1356,7 +1340,7 @@ public function shootPlayers(array $targetPlayers): array {
$results['TotalDamagePerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
$results['TotalShotsPerTargetPlayer'][$targetPlayer->getAccountID()] = 0;
}
if ($this->isDestroyed()) {
if ($this->isBusted()) {
$results['DeadBeforeShot'] = true;
return $results;
}
Expand Down Expand Up @@ -1392,7 +1376,7 @@ public function shootPlayers(array $targetPlayers): array {
* @return TakenDamageData
*/
public function takeDamage(array $damage): array {
$alreadyDead = $this->isDestroyed();
$alreadyDead = $this->isBusted();
$shieldDamage = 0;
$cdDamage = 0;
$armourDamage = 0;
Expand All @@ -1412,7 +1396,7 @@ public function takeDamage(array $damage): array {
}

return [
'KillingShot' => !$alreadyDead && $this->isDestroyed(),
'KillingShot' => !$alreadyDead && $this->isBusted(),
'TargetAlreadyDead' => $alreadyDead,
'Shield' => $shieldDamage,
'CDs' => $cdDamage,
Expand Down Expand Up @@ -1515,6 +1499,55 @@ public function lootPort(AbstractPlayer $killer): int {
return $credits;
}

/**
* Claim port for your race after a successful port raid.
*/
public function claimPort(AbstractPlayer $killer): int {
$credits = IFloor($this->getCredits() * self::BASE_PAYOUT * self::CLAIM_PAYOUT);
if ($this->payout($killer, $credits, 'Claimed')) {
$this->setRaceID($killer->getRaceID());
}
return $credits;
}

/**
* Permanently destroy the port after a successful port raid.
*/
public function destroyPort(AbstractPlayer $killer): int {
$credits = 0;
if ($this->payout($killer, $credits, 'Destroyed')) {
// News Entry
$news = $this->getDisplayName() . ' was destroyed by ' . $killer->getBBLink();
if ($killer->hasCustomShipName()) {
$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
$news .= ' flying <span class="yellow">' . $named_ship . '</span>';
}
$news .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
$db = Database::getInstance();
$db->insert('news', [
'game_id' => $this->getGameID(),
'time' => Epoch::time(),
'news_message' => $news,
'killer_id' => $killer->getAccountID(),
'killer_alliance' => $killer->getAllianceID(),
'dead_id' => ACCOUNT_ID_PORT,
]);

// This wasn't a nice thing to do
$killer->decreaseAlignment(ALIGNMENT_LOSS_PORT_DESTROY);

self::removePort($this->getGameID(), $this->getSectorID());
}
return $credits;
}

/**
* Can ports be destroyed after a successful port raid?
*/
public function canBeDestroyed(): bool {
return $this->getGame()->canDestroyPorts() && ($this->getLevel() === 1);
}

/**
* @return array{}
*/
Expand Down
2 changes: 2 additions & 0 deletions src/lib/Smr/PortPayoutType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ enum PortPayoutType {

case Loot;
case Raze;
case Claim;
case Destroy;

}
1 change: 1 addition & 0 deletions src/pages/Admin/UniGen/CreateGame.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function build(Account $account, Template $template): void {
'startCredits' => 100000,
'ignoreStats' => false,
'relations' => MIN_GLOBAL_RELATIONS,
'destroyPorts' => false,
];
$template->assign('Game', $defaultGame);
$template->assign('SubmitValue', 'Create Game');
Expand Down
1 change: 1 addition & 0 deletions src/pages/Admin/UniGen/CreateGameProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function build(Account $account): never {
$game->setStartingCredits(Request::getInt('starting_credits'));
$game->setCreditsNeeded(Request::getInt('creds_needed'));
$game->setStartingRelations(Request::getInt('relations'));
$game->setDestroyPorts(Request::getBool('destroy_ports'));

// Start game disabled by default
$game->setEnabled(false);
Expand Down
1 change: 1 addition & 0 deletions src/pages/Admin/UniGen/EditGame.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public function build(Account $account, Template $template): void {
'startCredits' => $game->getStartingCredits(),
'ignoreStats' => $game->isIgnoreStats(),
'relations' => $relations,
'destroyPorts' => $game->canDestroyPorts(),
];
$template->assign('Game', $gameArray);

Expand Down
1 change: 1 addition & 0 deletions src/pages/Admin/UniGen/EditGameProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function build(Account $account): never {
$game->setIgnoreStats(Request::getBool('ignore_stats'));
$game->setStartingCredits(Request::getInt('starting_credits'));
$game->setCreditsNeeded(Request::getInt('creds_needed'));
$game->setDestroyPorts(Request::getBool('destroy_ports'));
if (!$game->hasStarted()) {
$game->setStartingRelations(Request::getInt('relations'));
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Player/AttackPlanetProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function build(AbstractPlayer $player): never {
'result' => $db->escapeObject($results, true),
]);

if ($planet->isDestroyed()) {
if ($planet->isBusted()) {
$db->update(
'player',
['land_on_planet' => 'FALSE'],
Expand Down
6 changes: 5 additions & 1 deletion src/pages/Player/AttackPort.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public function __construct(
}

public function build(AbstractPlayer $player, Template $template): void {
$port = $player->getSector()->getPort();
$sector = $player->getSector();
if (!$sector->hasPort()) {
(new CurrentSector(message: 'The port no longer exists!'))->go();
}
$port = $sector->getPort();

if ($this->results !== null) {
$template->assign('FullPortCombatResults', $this->results);
Expand Down
17 changes: 0 additions & 17 deletions src/pages/Player/AttackPortClaimProcessor.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/pages/Player/AttackPortConfirm.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function build(AbstractPlayer $player, Template $template): void {
}
$port = $sector->getPort();

if ($port->isDestroyed()) {
if ($port->isBusted()) {
(new AttackPort())->go();
}

Expand Down
16 changes: 14 additions & 2 deletions src/pages/Player/AttackPortPayoutProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ public function __construct(

public function build(AbstractPlayer $player): never {
$port = $player->getSectorPort();
if (!$port->isDestroyed()) {
if (!$port->exists()) {
create_error('The port no longer exists!');
}
if (!$port->isBusted()) {
create_error('The port is no longer defenceless!');
}

Expand All @@ -23,10 +26,19 @@ public function build(AbstractPlayer $player): never {
$credits = match ($payoutType) {
PortPayoutType::Raze => $port->razePort($player),
PortPayoutType::Loot => $port->lootPort($player),
PortPayoutType::Claim => $port->claimPort($player),
PortPayoutType::Destroy => $port->destroyPort($player),
};
$player->log(LOG_TYPE_TRADING, 'Player Triggers Payout: ' . $payoutType->name);
$port->update();
$msg = 'You have taken <span class="creds">' . number_format($credits) . '</span> from the port.';

$msg = match ($payoutType) {
PortPayoutType::Raze => 'You have razed the port and salvaged <span class="creds">' . number_format($credits) . '</span> credits from the wreckage.',
PortPayoutType::Loot => 'You have looted <span class="creds">' . number_format($credits) . '</span> credits from the port.',
PortPayoutType::Claim => 'You expel the port owners and claim the port for your race, skimming <span class="creds">' . number_format($credits) . '</span> credits from the coffers for your efforts.',
PortPayoutType::Destroy => 'The busted port drifts before you, inert and almost peaceful now that the defences have been neutralised. You notice a large breach in its central hull, and see its primary power source through disfigured metal teeth. Either out of spite or cold calculation, you take aim at the exposed nuclear core and pull the trigger. It is instantly vaporized. Nothing remains of the port and its inhabitants apart from a scintillating cloud of radioactive dust. You monster.',
};

$container = new CurrentSector(message: $msg);
$container->go();
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Player/AttackPortProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function build(AbstractPlayer $player): never {
create_error('This port does not exist.');
}

if ($port->isDestroyed()) {
if ($port->isBusted()) {
(new AttackPort())->go();
}

Expand Down
Loading

0 comments on commit 786ba90

Please sign in to comment.