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
4 changes: 2 additions & 2 deletions app/Http/Controllers/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -891,11 +891,11 @@ private function getExtra($page, array $options, int $perPage = 10, int $offset
break;
case 'scoresRecent':
$transformer = new ScoreTransformer();
$includes = ScoreTransformer::USER_PROFILE_INCLUDES;
$includes = [...ScoreTransformer::USER_PROFILE_INCLUDES, 'metadata'];
$query = $this->user->soloScores()
->recent($this->mode, $options['includeFails'] ?? false)
->reorderBy('ended_at', 'desc')
->with(ScoreTransformer::USER_PROFILE_INCLUDES_PRELOAD);
->with([...ScoreTransformer::USER_PROFILE_INCLUDES_PRELOAD, 'metadata']);
$userRelationColumn = 'user';
break;
}
Expand Down
19 changes: 19 additions & 0 deletions app/Models/ScoreMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Models;

/**
* @property int $rank_delta
* @property int $pp_delta
*/
class ScoreMetadata extends Model
{
public $incrementing = false;

protected $primaryKey = 'score_id';
}
7 changes: 7 additions & 0 deletions app/Models/Solo/Score.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use App\Models\Model;
use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink;
use App\Models\Score as LegacyScore;
use App\Models\ScoreMetadata;
use App\Models\ScoreToken;
use App\Models\Traits;
use App\Models\User;
Expand Down Expand Up @@ -164,6 +165,11 @@ public function legacyScore(): MorphTo
return $this->morphTo(__FUNCTION__, 'legacy_score_type', 'legacy_best_id');
}

public function metadata()
{
return $this->hasOne(ScoreMetadata::class, 'score_id');
}

public function user()
{
return $this->belongsTo(User::class, 'user_id');
Expand Down Expand Up @@ -286,6 +292,7 @@ public function getAttribute($key)
'build',
'legacyReplayViewCount',
'legacyScore',
'metadata',
'performance',
'processHistory',
'reportedIn',
Expand Down
14 changes: 14 additions & 0 deletions app/Transformers/ScoreTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class ScoreTransformer extends TransformerAbstract
// Only for MultiplayerScoreLink
'position',
'scores_around',

'metadata',
];

protected array $defaultIncludes = [
Expand Down Expand Up @@ -198,6 +200,18 @@ public function includeMatch(LegacyMatch\Score $score)
]);
}

public function includeMetadata(SoloScore $score)
{
if (!$score->metadata) {
return null;
}

return $this->primitive([
'rank_delta' => $score->metadata->rank_delta,
'pp_delta' => $score->metadata->pp_delta,
]);
}

public function includePosition(MultiplayerScoreLink $scoreLink)
{
return $this->primitive($scoreLink->position());
Expand Down
29 changes: 29 additions & 0 deletions database/migrations/2026_02_23_214600_score_metadata_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('score_metadata', function (Blueprint $table) {
$table->unsignedInteger('score_id');
$table->integer('rank_delta');
$table->integer('pp_delta');
$table->timestamps();
$table->unique(['score_id']);
});
}

public function down(): void
{
Schema::dropIfExists('score_metadata');
}
};
33 changes: 33 additions & 0 deletions resources/css/bem/play-detail.less
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@

@media @desktop {
.center-content();
flex-direction: column;
position: relative;
right: auto;
bottom: auto;
Expand Down Expand Up @@ -219,6 +220,38 @@
}
}

&__pp-delta {
font-size: @font-size--normal;

&--up {
color: hsl(var(--hsl-lime-1));
}

&--down {
color: hsl(var(--hsl-red-1));
}
}

&__rank-delta {
font-size: @font-size--normal;

&::before {
.fas();
.fa-fw();
content: var(--rank-delta-icon);
}

&--up {
color: hsl(var(--hsl-lime-1));
--rank-delta-icon: @fa-var-arrow-up;
}

&--down {
color: hsl(var(--hsl-red-1));
--rank-delta-icon: @fa-var-arrow-down;
}
}

&__pp-unit {
font-size: @font-size--normal;
color: @osu-colour-l3;
Expand Down
6 changes: 6 additions & 0 deletions resources/js/interfaces/score-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ type ScoreJsonAttributes = {
user_id: number;
} & (ScoreJsonAttributesLegacyMatch | ScoreJsonAttributesSolo | ScoreJsonAttributesMultiplayer);

interface ScoreMetadata {
pp_delta: number;
rank_delta: number;
}

export interface ScoreJsonDefaultIncludes {
current_user_attributes: {
pin?: ScoreCurrentUserPinJson;
Expand All @@ -98,6 +103,7 @@ export interface ScoreJsonAvailableIncludes {
beatmap: BeatmapExtendedJson;
beatmapset: BeatmapsetJson;
match: Match;
metadata: ScoreMetadata | null;
rank_country: number;
rank_global: number;
user: UserJson;
Expand Down
24 changes: 20 additions & 4 deletions resources/js/profile-page/play-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,26 @@ export default class PlayDetail extends React.PureComponent<Props, State> {

<div className={`${bn}__pp`}>
{shouldShowPp(beatmap) ? (
<PpValue
score={score}
suffix={<span className={`${bn}__pp-unit`}>pp</span>}
/>
<>
<PpValue
score={score}
suffix={<span className={`${bn}__pp-unit`}>pp</span>}
/>
{score.metadata != null && (
<>
{score.metadata.pp_delta !== 0 && (
<div className={classWithModifiers(`${bn}__pp-delta`, score.metadata.pp_delta > 0 ? 'up' : 'down')}>
{score.metadata.pp_delta > 0 ? '+' : ''}{formatNumber(Math.round(score.metadata.pp_delta))}pp
</div>
)}
{score.metadata.rank_delta !== 0 && (
<div className={classWithModifiers(`${bn}__rank-delta`, score.metadata.rank_delta < 0 ? 'up' : 'down')}>
{formatNumber(Math.abs(score.metadata.rank_delta))}
</div>
)}
</>
)}
</>
) : (
<span title={trans('users.show.extra.top_ranks.not_ranked')}>
{(beatmap.status === 'loved') ? (
Expand Down