mirror of
https://github.com/motajs/template.git
synced 2026-07-17 09:21:09 +08:00
refactor: 移动顶层实施对象
This commit is contained in:
parent
327bdc840b
commit
3d72954bb2
@ -1,4 +1,4 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import { ITileLocator, logger } from '@motajs/common';
|
||||
import {
|
||||
FaceDirection,
|
||||
IFaceHandler,
|
||||
@ -10,13 +10,11 @@ import {
|
||||
} from '@user/data-common';
|
||||
import {
|
||||
HeroMoveCode,
|
||||
IHeroHitAction,
|
||||
IHeroHitHandler,
|
||||
IHeroLocation,
|
||||
IHeroMover,
|
||||
IHeroMoverConfig,
|
||||
IPassCheckerHandler,
|
||||
ITerrainPassChecker
|
||||
IHeroMoveTopHandler,
|
||||
IHeroMoveTopImpl
|
||||
} from './types';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
@ -26,17 +24,17 @@ export class HeroMover<T extends IHeroLocation>
|
||||
{
|
||||
readonly state: IDataCommon;
|
||||
|
||||
/** 本次移动是否不记录进路线系统 */
|
||||
/** 是否不记录进路线系统 */
|
||||
private noRoute: boolean = false;
|
||||
/** 本次移动是否忽略地形碰撞检测 */
|
||||
/** 是否忽略地形碰撞检测 */
|
||||
private ignoreTerrain: boolean = false;
|
||||
/** 本次移动是否在特定时机触发自动存档 */
|
||||
/** 是否在特定时机触发自动存档 */
|
||||
private autoSave: boolean = false;
|
||||
/** 是否允许到达地图外 */
|
||||
private allowOutBound: boolean = false;
|
||||
|
||||
/** 地形通行判定器 */
|
||||
private terrainChecker: ITerrainPassChecker | null = null;
|
||||
/** 撞击行为对象 */
|
||||
private hitAction: IHeroHitAction | null = null;
|
||||
/** 勇士移动顶层实现对象 */
|
||||
private topImpl: IHeroMoveTopImpl | null = null;
|
||||
|
||||
constructor(
|
||||
readonly tile: T,
|
||||
@ -46,7 +44,7 @@ export class HeroMover<T extends IHeroLocation>
|
||||
this.state = tile.state;
|
||||
}
|
||||
|
||||
config(config: Readonly<IHeroMoverConfig>): this {
|
||||
config(config: Partial<IHeroMoverConfig>): this {
|
||||
if (!isNil(config.noRoute)) {
|
||||
this.noRoute = config.noRoute;
|
||||
}
|
||||
@ -56,6 +54,9 @@ export class HeroMover<T extends IHeroLocation>
|
||||
if (!isNil(config.autoSave)) {
|
||||
this.autoSave = config.autoSave;
|
||||
}
|
||||
if (!isNil(config.allowOutBound)) {
|
||||
this.allowOutBound = config.allowOutBound;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -63,95 +64,104 @@ export class HeroMover<T extends IHeroLocation>
|
||||
return {
|
||||
noRoute: this.noRoute,
|
||||
ignoreTerrain: this.ignoreTerrain,
|
||||
autoSave: this.autoSave
|
||||
autoSave: this.autoSave,
|
||||
allowOutBound: this.allowOutBound
|
||||
};
|
||||
}
|
||||
|
||||
useTerrainChecker(checker: ITerrainPassChecker | null): void {
|
||||
this.terrainChecker = checker;
|
||||
}
|
||||
|
||||
useHitAction(action: IHeroHitAction | null): void {
|
||||
this.hitAction = action;
|
||||
useTopImplementation(impl: IHeroMoveTopImpl | null): void {
|
||||
this.topImpl = impl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通行性检查信息对象
|
||||
* 创建顶层信息对象
|
||||
* @param curr 勇士所在位置
|
||||
* @param dir 勇士移动方向
|
||||
* @param floorId 当前楼层 id
|
||||
*/
|
||||
private createPassHandler(
|
||||
private createHandler(
|
||||
curr: ITileLocator,
|
||||
dir: FaceDirection,
|
||||
step: Readonly<ObjectMoveStep>,
|
||||
floorId: string | undefined
|
||||
): IPassCheckerHandler {
|
||||
const { x, y } = this.faceHandler.movement(dir);
|
||||
const nx = curr.x + x;
|
||||
const ny = curr.y + y;
|
||||
return {
|
||||
currLoc: curr,
|
||||
nextLoc: { x: nx, y: ny },
|
||||
direction: dir,
|
||||
floorId,
|
||||
face: this.faceHandler,
|
||||
state: this.state
|
||||
};
|
||||
): IHeroMoveTopHandler {
|
||||
switch (step.type) {
|
||||
case ObjectMoveType.Dir:
|
||||
case ObjectMoveType.DirFace:
|
||||
case ObjectMoveType.Special: {
|
||||
const { x, y } = this.faceHandler.movement(this.moveDirection);
|
||||
const nx = curr.x + x;
|
||||
const ny = curr.y + y;
|
||||
return {
|
||||
currLoc: { x: curr.x, y: curr.y },
|
||||
nextLoc: { x: nx, y: ny },
|
||||
direction: this.moveDirection,
|
||||
floorId,
|
||||
face: this.faceHandler,
|
||||
state: this.state
|
||||
};
|
||||
}
|
||||
case ObjectMoveType.Jump:
|
||||
case ObjectMoveType.Teleport: {
|
||||
const nx = step.rel ? curr.x + step.x : step.x;
|
||||
const ny = step.rel ? curr.y + step.y : step.y;
|
||||
return {
|
||||
currLoc: { x: curr.x, y: curr.y },
|
||||
nextLoc: { x: nx, y: ny },
|
||||
direction: this.moveDirection,
|
||||
floorId,
|
||||
face: this.faceHandler,
|
||||
state: this.state
|
||||
};
|
||||
}
|
||||
case ObjectMoveType.AnimDir:
|
||||
case ObjectMoveType.Face:
|
||||
case ObjectMoveType.Speed: {
|
||||
return {
|
||||
currLoc: { x: curr.x, y: curr.y },
|
||||
nextLoc: { x: curr.x, y: curr.y },
|
||||
direction: this.moveDirection,
|
||||
floorId,
|
||||
face: this.faceHandler,
|
||||
state: this.state
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建撞击行为信息对象
|
||||
* @param curr 勇士所在位置
|
||||
* @param dir 勇士移动方向
|
||||
* @param floorId 当前楼层 id
|
||||
*/
|
||||
private createHitHandler(
|
||||
curr: ITileLocator,
|
||||
dir: FaceDirection,
|
||||
floorId: string | undefined
|
||||
): IHeroHitHandler {
|
||||
const { x, y } = this.faceHandler.movement(dir);
|
||||
const nx = curr.x + x;
|
||||
const ny = curr.y + y;
|
||||
return {
|
||||
currLoc: curr,
|
||||
nextLoc: { x: nx, y: ny },
|
||||
direction: dir,
|
||||
floorId,
|
||||
state: this.state
|
||||
};
|
||||
protected async onMoveStart(): Promise<void> {
|
||||
if (!this.topImpl) {
|
||||
logger.warn(144);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveStart(): Promise<void> {}
|
||||
|
||||
protected async onMoveEnd(): Promise<void> {}
|
||||
|
||||
protected async onStepStart(
|
||||
step: Readonly<ObjectMoveStep>,
|
||||
tile: IHeroLocation
|
||||
): Promise<number> {
|
||||
if (!this.terrainChecker) return HeroMoveCode.CannotMove;
|
||||
if (!this.topImpl) return HeroMoveCode.Stop;
|
||||
|
||||
const type = step.type;
|
||||
const handler = this.createHandler(tile, step, tile.floorId);
|
||||
|
||||
// 边界判断
|
||||
const { x: nx, y: ny } = handler.nextLoc;
|
||||
const inBound = this.topImpl.inBound(nx, ny, handler.floorId);
|
||||
if (!this.allowOutBound && !inBound) return HeroMoveCode.CannotMove;
|
||||
|
||||
// 移动步判断
|
||||
if (
|
||||
type === ObjectMoveType.Dir ||
|
||||
type === ObjectMoveType.DirFace ||
|
||||
type === ObjectMoveType.Special
|
||||
) {
|
||||
const dir = this.moveDirection;
|
||||
const handler = this.createPassHandler(tile, dir, tile.floorId);
|
||||
const { x: nx, y: ny } = handler.nextLoc;
|
||||
const inBound = this.terrainChecker.inBound(nx, ny, tile.floorId);
|
||||
// 目标点不在地图内
|
||||
if (!inBound) return HeroMoveCode.CannotMove;
|
||||
|
||||
if (this.ignoreTerrain) {
|
||||
return HeroMoveCode.Step;
|
||||
} else {
|
||||
const canPass = this.terrainChecker.canPass(handler);
|
||||
const canPass = this.topImpl.canPass(handler);
|
||||
if (canPass) {
|
||||
const hit = this.terrainChecker.shouldHit(handler);
|
||||
const hit = this.topImpl.shouldHit(handler);
|
||||
if (hit) return HeroMoveCode.Hit;
|
||||
else return HeroMoveCode.Step;
|
||||
} else {
|
||||
@ -160,14 +170,12 @@ export class HeroMover<T extends IHeroLocation>
|
||||
}
|
||||
}
|
||||
|
||||
// 跳跃和瞬移仅需要进行边界判断
|
||||
// 跳跃和瞬移
|
||||
if (type === ObjectMoveType.Jump || type === ObjectMoveType.Teleport) {
|
||||
const nx = step.rel ? tile.x + step.x : step.x;
|
||||
const ny = step.rel ? tile.y + step.y : step.y;
|
||||
if (this.ignoreTerrain) {
|
||||
return HeroMoveCode.Step;
|
||||
} else {
|
||||
if (this.terrainChecker.inBound(nx, ny, tile.floorId)) {
|
||||
if (inBound) {
|
||||
return HeroMoveCode.Step;
|
||||
} else {
|
||||
return HeroMoveCode.Stop;
|
||||
@ -175,6 +183,7 @@ export class HeroMover<T extends IHeroLocation>
|
||||
}
|
||||
}
|
||||
|
||||
// 其他的一律可以直接执行
|
||||
return HeroMoveCode.Step;
|
||||
}
|
||||
|
||||
@ -184,7 +193,9 @@ export class HeroMover<T extends IHeroLocation>
|
||||
tile: IHeroLocation,
|
||||
controller: Readonly<IMoverController>
|
||||
): Promise<ITileLocator> {
|
||||
const type = step.type;
|
||||
if (!this.topImpl) return { x: tile.x, y: tile.y };
|
||||
|
||||
const handler = this.createHandler(tile, step, tile.floorId);
|
||||
|
||||
// 不能移动或停止
|
||||
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Stop) {
|
||||
@ -195,12 +206,7 @@ export class HeroMover<T extends IHeroLocation>
|
||||
|
||||
// 撞击时也使用当前位置,同时处理撞击行为
|
||||
if (code === HeroMoveCode.Hit) {
|
||||
// 执行撞击行为
|
||||
if (this.hitAction) {
|
||||
const dir = this.moveDirection;
|
||||
const handler = this.createHitHandler(tile, dir, tile.floorId);
|
||||
await this.hitAction.hit(handler);
|
||||
}
|
||||
await this.topImpl.hit(handler);
|
||||
// 这里同样不能 await
|
||||
controller.stop();
|
||||
return { x: tile.x, y: tile.y };
|
||||
@ -208,30 +214,7 @@ export class HeroMover<T extends IHeroLocation>
|
||||
|
||||
// 正常移动
|
||||
if (code === HeroMoveCode.Step) {
|
||||
if (
|
||||
type === ObjectMoveType.Teleport ||
|
||||
type === ObjectMoveType.Jump
|
||||
) {
|
||||
// 瞬移行为
|
||||
if (step.rel) {
|
||||
return { x: tile.x + step.x, y: tile.y + step.y };
|
||||
} else {
|
||||
return { x: step.x, y: step.y };
|
||||
}
|
||||
} else if (
|
||||
type === ObjectMoveType.Dir ||
|
||||
type === ObjectMoveType.DirFace ||
|
||||
type === ObjectMoveType.Special
|
||||
) {
|
||||
// 移动行为
|
||||
const { x, y } = this.faceHandler.movement(this.moveDirection);
|
||||
const nx = tile.x + x;
|
||||
const ny = tile.y + y;
|
||||
return { x: nx, y: ny };
|
||||
} else {
|
||||
// 其他行为
|
||||
return { x: tile.x, y: tile.y };
|
||||
}
|
||||
return handler.nextLoc;
|
||||
}
|
||||
|
||||
return { x: tile.x, y: tile.y };
|
||||
|
||||
@ -230,7 +230,7 @@ export const enum HeroMoveCode {
|
||||
CannotMove
|
||||
}
|
||||
|
||||
export interface IHeroMoveHandlerBase extends IDataCommonExtended {
|
||||
export interface IHeroMoveTopHandler extends IDataCommonExtended {
|
||||
/** 当前位置 */
|
||||
readonly currLoc: ITileLocator;
|
||||
/** 要移动至的位置 */
|
||||
@ -239,16 +239,11 @@ export interface IHeroMoveHandlerBase extends IDataCommonExtended {
|
||||
readonly direction: FaceDirection;
|
||||
/** 当前楼层 id */
|
||||
readonly floorId: string | undefined;
|
||||
}
|
||||
|
||||
export interface IPassCheckerHandler extends IHeroMoveHandlerBase {
|
||||
/** 朝向管理对象 */
|
||||
readonly face: IFaceHandler<FaceDirection>;
|
||||
}
|
||||
|
||||
export interface IHeroHitHandler extends IHeroMoveHandlerBase {}
|
||||
|
||||
export interface ITerrainPassChecker {
|
||||
export interface IHeroMoveTopImpl {
|
||||
/**
|
||||
* 检查目标位置是否在地图范围内
|
||||
* @param x 横坐标
|
||||
@ -261,30 +256,30 @@ export interface ITerrainPassChecker {
|
||||
* 判断在指定楼层中,从指定坐标向指定方向移动一格是否可通行
|
||||
* @param handler 通行性检查对象
|
||||
*/
|
||||
canPass(handler: IPassCheckerHandler): boolean;
|
||||
canPass(handler: IHeroMoveTopHandler): boolean;
|
||||
|
||||
/**
|
||||
* 判断在指定楼层中,从指定坐标向指定方向移动时是否应该产生撞击,撞击将会触发目标位置的触发器
|
||||
* @param handler 通行性检查对象
|
||||
*/
|
||||
shouldHit(handler: IPassCheckerHandler): boolean;
|
||||
}
|
||||
shouldHit(handler: IHeroMoveTopHandler): boolean;
|
||||
|
||||
export interface IHeroHitAction {
|
||||
/**
|
||||
* 勇士撞击某一个图块时执行的内容,一般用于触发目标位置的触发器
|
||||
* @param handler 撞击行为对象
|
||||
*/
|
||||
hit(handler: IHeroHitHandler): Promise<void>;
|
||||
hit(handler: IHeroMoveTopHandler): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IHeroMoverConfig {
|
||||
/** 本次移动是否不记录进路线系统 */
|
||||
noRoute?: boolean;
|
||||
/** 本次移动是否忽略地形碰撞检测 */
|
||||
ignoreTerrain?: boolean;
|
||||
/** 本次移动是否在特定时机触发自动存档 */
|
||||
autoSave?: boolean;
|
||||
/** 是否不记录进路线系统 */
|
||||
noRoute: boolean;
|
||||
/** 是否忽略地形碰撞检测 */
|
||||
ignoreTerrain: boolean;
|
||||
/** 是否在特定时机触发自动存档 */
|
||||
autoSave: boolean;
|
||||
/** 是否允许到达地图外 */
|
||||
allowOutBound: boolean;
|
||||
}
|
||||
|
||||
export interface IHeroMover<T extends IHeroLocation>
|
||||
@ -293,7 +288,7 @@ export interface IHeroMover<T extends IHeroLocation>
|
||||
* 配置本次移动的行为模式
|
||||
* @param config 配置对象,未传入的字段保持当前值
|
||||
*/
|
||||
config(config: Readonly<IHeroMoverConfig>): this;
|
||||
config(config: Partial<IHeroMoverConfig>): this;
|
||||
|
||||
/**
|
||||
* 获取当前移动配置的只读快照
|
||||
@ -301,16 +296,10 @@ export interface IHeroMover<T extends IHeroLocation>
|
||||
getConfig(): Readonly<IHeroMoverConfig>;
|
||||
|
||||
/**
|
||||
* 设置地形通行判定器,传入 `null` 移表示移除
|
||||
* @param checker 地形判定器
|
||||
* 设置勇士移动的顶层实现对象,主要用于进行各种判定与勇士行为
|
||||
* @param impl 顶层实现对象
|
||||
*/
|
||||
useTerrainChecker(checker: ITerrainPassChecker | null): void;
|
||||
|
||||
/**
|
||||
* 设置勇士撞击行为的执行对象,传入 `null` 表示移除。
|
||||
* @param action 撞击行为对象
|
||||
*/
|
||||
useHitAction(action: IHeroHitAction | null): void;
|
||||
useTopImplementation(impl: IHeroMoveTopImpl | null): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -66,8 +66,7 @@ import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ISaveSystem, SaveSystem } from './save';
|
||||
import { DefaultPassChecker } from './hero';
|
||||
import { DefaultHitAction } from './hero/hitAction';
|
||||
import { DefaultHeroMoveTopImpl } from './hero';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
||||
@ -211,14 +210,11 @@ export class CoreState implements ICoreState {
|
||||
});
|
||||
|
||||
// 勇士顶层初始化
|
||||
const passChecker = new DefaultPassChecker(this.maps);
|
||||
const hitAction = new DefaultHitAction(
|
||||
this.maps,
|
||||
this.triggerCollector,
|
||||
this
|
||||
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(
|
||||
this,
|
||||
this.triggerCollector
|
||||
);
|
||||
this.hero.location.mover.useTerrainChecker(passChecker);
|
||||
this.hero.location.mover.useHitAction(hitAction);
|
||||
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import {
|
||||
IHeroHitAction,
|
||||
IHeroHitHandler,
|
||||
IMapStore,
|
||||
IStateBase
|
||||
} from '@user/data-base';
|
||||
import { ITriggerCollector, ITriggerHandler } from '@user/data-system';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class DefaultHitAction implements IHeroHitAction {
|
||||
constructor(
|
||||
readonly maps: IMapStore,
|
||||
readonly collector: ITriggerCollector,
|
||||
readonly state: IStateBase
|
||||
) {}
|
||||
|
||||
async hit(handler: IHeroHitHandler): Promise<void> {
|
||||
if (isNil(handler.floorId)) return;
|
||||
|
||||
const map = this.maps.getLayerState(handler.floorId);
|
||||
if (!map) return;
|
||||
const event = map.eventLayer;
|
||||
if (!event) return;
|
||||
|
||||
const { x, y } = handler.nextLoc;
|
||||
const triggers = this.collector.collect(x, y, event);
|
||||
|
||||
const triggerHandler: ITriggerHandler = {
|
||||
state: this.state,
|
||||
layer: map,
|
||||
mapLayer: event,
|
||||
locator: handler.nextLoc
|
||||
};
|
||||
|
||||
return triggers.trigger(triggerHandler);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './passChecker';
|
||||
export * from './moverImpl';
|
||||
export * from './types';
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import { FaceDirection, PassBit } from '@user/data-common';
|
||||
import {
|
||||
IHeroMoveTopHandler,
|
||||
IHeroMoveTopImpl,
|
||||
IMapStore,
|
||||
IPassCheckerHandler,
|
||||
ITerrainPassChecker
|
||||
IStateBase
|
||||
} from '@user/data-base';
|
||||
import { FaceDirection, PassBit } from '@user/data-common';
|
||||
import { ITriggerCollector, ITriggerHandler } from '@user/data-system';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class DefaultPassChecker implements ITerrainPassChecker {
|
||||
constructor(readonly maps: IMapStore) {}
|
||||
export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
/** 地图存储对象 */
|
||||
private readonly maps: IMapStore;
|
||||
|
||||
constructor(
|
||||
private readonly state: IStateBase,
|
||||
private readonly collector: ITriggerCollector
|
||||
) {
|
||||
this.maps = state.maps;
|
||||
}
|
||||
|
||||
//#region 通行性判断
|
||||
|
||||
/**
|
||||
* 将朝向转换为对应的通行性位掩码。
|
||||
@ -36,7 +48,7 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
}
|
||||
|
||||
canPass(handler: IPassCheckerHandler): boolean {
|
||||
canPass(handler: IHeroMoveTopHandler): boolean {
|
||||
const { currLoc, nextLoc, direction, floorId, face } = handler;
|
||||
if (isNil(floorId)) return false;
|
||||
|
||||
@ -96,7 +108,7 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
||||
return true;
|
||||
}
|
||||
|
||||
shouldHit(handler: IPassCheckerHandler): boolean {
|
||||
shouldHit(handler: IHeroMoveTopHandler): boolean {
|
||||
const { nextLoc, floorId } = handler;
|
||||
if (isNil(floorId)) return false;
|
||||
const layerState = this.maps.getLayerState(floorId);
|
||||
@ -110,4 +122,31 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
||||
if (!next || !next.raw) return false;
|
||||
return !next.raw.eventPass;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 触发器行为
|
||||
|
||||
async hit(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
if (isNil(handler.floorId)) return;
|
||||
|
||||
const map = this.maps.getLayerState(handler.floorId);
|
||||
if (!map) return;
|
||||
const event = map.eventLayer;
|
||||
if (!event) return;
|
||||
|
||||
const { x, y } = handler.nextLoc;
|
||||
const triggers = this.collector.collect(x, y, event);
|
||||
|
||||
const triggerHandler: ITriggerHandler = {
|
||||
state: this.state,
|
||||
layer: map,
|
||||
mapLayer: event,
|
||||
locator: handler.nextLoc
|
||||
};
|
||||
|
||||
return triggers.trigger(triggerHandler);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@ -200,6 +200,7 @@
|
||||
"140": "Different priority of combat script is expected, but got a same one.",
|
||||
"141": "Some issue may occur in binded damage system on combat flow, please check the console.",
|
||||
"142": "Expected a specific tile number after convert id '$1' to number, but got null.",
|
||||
"143": "Cannot create dynamic tile for $1 since its raw data is not found."
|
||||
"143": "Cannot create dynamic tile for $1 since its raw data is not found.",
|
||||
"144": "A hero move top implementation object binding is required for hero moving."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user