Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions app/Api/V2/UserAwards/UserAwardKind.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum UserAwardKind: string
case Completed = 'completed';
case Event = 'event';
case Mastered = 'mastered';
case MediaContribution = 'media-contribution';
case PatreonSupporter = 'patreon-supporter';
case Playtest = 'playtest';

Expand All @@ -33,6 +34,7 @@ public static function fromAward(PlayerBadge $award): self
AwardType::AchievementPointsYield => self::AchievementPointsYield,
AwardType::PatreonSupporter => self::PatreonSupporter,
AwardType::CertifiedLegend => self::CertifiedLegend,
AwardType::MediaContribution => self::MediaContribution,
};
}

Expand All @@ -53,6 +55,7 @@ public function apply(Builder $query): Builder
self::AchievementPointsYield => $query->where('award_type', AwardType::AchievementPointsYield),
self::PatreonSupporter => $query->where('award_type', AwardType::PatreonSupporter),
self::CertifiedLegend => $query->where('award_type', AwardType::CertifiedLegend),
self::MediaContribution => $query->where('award_type', AwardType::MediaContribution),
};
}

Expand Down
6 changes: 6 additions & 0 deletions app/Api/V2/UserAwards/UserAwardPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function title(): ?string
AwardType::AchievementPointsYield => 'Achievement Points Earned by Others',
AwardType::PatreonSupporter => 'Patreon Supporter',
AwardType::CertifiedLegend => 'Certified Legend',
AwardType::MediaContribution => 'Media Contribution',
};
}

Expand All @@ -52,6 +53,7 @@ public function badgeUrl(): ?string
AwardType::AchievementPointsYield => asset("/assets/images/badge/contribPoints-{$this->award->award_key}.png"),
AwardType::PatreonSupporter => asset('/assets/images/badge/patreon.png'),
AwardType::CertifiedLegend => asset('/assets/images/badge/legend.png'),
AwardType::MediaContribution => asset("/assets/images/badge/mediaContrib-{$this->award->award_key}.png"),
};
}

Expand All @@ -75,6 +77,10 @@ public function context(): array
'tier' => $this->award->award_key,
'threshold' => PlayerBadge::getBadgeThreshold($this->award->award_type, $this->award->award_key),
],
AwardType::MediaContribution => [
'tier' => $this->award->award_key,
'threshold' => PlayerBadge::getBadgeThreshold($this->award->award_type, $this->award->award_key),
],
AwardType::PatreonSupporter, AwardType::CertifiedLegend => [
'siteAwardType' => $this->award->award_type->value,
],
Expand Down
3 changes: 3 additions & 0 deletions app/Community/Actions/CreateGameClaimAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Models\Game;
use App\Models\Ticket;
use App\Models\User;
use App\Platform\Actions\RevalidateMediaContributionBadgeEligibilityAction;
use App\Support\Alerts\ClaimWithUnresolvedTicketsAlert;
use App\Support\Cache\CacheKey;
use Carbon\Carbon;
Expand Down Expand Up @@ -67,6 +68,8 @@ public function execute(Game $game, ?User $currentUser = null): AchievementSetCl
'finished_at' => $expiresAt,
]);

(new RevalidateMediaContributionBadgeEligibilityAction())->execute($currentUser);

Cache::forget(CacheKey::buildUserExpiringClaimsCacheKey($currentUser->username));

addArticleComment("Server", CommentableType::SetClaim, $game->id,
Expand Down
2 changes: 2 additions & 0 deletions app/Community/Actions/DropGameClaimAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Community\Enums\CommentableType;
use App\Models\AchievementSetClaim;
use App\Models\User;
use App\Platform\Actions\RevalidateMediaContributionBadgeEligibilityAction;
use App\Support\Cache\CacheKey;
use Carbon\Carbon;
use GuzzleHttp\Client;
Expand All @@ -25,6 +26,7 @@ public function execute(AchievementSetClaim $claim, User $actingUser): void
$claim->save();

Cache::forget(CacheKey::buildUserExpiringClaimsCacheKey($claim->user->username));
(new RevalidateMediaContributionBadgeEligibilityAction())->execute($claim->user);

// If the primary claim was dropped and there's a collaboration claim, promote it to primary.
$firstCollabClaim = ($claim->claim_type === ClaimType::Primary) ?
Expand Down
5 changes: 5 additions & 0 deletions app/Community/Actions/UpdateGameClaimAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Notifications\Achievement\SetAchievementsPublishedNotification;
use App\Notifications\Achievement\SetRevisionNotification;
use App\Platform\Actions\CheckForAchievementSetChangesAction;
use App\Platform\Actions\RevalidateMediaContributionBadgeEligibilityAction;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Auth;
Expand Down Expand Up @@ -97,6 +98,10 @@ public function execute(AchievementSetClaim $claim, array $newValues): void
if ($claim->isDirty()) {
$claim->save();

if ($claim->wasChanged('status')) {
(new RevalidateMediaContributionBadgeEligibilityAction())->execute($claim->user);
}

addArticleComment("Server", CommentableType::SetClaim, $claim->game_id, $auditMessage);
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/Community/Enums/AwardType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ enum AwardType: string

case Playtest = 'playtest';

case MediaContribution = 'media_contribution';

/**
* Returns all standard award type cases, excluding Event.
* Event is excluded because it's handled specially and shouldn't
Expand All @@ -41,6 +43,7 @@ public static function standardCases(): array
self::PatreonSupporter,
self::CertifiedLegend,
self::GameBeaten,
self::MediaContribution,
];
}

Expand Down Expand Up @@ -68,6 +71,7 @@ public function label(): string
self::GameBeaten => 'Game Beaten',
self::Event => 'Event',
self::Playtest => 'Playtest',
self::MediaContribution => 'Media Contribution',
};
}

Expand All @@ -87,6 +91,7 @@ public function toLegacyInteger(): int
self::GameBeaten => 8,
self::Event => 9,
self::Playtest => 10,
self::MediaContribution => 11,
};
}

Expand All @@ -105,6 +110,7 @@ public static function fromLegacyInteger(int $value): self
8 => self::GameBeaten,
9 => self::Event,
10 => self::Playtest,
11 => self::MediaContribution,
default => throw new InvalidArgumentException("Invalid legacy AwardType value: {$value}"),
};
}
Expand Down
4 changes: 4 additions & 0 deletions app/Filament/Resources/GameScreenshotModerationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Models\User;
use App\Platform\Actions\ApproveGameScreenshotAction;
use App\Platform\Actions\RejectGameScreenshotAction;
use App\Platform\Actions\RevalidateMediaContributionBadgeEligibilityAction;
use App\Platform\Enums\GameScreenshotRejectionReason;
use App\Platform\Enums\GameScreenshotStatus;
use App\Platform\Enums\ScreenshotType;
Expand Down Expand Up @@ -214,6 +215,9 @@ public static function table(Table $table): Table
->action(function (GameScreenshot $record) use ($user) {
try {
(new ApproveGameScreenshotAction())->execute($record, $user);
if ($record->capturedBy) {
(new RevalidateMediaContributionBadgeEligibilityAction())->execute($record->capturedBy);
}

Notification::make()
->success()
Expand Down
44 changes: 44 additions & 0 deletions app/Helpers/render/site-award.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use App\Community\Enums\AwardType;
use App\Models\Event;
use App\Models\EventAward;
use App\Models\GameScreenshot;
use App\Models\PlayerBadge;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -349,6 +351,19 @@ function RenderAward(
$imagepath = asset('/assets/images/badge/patreon.png');
$imgclass = 'goldimage';
$linkdest = route('patreon-supporter.index');
} elseif ($awardTypeEnum === AwardType::MediaContribution) {
$description = getMediaContributionDescription($ownerUsername, (int) $awardData);
echo avatar("mediaContributionAward", $awardData,
tooltip: "<div class='p-2 w-fit max-w-[320px] text-pretty text-menu-link flex flex-col gap-1'><p class='font-bold'>Media Contribution</p>{$description}<p class='italic'>{$awardDate}</p></div>",
iconUrl: asset("/assets/images/badge/mediaContrib-$awardData.png"),
iconSize: $imageSize,
iconClass: 'goldimage',
context: $ownerUsername,
altText: 'Media Contribution',
hasLink: false,
);

return;
Comment thread
Jamiras marked this conversation as resolved.
} elseif ($awardTypeEnum === AwardType::CertifiedLegend) {
$tooltip = 'Specially Awarded to a Certified RetroAchievements Legend';
$imagepath = asset('/assets/images/badge/legend.png');
Expand Down Expand Up @@ -440,6 +455,8 @@ function RenderAwardOrderTable(
$awardTitle = "Achievement Points Earned by Others";
} elseif ($awardTypeEnum === AwardType::PatreonSupporter) {
$awardTitle = "Patreon Supporter";
} elseif ($awardTypeEnum === AwardType::MediaContribution) {
$awardTitle = "Media Contribution";
} elseif ($awardTypeEnum === AwardType::CertifiedLegend) {
$awardTitle = "Certified Legend";
}
Expand Down Expand Up @@ -548,6 +565,33 @@ function getInitialSectionOrders(array $gameAwards, array $eventAwards, array $s
];
}

function getMediaContributionDescription(string $username, int $currentTier): string
{
$currentThreshold = PlayerBadge::getBadgeThreshold(AwardType::MediaContribution, $currentTier);
$nextThreshold = PlayerBadge::getBadgeThreshold(AwardType::MediaContribution, $currentTier + 1);

$formattedCurrent = number_format($currentThreshold);
$achievement = "<p class='text-balance'>Awarded for contributing <span class='font-semibold'>{$formattedCurrent}</span> approved screenshots to game galleries.</p>";

if ($nextThreshold === 0) {
return $achievement;
}

$user = User::whereName($username)->first();
$eligibleCount = $user
? GameScreenshot::query()->eligibleForMediaContributionBy($user)->count()
: 0;

$remaining = $nextThreshold - $eligibleCount;
if ($remaining <= 0) {
return $achievement;
}

$formattedRemaining = number_format($remaining);

return $achievement . "<p class='opacity-70'>{$formattedRemaining} more to next tier.</p>";
}

function generateManualMoveButtons(
int $awardCounter,
int $moveValue,
Expand Down
24 changes: 24 additions & 0 deletions app/Models/GameScreenshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Community\Enums\ClaimStatus;
use App\Platform\Enums\GameScreenshotRejectionReason;
use App\Platform\Enums\GameScreenshotStatus;
use App\Platform\Enums\ScreenshotType;
Expand Down Expand Up @@ -153,6 +154,29 @@ public function scopePrimary(Builder $query): Builder
return $query->where('is_primary', true);
}

/**
* Grab screenshots that currently count toward a user's media contribution badge.
*
* @param Builder<GameScreenshot> $query
* @return Builder<GameScreenshot>
*/
public function scopeEligibleForMediaContributionBy(Builder $query, User $user): Builder
{
return $query->approved()
->where('captured_by_user_id', $user->id)
->whereColumn('captured_by_user_id', '!=', 'reviewed_by_user_id')
->whereDoesntHave('game.achievements', function (Builder $query) use ($user) {
/** @var Builder<Achievement> $query */
$query->withTrashed()->where('user_id', $user->id);
})
->whereDoesntHave('game.achievementSetClaims', function (Builder $query) use ($user) {
/** @var Builder<AchievementSetClaim> $query */
$query
->where('user_id', $user->id)
->where('status', '!=', ClaimStatus::Dropped);
});
}

/**
* This is separate, but complementary, to GameScreenshotPolicy
* stuff because we actually need to filter a query by these values.
Expand Down
12 changes: 12 additions & 0 deletions app/Models/PlayerBadge.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,21 @@ class PlayerBadge extends BaseModel
50_000_000,
];

private const MEDIA_CONTRIBUTION_BOUNDARIES = [
10,
30,
100,
300,
1000,
3000,
];

private static function getThresholds(AwardType $awardType): ?array
{
return match ($awardType) {
AwardType::AchievementUnlocksYield => self::DEVELOPER_COUNT_BOUNDARIES,
AwardType::AchievementPointsYield => self::DEVELOPER_POINT_BOUNDARIES,
AwardType::MediaContribution => self::MEDIA_CONTRIBUTION_BOUNDARIES,
default => null,
};
}
Expand Down Expand Up @@ -199,6 +209,7 @@ public function isCountedAsSiteAward(): bool
AwardType::PatreonSupporter,
AwardType::CertifiedLegend,
AwardType::Playtest,
AwardType::MediaContribution,
], true);
}

Expand Down Expand Up @@ -286,6 +297,7 @@ public function scopeCanonicalForApiUser(Builder $query, int $userId): Builder
AwardType::AchievementPointsYield->value,
AwardType::PatreonSupporter->value,
AwardType::CertifiedLegend->value,
AwardType::MediaContribution->value,
];
$gameTypes = AwardType::gameValues();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace App\Platform\Actions;

use App\Community\Enums\AwardType;
use App\Models\GameScreenshot;
use App\Models\PlayerBadge;
use App\Models\User;
use App\Platform\Events\SiteBadgeAwarded;

class RevalidateMediaContributionBadgeEligibilityAction
{
public function execute(User $user): ?PlayerBadge
{
$eligibleCount = GameScreenshot::query()
->eligibleForMediaContributionBy($user)
->count();
$expectedTier = PlayerBadge::getNewBadgeTier(AwardType::MediaContribution, 0, $eligibleCount);

$existingBadges = $user->playerBadges()
->where('award_type', AwardType::MediaContribution)
->orderByDesc('award_key')
->get();

if ($expectedTier === null) {
// Media contribution badges represent current community screenshot credit.
// If later dev activity makes those screenshots ineligible, remove the badge.
if ($existingBadges->isNotEmpty()) {
PlayerBadge::whereKey($existingBadges->modelKeys())->delete();
}

return null;
}

$previousHighestBadge = $existingBadges->first();

// If only some screenshots stopped counting, keep the earned tier that still
// matches current eligibility and remove any higher tiers.
$tooHighIds = $existingBadges
->where('award_key', '>', $expectedTier)
->modelKeys();
if ($tooHighIds) {
PlayerBadge::whereKey($tooHighIds)->delete();
}

$expectedBadge = $existingBadges->first(
fn (PlayerBadge $badge) => $badge->award_key === $expectedTier && $badge->award_tier === 0,
);
if ($expectedBadge) {
return $expectedBadge;
}

$newBadge = AddSiteAward(
user: $user,
awardType: AwardType::MediaContribution,
data: $expectedTier,
displayOrder: $previousHighestBadge?->order_column ?? PlayerBadge::getNextDisplayOrder($user),
);

if ($previousHighestBadge === null || $newBadge->award_key > $previousHighestBadge->award_key) {
SiteBadgeAwarded::dispatch($newBadge);
}

return $newBadge;
}
}
Loading
Loading