diff --git a/entities/player.json b/entities/player.json index cd024c85..9502b358 100644 --- a/entities/player.json +++ b/entities/player.json @@ -8,10 +8,23 @@ "is_experimental": false, "properties": { // used in RP/entities/player.json - "sm:minimap": { + "lw:minimap": { "type": "bool", "default": false, "client_sync": true + }, + + "lw:newbie": { + "type": "bool", + "default": false, + "client_sync": false + }, + + "lw:minigame_team": { + "type": "enum", + "values": ["no", "green", "red", "blue", "yellow"], + "default": "no", + "client_sync": false } } }, @@ -40,13 +53,6 @@ "visible": false, "display_on_screen_animation": true }, - { - "effect": "saturation", - "duration": 2, - "amplifier": 255, - "visible": false, - "display_on_screen_animation": false - }, { "effect": "fire_resistance", "duration": 2, @@ -114,24 +120,6 @@ } }, "components": { - "minecraft:damage_sensor": { - "triggers": [ - { - "on_damage": { - "filters": { - "all_of": [ - { - "test": "has_tag", - "subject": "self", - "value": "protected" - } - ] - } - }, - "deals_damage": false - } - ] - }, "minecraft:type_family": { "family": ["player"] }, @@ -259,6 +247,74 @@ "event": "player:no_container_open" } ] + }, + "minecraft:damage_sensor": { + "triggers": [ + { + "on_damage": { + "filters": { + "all_of": [ + { + "test": "has_tag", + "subject": "self", + "value": "protected" + } + ] + } + }, + "deals_damage": false + }, + + { + "on_damage": { + "filters": { + "all_of": [ + { "all_of": [{ "test": "bool_property", "domain": "lw:newbie" }] }, + { + "any_of": [ + { "test": "is_family", "subject": "other", "value": "player" }, + { "test": "has_damage", "value": "fire_tick" } + ] + } + ] + } + }, + "deals_damage": false //if all of these filters evaluate to true in the current attack interaction, the target will not be hurt. + }, + { + "on_damage": { + "filters": { + "any_of": [ + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "green" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "green" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "blue" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "blue" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "red" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "red" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "yellow" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "yellow" } + ] + } + ] + } + }, + "deals_damage": false //if any of these filters evaluate to true in the current attack interaction, the target will not be hurt. + } + ] } }, "events": { diff --git a/src/lib/assets/player-json.ts b/src/lib/assets/player-json.ts new file mode 100644 index 00000000..4fe1ac34 --- /dev/null +++ b/src/lib/assets/player-json.ts @@ -0,0 +1,374 @@ +// prettier-ignore +/* eslint-disable */ +// This file is autogenerated by tools/build.js. +// Do not modify manually. + +export const playerJson = { + "format_version": "1.17.10", + "minecraft:entity": { + "description": { + "identifier": "minecraft:player", + "is_spawnable": false, + "is_summonable": true, + "is_experimental": false, + "properties": { + // used in RP/entities/player.json + "lw:minimap": { + "type": "bool", + "default": false, + "client_sync": true + }, + + "lw:newbie": { + "type": "bool", + "default": false, + "client_sync": false + }, + + "lw:minigame_team": { + "type": "enum", + "values": ["no", "green", "red", "blue", "yellow"], + "default": "no", + "client_sync": false + } + } + }, + "component_groups": { + "spawn": { + "minecraft:spell_effects": { + "add_effects": [ + { + "effect": "instant_health", + "duration": 2, + "amplifier": 255, + "visible": false, + "display_on_screen_animation": false + }, + { + "effect": "weakness", + "duration": 2, + "amplifier": 255, + "visible": false, + "display_on_screen_animation": false + }, + { + "effect": "resistance", + "duration": 2, + "amplifier": 255, + "visible": false, + "display_on_screen_animation": true + }, + { + "effect": "fire_resistance", + "duration": 2, + "amplifier": 255, + "visible": false, + "display_on_screen_animation": false + } + ] + } + }, + "warn": { + "minecraft:spell_effects": { + "add_effects": [ + { + "effect": "resistance", + "duration": 2, + "amplifier": 1, + "visible": false, + "display_on_screen_animation": true + } + ] + } + }, + "not_in_spawn": { + "minecraft:spell_effects": { + "remove_effects": ["instant_health", "weakness", "resistance", "saturation", "fire_resistance"] + } + }, + "kick": { + "minecraft:instant_despawn": {}, + "minecraft:explode": {} + }, + "minecraft:add_bad_omen": { + "minecraft:spell_effects": { + "add_effects": [ + { + "effect": "bad_omen", + "duration": 6000, + "display_on_screen_animation": true + } + ] + }, + "minecraft:timer": { + "time": [0.0, 0.0], + "looping": false, + "time_down_event": { + "event": "minecraft:clear_add_bad_omen", + "target": "self" + } + } + }, + "minecraft:clear_bad_omen_spell_effect": { + "minecraft:spell_effects": {} + }, + "minecraft:raid_trigger": { + "minecraft:raid_trigger": { + "triggered_event": { + "event": "minecraft:remove_raid_trigger", + "target": "self" + } + }, + "minecraft:spell_effects": { + "remove_effects": "bad_omen" + } + } + }, + "components": { + "minecraft:type_family": { + "family": ["player"] + }, + "minecraft:is_hidden_when_invisible": {}, + "minecraft:loot": { + "table": "loot_tables/empty.json" + }, + "minecraft:collision_box": { + "width": 0.6, + "height": 1.8 + }, + "minecraft:can_climb": {}, + "minecraft:movement": { + "value": 0.1 + }, + "minecraft:hurt_on_condition": { + "damage_conditions": [ + { + "filters": { + "test": "in_lava", + "subject": "self", + "operator": "==", + "value": true + }, + "cause": "lava", + "damage_per_tick": 4 + } + ] + }, + "minecraft:attack": { + "damage": 1 + }, + "minecraft:player.saturation": { + "value": 20 + }, + "minecraft:player.exhaustion": { + "value": 0, + "max": 4 + }, + "minecraft:player.level": { + "value": 0, + "max": 24791 + }, + "minecraft:player.experience": { + "value": 0, + "max": 1 + }, + "minecraft:breathable": { + "total_supply": 15, + "suffocate_time": -1, + "inhale_time": 3.75, + "generates_bubbles": false + }, + "minecraft:nameable": { + "always_show": true, + "allow_name_tag_renaming": false + }, + "minecraft:physics": {}, + "minecraft:pushable": { + "is_pushable": false, + "is_pushable_by_piston": true + }, + "minecraft:insomnia": { + "days_until_insomnia": 3 + }, + "minecraft:rideable": { + "seat_count": 2, + "family_types": ["parrot_tame"], + "pull_in_entities": true, + "seats": [ + { + "position": [0.4, -0.2, -0.1], + "min_rider_count": 0, + "max_rider_count": 0, + "lock_rider_rotation": 0 + }, + { + "position": [-0.4, -0.2, -0.1], + "min_rider_count": 1, + "max_rider_count": 2, + "lock_rider_rotation": 0 + } + ] + }, + "minecraft:conditional_bandwidth_optimization": {}, + "minecraft:block_climber": {}, + "minecraft:environment_sensor": { + "triggers": [ + { + "filters": { + "all_of": [ + { + "test": "has_mob_effect", + "subject": "self", + "value": "bad_omen" + }, + { + "test": "is_in_village", + "subject": "self", + "value": true + } + ] + }, + "event": "minecraft:trigger_raid" + }, + + { + "filters": { + "all_of": [ + { + "test": "has_container_open" + } + ] + }, + "event": "player:has_container_open" + }, + { + "filters": { + "none_of": [ + { + "test": "has_container_open" + } + ] + }, + "event": "player:no_container_open" + } + ] + }, + "minecraft:damage_sensor": { + "triggers": [ + { + "on_damage": { + "filters": { + "all_of": [ + { + "test": "has_tag", + "subject": "self", + "value": "protected" + } + ] + } + }, + "deals_damage": false + }, + + { + "on_damage": { + "filters": { + "all_of": [ + { "all_of": [{ "test": "bool_property", "domain": "lw:newbie" }] }, + { + "any_of": [ + { "test": "is_family", "subject": "other", "value": "player" }, + { "test": "has_damage", "value": "fire_tick" } + ] + } + ] + } + }, + "deals_damage": false //if all of these filters evaluate to true in the current attack interaction, the target will not be hurt. + }, + { + "on_damage": { + "filters": { + "any_of": [ + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "green" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "green" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "blue" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "blue" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "red" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "red" } + ] + }, + { + "all_of": [ + { "test": "enum_property", "domain": "lw:can_climb", "value": "yellow" }, + { "test": "enum_property", "domain": "lw:can_climb", "value": "yellow" } + ] + } + ] + } + }, + "deals_damage": false //if any of these filters evaluate to true in the current attack interaction, the target will not be hurt. + } + ] + } + }, + "events": { + "player:no_container_open": {}, + "player:has_container_open": {}, + "player:warn": { + "add": { + "component_groups": ["warn"] + } + }, + "player:spawn": { + "add": { + "component_groups": ["spawn"] + } + }, + "player:not_in_spawn": { + "add": { + "component_groups": ["notspawn"] + } + }, + "player:portal": {}, + "player:kick": { + "add": { + "component_groups": ["kick"] + } + }, + "minecraft:gain_bad_omen": { + "add": { + "component_groups": ["minecraft:add_bad_omen"] + } + }, + "minecraft:clear_add_bad_omen": { + "remove": { + "component_groups": ["minecraft:add_bad_omen"] + }, + "add": { + "component_groups": ["minecraft:clear_bad_omen_spell_effect"] + } + }, + "minecraft:trigger_raid": { + "add": { + "component_groups": ["minecraft:raid_trigger"] + } + }, + "minecraft:remove_raid_trigger": { + "remove": { + "component_groups": ["minecraft:raid_trigger"] + } + } + } + } +} as const \ No newline at end of file diff --git a/src/lib/assets/player-properties.ts b/src/lib/assets/player-properties.ts new file mode 100644 index 00000000..3fee15a8 --- /dev/null +++ b/src/lib/assets/player-properties.ts @@ -0,0 +1,5 @@ +import { playerJson } from './player-json' + +export const PlayerProperties = Object.fromEntries( + Object.keys(playerJson['minecraft:entity'].description.properties).map(e => [e, e]), +) diff --git a/src/lib/assets/text.ts b/src/lib/assets/text.ts index cfe69bcb..683c030b 100644 --- a/src/lib/assets/text.ts +++ b/src/lib/assets/text.ts @@ -53,3 +53,5 @@ export const text: Record string> = { §fФейрверков запущено: §9${fla} §8-----------------------------`, } + +export const developerKnow = `Разработчики уже оповещены о проблеме и работают над ее исправлением.` diff --git a/src/lib/command/utils.ts b/src/lib/command/utils.ts index fee0917a..d79d1df8 100644 --- a/src/lib/command/utils.ts +++ b/src/lib/command/utils.ts @@ -5,6 +5,7 @@ import { t } from 'lib/text' import { inaccurateSearch } from '../search' import { LiteralArgumentType, LocationArgumentType } from './argument-types' import { CommandContext } from './context' +import { developerKnow } from 'lib/assets/text' export function parseCommand(message: string, prefixSize = 1) { const command = message.slice(prefixSize).trim() @@ -198,9 +199,7 @@ export function sendCallback( ...argsToReturn, ) as Promise | void) } catch (e) { - event.sender.warn( - 'При выполнении команды произошла ошибка. Разработчики уже оповещены о проблеме и работают над ее исправлением.', - ) + event.sender.warn(`При выполнении команды произошла ошибка. ${developerKnow}`) console.error(e) } })() diff --git a/src/lib/form/array.ts b/src/lib/form/array.ts index ed9be921..7d397bd5 100644 --- a/src/lib/form/array.ts +++ b/src/lib/form/array.ts @@ -30,7 +30,11 @@ export declare namespace ArrayForm { } } -export class ArrayForm = SettingsConfigParsed> { +export class ArrayForm< + T, + C extends SettingsConfig = SettingsConfig, + F extends SettingsConfigParsed = SettingsConfigParsed, +> { private config: ArrayForm.Options = { filters: { [SETTINGS_GROUP_NAME]: 'Пустые фильтры', @@ -39,7 +43,7 @@ export class ArrayForm() + onFirstTimeSpawn = new EventSignal() + eventsDefaultSubscribers = { time: this.onMoveAfterJoin.subscribe(({ player, firstJoin }) => { if (!firstJoin) player.tell(`${timeNow()}, ${player.name}!\n§r§3Время §b• §3${shortTime()}`) @@ -35,6 +37,7 @@ class JoinBuilder { if (!initialSpawn) return if (player.scores.joinDate === 0) player.scores.joinDate = ~~(Date.now() / 1000) this.setPlayerJoinPosition(player) + EventSignal.emit(this.onFirstTimeSpawn, player) }), } diff --git a/src/lib/roles.ts b/src/lib/roles.ts index 34189d04..97821d1b 100644 --- a/src/lib/roles.ts +++ b/src/lib/roles.ts @@ -124,9 +124,12 @@ export function getRoleAndName( nameSpacing?: boolean } = {}, ) { + const id = playerID instanceof Player ? playerID.id : playerID + const db = Player.database[id] let display = '' if (useRole) { + if (db.survival.newbie) display += '§bНовичок ' const role = getRole(playerID) if (role !== 'member') { display += ROLES[role] diff --git a/src/lib/rpg/boss.ts b/src/lib/rpg/boss.ts index 50435e95..ceff5a9f 100644 --- a/src/lib/rpg/boss.ts +++ b/src/lib/rpg/boss.ts @@ -43,6 +43,12 @@ export class Boss { })) } + static isBoss(entityOrId: Entity | string) { + const id = entityOrId instanceof Entity ? entityOrId.id : entityOrId + + return this.all.some(e => e.entity?.id === id) + } + entity: Entity | undefined region?: Region diff --git a/src/lib/rpg/menu.ts b/src/lib/rpg/menu.ts index 782a091e..bac4401c 100644 --- a/src/lib/rpg/menu.ts +++ b/src/lib/rpg/menu.ts @@ -1,7 +1,8 @@ -import { ContainerSlot, EquipmentSlot, ItemLockMode, ItemStack, ItemTypes, Player } from '@minecraft/server' +import { ContainerSlot, EquipmentSlot, ItemLockMode, ItemStack, ItemTypes, Player, world } from '@minecraft/server' import { InventoryInterval } from 'lib/action' import { CustomItems } from 'lib/assets/config' import { MessageForm } from 'lib/form/message' +import { util } from 'lib/util' import { Vector } from 'lib/vector' import { WeakPlayerMap, WeakPlayerSet } from 'lib/weak-player-storage' import { ActionForm } from '../form/action' @@ -21,28 +22,20 @@ export class Menu { return item.clone() } - static item = this.createItem() + static itemStack = this.createItem() - static command - - static give + static item = createPublicGiveItemCommand('menu', this.itemStack, another => this.isMenu(another)) static { - const { give, command } = createPublicGiveItemCommand('menu', this.item, another => this.isMenu(another)) - - this.give = give - - this.command = command - - // world.afterEvents.itemUse.subscribe(({ source: player, itemStack }) => { - // if (!(player instanceof Player)) return - // if (!this.isMenu(itemStack)) return - - // util.catch(() => { - // const menu = this.open(player) - // if (menu) menu.show(player) - // }) - // }) + world.afterEvents.itemUse.subscribe(({ source: player, itemStack }) => { + if (!(player instanceof Player)) return + if (!this.isMenu(itemStack)) return + + util.catch(() => { + const menu = this.open(player) + if (menu) menu.show(player) + }) + }) } static open(player: Player): ActionForm | false { @@ -56,7 +49,7 @@ export class Menu { } static isMenu(slot: Pick) { - return this.isCompass(slot) || slot.typeId === this.item.typeId + return this.isCompass(slot) || slot.typeId === this.itemStack.typeId } } @@ -97,9 +90,9 @@ export class Compass { if (isMenu) this.updateCompassInSlot(slot, player) if ((isMainhand && isMenu) || isOffhandMenu) { - if (!this.forceHide.has(player)) player.setProperty('sm:minimap', true) + if (!this.forceHide.has(player)) player.setProperty('lw:minimap', true) } else { - if (isMainhand) player.setProperty('sm:minimap', false) + if (isMainhand) player.setProperty('lw:minimap', false) return } @@ -112,7 +105,7 @@ export class Compass { const target = this.players.get(player) if (!target) { - if (Menu.isCompass(slot)) slot.setItem(Menu.item) + if (Menu.isCompass(slot)) slot.setItem(Menu.itemStack) return } diff --git a/src/lib/rpg/npc.ts b/src/lib/rpg/npc.ts index 71016e38..8a633969 100644 --- a/src/lib/rpg/npc.ts +++ b/src/lib/rpg/npc.ts @@ -2,6 +2,7 @@ import { Entity, PlayerInteractWithEntityBeforeEvent, World, system, world } fro import { MinecraftEntityTypes } from '@minecraft/vanilla-data' import { Temporary, Vector, isChunkUnloaded } from 'lib' +import { developerKnow } from 'lib/assets/text' import { location } from 'lib/location' import { t } from 'lib/text' import { Place } from './place' @@ -97,7 +98,7 @@ export class Npc { return event.player.fail(`§f${npcName}: §cЯ не могу с вами говорить. Приходите позже.`) } } catch (e) { - event.player.fail('Не удалось открыть диалог. Сообщите об этом администрации.') + event.player.warn(`Не удалось открыть диалог. ${developerKnow}`) console.error(e) } }) diff --git a/src/lib/text.ts b/src/lib/text.ts index 055ec5ef..a4ccd1a1 100644 --- a/src/lib/text.ts +++ b/src/lib/text.ts @@ -10,14 +10,16 @@ export type MaybeRawText = string | RawText type TSA = TemplateStringsArray type Fn = (text: TSA, ...args: unknown[]) => Text -type OptionsModifiers = 'error' | 'header' +type OptionsModifiers = 'error' | 'warn' | 'header' interface MultiStatic { raw: (text: TSA, ...units: (string | RawText | RawMessage)[]) => RawText roles: (text: TSA, ...players: Player[]) => Text badge: (text: TSA, n: number) => Text num: (text: TSA, n: number, plurals: Plurals) => Text - time: (text: TSA, time: number) => Text - ttime: (time: number) => Text + + time(time: number): Text + time(text: TSA, time: number): Text + options: (options: ColorizingOptions) => Multi } type Multi = MultiStatic & Record> @@ -36,13 +38,13 @@ function createGroup(options: ColorizingOptions = {}, modifier = false) { t.roles = createSingle({ roles: true, ...options }) t.badge = createBadge(options) t.num = createNum(options) - t.time = createTime(options) - t.ttime = time => t.time`${time}` + t.time = createOverload(createTime(options), (t, time: number) => t`${time}`) t.raw = createRaw(options) if (!modifier) { t.header = createGroup({ text: '§6', ...options, unit: '§f§l' }, true) t.error = createGroup({ text: '§c', unit: '§f', ...options }, true) + t.warn = createGroup({ text: '§e', unit: '§f', ...options }, true) } t.options = options => createGroup(options) return t @@ -108,6 +110,20 @@ function createNum(options: ColorizingOptions): (text: TSA, n: number, plurals: }) } +function createOverload Text, O extends (t: T, ...args: any[]) => Text>( + tsa: T, + overload: O, +) { + return (...args: unknown[]) => { + if (isTSA(args[0])) return tsa(args[0], ...args.slice(1)) + else return overload(tsa, ...args) + } +} + +function isTSA(arg: unknown): arg is TemplateStringsArray { + return Array.isArray(arg) && 'raw' in arg +} + function createTime(options: ColorizingOptions): (text: TSA, time: number) => Text { return createSingle(options, (text, unit) => { if (typeof unit !== 'number') return text + textUnitColorize(unit, options) diff --git a/src/lib/util.ts b/src/lib/util.ts index 49e2eab2..bed79645 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -54,7 +54,7 @@ export const util = { * @param minLength - Minimal items count to paginate. If array has less then this count, array is returned. Default * is `perPage` */ - paginate(array: T[], perPage = 10, startPage = 1, minLength: number = perPage) { + paginate(array: readonly T[], perPage = 10, startPage = 1, minLength: number = perPage) { if (array.length <= minLength) return { array, canGoNext: false, canGoBack: false, maxPages: 1, page: 1 } const maxPages = Math.ceil(array.length / perPage) diff --git a/src/modules/indicator/health.ts b/src/modules/indicator/health.ts index 8fecd7b4..78c8a07a 100644 --- a/src/modules/indicator/health.ts +++ b/src/modules/indicator/health.ts @@ -1,7 +1,7 @@ import { Entity, Player, system, world } from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { ms, Vector } from 'lib' +import { Boss, ms, Vector } from 'lib' import { CustomEntityTypes } from 'lib/assets/config' import { ClosingChatSet } from 'lib/extensions/player' @@ -9,8 +9,6 @@ import { isNotPlaying } from 'lib/game-utils' import { NOT_MOB_ENTITIES } from 'lib/region/config' import { PlayerNameTagModifiers, setNameTag } from 'modules/indicator/playerNameTag' -// TODO Rewrite in class - interface BaseHurtEntity { hurtEntity: Entity damage: number @@ -27,9 +25,9 @@ interface SingleHurtEntity { } class HealthIndicator { - hurtEntities = new Map() + private hurtEntities = new Map() - indicatorTag = 'HEALTH_INDICATOR' + private indicatorTag = 'HEALTH_INDICATOR' /** Entities that have nameTag "always_show": true and dont have boss_bar */ alwaysShows: string[] = [MinecraftEntityTypes.Player] @@ -52,6 +50,7 @@ class HealthIndicator { !(event.hurtEntity.matches({ families: this.allowedFamilies }) || event.hurtEntity instanceof Player) ) return + if (Boss.isBoss(event.hurtEntity)) return // Not trigget by close chat if (ClosingChatSet.has(event.hurtEntity.id)) return @@ -115,7 +114,7 @@ class HealthIndicator { } /** Gets damage indicator name depending on entity's currnet heart and damage applied */ - getBar(entity: Entity, hp = entity.getComponent('health')): string { + private getBar(entity: Entity, hp = entity.getComponent('health')): string { if (entity instanceof Player && isNotPlaying(entity)) return '' const hurtEntity = this.hurtEntities.get(entity.id) @@ -146,9 +145,9 @@ class HealthIndicator { return bar } - barSymbol = '|' + private barSymbol = '|' - updateIndicator({ entity, damage = 0 }: { entity: Entity; damage?: number }) { + private updateIndicator({ entity, damage = 0 }: { entity: Entity; damage?: number }) { if (!entity.isValid()) return const info = this.createHurtEntityRecord(entity) @@ -191,20 +190,20 @@ class HealthIndicator { return info } - spawnIndicator(entity: Entity) { + private spawnIndicator(entity: Entity) { const indicator = entity.dimension.spawnEntity(CustomEntityTypes.FloatingText, entity.getHeadLocation()) indicator.addTag(this.indicatorTag) return indicator } - getIndicators() { + private getIndicators() { return world.overworld.getEntities({ type: CustomEntityTypes.FloatingText, tags: [this.indicatorTag], }) } - getIDs(entities: Entity[]) { + private getIDs(entities: Entity[]) { return entities.map(entity => { let id try { diff --git a/src/modules/loader.ts b/src/modules/loader.ts index bac8374f..a8fcfdec 100644 --- a/src/modules/loader.ts +++ b/src/modules/loader.ts @@ -3,12 +3,12 @@ import 'lib' // import '../test/test' import './chat/chat' import './commands/index' -import './pvp/import' -import './world-edit/commands/camera/visual' -// import './indicator/index' +import './indicator/index' import './places/stone-quarry/index' +import './pvp/import' import './survival/import' import './test/test' +import './world-edit/commands/camera/visual' import './world-edit/import' import 'lib/load/message2' diff --git a/src/modules/places/spawn.ts b/src/modules/places/spawn.ts index ecca769a..220f6176 100644 --- a/src/modules/places/spawn.ts +++ b/src/modules/places/spawn.ts @@ -87,7 +87,7 @@ class SpawnBuilder extends AreaWithInventory { xp: 0, health: 20, equipment: {}, - slots: { 0: Menu.item }, + slots: { 0: Menu.itemStack }, }, clearAll: true, }) diff --git a/src/modules/places/village-of-explorers/village-of-explorers.ts b/src/modules/places/village-of-explorers/village-of-explorers.ts index f9ff86c9..ee1cab79 100644 --- a/src/modules/places/village-of-explorers/village-of-explorers.ts +++ b/src/modules/places/village-of-explorers/village-of-explorers.ts @@ -16,6 +16,7 @@ class VillageOfExporersBuilder extends City { this.create() } + // TODO Resistance to frogs cuz they kill boss in one shot lol slimeBoss = Boss.create() .group(this.group) .id('slime') diff --git a/src/modules/pvp/import.ts b/src/modules/pvp/import.ts index caf51471..b4c50099 100644 --- a/src/modules/pvp/import.ts +++ b/src/modules/pvp/import.ts @@ -2,3 +2,4 @@ import './explosible-fireworks' import './fireball-and-ice-bomb' import './raid' import './throwable-tnt' +import './newbie' diff --git a/src/modules/pvp/newbie.ts b/src/modules/pvp/newbie.ts new file mode 100644 index 00000000..b18fdb66 --- /dev/null +++ b/src/modules/pvp/newbie.ts @@ -0,0 +1,79 @@ +import { EntityDamageCause, Player, world } from '@minecraft/server' +import { isBuilding, Join, prompt } from 'lib' +import { PlayerProperties } from 'lib/assets/player-properties' +import { t } from 'lib/text' +import { PlayerNameTagModifiers } from 'modules/indicator/playerNameTag' + +const prefix = 'newbie' +const property = PlayerProperties['lw:newbie'] + +export function askForExitingNewbieMode(player: Player, reason: Text, callback: VoidFunction, back: VoidFunction) { + prompt( + player, + 'Если вы совершите это действие, вы потеряете статус новичка:\n - Другие игроки смогут наносить вам урон\n - Другие игроки смогут забирать ваш лут после смерти', + 'Я больше не новичок', + () => { + exitNewbieMode(player, reason) + callback() + }, + 'НЕТ, НАЗАД', + back, + ) +} + +export function exitNewbieMode(player: Player, reason: Text) { + player.warn(t.warn`Вы ${reason}, поэтому вышли из режима новичка.`) + delete player.database.survival.newbie + player.setProperty(property, false) + player.log(prefix, 'Exited newbie mode because', reason) +} + +function enterNewbieMode(player: Player) { + player.database.survival.newbie = 1 + player.setProperty(property, true) +} + +Join.onFirstTimeSpawn.subscribe(enterNewbieMode) + +world.afterEvents.entityHurt.subscribe(({ hurtEntity, damage, damageSource: { damagingEntity, cause } }) => { + if (!(hurtEntity instanceof Player)) return + if (damage === -17179869184) return + + const health = hurtEntity.getComponent('health') + const denyDamage = () => { + hurtEntity.log( + prefix, + 'Recieved damage', + damage.toFixed(2), + 'health', + health?.currentValue.toFixed(2), + 'with cause', + cause, + ) + if (health) health.setCurrentValue(health.currentValue + damage) + hurtEntity.teleport(hurtEntity.location) + } + + if (hurtEntity.database.survival.newbie && cause === EntityDamageCause.fireTick) { + denyDamage() + } else if (damagingEntity instanceof Player && damagingEntity.database.survival.newbie) { + denyDamage() + askForExitingNewbieMode( + damagingEntity, + 'ударили игрока', + () => hurtEntity.applyDamage(damage, { damagingEntity, cause }), + () => damagingEntity.info('Будь осторожнее в следующий раз.'), + ) + // TODO When called multiple times in the same minute warn and auto exit newbie mode + } +}) + +PlayerNameTagModifiers.push(p => (p.database.survival.newbie && !isBuilding(p) ? ' \n§bНовичок§r' : false)) + +new Command('newbie') + .setPermissions('techAdmin') + .setDescription('Вводит в режим новчика') + .executes(ctx => { + enterNewbieMode(ctx.player) + ctx.reply('Успешно') + }) diff --git a/src/modules/pvp/raid.ts b/src/modules/pvp/raid.ts index 08fd9344..06b9e0db 100644 --- a/src/modules/pvp/raid.ts +++ b/src/modules/pvp/raid.ts @@ -24,7 +24,7 @@ const locktext = 'Вы находитесь в режиме рейдблока.' new LockAction(player => { const raidLockTime = player.scores.raid if (raidLockTime > 0) { - return { lockText: `${locktext} Осталось ${t.error.ttime(raidLockTime * 1000)}` } + return { lockText: `${locktext} Осталось ${t.error.time(raidLockTime * 1000)}` } } else return false }, locktext) diff --git a/src/modules/quests/learning/index.ts b/src/modules/quests/learning/index.ts index cd0ee117..3f2fd47d 100644 --- a/src/modules/quests/learning/index.ts +++ b/src/modules/quests/learning/index.ts @@ -41,7 +41,7 @@ class Learning { // anarchy system.delay(() => { this.startAxeGiveCommand.ensure(player) - player.container?.setItem(8, Menu.item) + player.container?.setItem(8, Menu.itemStack) }) } diff --git a/src/modules/test/properties.ts b/src/modules/test/properties.ts new file mode 100644 index 00000000..5d452c60 --- /dev/null +++ b/src/modules/test/properties.ts @@ -0,0 +1,33 @@ +import { Player } from '@minecraft/server' +import { ArrayForm } from 'lib' +import { playerJson } from 'lib/assets/player-json' + +new Command('props') + .setDescription('Player properties menu') + .setPermissions('techAdmin') + .executes(ctx => propsMenu(ctx.player)) + +const properties = playerJson['minecraft:entity'].description.properties +type Prop = ValueOf + +function propsMenu(player: Player) { + new ArrayForm('Properties', Object.entries(properties)) + .button(([id, prop]) => { + const current = player.getProperty(id) + return [`${id} §7${current}`, () => propMenu(prop, current, player, id)] + }) + .show(player) +} + +function propMenu(prop: Prop, current: string | number | boolean | undefined, player: Player, id: string) { + new ArrayForm('Choose value', prop.type === 'enum' ? prop.values : [true, false]) + .button(value => [ + `${value === current ? '§a>§7 ' : '§f'}${value.toString()}`, + () => { + player.setProperty(id, value) + propMenu(prop, value, player, id) + }, + ]) + .back(() => propsMenu(player)) + .show(player) +} diff --git a/src/modules/test/test.ts b/src/modules/test/test.ts index 688ca62f..3f57fbbf 100644 --- a/src/modules/test/test.ts +++ b/src/modules/test/test.ts @@ -32,6 +32,7 @@ import { ModalForm } from 'lib/form/modal' import { Compass } from 'lib/rpg/menu' import { Rewards } from 'lib/shop/rewards' import './enchant' +import './properties' // import './simulatedPlayer' // There you can create simple one time tests taht will be run using .test command diff --git a/src/modules/world-edit/commands/camera/visual.ts b/src/modules/world-edit/commands/camera/visual.ts index 84c70f24..365c5a48 100644 --- a/src/modules/world-edit/commands/camera/visual.ts +++ b/src/modules/world-edit/commands/camera/visual.ts @@ -2,7 +2,7 @@ import { Player, system } from '@minecraft/server' import { InventoryStore } from 'lib/database/inventory' import { CURRENT_BUILDERS, isBuilding } from 'lib/game-utils' import { Join } from 'lib/player-join' -import { ROLES, getRole } from 'lib/roles' +import { getRoleAndName } from 'lib/roles' import { PlayerNameTagModifiers } from 'modules/indicator/playerNameTag' const builderInventory = new InventoryStore('build') @@ -49,7 +49,7 @@ system.runPlayerInterval( ) // Insert role value right after name -PlayerNameTagModifiers.splice(1, 0, p => isBuilding(p) && `\n${ROLES[getRole(p.id)]}`) +PlayerNameTagModifiers.push(p => isBuilding(p) && `\n${getRoleAndName(p.id, { name: false })}`) function setBuildingTip(player: Player, value = true) { player.onScreenDisplay.setTip(1, value ? 'Режим стройки' : '') diff --git a/src/modules/world-edit/lib/world-edit.ts b/src/modules/world-edit/lib/world-edit.ts index 2067a5f3..e8c47cb1 100644 --- a/src/modules/world-edit/lib/world-edit.ts +++ b/src/modules/world-edit/lib/world-edit.ts @@ -396,7 +396,7 @@ export class WorldEdit { 'блок', 'блока', 'блоков', - ])} за ${t.options({ unit: '§f', text: '§3' }).ttime(Date.now() - startTime)}.` + ])} за ${t.options({ unit: '§f', text: '§3' }).time(Date.now() - startTime)}.` if (replaceTargets.filter(Boolean).length) { reply += `§3, заполняемые блоки: §f${stringifyReplaceTargets(replaceTargets)}` diff --git a/tools/build.js b/tools/build.js index e48b516d..5bf2db84 100644 --- a/tools/build.js +++ b/tools/build.js @@ -1,8 +1,25 @@ // @ts-check +import { readFileSync, writeFileSync } from 'fs' import { buildArgumentsWithDist, buildCommand } from './build/cli.js' import { generateManigestJson } from './build/manifest.js' +const playerJsonAsset = 'src/lib/assets/player-json.ts' +try { + const player = readFileSync('entities/player.json').toString() + writeFileSync( + playerJsonAsset, + `// prettier-ignore +/* eslint-disable */ +// This file is autogenerated by tools/build.js. +// Do not modify manually. + +export const playerJson = ${player.trimEnd()} as const`, + ) +} catch (e) { + console.warn('Unable to update', playerJsonAsset + ':', e) +} + const args = buildArgumentsWithDist('scripts') buildCommand(args, { entryPoints: [!args.test ? 'src/index.ts' : 'src/test/loader.ts'], diff --git a/tools/build/cli.js b/tools/build/cli.js index 2f4e5078..fbc0568c 100644 --- a/tools/build/cli.js +++ b/tools/build/cli.js @@ -116,9 +116,7 @@ export function buildCommand({ test, dev, world, port, vitest, outfile, entry }, if (dev) logger.success(`Watching for changes at ${at}${test ? ' Test build is enabled.' : ''}`) else logger.info(`Built for ${mode} at ${at} ${time}`) process.send?.('ready') - } else if (dev) { - if (changed) process.send?.(['reload', changed]) - } + } else if (dev) process.send?.(['reload', changed]) firstBuild = false }) diff --git a/tsconfig.json b/tsconfig.json index d26769d0..a46afe1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -83,7 +83,7 @@ { "module": "node:" }, { "module": "node:test" }, { "module": "crypto" }, - { "module": "src/test/__mocks__" }, + { "module": "src/test/__mocks__/**" }, { "module": "vitest/dist/**" } ] }