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
21 changes: 21 additions & 0 deletions app/Contracts/Modules/ExtendsPlanetService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace OGame\Contracts\Modules;

use OGame\Services\PlanetService;

/**
* Contract for modules that inject planet-level calculations,
* such as additional resource production (population, food, artifacts).
*
* Register implementations via app()->tag() in bootModule():
* app()->tag(MyPlanetExtension::class, 'module.planet_extensions');
*/
interface ExtendsPlanetService
{
/**
* Called during planet resource production calculation.
* Modify the planet state or accumulate production values as needed.
*/
public function extendResourceProduction(PlanetService $planet): void;
}
21 changes: 21 additions & 0 deletions app/Contracts/Modules/ExtendsPlayerService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace OGame\Contracts\Modules;

use OGame\Services\PlayerService;

/**
* Contract for modules that inject player-level data or state,
* such as per-player artifact counts or lifeform progress.
*
* Register implementations via app()->tag() in bootModule():
* app()->tag(MyPlayerExtension::class, 'module.player_extensions');
*/
interface ExtendsPlayerService
{
/**
* Called when the PlayerService is booted for a player.
* Attach module-specific data to the player as needed.
*/
public function extendPlayer(PlayerService $player): void;
}
23 changes: 23 additions & 0 deletions app/Contracts/Modules/ProvidesGameObjects.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace OGame\Contracts\Modules;

use OGame\GameObjects\Models\Abstracts\GameObject;

/**
* Contract for modules that contribute additional game objects
* (buildings, ships, defense, research, etc.) to the ObjectService registry.
*
* Call ObjectService::registerModuleObjects($this->getGameObjects()) in bootModule().
*
* @see \OGame\Services\ObjectService::registerModuleObjects()
*/
interface ProvidesGameObjects
{
/**
* Return the array of GameObject instances provided by this module.
*
* @return array<GameObject>
*/
public function getGameObjects(): array;
}
27 changes: 27 additions & 0 deletions app/Contracts/Modules/ProvidesHighscoreCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace OGame\Contracts\Modules;

/**
* Contract for modules that add custom highscore categories.
*
* Register implementations via app()->tag() in bootModule():
* app()->tag(MyHighscoreCategory::class, 'module.highscore_categories');
*/
interface ProvidesHighscoreCategory
{
/**
* Return the unique string identifier for this highscore category.
*/
public function getCategoryId(): string;

/**
* Return the display name for this highscore category.
*/
public function getCategoryLabel(): string;

/**
* Return the score value for the given user ID.
*/
public function getScoreForUser(int $userId): int;
}
26 changes: 26 additions & 0 deletions app/Contracts/Modules/ProvidesQueueProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace OGame\Contracts\Modules;

use OGame\Services\PlanetService;

/**
* Contract for modules that bring their own queue service (buildings, tech, etc.)
*
* The core calls processQueue() on every page load at the same point it processes
* its own building, research, and unit queues. Modules must tag their implementations
* with 'module.queue_processors' in bootModule():
*
* app()->tag(MyQueueProcessor::class, 'module.queue_processors');
*
* The implementation is responsible for retrieving and processing any finished
* queue items for the given planet.
*/
interface ProvidesQueueProcessor
{
/**
* Process any finished module queue items for the given planet.
* Called on every page load during the normal queue processing cycle.
*/
public function processQueue(PlanetService $planet): void;
}
8 changes: 8 additions & 0 deletions app/GameObjects/Models/Abstracts/GameObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ abstract class GameObject
*/
public string $class_name = '';

/**
* Optional module-defined sub-type discriminator.
* Allows modules to semantically distinguish their objects while still
* routing through existing queue services via the primary $type.
* Example: 'lifeform_building', 'lifeform_tech'
*/
public ?string $subType = null;

public string $description;
public string $description_long;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,35 @@ abstract class ObjectPropertyService
*/
protected string $propertyName = '';

/**
* Registry of module-provided bonus modifier callables, keyed by property name.
* Each callable receives (PlayerService $player, GameObject $object) and returns int percentage.
* Applied to base_value, additive alongside the research bonus.
*
* @var array<string, array<callable>>
*/
private static array $bonusModifiers = [];

public function __construct(protected GameObject $parent_object, protected int $base_value)
{
}

/**
* Register an additional bonus percentage for a named property.
*
* Usage in a module's bootModule():
* ObjectPropertyService::registerBonusModifier('attack', function (PlayerService $player, GameObject $object): int {
* return $player->getResearchLevel('my_tech') * 5;
* });
*
* @param string $property The property name: 'attack', 'shield', 'structural_integrity', 'capacity', 'fuel', etc.
* @param callable $fn Receives (PlayerService, GameObject), returns int percentage applied to base_value
*/
public static function registerBonusModifier(string $property, callable $fn): void
{
self::$bonusModifiers[$property][] = $fn;
}

/**
* Get the bonus percentage for a property.
*
Expand All @@ -40,28 +65,47 @@ abstract protected function getBonusPercentage(PlayerService $player): int;
*/
public function calculateProperty(PlayerService $player): GameObjectPropertyDetails
{
$bonusPercentage = $this->getBonusPercentage($player);
// Use integer arithmetic to avoid floating point precision issues
$bonusValue = intdiv($this->base_value * $bonusPercentage, 100);
// Research bonus applied to base_value
$researchBonus = $this->getBonusPercentage($player);
$researchBonusValue = intdiv($this->base_value * $researchBonus, 100);

// Module bonus modifiers — each returns an int percentage applied to base_value,
// additive alongside the research bonus (same pattern as character class bonuses).
$moduleBonus = 0;
$moduleBreakdown = [];
foreach (self::$bonusModifiers[$this->propertyName] ?? [] as $fn) {
$pct = $fn($player, $this->parent_object);
if ($pct !== 0) {
$moduleBonus += $pct;
$moduleBreakdown[] = [
'type' => 'Module bonus',
'value' => intdiv($this->base_value * $pct, 100),
'percentage' => $pct,
];
}
}
$moduleBonusValue = intdiv($this->base_value * $moduleBonus, 100);

$totalValue = $this->base_value + $researchBonusValue + $moduleBonusValue;

$totalValue = $this->base_value + $bonusValue;
$bonuses = [];
if ($researchBonus > 0) {
$bonuses[] = [
'type' => 'Research bonus',
'value' => $researchBonusValue,
'percentage' => $researchBonus,
];
}
foreach ($moduleBreakdown as $entry) {
$bonuses[] = $entry;
}

// Prepare the breakdown for future-proofing (assuming more components might be added)
// TODO: add model for breakdown
// TODO: Add more components to the breakdown if necessary like class bonuses, premium member
// bonuses, item bonuses etc.
$breakdown = [
'rawValue' => $this->base_value,
'bonuses' => [
[
'type' => 'Research bonus',
'value' => $bonusValue,
'percentage' => $bonusPercentage,
],
],
'rawValue' => $this->base_value,
'bonuses' => $bonuses,
'totalValue' => $totalValue,
];

return new GameObjectPropertyDetails($this->base_value, $bonusValue, $totalValue, $breakdown);
return new GameObjectPropertyDetails($this->base_value, $researchBonusValue + $moduleBonusValue, $totalValue, $breakdown);
}
}
93 changes: 93 additions & 0 deletions app/Http/Controllers/Admin/ModulesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace OGame\Http\Controllers\Admin;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use OGame\Http\Controllers\OGameController;
use OGame\Modules\ModuleServiceProvider;

class ModulesController extends OGameController
{
private string $disabledFile;

public function __construct()
{
$this->disabledFile = storage_path('app/modules-disabled.json');
}

/**
* List all discovered modules (from config/modules.php and Composer auto-discovery)
* with their enabled/disabled state.
*/
public function index(): View
{
$disabled = $this->loadDisabled();
$modules = [];

foreach (ModuleServiceProvider::allDiscovered() as $providerClass) {
if (!is_subclass_of($providerClass, ModuleServiceProvider::class)) {
continue;
}
/** @var ModuleServiceProvider $provider */
$provider = new $providerClass(app());
$manifest = $provider->getModuleManifest();

$modules[] = [
'provider' => $providerClass,
'id' => $provider->moduleId(),
'name' => $manifest['name'] ?? $provider->moduleId(),
'description' => $manifest['description'] ?? '',
'version' => $manifest['version'] ?? '?',
'enabled' => !in_array($providerClass, $disabled),
'missing' => false,
];
}

return view('ingame.admin.modules', ['modules' => $modules]);
}

/**
* Toggle a module on or off.
* Changes take effect on the next request (no cache:clear needed).
*/
public function toggle(Request $request): RedirectResponse
{
$providerClass = $request->input('provider');

if (!is_string($providerClass) || !is_subclass_of($providerClass, ModuleServiceProvider::class)) {
return redirect()->back()->with('error', 'Invalid module provider class.');
}

$disabled = $this->loadDisabled();

if (in_array($providerClass, $disabled)) {
$disabled = array_values(array_filter($disabled, fn ($p) => $p !== $providerClass));
$message = 'Module enabled. Reload the page to apply.';
} else {
$disabled[] = $providerClass;
$message = 'Module disabled. Reload the page to apply.';
}

$this->saveDisabled($disabled);

return redirect()->route('admin.modules.index')->with('success', $message);
}

private function loadDisabled(): array
{
if (!file_exists($this->disabledFile)) {
return [];
}
return json_decode((string) file_get_contents($this->disabledFile), true) ?? [];
}

private function saveDisabled(array $disabled): void
{
if (!is_dir(dirname($this->disabledFile))) {
mkdir(dirname($this->disabledFile), 0755, true);
}
file_put_contents($this->disabledFile, json_encode(array_values($disabled), JSON_PRETTY_PRINT));
}
}
Loading
Loading