diff --git a/app/Factories/GameMessageFactory.php b/app/Factories/GameMessageFactory.php new file mode 100644 index 00000000..6e196066 --- /dev/null +++ b/app/Factories/GameMessageFactory.php @@ -0,0 +1,82 @@ + + */ + private static array $gameMessageClasses = [ + 'welcome_message' => WelcomeMessage::class, + 'return_of_fleet_with_resources' => ReturnOfFleetWithResources::class, + 'return_of_fleet' => ReturnOfFleet::class, + 'transport_arrived' => TransportArrived::class, + 'transport_received' => TransportReceived::class, + 'colony_established' => ColonyEstablished::class, + 'fleet_deployment' => FleetDeployment::class, + 'fleet_deployment_with_resources' => FleetDeploymentWithResources::class, + ]; + + /** + * @return array + * @throws BindingResolutionException + */ + public static function getAllGameMessages(): array + { + $gameMessages = []; + foreach (self::$gameMessageClasses as $id => $class) { + $gameMessages[$id] = app()->make($class); + } + return $gameMessages; + } + + /** + * @param string $key + * + * @return GameMessage + * @throws BindingResolutionException + */ + public static function createGameMessage(string $key): GameMessage + { + if (!isset(self::$gameMessageClasses[$key])) { + throw new BindingResolutionException("GameMessage with key $key not found."); + } + + return app()->make(self::$gameMessageClasses[$key]); + } + + /** + * Get all class keys that have a certain tab and optionally subtab. This is for knowing which messages to display + * in the game messages page in what tab/subtab. + * + * @param string $tab + * @param string|null $subtab + * @return array + * @throws BindingResolutionException + */ + public static function GetGameMessageKeysByTab(string $tab, ?string $subtab = null): array + { + $matchingKeys = []; + + foreach (self::$gameMessageClasses as $id => $class) { + $gameMessage = app()->make($class); + if ($gameMessage->getTab() === $tab && ($subtab === null || $gameMessage->getSubtab() === $subtab)) { + $matchingKeys[] = $id; + } + } + + return $matchingKeys; + } +} diff --git a/app/GameMessages/Abstracts/GameMessage.php b/app/GameMessages/Abstracts/GameMessage.php new file mode 100644 index 00000000..15f212db --- /dev/null +++ b/app/GameMessages/Abstracts/GameMessage.php @@ -0,0 +1,126 @@ + The params that the message requires to be filled. + */ + protected array $params; + + /** + * @var string The tab of the message. This is used to group messages in the game messages page. + */ + protected string $tab; + + /** + * @var string The subtab of the message. This is used to group messages in the game messages page. + */ + protected string $subtab; + + public function __construct() + { + $this->initialize(); + } + + /** + * Initialize the message with the key, params, tab and subtab. + * + * @return void + */ + abstract protected function initialize(): void; + + /** + * Get the key of the message. + * + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Returns the static sender of the message. + * + * @return string + */ + public function getFrom(): string + { + return __('t_messages.' . $this->key . '.from'); + } + + /** + * Returns the subject of the message. + * + * @return string + */ + public function getSubject(): string + { + return __('t_messages.' . $this->key. '.subject'); + } + + /** + * Get the body of the message filled with provided params. + * + * @param array $params + * @return string + */ + public function getBody(array $params): string + { + // Check if all the params are provided by checking all individual param names. + foreach ($this->params as $param) { + if (!array_key_exists($param, $params)) { + // Replace param in message with "?undefined?" to indicate that the param is missing. + $params[$param] = '?undefined?'; + } + } + + // Certain reserved params such as resources should be formatted with number_format. + foreach ($params as $key => $value) { + if (in_array($key, ['metal', 'crystal', 'deuterium'])) { + $params[$key] = AppUtil::formatNumber((int)$value); + } + } + + return __('t_messages.' . $this->key . '.body', $params); + } + + /** + * Get the params that the message requires to be filled. + * + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * Get the tab of the message. This is used to group messages in the game messages page. + * + * @return string + */ + public function getTab(): string + { + return $this->tab; + } + + /** + * Get the subtab of the message. This is used to group messages in the game messages page. + * + * @return string + */ + public function getSubtab(): string + { + return $this->subtab; + } +} diff --git a/app/GameMessages/ColonyEstablished.php b/app/GameMessages/ColonyEstablished.php new file mode 100644 index 00000000..cec6e30c --- /dev/null +++ b/app/GameMessages/ColonyEstablished.php @@ -0,0 +1,16 @@ +key = 'colony_established'; + $this->params = ['coordinates']; + $this->tab = 'economy'; + $this->subtab = 'economy'; + } +} diff --git a/app/GameMessages/FleetDeployment.php b/app/GameMessages/FleetDeployment.php new file mode 100644 index 00000000..6b3a4d0d --- /dev/null +++ b/app/GameMessages/FleetDeployment.php @@ -0,0 +1,16 @@ +key = 'fleet_deployment'; + $this->params = ['from', 'to']; + $this->tab = 'fleets'; + $this->subtab = 'other'; + } +} diff --git a/app/GameMessages/FleetDeploymentWithResources.php b/app/GameMessages/FleetDeploymentWithResources.php new file mode 100644 index 00000000..4957348d --- /dev/null +++ b/app/GameMessages/FleetDeploymentWithResources.php @@ -0,0 +1,16 @@ +key = 'fleet_deployment_with_resources'; + $this->params = ['from', 'to', 'metal', 'crystal', 'deuterium']; + $this->tab = 'fleets'; + $this->subtab = 'other'; + } +} diff --git a/app/GameMessages/ReturnOfFleet.php b/app/GameMessages/ReturnOfFleet.php new file mode 100644 index 00000000..814185fb --- /dev/null +++ b/app/GameMessages/ReturnOfFleet.php @@ -0,0 +1,16 @@ +key = 'return_of_fleet'; + $this->params = ['from', 'to']; + $this->tab = 'fleets'; + $this->subtab = 'other'; + } +} diff --git a/app/GameMessages/ReturnOfFleetWithResources.php b/app/GameMessages/ReturnOfFleetWithResources.php new file mode 100644 index 00000000..7e047efe --- /dev/null +++ b/app/GameMessages/ReturnOfFleetWithResources.php @@ -0,0 +1,16 @@ +key = 'return_of_fleet_with_resources'; + $this->params = ['from', 'to', 'metal', 'crystal', 'deuterium']; + $this->tab = 'fleets'; + $this->subtab = 'other'; + } +} diff --git a/app/GameMessages/TransportArrived.php b/app/GameMessages/TransportArrived.php new file mode 100644 index 00000000..b847c39c --- /dev/null +++ b/app/GameMessages/TransportArrived.php @@ -0,0 +1,16 @@ +key = 'transport_arrived'; + $this->params = ['from', 'to', 'metal', 'crystal', 'deuterium']; + $this->tab = 'fleets'; + $this->subtab = 'transport'; + } +} diff --git a/app/GameMessages/TransportReceived.php b/app/GameMessages/TransportReceived.php new file mode 100644 index 00000000..e081f253 --- /dev/null +++ b/app/GameMessages/TransportReceived.php @@ -0,0 +1,16 @@ +key = 'transport_received'; + $this->params = ['from', 'to', 'metal', 'crystal', 'deuterium']; + $this->tab = 'fleets'; + $this->subtab = 'other'; + } +} diff --git a/app/GameMessages/WelcomeMessage.php b/app/GameMessages/WelcomeMessage.php new file mode 100644 index 00000000..7d3e0479 --- /dev/null +++ b/app/GameMessages/WelcomeMessage.php @@ -0,0 +1,16 @@ +key = 'welcome_message'; + $this->params = ['player']; + $this->tab = 'universe'; + $this->subtab = 'universe'; + } +} diff --git a/app/GameMissions/Abstracts/GameMission.php b/app/GameMissions/Abstracts/GameMission.php index fd9855d7..131a2108 100644 --- a/app/GameMissions/Abstracts/GameMission.php +++ b/app/GameMissions/Abstracts/GameMission.php @@ -6,6 +6,8 @@ use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Carbon; use OGame\Factories\PlanetServiceFactory; +use OGame\GameMessages\ReturnOfFleet; +use OGame\GameMessages\ReturnOfFleetWithResources; use OGame\GameMissions\Models\MissionPossibleStatus; use OGame\GameObjects\Models\UnitCollection; use OGame\Models\FleetMission; @@ -310,21 +312,22 @@ protected function sendFleetReturnMessage(FleetMission $mission, PlayerService $ } if ($return_resources->sum() > 0) { - $body = __('t_messages.return_of_fleet', [ + $params = [ 'from' => $from, 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', - 'metal' => $mission->metal, - 'crystal' => $mission->crystal, - 'deuterium' => $mission->deuterium, - ]); + 'metal' => (string)$mission->metal, + 'crystal' => (string)$mission->crystal, + 'deuterium' => (string)$mission->deuterium, + ]; + $this->messageService->sendSystemMessageToPlayer($targetPlayer, ReturnOfFleetWithResources::class, $params); } else { - $body = __('t_messages.return_of_fleet_no_goods', [ + $params = [ 'from' => $from, 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', - ]); - } + ]; + $this->messageService->sendSystemMessageToPlayer($targetPlayer, ReturnOfFleet::class, $params); - $this->messageService->sendMessageToPlayer($targetPlayer, 'Return of a fleet', $body, 'return_of_fleet'); + } } /** diff --git a/app/GameMissions/ColonisationMission.php b/app/GameMissions/ColonisationMission.php index bb29cfab..9fce8dcc 100644 --- a/app/GameMissions/ColonisationMission.php +++ b/app/GameMissions/ColonisationMission.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Container\BindingResolutionException; use OGame\Factories\PlanetServiceFactory; use OGame\Factories\PlayerServiceFactory; +use OGame\GameMessages\ColonyEstablished; use OGame\GameMissions\Abstracts\GameMission; use OGame\GameMissions\Models\MissionPossibleStatus; use OGame\GameObjects\Models\UnitCollection; @@ -92,8 +93,10 @@ protected function processArrival(FleetMission $mission): void // Create a new planet at the target coordinates. $target_planet = $planetServiceFactory->createAdditionalForPlayer($player, new Coordinate($mission->galaxy_to, $mission->system_to, $mission->position_to)); - // Success message - $this->messageService->sendMessageToPlayer($target_planet->getPlayer(), 'Settlement Report', 'The fleet has arrived at the assigned coordinates [coordinates]' . $target_planet->getPlanetCoordinates()->asString() . '[/coordinates], found a new planet there and are beginning to develop upon it immediately.', 'colony_established'); + // Send success message + $this->messageService->sendSystemMessageToPlayer($player, ColonyEstablished::class, [ + 'coordinates' => $target_planet->getPlanetCoordinates()->asString(), + ]); // Add resources to the target planet if the mission has any. $resources = $this->fleetMissionService->getResources($mission); diff --git a/app/GameMissions/DeploymentMission.php b/app/GameMissions/DeploymentMission.php index e89d51e4..fdf91cc6 100644 --- a/app/GameMissions/DeploymentMission.php +++ b/app/GameMissions/DeploymentMission.php @@ -4,6 +4,8 @@ use Illuminate\Contracts\Container\BindingResolutionException; use OGame\Factories\PlanetServiceFactory; +use OGame\GameMessages\FleetDeployment; +use OGame\GameMessages\FleetDeploymentWithResources; use OGame\GameMissions\Abstracts\GameMission; use OGame\GameMissions\Models\MissionPossibleStatus; use OGame\GameObjects\Models\UnitCollection; @@ -48,13 +50,18 @@ protected function processArrival(FleetMission $mission): void // Send a message to the player that the mission has arrived // TODO: make message content translatable by using tokens instead of directly inserting dynamic content. if ($resources->sum() > 0) { - $this->messageService->sendMessageToPlayer($target_planet->getPlayer(), 'Fleet deployment', 'One of your fleets from [planet]' . $mission->planet_id_from . '[/planet] has reached [planet]' . $mission->planet_id_to . '[/planet] and delivered its goods: - -Metal: ' . $mission->metal . ' -Crystal: ' . $mission->crystal . ' -Deuterium: ' . $mission->deuterium, 'fleet_deployment'); + $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), FleetDeploymentWithResources::class, [ + 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'metal' => (string)$mission->metal, + 'crystal' => (string)$mission->crystal, + 'deuterium' => (string)$mission->deuterium + ]); } else { - $this->messageService->sendMessageToPlayer($target_planet->getPlayer(), 'Fleet deployment', 'One of your fleets from [planet]' . $mission->planet_id_from . '[/planet] has reached [planet]' . $mission->planet_id_to . '[/planet]. The fleet doesn`t deliver goods.', 'fleet_deployment'); + $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), FleetDeployment::class, [ + 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + ]); } // Mark the arrival mission as processed diff --git a/app/GameMissions/TransportMission.php b/app/GameMissions/TransportMission.php index e31a0017..f6b38e64 100644 --- a/app/GameMissions/TransportMission.php +++ b/app/GameMissions/TransportMission.php @@ -4,6 +4,8 @@ use Illuminate\Contracts\Container\BindingResolutionException; use OGame\Factories\PlanetServiceFactory; +use OGame\GameMessages\TransportArrived; +use OGame\GameMessages\TransportReceived; use OGame\GameMissions\Abstracts\GameMission; use OGame\GameMissions\Models\MissionPossibleStatus; use OGame\GameObjects\Models\UnitCollection; @@ -47,13 +49,23 @@ protected function processArrival(FleetMission $mission): void // Send a message to the origin player that the mission has arrived // TODO: make message content translatable by using tokens instead of directly inserting dynamic content. - $this->messageService->sendMessageToPlayer($origin_planet->getPlayer(), 'Reaching a planet', 'Your fleet from planet [planet]' . $mission->planet_id_from . '[/planet] reaches the planet [planet]' . $mission->planet_id_to . '[/planet] and delivers its goods: -Metal: ' . $mission->metal . ' Crystal: ' . $mission->crystal . ' Deuterium: ' . $mission->deuterium, 'transport_arrived'); + $this->messageService->sendSystemMessageToPlayer($origin_planet->getPlayer(), TransportArrived::class, [ + 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'metal' => (string)$mission->metal, + 'crystal' => (string)$mission->crystal, + 'deuterium' => (string)$mission->deuterium, + ]); if ($origin_planet->getPlayer()->getId() !== $target_planet->getPlayer()->getId()) { // Send a message to the target player that the mission has arrived - $this->messageService->sendMessageToPlayer($target_planet->getPlayer(), 'Incoming fleet', 'An incoming fleet from planet [planet]' . $mission->planet_id_from . '[/planet] has reached your planet [planet]' . $mission->planet_id_to . '[/planet] and delivered its goods: -Metal: ' . $mission->metal . ' Crystal: ' . $mission->crystal . ' Deuterium: ' . $mission->deuterium, 'transport_received'); + $this->messageService->sendSystemMessageToPlayer($target_planet->getPlayer(), TransportReceived::class, [ + 'from' => '[planet]' . $mission->planet_id_from . '[/planet]', + 'to' => '[planet]' . $mission->planet_id_to . '[/planet]', + 'metal' => (string)$mission->metal, + 'crystal' => (string)$mission->crystal, + 'deuterium' => (string)$mission->deuterium, + ]); } // Mark the arrival mission as processed diff --git a/app/Models/Message.php b/app/Models/Message.php index eedcc167..c68efe73 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -34,12 +34,25 @@ * @method static \Illuminate\Database\Eloquent\Builder|Message whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|Message whereUserId($value) * @method static \Illuminate\Database\Eloquent\Builder|Message whereViewed($value) + * @property array $params + * @method static \Illuminate\Database\Eloquent\Builder|Message whereParams($value) + * @property string $key + * @method static \Illuminate\Database\Eloquent\Builder|Message whereKey($value) * @mixin \Eloquent */ class Message extends Model { use HasFactory; + /** + * Treat the params column as an array so its contents get stored/retrieved as JSON. + * + * @var array + */ + protected $casts = [ + 'params' => 'array', + ]; + /** * Get the user that owns the research queue record. */ diff --git a/app/Services/MessageService.php b/app/Services/MessageService.php index 528af9c7..214055b5 100644 --- a/app/Services/MessageService.php +++ b/app/Services/MessageService.php @@ -2,6 +2,10 @@ namespace OGame\Services; +use Illuminate\Contracts\Container\BindingResolutionException; +use OGame\Factories\GameMessageFactory; +use OGame\GameMessages\Abstracts\GameMessage; +use OGame\GameMessages\WelcomeMessage; use OGame\Models\Message; use OGame\ViewModels\MessageViewModel; @@ -15,12 +19,39 @@ class MessageService { /** - * Define tabs and subtabs which message types they contain. + * Define tab and subtab structure. * - * @var array>> $tabs + * @var array> $tabs */ - // TODO: refactor this to a typed array/class so sending messages with types is typesafe. protected array $tabs = [ + 'fleets' => [ + 'espionage', + 'combat_reports', + 'expeditions', + 'transport', + 'other', + ], + 'communication' => [ + 'messages', + 'information', + ], + 'economy' => [ + 'economy', + ], + 'universe' => [ + 'universe', + ], + 'system' => [ + 'system', + ], + 'favorites' => [ + 'favorites', + ], + ]; + + // TODO: tab array defined below is not used anymore but it's kept for reference purposes when implementing + // more message types. + /*protected array $tabs = [ 'fleets' => [ 'espionage' => [ 1, // Espionage report for foreign planet @@ -76,7 +107,8 @@ class MessageService 99, // TODO: Implement favorites ], ], - ]; + ];*/ + /** * The PlayerService object. @@ -104,12 +136,15 @@ public function getMessagesForTab(string $tab, string $subtab): array { // If subtab is empty, we use the first subtab of the tab. if (empty($subtab)) { - $subtab = array_key_first($this->tabs[$tab]); + $subtab = $this->tabs[$tab][0]; } + // Get all messages of user where type is in the tab and subtab array. Order by created_at desc. + $messageKeys = GameMessageFactory::GetGameMessageKeysByTab($tab, (string)$subtab); + // Get all messages of user where type is in the tab and subtab array. Order by created_at desc. $messages = Message::where('user_id', $this->player->getId()) - ->whereIn('type', $this->tabs[$tab][$subtab]) + ->whereIn('key', $messageKeys) ->orderBy('created_at', 'desc') ->get(); @@ -135,61 +170,56 @@ public function getUnreadMessagesCount(): int ->count(); } + /** + * @throws BindingResolutionException + */ public function getUnreadMessagesCountForTab(string $tab): int { - // Get all ids of the subtabs in this tab. - $subtabIds = []; - foreach ($this->tabs[$tab] as $subtab => $types) { - foreach ($types as $type) { - $subtabIds[] = $type; - } - } + // Get all keys for the tab. + $messageKeys = GameMessageFactory::GetGameMessageKeysByTab($tab); return Message::where('user_id', $this->player->getId()) - ->whereIn('type', $subtabIds) + ->whereIn('key', $messageKeys) ->where('viewed', 0) ->count(); } + /** + * @throws BindingResolutionException + */ public function getUnreadMessagesCountForSubTab(string $tab, string $subtab): int { + // Get all keys for the subtab. + $messageKeys = GameMessageFactory::GetGameMessageKeysByTab($tab, $subtab); + return Message::where('user_id', $this->player->getId()) - ->whereIn('type', $this->tabs[$tab][$subtab]) + ->whereIn('key', $messageKeys) ->where('viewed', 0) ->count(); } /** - * Sends a message to a player. + * Sends a system message to a player by using a template and passing params. * * @param PlayerService $player - * @param string $subject - * @param string $body - * @param string $type + * @param class-string $gameMessageClass + * @param array $params * @return void */ - public function sendMessageToPlayer(PlayerService $player, string $subject, string $body, string $type): void + public function sendSystemMessageToPlayer(PlayerService $player, string $gameMessageClass, array $params): void { - // Convert type string to type int based on tabs array multiple levels. - $typeId = 0; - if (is_string($type)) { - foreach ($this->tabs as $tab => $subtabs) { - foreach ($subtabs as $subtab => $types) { - foreach ($types as $arrayTypeKey => $arrayTypeId) { - if ($type === $arrayTypeKey) { - $typeId = $arrayTypeId; - break; - } - } - } - } + // Ensure the provided class is a subclass of GameMessage + if (!is_subclass_of($gameMessageClass, GameMessage::class)) { + throw new \InvalidArgumentException('Invalid game message class.'); } + /** @var GameMessage $gameMessage */ + $gameMessage = new $gameMessageClass(); + $message = new Message(); $message->user_id = $player->getId(); - $message->type = $typeId; - $message->subject = $subject; - $message->body = $body; + $message->key = $gameMessage->getKey(); + $message->params = $params; $message->save(); } @@ -200,28 +230,7 @@ public function sendMessageToPlayer(PlayerService $player, string $subject, stri */ public function sendWelcomeMessage(): void { - $this->sendMessageToPlayer($this->player, 'Welcome to OGameX!', 'Greetings Emperor [player]' . $this->player->getId() . '[/player]! - -Congratulations on starting your illustrious career. I will be here to guide you through your first steps. - -On the left you can see the menu which allows you to supervise and govern your galactic empire. - -You’ve already seen the Overview. Resources and Facilities allow you to construct buildings to help you expand your empire. Start by building a Solar Plant to harvest energy for your mines. - -Then expand your Metal Mine and Crystal Mine to produce vital resources. Otherwise, simply take a look around for yourself. You’ll soon feel well at home, I’m sure. - -You can find more help, tips and tactics here: - -Discord Chat: Discord Server -Forum: OGameX Forum -Support: Game Support - -You’ll only find current announcements and changes to the game in the forums. - - -Now you’re ready for the future. Good luck! - -This message will be deleted in 7 days.', 'welcome_message'); + $this->sendSystemMessageToPlayer($this->player, WelcomeMessage::class, ['player' => '[player]' . $this->player->getId() . '[/player]']); } /** diff --git a/app/ViewModels/MessageViewModel.php b/app/ViewModels/MessageViewModel.php index 4066907c..c486cda4 100644 --- a/app/ViewModels/MessageViewModel.php +++ b/app/ViewModels/MessageViewModel.php @@ -5,6 +5,8 @@ use Illuminate\Support\Carbon; use OGame\Factories\PlanetServiceFactory; use OGame\Factories\PlayerServiceFactory; +use OGame\Factories\GameMessageFactory; +use OGame\GameMessages\Abstracts\GameMessage; use OGame\Models\Message; use OGame\Models\Planet\Coordinate; @@ -15,12 +17,17 @@ class MessageViewModel { public int $id; public int $user_id; - public int $type; - public string $subject; - public string $body; + public string $key; + public ?string $subject; + public ?string $body; + /** + * @var array $params + */ + public ?array $params; public int $viewed; public ?Carbon $created_at; public ?Carbon $updated_at; + private ?GameMessage $gameMessage = null; /** * Constructor @@ -29,23 +36,21 @@ public function __construct(Message $message) { $this->id = $message->id; $this->user_id = $message->user_id; - $this->type = $message->type; + $this->key = $message->key; $this->subject = $message->subject; $this->body = $message->body; + $this->params = $message->params; $this->viewed = $message->viewed; $this->created_at = $message->created_at; $this->updated_at = $message->updated_at; + + $gameMessage = GameMessageFactory::createGameMessage($this->key); + $this->gameMessage = $gameMessage; } public function getFrom(): string { - // From is based on the type of the message and/or the user_id/alliance_id. - switch ($this->type) { - case 63: // colony_established - return 'Settlers'; - default: - return 'Fleet Command'; - } + return $this->gameMessage->getFrom(); } public function getId(): int @@ -55,12 +60,24 @@ public function getId(): int public function getSubject(): string { + if ($this->gameMessage !== null) { + // TODO: do we need replacements here or is the subject always static? + return $this->gameMessage->getSubject(); + } + return $this->subject; } public function getBody(): string { - $body = nl2br($this->body); + if ($this->gameMessage !== null) { + // TODO: retrieve dynamic params from message record from DB and use them here. + // Params are retrieved as keys not the values? + $body = nl2br($this->gameMessage->getBody($this->params)); + } else { + // TODO: implement dynamic messages without templates (e.g. mass messages from admin to players) + $body = nl2br($this->body); + } // Find and replace the following placeholders: // [player]{playerId}[/player] with the player name. diff --git a/database/migrations/2024_04_08_075348_create_messages_table.php b/database/migrations/2024_04_08_075348_create_messages_table.php index f86ca083..a706d36f 100644 --- a/database/migrations/2024_04_08_075348_create_messages_table.php +++ b/database/migrations/2024_04_08_075348_create_messages_table.php @@ -15,44 +15,7 @@ public function up(): void // Foreign key to the user that this message belongs to. $table->integer('user_id', false, true); $table->foreign('user_id')->references('id')->on('users'); - // Message type - // ================= - // Fleets - // ================= - // - Espionage - // > Espionage report = 1 - // > Espionage action on own planet = 2 - // - Combat reports - // > Combat report = 11 - // - Expeditions - // > Expedition report = 21 - // - Transport - // > Transport report (TODO: find example) = 31 - // - Other - // > Return of fleet = 41 - // > Outlaw notification = 42 - // > Wreckage created on own planet after battle = 43 - // ================= - // Communication - // ================= - // - Messages - // > Buddy request/confirm/delete = 51 - // > Alliance message = 52 - // ================= - // Economy - // ================= - // > Production canceled = 61 - // > Repair completed = 62 - // ================= - // Universe - // ================= - // > Welcome message = 71 - // > Starter bonus = 72 - // > Promotions/sales = 73 - // ================= - // OGame - // ================= - // > Officer runs out = 81 + // Message type. $table->tinyInteger('type'); // Subject can contain inline planet ID or a user ID which will be expanded to planet name or username by the game engine. $table->text('subject'); diff --git a/database/migrations/2024_05_15_164736_update_messages_table.php b/database/migrations/2024_05_15_164736_update_messages_table.php new file mode 100644 index 00000000..bedf4bc9 --- /dev/null +++ b/database/migrations/2024_05_15_164736_update_messages_table.php @@ -0,0 +1,81 @@ +id(); + // Foreign key to the user that this message belongs to. + $table->integer('user_id', false, true); + $table->foreign('user_id')->references('id')->on('users'); + $table->string('key'); + // Subject can contain inline planet ID or a user ID which will be expanded to planet name or username by the game engine. + $table->text('subject')->nullable(); + // Foreign key to planet that this message actions should be performed on (attack, spy etc). Null if not applicable. + $table->integer('action_planet_id', false, true)->nullable(); + $table->foreign('action_planet_id')->references('id')->on('planets'); + // Foreign key sender user ID (e.g. if the message is from a user in alliance circular message context). + $table->integer('sender_user_id', false, true)->nullable(); + $table->foreign('sender_user_id')->references('id')->on('users'); + // Foreign key alliance ID (e.g. if message is from alliance notification context). + // TODO: add when alliances are implemented. + $table->text('body')->nullable(); + $table->text('params')->nullable(); + $table->boolean('viewed')->default(false); + // TODO: message can be shared with alliance members with certain rank. Have to add status columns for this. + $table->timestamps(); + + // Add indexes for performance: + $table->index(['user_id', 'key', 'created_at']); // For message listing + $table->index(['user_id', 'key', 'viewed']); // For unread messages count + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Drop new table definition. + Schema::dropIfExists('messages'); + + // Recreate old table definition. + Schema::create('messages', function (Blueprint $table) { + $table->id(); + // Foreign key to the user that this message belongs to. + $table->integer('user_id', false, true); + $table->foreign('user_id')->references('id')->on('users'); + // Message type. + $table->tinyInteger('type'); + // Subject can contain inline planet ID or a user ID which will be expanded to planet name or username by the game engine. + $table->text('subject'); + // Foreign key to planet that this message actions should be performed on (attack, spy etc). Null if not applicable. + $table->integer('action_planet_id', false, true)->nullable(); + $table->foreign('action_planet_id')->references('id')->on('planets'); + // Foreign key sender user ID (e.g. if the message is from a user in alliance circular message context). + $table->integer('sender_user_id', false, true)->nullable(); + $table->foreign('sender_user_id')->references('id')->on('users'); + // Foreign key alliance ID (e.g. if message is from alliance notification context). + // TODO: add when alliances are implemented. + $table->text('body'); + $table->boolean('viewed')->default(false); + // TODO: message can be shared with alliance members with certain rank. Have to add status columns for this. + $table->timestamps(); + + // Add indexes for performance: + $table->index(['user_id', 'type', 'viewed']); // For unread messages count + $table->index(['user_id', 'type', 'created_at']); // For message listing + }); + } +}; diff --git a/resources/lang/en/t_messages.php b/resources/lang/en/t_messages.php index ff2de5f4..131df2c3 100644 --- a/resources/lang/en/t_messages.php +++ b/resources/lang/en/t_messages.php @@ -2,16 +2,94 @@ return [ // ------------------------ - 'return_of_fleet' => 'Your fleet is returning from planet :from to planet :to and delivered its goods: + 'welcome_message' => [ + 'from' => 'OGameX', + 'subject' => 'Welcome to OGameX!', + 'body' => 'Greetings Emperor :player! + +Congratulations on starting your illustrious career. I will be here to guide you through your first steps. + +On the left you can see the menu which allows you to supervise and govern your galactic empire. + +You’ve already seen the Overview. Resources and Facilities allow you to construct buildings to help you expand your empire. Start by building a Solar Plant to harvest energy for your mines. + +Then expand your Metal Mine and Crystal Mine to produce vital resources. Otherwise, simply take a look around for yourself. You’ll soon feel well at home, I’m sure. + +You can find more help, tips and tactics here: + +Discord Chat: Discord Server +Forum: OGameX Forum +Support: Game Support + +You’ll only find current announcements and changes to the game in the forums. + + +Now you’re ready for the future. Good luck! + +This message will be deleted in 7 days.', + ], + + // ------------------------ + 'return_of_fleet_with_resources' => [ + 'from' => 'Fleet Command', + 'subject' => 'Return of a fleet', + 'body' => 'Your fleet is returning from planet :from to planet :to and delivered its goods: Metal: :metal Crystal: :crystal Deuterium: :deuterium', + ], // ------------------------ - - 'return_of_fleet_no_goods' => 'Your fleet is returning from planet :from to planet :to. + 'return_of_fleet' => [ + 'from' => 'Fleet Command', + 'subject' => 'Return of a fleet', + 'body' => 'Your fleet is returning from planet :from to planet :to. The fleet doesn\'t deliver goods.', + ], + + // ------------------------ + 'fleet_deployment_with_resources' => [ + 'from' => 'Fleet Command', + 'subject' => 'Return of a fleet', + 'body' => 'One of your fleets from :from has reached :to and delivered its goods: + +Metal: :metal +Crystal: :crystal +Deuterium: :deuterium', + ], + + // ------------------------ + 'fleet_deployment' => [ + 'from' => 'Fleet Command', + 'subject' => 'Return of a fleet', + 'body' => 'One of your fleets from :from has reached :to. The fleet doesn`t deliver goods.', + ], + + // ------------------------ + 'transport_arrived' => [ + 'from' => 'Fleet Command', + 'subject' => 'Reaching a planet', + 'body' => 'Your fleet from planet :from reaches the planet :to and delivers its goods: +Metal: :metal Crystal: :crystal Deuterium: :deuterium', + ], + + // ------------------------ + 'transport_received' => [ + 'from' => 'Fleet Command', + 'subject' => 'Incoming fleet', + 'body' => 'An incoming fleet from planet :from has reached your planet :to and delivered its goods: +Metal: :metal Crystal: :crystal Deuterium: :deuterium', + ], + + // ------------------------ + 'colony_established' => [ + 'from' => 'Fleet Command', + 'subject' => 'Settlement Report', + 'body' => 'The fleet has arrived at the assigned coordinates :coordinates, found a new planet there and are beginning to develop upon it immediately.', + ], + + // ------------------------ ]; diff --git a/resources/lang/nl/t_messages.php b/resources/lang/nl/t_messages.php new file mode 100644 index 00000000..40f75eb2 --- /dev/null +++ b/resources/lang/nl/t_messages.php @@ -0,0 +1,20 @@ + 'Terugkeer van een vloot', + 'return_of_fleet_body' => 'Je vloot keert terug van planeet :from naar planeet :to. + +De vloot levert: + +Metaal: :metal +Kristal: :crystal +Deuterium: :deuterium', + + // ------------------------ + 'return_of_fleet_no_goods_subject' => 'Terugkeer van een vloot', + 'return_of_fleet_no_goods_body' => 'Je vloot keert terug van planeet :from naar planeet :to. + +De vloot levert geen grondstoffen af.', + +]; diff --git a/tests/AccountTestCase.php b/tests/AccountTestCase.php index 36724e7f..6b06bf50 100644 --- a/tests/AccountTestCase.php +++ b/tests/AccountTestCase.php @@ -7,12 +7,14 @@ use Illuminate\Support\Str; use Illuminate\Testing\TestResponse; use OGame\Factories\PlanetServiceFactory; +use OGame\Models\Message; use OGame\Models\Planet; use OGame\Models\Planet\Coordinate; use OGame\Models\Resources; use OGame\Models\User; use OGame\Services\PlanetService; use OGame\Services\PlayerService; +use OGame\ViewModels\MessageViewModel; /** * Base class for tests that require account context. Common setup includes signup of new account and login. @@ -620,7 +622,8 @@ protected function playerSetAllMessagesRead(): void } /** - * Asserts that a message has been received in the specified tab/subtab and that it contains the specified text. + * Asserts that a message has been received in the frontend on the specified tab/subtab + * and that it contains the specified text. * * @param string $tab * @param string $subtab @@ -640,4 +643,25 @@ protected function assertMessageReceivedAndContains(string $tab, string $subtab, $response->assertSee($needle, false); } } + + /** + * Asserts that a message has been received in the database for a specific player and that it contains the specified text. + * + * @param PlayerService $player + * @param array $must_contain + * @return void + */ + protected function assertMessageReceivedAndContainsDatabase(PlayerService $player, array $must_contain): void + { + $lastMessage = Message::where('user_id', $player->getId()) + ->orderBy('id', 'desc') + ->first(); + + // Get the message body. + $lastMessageViewModel = new MessageViewModel($lastMessage); + + foreach ($must_contain as $needle) { + $this->assertStringContainsString($needle, $lastMessageViewModel->getBody()); + } + } } diff --git a/tests/Feature/FleetDispatch/FleetDispatchColoniseTest.php b/tests/Feature/FleetDispatch/FleetDispatchColoniseTest.php index f0435a1d..746e003a 100644 --- a/tests/Feature/FleetDispatch/FleetDispatchColoniseTest.php +++ b/tests/Feature/FleetDispatch/FleetDispatchColoniseTest.php @@ -136,12 +136,10 @@ public function testDispatchFleetColonizeEmptyPlanet(): void $this->assertNotNull($newPlanet, 'New planet cannot be loaded while it should have been created.'); // Assert that last message sent to current player contains the new planet colonize confirm message. - $lastMessage = Message::where('user_id', $this->currentUserId) - ->orderBy('id', 'desc') - ->first(); - - $this->assertStringContainsString('The fleet has arrived', $lastMessage->body); - $this->assertStringContainsString('found a new planet there and are beginning to develop upon it immediately.', $lastMessage->body); + $this->assertMessageReceivedAndContainsDatabase($this->planetService->getPlayer(), [ + 'The fleet has arrived', + 'found a new planet there and are beginning to develop upon it immediately.', + ]); } /** @@ -315,15 +313,10 @@ public function testDispatchFleetRecallMission(): void $this->planetService->reloadPlanet(); $this->assertTrue($this->planetService->hasResources(new Resources(5000, 5000, 0, 0)), 'Resources are not returned to origin planet after recalling mission.'); - // Assert that the last message sent to the player contains the recall message. - $lastMessage = Message::where('user_id', $this->currentUserId) - ->orderBy('id', 'desc') - ->first(); - - // Verify that message contains "from" as coordinates instead of [planet] tags because the target position - // which was being attempted to colonize is not a planet. - $this->assertStringContainsString('Your fleet is returning from planet [coordinates]', $lastMessage->body); - // Verify that message contains the resources that were returned. - $this->assertStringContainsString('Metal: 5000', $lastMessage->body); + // Assert that the last message sent contains the return trip message. + $this->assertMessageReceivedAndContainsDatabase($this->planetService->getPlayer(), [ + 'Your fleet is returning from planet', + 'Metal: 5,000', + ]); } } diff --git a/tests/Feature/FleetDispatch/FleetDispatchDeployTest.php b/tests/Feature/FleetDispatch/FleetDispatchDeployTest.php index b938940f..84c5ce06 100644 --- a/tests/Feature/FleetDispatch/FleetDispatchDeployTest.php +++ b/tests/Feature/FleetDispatch/FleetDispatchDeployTest.php @@ -376,7 +376,8 @@ public function testDispatchFleetRecallMission(): void Carbon::setTestNow(Carbon::createFromTimestamp($fleetMission->time_arrival)); // Do a request to trigger the update logic. - $this->get('/overview'); + $response = $this->get('/overview'); + $response->assertStatus(200); // Assert that the return trip is processed. $fleetMission = $fleetMissionService->getFleetMissionById($fleetMissionId, false); diff --git a/tests/Feature/FleetDispatch/FleetDispatchTransportTest.php b/tests/Feature/FleetDispatch/FleetDispatchTransportTest.php index e973dc70..8fe11451 100644 --- a/tests/Feature/FleetDispatch/FleetDispatchTransportTest.php +++ b/tests/Feature/FleetDispatch/FleetDispatchTransportTest.php @@ -133,12 +133,10 @@ public function testDispatchFleetToOtherPlayer(): void $response->assertStatus(200); // Assert that last message sent to second player contains the transport confirm message. - $lastMessage = Message::where('user_id', $foreignPlanet->getPlayer()->getId()) - ->orderBy('id', 'desc') - ->first(); - - $this->assertStringContainsString('An incoming fleet from planet', $lastMessage->body); - $this->assertStringContainsString('has reached your planet', $lastMessage->body); + $this->assertMessageReceivedAndContainsDatabase($foreignPlanet->getPlayer(), [ + 'An incoming fleet from planet', + 'has reached your planet', + ]); } /** diff --git a/tests/Feature/MessagesTest.php b/tests/Feature/MessagesTest.php index 6b4395be..669cbea6 100644 --- a/tests/Feature/MessagesTest.php +++ b/tests/Feature/MessagesTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use OGame\Factories\GameMessageFactory; use Tests\AccountTestCase; /** @@ -20,4 +21,33 @@ public function testRegistrationMessageReceived(): void 'Greetings Emperor ' . $this->currentUsername . '!', ]); } + + /** + * Test all GameMessage classes to make sure they can be instantiated and the subject and body return a non-empty string. + */ + public function testGameMessages(): void + { + $gameMessages = GameMessageFactory::getAllGameMessages(); + foreach ($gameMessages as $gameMessage) { + // Get required params and fill in random values. + $params = $gameMessage->getParams(); + $filledParams = []; + foreach ($params as $param) { + $filledParams[$param] = 'test'; + } + + // Check if message raw translation contains all the required params. + foreach ($params as $param) { + $this->assertStringContainsString(':' . $param, __('t_messages.' . $gameMessage->getKey() . '.body'), 'Lang body does not contain :' . $param . ' for ' . get_class($gameMessage)); + } + + // Check that the message has a valid subject and body defined. + $this->assertNotEmpty($gameMessage->getSubject(), 'Subject is empty for ' . get_class($gameMessage)); + $this->assertNotEmpty($gameMessage->getBody($filledParams), 'Body is empty for ' . get_class($gameMessage)); + + // Also assert that it does not contains "t_messages" string as this indicates the translation is missing. + $this->assertStringNotContainsString('t_messages', $gameMessage->getSubject(), 'Subject contains t_messages for ' . get_class($gameMessage)); + $this->assertStringNotContainsString('t_messages', $gameMessage->getBody($filledParams), 'Body contains t_messages for ' . get_class($gameMessage)); + } + } }