Skip to content

Commit a7842dc

Browse files
committed
Add speedrun target
1 parent af9b626 commit a7842dc

File tree

5 files changed

+122
-63
lines changed

5 files changed

+122
-63
lines changed

src/lib/database/scoreboard.ts

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Entity, Player, ScoreboardObjective, ScoreNames, world } from '@minecra
22
import { expand } from 'lib/extensions/extend'
33
import { capitalize } from 'lib/util'
44

5-
type LushWayGameModes = 'anarchy'
6-
75
declare module '@minecraft/server' {
86
namespace ScoreNames {
97
type Stat =
@@ -16,11 +14,11 @@ declare module '@minecraft/server' {
1614
| 'kills'
1715
| 'deaths'
1816

19-
type OnlineTime = `${'total' | LushWayGameModes}OnlineTime`
17+
type GameModes = 'anarchy'
2018

21-
type Date = `${'lastSeen' | 'join'}Date`
19+
type OnlineTime = `${'total' | GameModes}OnlineTime`
2220

23-
type GameModes = 'anarchy'
21+
type Date = `${'lastSeen' | 'join'}Date`
2422

2523
type GameModesStat = `${GameModes}${Capitalize<Stat | Date>}`
2624

@@ -42,7 +40,6 @@ export const scoreboardDisplayNames: Record<import('@minecraft/server').ScoreNam
4240
deaths: 'Смертей',
4341
raid: 'Рейд-блок',
4442
totalOnlineTime: 'Онлайн всего',
45-
anarchyOnlineTime: 'Онлайн на анархии',
4643
blocksPlaced: 'Блоков поставлено',
4744
blocksBroken: 'Блоков сломано',
4845
fireworksLaunched: 'Фейрверков запущено',
@@ -52,6 +49,7 @@ export const scoreboardDisplayNames: Record<import('@minecraft/server').ScoreNam
5249
joinTimes: 'Всего входов на сервер',
5350
joinDate: 'Время первого входа',
5451

52+
anarchyOnlineTime: 'Онлайн на анархии',
5553
anarchyBlocksPlaced: 'Блоков поставлено',
5654
anarchyBlocksBroken: 'Блоков сломано',
5755
anarchyFireworksLaunched: 'Фейрверков запущено',
@@ -79,12 +77,12 @@ const statScores: Record<ScoreNames.Stat, string> = {
7977
deaths: '',
8078
}
8179

82-
const scorebaordStatNames = Object.keys(statScores)
80+
const scoreboardStatNames = Object.keys(statScores)
8381
export const scoreboardObjectiveNames = {
84-
stats: scorebaordStatNames,
85-
gameModeStats: scorebaordStatNames
82+
stats: scoreboardStatNames,
83+
gameModeStats: scoreboardStatNames
8684
.map(e => `anarchy${capitalize(e)}`)
87-
.concat(scorebaordStatNames) as ScoreNames.GameModesStat[],
85+
.concat(scoreboardStatNames) as ScoreNames.GameModesStat[],
8886
}
8987

9088
const untypedDisplayNames: Record<string, string> = scoreboardDisplayNames
@@ -107,6 +105,10 @@ Reflect.defineProperty(Player.prototype, 'scores', {
107105
})
108106

109107
export class ScoreboardDB {
108+
static defineName(id: string, name: string) {
109+
untypedDisplayNames[id] = name
110+
}
111+
110112
static getOrCreateProxyFor(playerId: string) {
111113
const scoreboardPlayer = players[playerId]
112114
if (scoreboardPlayer) {
@@ -141,13 +143,14 @@ export class ScoreboardDB {
141143
}
142144
}
143145

144-
static objectives: Record<string, ScoreboardObjective> = {}
146+
private static objectives = new Map<string, ScoreboardObjective>()
145147

146-
static objective(name: string, displayName = name) {
147-
if (this.objectives[name]) return this.objectives[name]
148-
149-
const objective = (this.objectives[name] =
150-
world.scoreboard.getObjective(name) ?? world.scoreboard.addObjective(name, displayName))
148+
static objective(id: string, displayName = id) {
149+
let objective = this.objectives.get(id)
150+
if (!objective) {
151+
objective = world.scoreboard.getObjective(id) ?? world.scoreboard.addObjective(id, displayName)
152+
this.objectives.set(id, objective)
153+
}
151154

152155
return objective
153156
}
@@ -163,28 +166,16 @@ export class ScoreboardDB {
163166
this.scoreboard = ScoreboardDB.objective(name, displayName)
164167
}
165168

166-
/**
167-
* @param {Entity | string} id
168-
* @param {number} value
169-
*/
170169
set(id: Entity | string, value: number) {
171170
if (typeof id !== 'string') id = id.id
172171
this.scoreboard.setScore(id, value)
173172
}
174173

175-
/**
176-
* @param {Entity | string} id
177-
* @param {number} value
178-
*/
179174
add(id: Entity | string, value: number) {
180175
if (typeof id !== 'string') id = id.id
181176
this.scoreboard.setScore(id, this.get(id) + value)
182177
}
183178

184-
/**
185-
* @param {Entity | string} id
186-
* @returns {number}
187-
*/
188179
get(id: Entity | string): number {
189180
if (typeof id !== 'string') id = id.id
190181
try {
@@ -193,21 +184,4 @@ export class ScoreboardDB {
193184
return 0
194185
}
195186
}
196-
197-
reset() {
198-
this.scoreboard.getParticipants().forEach(e => this.scoreboard.removeParticipant(e))
199-
}
200187
}
201-
202-
/*
203-
const objective = new ScoreboardDB('objectiveName', 'display name')
204-
205-
const score = objective.get(player)
206-
objective.set(player, 1)
207-
objective.add(player, 1)
208-
209-
objective.nameSet('custom name', 1)
210-
objective.nameGet('custom name')
211-
212-
objective.reset()
213-
*/

src/lib/rpg/leaderboard.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity, Player, ScoreboardObjective, system, world } from '@minecraft/server'
1+
import { Entity, Player, ScoreboardObjective, ScoreboardScoreInfo, system, world } from '@minecraft/server'
22
import { CustomEntityTypes } from 'lib/assets/custom-entity-types'
33
import { t } from 'lib/text'
44
import { Vec } from 'lib/vector'
@@ -12,6 +12,9 @@ export interface LeaderboardInfo {
1212
dimension: DimensionType
1313
}
1414

15+
const biggest = (a: ScoreboardScoreInfo, b: ScoreboardScoreInfo) => b.score - a.score
16+
const smallest = (a: ScoreboardScoreInfo, b: ScoreboardScoreInfo) => a.score - b.score
17+
1518
export class Leaderboard {
1619
static db = table<LeaderboardInfo>('leaderboard')
1720

@@ -20,11 +23,12 @@ export class Leaderboard {
2023
static entityId = CustomEntityTypes.FloatingText
2124

2225
static parseCustomScore(scoreboardId: string, score: number, convertToMetricNumbers = false) {
23-
// TODO Localize somehow?
2426
if (scoreboardId.endsWith('Time')) {
25-
return t.time`${score}`
27+
return t.timeHHMMSS(score * 2.5)
2628
} else if (scoreboardId.endsWith('Date')) {
2729
return new Date(score * 1000).format()
30+
} else if (scoreboardId.endsWith('SpeedRun')) {
31+
return t.timeHHMMSS(score)
2832
}
2933

3034
if (convertToMetricNumbers) {
@@ -92,10 +96,13 @@ export class Leaderboard {
9296
}
9397

9498
updateLeaderboard() {
99+
if (!this.entity.isValid) return
100+
95101
type Nullable<T> = T | null
96102

97103
const scoreboard = this.scoreboard
98104
const dname = scoreboard.displayName
105+
const id = this.scoreboard.id
99106
const name = dname.charAt(0).toUpperCase() + dname.slice(1)
100107
const style =
101108
(Leaderboard.styles[this.info.style] as Nullable<ValueOf<typeof Leaderboard.styles>>) ?? Leaderboard.styles.gray
@@ -104,11 +111,9 @@ export class Leaderboard {
104111
let leaderboard = ``
105112
for (const [i, scoreInfo] of scoreboard
106113
.getScores()
107-
108-
.sort((a, b) => b.score - a.score)
109-
110-
.filter((_, i) => i < 10)
114+
.sort(id.endsWith('SpeedRun') ? smallest : biggest)
111115
.entries()) {
116+
if (i >= 10) continue
112117
const { pos: t, nick: n, score: s } = style
113118

114119
const name =
@@ -117,10 +122,10 @@ export class Leaderboard {
117122

118123
leaderboard += `§ы§${t}#${i + 1}§r `
119124
leaderboard += ${n}${name}§r `
120-
leaderboard += ${s}${Leaderboard.parseCustomScore(this.scoreboard.id, scoreInfo.score, true)}§r\n`
125+
leaderboard += ${s}${Leaderboard.parseCustomScore(id, scoreInfo.score, true)}§r\n`
121126
}
122127

123-
if (this.entity.isValid) this.entity.nameTag = `§ы§l§${style.objName}${name}\n§ы§l${filler}§r\n${leaderboard}`
128+
this.entity.nameTag = `§ы§l§${style.objName}${name}\n§ы§l${filler}§r\n${leaderboard}`
124129
}
125130
}
126131

@@ -130,10 +135,8 @@ system.runInterval(
130135
if (typeof leaderboard === 'undefined') continue
131136
const info = Leaderboard.all.get(id)
132137

133-
if (info) {
134-
if (info.entity.isValid) {
135-
info.updateLeaderboard()
136-
}
138+
if (info?.entity) {
139+
if (info.entity.isValid) info.updateLeaderboard()
137140
} else {
138141
const entity = world[leaderboard.dimension]
139142
.getEntities({ location: leaderboard.location, tags: [Leaderboard.tag], type: Leaderboard.entityId })

src/lib/text.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,9 @@ function createNum(options: ColorOptions): (text: TSA, n: number, plurals: Plura
176176
})
177177
}
178178

179-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
180-
function createOverload<T extends (text: TSA, ...args: any[]) => Text, O extends (t: T, ...args: any[]) => Text>(
179+
function createOverload<T extends (text: TSA, ...args: any[]) => Text>(
181180
tsa: T,
182-
overload: O,
181+
overload: (t: T, ...args: any[]) => Text,
183182
) {
184183
return (...args: unknown[]) => {
185184
if (isTSA(args[0])) return tsa(args[0], ...args.slice(1))

src/modules/survival/import.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import '../places/import'
44
import '../quests/index'
55
import './stats'
66

7+
import './cleanup'
78
import './death-quest-and-gravestone'
89
import './menu'
910
import './random-teleport'
1011
import './realtime'
11-
import './sidebar'
12-
import './cleanup'
1312
import './recurring-events'
13+
import './sidebar'
14+
import './speedrun/target'
1415

1516
import type { VectorInDimension } from 'lib/utils/point'
1617

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Player } from '@minecraft/server'
2+
import { InventoryInterval, ScoreboardDB } from 'lib'
3+
import { form } from 'lib/form/new'
4+
import { t } from 'lib/text'
5+
import { BaseItem } from 'modules/places/base/base'
6+
7+
export enum SpeedRunTarget {
8+
GetBaseItem = 'GetBaseItem',
9+
FullNetheriteArmor = 'FullNetheriteArmor',
10+
AllQuests = 'AllQuests',
11+
AllAchievements = 'AllAchievements',
12+
MillionOfMoney = 'MillionOfMoney',
13+
}
14+
15+
const speedRunNames: Record<SpeedRunTarget, string> = {
16+
[SpeedRunTarget.AllAchievements]: 'Все достижения',
17+
[SpeedRunTarget.GetBaseItem]: 'Получить базу',
18+
[SpeedRunTarget.AllQuests]: 'Все задания',
19+
[SpeedRunTarget.MillionOfMoney]: '1.000.000 монет',
20+
[SpeedRunTarget.FullNetheriteArmor]: 'Полная незеритовая броня',
21+
}
22+
23+
for (const [target, name] of Object.entries(speedRunNames)) ScoreboardDB.defineName(target, name)
24+
25+
function finishSpeedRun(player: Player, target: SpeedRunTarget) {
26+
if (!player.database.speedrunTarget) return
27+
28+
player.database.speedrunTarget.finished = true
29+
const start = player.scores.anarchyOnlineTime * 2.5 // ms
30+
const took = Date.now() - start
31+
const previous = (player.scores as Record<string, number>)[target] ?? 0
32+
33+
const name = speedRunNames[target]
34+
if (previous === 0 || took < previous) {
35+
;(player.scores as Record<string, number>)[target] = took
36+
if (previous === 0) {
37+
player.success(t`Ваш первый рекорд ${name} поставлен! Это заняло ${t.timeHHMMSS(took)}`)
38+
} else {
39+
player.success(t`Вы побили ваш предыдущий рекорд ${name}! ${t.timeHHMMSS(took)} -> ${t.timeHHMMSS(previous)}`)
40+
}
41+
} else {
42+
player.fail(
43+
t`Вы не смогли побить ваш предыдущий рекорд ${name}! ${t.timeHHMMSS(took)} -> ${t.timeHHMMSS(previous)}`,
44+
)
45+
}
46+
}
47+
48+
function isSpeedRunningFor(player: Player, target: SpeedRunTarget) {
49+
return player.database.speedrunTarget?.target === target && !player.database.speedrunTarget.finished
50+
}
51+
52+
declare module '@minecraft/server' {
53+
interface PlayerDatabase {
54+
speedrunTarget?: {
55+
target: string
56+
finished: boolean
57+
}
58+
}
59+
}
60+
61+
const baseTypeId = BaseItem.itemStack.typeId
62+
InventoryInterval.slots.subscribe(({ player, slot }) => {
63+
if (!isSpeedRunningFor(player, SpeedRunTarget.GetBaseItem)) return
64+
if (slot.isValid && slot.typeId === baseTypeId && BaseItem.isItem(slot.getItem())) {
65+
finishSpeedRun(player, SpeedRunTarget.GetBaseItem)
66+
}
67+
})
68+
69+
const speedrunForm = form((f, player) => {
70+
f.title('Speedrun')
71+
for (const [target, name] of Object.entries(speedRunNames)) {
72+
f.button(name, () => {
73+
player.database.speedrunTarget = {
74+
target,
75+
finished: false,
76+
}
77+
player.info(t`Спидран '${name}' начат. Для сброса времени воспользуйтесь .wipe`)
78+
})
79+
}
80+
})
81+
82+
new Command('target').setAliases('speedrun').executes(speedrunForm.command)

0 commit comments

Comments
 (0)