Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/Facades/AppUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace OGame\Facades;

use Illuminate\Support\Facades\Facade;
use RuntimeException;

class AppUtil extends Facade
{
Expand Down Expand Up @@ -203,4 +204,33 @@ public static function parseResourceValue(string|int|float|null $value): int
default => $number,
};
}

/**
* Select a random key from a weighted associative array.
*
* @param array<int|string, int> $weights Associative array of [key => weight]
* @return int|string The selected key
*/
public static function selectWeightedRandom(array $weights): int|string
{
if ($weights === []) {
throw new RuntimeException('Cannot select a weighted random key from an empty array.');
}

// max(1, ...) keeps random_int valid when every weight is zero; the loop then never
// matches and we fall through to the first key below.
$totalWeight = array_sum($weights);
$rand = random_int(1, max(1, $totalWeight));

$cumulative = 0;
foreach ($weights as $key => $weight) {
$cumulative += $weight;
if ($rand <= $cumulative) {
return $key;
}
}

// Reached only when all weights are zero; return the first key deterministically.
return array_key_first($weights);
}
}
50 changes: 50 additions & 0 deletions app/GameMessages/Abstracts/ExpeditionGameMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ protected function initialize(): void
*/
protected static int $numberOfVariations;

/**
* Number of message variations in each find-variant tier. Subclasses that support
* variant messages set these so the body keys are laid out as:
* [1 .. normal] normal, then rare, then exceptional.
* The three MUST sum to $numberOfVariations. Left at 0 for messages without tiers,
* in which case getRandomMessageVariationIdForVariant() falls back to the full range.
*
* @var int
*/
protected static int $normalVariations = 0;

/**
* @var int
*/
protected static int $rareVariations = 0;

/**
* @var int
*/
protected static int $exceptionalVariations = 0;

/**
* The base key for the message.
* @var string
Expand Down Expand Up @@ -67,6 +88,35 @@ public static function getRandomMessageVariationId(): int
return random_int(1, static::$numberOfVariations);
}

/**
* Get a random message variation id for a specific find variant (normal/rare/exceptional).
* The variation ids are laid out in tiers (normal, then rare, then exceptional) sized by
* $normalVariations / $rareVariations / $exceptionalVariations. Messages that do not define
* tiers fall back to a random id across the full range.
*
* @param string $variant 'normal', 'rare', or 'exceptional'
* @return int
*/
public static function getRandomMessageVariationIdForVariant(string $variant): int
{
// No tiers configured: behave like a plain random variation.
if (static::$normalVariations === 0) {
return static::getRandomMessageVariationId();
}

return match ($variant) {
'rare' => random_int(
static::$normalVariations + 1,
static::$normalVariations + static::$rareVariations
),
'exceptional' => random_int(
static::$normalVariations + static::$rareVariations + 1,
static::$normalVariations + static::$rareVariations + static::$exceptionalVariations
),
default => random_int(1, static::$normalVariations),
};
}

/**
* Get the number of variations for this expedition message.
* This is used for testing to verify that all translation variations exist.
Expand Down
6 changes: 5 additions & 1 deletion app/GameMessages/ExpeditionGainDarkMatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ class ExpeditionGainDarkMatter extends ExpeditionGameMessage
*
* @var int
*/
protected static int $numberOfVariations = 8;
protected static int $numberOfVariations = 10;

protected static int $normalVariations = 7;
protected static int $rareVariations = 2;
protected static int $exceptionalVariations = 1;

/**
* Overrides the body of the message to append the Dark Matter amount based on the params.
Expand Down
6 changes: 5 additions & 1 deletion app/GameMessages/ExpeditionGainResources.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ class ExpeditionGainResources extends ExpeditionGameMessage
*
* @var int
*/
protected static int $numberOfVariations = 6;
protected static int $numberOfVariations = 9;

protected static int $normalVariations = 4;
protected static int $rareVariations = 4;
protected static int $exceptionalVariations = 1;

/**
* Overides the body of the message to append the captured resource type and amount based on the params.
Expand Down
6 changes: 5 additions & 1 deletion app/GameMessages/ExpeditionGainShips.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ class ExpeditionGainShips extends ExpeditionGameMessage
*
* @var int
*/
protected static int $numberOfVariations = 7;
protected static int $numberOfVariations = 8;

protected static int $normalVariations = 5;
protected static int $rareVariations = 2;
protected static int $exceptionalVariations = 1;

/**
* Overides the body of the message to append the captured resource type and amount based on the params.
Expand Down
87 changes: 52 additions & 35 deletions app/GameMissions/ExpeditionMission.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OGame\Enums\FleetMissionStatus;
use OGame\Enums\FleetSpeedType;
use OGame\Enums\HighscoreTypeEnum;
use OGame\Facades\AppUtil;
use OGame\GameMessages\ExpeditionBattleAliens;
use OGame\GameMessages\ExpeditionBattlePirates;
use OGame\GameMessages\ExpeditionFailed;
Expand Down Expand Up @@ -281,28 +282,8 @@ private function processExpeditionFailedAndDelayOutcome(FleetMission $mission):
// Load the mission owner user
$player = $this->playerServiceFactory->make($mission->user_id, true);

// Define weighted delay factors 2,3,5 probability of 89%, 10%, 1%
$delayFactors = [
2 => 89,
3 => 10,
5 => 1
];

// Calculate total weight and generate random number
$totalWeight = array_sum($delayFactors);
$rand = mt_rand(1, $totalWeight);

// Select multiplier based on cumulative weight
$cumulativeWeight = 0;
$selectedMultiplier = 2; // fallback default

foreach ($delayFactors as $factor => $weight) {
$cumulativeWeight += $weight;
if ($rand <= $cumulativeWeight) {
$selectedMultiplier = $factor;
break;
}
}
// Select delay factor (2x/3x/5x holding time) with weights 89%/10%/1%.
$selectedMultiplier = (int)AppUtil::selectWeightedRandom([2 => 89, 3 => 10, 5 => 1]);

// Calculate base additional return trip time based on holding time
// Formula: Base Delay = Delay factor × Holding time
Expand Down Expand Up @@ -367,8 +348,12 @@ private function processExpeditionGainResourcesOutcome(FleetMission $mission): R
$resourcesInCargo = $mission->metal + $mission->crystal + $mission->deuterium;
$maxCargoCapacity = $totalCargoCapacity - $resourcesInCargo;

// Determine the max resource find.
$maxResourceFind = $this->determineMaxResourceFind($mission);
// Determine find variant (normal/rare/exceptional) and apply its reward multiplier.
$variantData = $this->selectExpeditionFindVariant();
$variant = $variantData['variant'];

// Determine the max resource find and scale by the variant multiplier.
$maxResourceFind = (int)($this->determineMaxResourceFind($mission) * $variantData['multiplier']);

// Determine the resource type: metal, crystal or deuterium.
$cargoCapacityConstrainedAmount = 0;
Expand Down Expand Up @@ -397,8 +382,8 @@ private function processExpeditionGainResourcesOutcome(FleetMission $mission): R
}

// Send a message to the player with the resources found outcome.
// Choose a random message variation id based on the number of available outcomes.
$message_variation_id = ExpeditionGainResources::getRandomMessageVariationId();
// Choose a message variation id matching the find variant (normal/rare/exceptional).
$message_variation_id = ExpeditionGainResources::getRandomMessageVariationIdForVariant($variant);
$this->messageService->sendSystemMessageToPlayer($player, ExpeditionGainResources::class, ['message_variation_id' => $message_variation_id, 'resource_type' => $resource_type->value, 'resource_amount' => $cargoCapacityConstrainedAmount]);

return $resourcesFound;
Expand Down Expand Up @@ -511,8 +496,12 @@ private function processExpeditionGainShipsOutcome(FleetMission $mission): UnitC
$resourcesInCargo = $mission->metal + $mission->crystal + $mission->deuterium;
$maxCargoCapacity = $totalCargoCapacity - $resourcesInCargo;

// Determine the max ship find (uses ships multiplier).
$maxShipFind = $this->determineMaxShipFind($mission);
// Determine find variant (normal/rare/exceptional) and apply its reward multiplier.
$variantData = $this->selectExpeditionFindVariant();
$variant = $variantData['variant'];

// Determine the max ship find and scale by the variant multiplier.
$maxShipFind = (int)($this->determineMaxShipFind($mission) * $variantData['multiplier']);
$cargoCapacityConstrainedAmount = min($maxCargoCapacity, $maxShipFind);

// Select 1-6 random ship types from possible ships.
Expand Down Expand Up @@ -579,8 +568,8 @@ private function processExpeditionGainShipsOutcome(FleetMission $mission): UnitC
}

// Send a message to the player with the units found outcome.
// Choose a random message variation id based on the number of available outcomes.
$message_variation_id = ExpeditionGainShips::getRandomMessageVariationId();
// Choose a message variation id matching the find variant (normal/rare/exceptional).
$message_variation_id = ExpeditionGainShips::getRandomMessageVariationIdForVariant($variant);
$this->messageService->sendSystemMessageToPlayer($player, ExpeditionGainShips::class, ['message_variation_id' => $message_variation_id] + $message_params);

return $units;
Expand All @@ -603,11 +592,15 @@ private function processExpeditionGainDarkMatterOutcome(FleetMission $mission):
// $hasPathfinder = $fleetUnits->hasUnit($objectService->getShipObjectByMachineName('pathfinder'));
$hasPathfinder = false;

// Calculate Dark Matter reward
// Determine find variant (normal/rare/exceptional) and apply its reward multiplier.
$variantData = $this->selectExpeditionFindVariant();
$variant = $variantData['variant'];

// Calculate Dark Matter reward and scale by the variant multiplier.
$darkMatterService = app(DarkMatterService::class);
$darkMatterAmount = $darkMatterService->calculateExpeditionReward($hasPathfinder);
$darkMatterAmount = (int)($darkMatterService->calculateExpeditionReward($hasPathfinder) * $variantData['multiplier']);

// Apply dark matter rewards multiplier
// Apply dark matter rewards multiplier from settings.
$settingsService = app(SettingsService::class);
$darkMatterMultiplier = $settingsService->expeditionRewardMultiplierDarkMatter();
$darkMatterAmount = (int)($darkMatterAmount * $darkMatterMultiplier);
Expand All @@ -626,7 +619,7 @@ private function processExpeditionGainDarkMatterOutcome(FleetMission $mission):
'Dark Matter found during expedition'
);

$message_variation_id = ExpeditionGainDarkMatter::getRandomMessageVariationId();
$message_variation_id = ExpeditionGainDarkMatter::getRandomMessageVariationIdForVariant($variant);
$this->messageService->sendSystemMessageToPlayer($player, ExpeditionGainDarkMatter::class, [
'message_variation_id' => $message_variation_id,
'dark_matter_amount' => $darkMatterAmount
Expand Down Expand Up @@ -917,6 +910,30 @@ private function createExpeditionBattleReport(
return $report->id;
}

/**
* Select a random find variant (normal, rare, or exceptional) using weighted probabilities.
* Normal: 89%, Rare: 10%, Exceptional: 1%.
*
* Returns the selected variant name and a corresponding reward multiplier:
* - normal: 1x
* - rare: 2–3x (random)
* - exceptional: 5–10x (random)
*
* @return array{variant: string, multiplier: int}
*/
protected function selectExpeditionFindVariant(): array
{
$selectedVariant = AppUtil::selectWeightedRandom(['normal' => 89, 'rare' => 10, 'exceptional' => 1]);

$multiplier = match($selectedVariant) {
'rare' => random_int(2, 3),
'exceptional' => random_int(5, 10),
default => 1,
};

return ['variant' => (string)$selectedVariant, 'multiplier' => $multiplier];
}

/**
* Select a random expedition outcome based on configured weights. Higher weight
* for a particular outcome means more chance of that outcome being selected
Expand Down Expand Up @@ -995,7 +1012,7 @@ private function selectRandomOutcome(FleetMission $mission): ExpeditionOutcomeTy
*
* @return int
*/
private function getBaseMaxFindFromHighscore(): int
protected function getBaseMaxFindFromHighscore(): int
{
// Max resources found is according to these params:
// Number 1 player highscore "general" points determines the max resources found:
Expand Down
11 changes: 2 additions & 9 deletions app/Services/NPCFleetGeneratorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace OGame\Services;

use Exception;
use OGame\Facades\AppUtil;
use OGame\GameObjects\Models\Units\UnitCollection;

/**
Expand Down Expand Up @@ -168,15 +169,7 @@ private function generateNPCTechLevels(PlayerService $playerService, string $npc
*/
private function selectBattleSizeTier(): int
{
$random = random_int(1, 100);

if ($random <= 89) {
return 1; // Normal (89%)
} elseif ($random <= 99) {
return 2; // Large (10%)
} else {
return 3; // Extra-large (1%)
}
return (int)AppUtil::selectWeightedRandom([1 => 89, 2 => 10, 3 => 1]);
}

/**
Expand Down
Loading
Loading