[+] mai2: support adding rival

pull/50/head
Clansty 2024-08-05 20:37:18 +08:00
parent 94ba1f0b09
commit 473f4a4295
No known key found for this signature in database
GPG Key ID: 3A6BE8BAF2EDE134
14 changed files with 88 additions and 20 deletions

View File

@ -99,6 +99,7 @@ export interface GenericGameSummary {
lastVersion: string
ratingComposition: { [key: string]: any }
recent: GenericGamePlaylog[]
rival?: boolean
}
export interface MusicMeta {

View File

@ -24,6 +24,8 @@ export const EN_REF_USER = {
'UserHome.RankDetail.Title': 'Achievement Details',
'UserHome.RankDetail.Level': 'Level',
'UserHome.B50': 'B50',
'UserHome.AddRival': "Add to Rival",
'UserHome.RemoveRival': "Remove from Rival",
}
export const EN_REF_Welcome = {

View File

@ -33,6 +33,8 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.RankDetail.Title': '评分详细',
'UserHome.RankDetail.Level': "等级",
'UserHome.B50': "B50",
'UserHome.AddRival': "添加劲敌",
'UserHome.RemoveRival': "移除劲敌",
}
const zhWelcome: typeof EN_REF_Welcome = {

View File

@ -305,6 +305,8 @@ export const GAME = {
post(`/api/v2/game/${game}/export`),
import: (game: GameName, data: any): Promise<Record<string, any>> =>
post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }),
setRival: (game: GameName, rivalUserName: string, isAdd: boolean) =>
post(`/api/v2/game/${game}/set-rival`, { rivalUserName, isAdd }),
}
export const DATA = {

View File

@ -47,6 +47,7 @@
let allMusics: AllMusic
let showDetailRank = false
let isLoading = false
USER.isLoggedIn() && USER.me().then(u => me = u)
@ -90,6 +91,13 @@
})
}).catch((e) => error = e.message);
}).catch((e) => { error = e.message; console.error(e) } );
const setRival = (isAdd: boolean) => {
isLoading = true
GAME.setRival(game, username, isAdd).then(() => {
d!.user.rival = isAdd
}).catch(e => error = e.message).finally(() => isLoading = false)
}
</script>
<main id="user-home" class="content">
@ -98,6 +106,11 @@
<img use:pfp={d.user.aquaUser} alt="" class="pfp" on:error={pfpNotFound}>
<div class="name-box">
<h2>{d.user.name}</h2>
{#if typeof d.user.rival === 'boolean' && game === 'mai2'}
<a class="clickable" on:click={()=>setRival(!d.user.rival)}>
{d.user.rival ? t("UserHome.RemoveRival") : t("UserHome.AddRival")}
</a>
{/if}
{#if me && me.username === username}
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
<Icon icon="eos-icons:rotating-gear"/>
@ -267,7 +280,7 @@
</div>
{/if}
<StatusOverlays {error} loading={!d} />
<StatusOverlays {error} loading={!d || isLoading} />
</main>
<style lang="sass">

View File

@ -24,7 +24,7 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
@API("trend")
abstract suspend fun trend(@RP username: String): List<TrendOut>
@API("user-summary")
abstract suspend fun userSummary(@RP username: String): GenericGameSummary
abstract suspend fun userSummary(@RP username: String, @RP token: String?): GenericGameSummary
@API("recent")
suspend fun recent(@RP username: String): List<IGenericGamePlaylog> = us.cardByName(username) { card ->
@ -93,7 +93,7 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
}
}
fun genericUserSummary(card: Card, ratingComp: Map<String, String>): GenericGameSummary {
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null): GenericGameSummary {
// Summary values: total plays, player rating, server-wide ranking
// number of each rank, max combo, number of full combo, number of all perfect
val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found")
@ -138,7 +138,8 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
lastVersion = user.lastRomVersion,
ratingComposition = ratingComp,
recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(15).reversed(),
lastPlayedHost = us.userRepo.findByKeychip(user.lastClientId)?.username
lastPlayedHost = us.userRepo.findByKeychip(user.lastClientId)?.username,
rival = rival
)
}
}
}

View File

@ -45,7 +45,9 @@ data class GenericGameSummary(
val ratingComposition: Map<String, Any>,
val recent: List<IGenericGamePlaylog>
val recent: List<IGenericGamePlaylog>,
val rival: Boolean?
)
data class GenericRankingPlayer(

View File

@ -29,7 +29,7 @@ class Chusan(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS)
) }
override suspend fun userSummary(@RP username: Str) = us.cardByName(username) { card ->
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
// Summary values: total plays, player rating, server-wide ranking
// number of each rank, max combo, number of full combo, number of all perfect
val extra = userGeneralDataRepository.findByUser_Card_ExtId(card.extId)
@ -41,4 +41,4 @@ class Chusan(
genericUserSummary(card, ratingComposition)
}
}
}

View File

@ -17,7 +17,7 @@ class Maimai2(
override val playlogRepo: Mai2UserPlaylogRepo,
override val userDataRepo: Mai2UserDataRepo,
val repos: Mai2Repos,
): GameApiController<Mai2UserDetail>("mai2", Mai2UserDetail::class) {
) : GameApiController<Mai2UserDetail>("mai2", Mai2UserDetail::class) {
override suspend fun trend(@RP username: Str): List<TrendOut> = us.cardByName(username) { card ->
findTrend(playlogRepo.findByUserCardExtId(card.extId)
.map { TrendLog(it.playDate, it.afterRating) })
@ -25,11 +25,13 @@ class Maimai2(
// Only show > S rank
override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 }
override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS),
) }
override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy {
mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS),
)
}
override suspend fun userSummary(@RP username: Str) = us.cardByName(username) { card ->
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
val extra = repos.userGeneralData.findByUser_Card_ExtId(card.extId)
.associate { it.propertyKey to it.propertyValue }
@ -38,7 +40,20 @@ class Maimai2(
"best15" to (extra["recent_rating_new"] ?: "")
)
genericUserSummary(card, ratingComposition)
// if isLogin than boolean or null
// use the type to check user login in frontend
val isMyRival = token?.let { t ->
us.jwt.auth(t) { u ->
if (u.username == username) return@auth null
us.cardByName(u.username) { myCard ->
val user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival").map { it.propertyValue.split(',') }.orElse(emptyList()).map { it.long() }
myRival.contains(user.id)
}
}
}
genericUserSummary(card, ratingComposition, isMyRival)
}
@API("user-rating")
@ -100,4 +115,30 @@ class Maimai2(
}
mapOf("newName" to newNameFull)
}
@PostMapping("set-rival")
suspend fun setRival(@RP token: String, @RP rivalUserName: String, @RP isAdd: Boolean) = us.jwt.auth(token) { u ->
us.cardByName(u.username) { myCard ->
val rivalCard = us.cardByName(rivalUserName) { it }
val rivalUser = repos.userData.findByCardExtId(rivalCard.extId).orElse(null) ?: (404 - "User not found")
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival").orElse(null)
?: Mai2UserGeneralData().apply {
user = repos.userData.findByCardExtId(myCard.extId).orElse(null) ?: (404 - "User not found")
propertyKey = "favorite_rival"
}
val myRivalList = myRival.propertyValue.split(',').toMutableSet()
if (isAdd && myRivalList.size >= 4) {
(400 - "Rival list is full")
} else if (isAdd) {
myRivalList.add(rivalUser.id.toString())
} else {
myRivalList.remove(rivalUser.id.toString())
}
myRival.propertyValue = myRivalList.joinToString(",")
repos.userGeneralData.save(myRival)
}
SUCCESS
}
}

View File

@ -28,7 +28,7 @@ class Ongeki(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS)
) }
override suspend fun userSummary(username: String) = us.cardByName(username) { card ->
override suspend fun userSummary(username: String, token: String?) = us.cardByName(username) { card ->
val extra = userGeneralDataRepository.findByUser_Card_ExtId(card.extId)
.associate { it.propertyKey to it.propertyValue }
@ -40,4 +40,4 @@ class Ongeki(
genericUserSummary(card, ratingComposition)
}
}
}

View File

@ -25,10 +25,10 @@ class Wacca(
.map { TrendLog(it.userPlayDate.utc().isoDate(), it.afterRating) })
}
override suspend fun userSummary(@RP username: String) = us.cardByName(username) { card ->
override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card ->
// TODO: Rating composition
genericUserSummary(card, mapOf())
}
override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 }
}
}

View File

@ -122,11 +122,13 @@ class Maimai2ServletController(
val getUserRivalData = UserReqHandler { req, userId ->
val rivalId = parsing { (req["rivalId"] as Number).toLong() }
// rivalId should store and fetch with the id column of table rather than card_ext_id
// or user will be able to get others' ext_id by setting them as rival
mapOf(
"userId" to userId,
"userRivalData" to mapOf(
"rivalId" to rivalId,
"rivalName" to (repos.userData.findByCardExtId(rivalId)()?.userName ?: "")
"rivalName" to (repos.userData.findById(rivalId)()?.userName ?: "")
)
)
}

View File

@ -38,7 +38,7 @@ public class GetUserRivalMusicHandler implements BaseHandler {
long userId = ((Number) request.get("userId")).longValue();
long rivalId = ((Number) request.get("rivalId")).intValue();
List<Mai2UserMusicDetail> details = userMusicDetailRepository.findByUser_Card_ExtId(rivalId);
List<Mai2UserMusicDetail> details = userMusicDetailRepository.findByUserId(rivalId);
List<UserRivalMusic> userRivalMusicList = new LinkedList<>();
Map<Integer, UserRivalMusic> userRivalMusicMap = new HashMap<>();
for (Mai2UserMusicDetail detail : details) {

View File

@ -96,6 +96,8 @@ interface Mai2UserMusicDetailRepo : Mai2UserLinked<Mai2UserMusicDetail> {
fun findByUserAndMusicIdAndLevel(user: Mai2UserDetail, musicId: Int, level: Int): Optional<Mai2UserMusicDetail>
fun findByUser_Card_ExtIdAndMusicIdIn(userId: Long, musicId: List<Int>): List<Mai2UserMusicDetail>
fun findByUserId(userId: Long): List<Mai2UserMusicDetail>
}
interface Mai2UserOptionRepo : Mai2UserLinked<Mai2UserOption>