mirror of
https://github.com/wassname/phaser.git
synced 2026-09-12 12:40:47 +08:00
Saving first iteration of the ScrollZone game object.
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
/// <reference path="../Game.ts" />
|
||||
/// <reference path="../geom/Quad.ts" />
|
||||
|
||||
/**
|
||||
* Phaser - ScrollZone
|
||||
*
|
||||
* Creates a scrolling region of the given width and height from an image in the cache.
|
||||
* The ScrollZone can be positioned anywhere in-world like a normal game object.
|
||||
* The image within it is scrolled via the scrollSpeed.x/y properties.
|
||||
* If you create a scroll zone larger than the given source image it will create a DynamicTexture and fill it with a pattern of the source image.
|
||||
*/
|
||||
|
||||
module Phaser {
|
||||
|
||||
export class ScrollZone extends GameObject {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(game: Game, key:string, x: number, y: number, width: number, height: number) {
|
||||
|
||||
super(game, x, y, width, height);
|
||||
|
||||
// Our seamless scrolling quads
|
||||
this._A = new Quad(0, 0, width, height);
|
||||
this._B = new Quad();
|
||||
this._C = new Quad();
|
||||
this._D = new Quad();
|
||||
|
||||
this._scroll = new MicroPoint();
|
||||
|
||||
this.offset = new MicroPoint();
|
||||
this.scrollSpeed = new MicroPoint();
|
||||
|
||||
if (this._game.cache.getImage(key))
|
||||
{
|
||||
this._texture = this._game.cache.getImage(key);
|
||||
this.bounds.width = width;
|
||||
this.bounds.height = height;
|
||||
this._sourceWidth = this._texture.width;
|
||||
this._sourceHeight = this._texture.height;
|
||||
|
||||
// If the Scrolling Zone is BIGGER than the texture then we need to create a repeating pattern DynamicTexture
|
||||
if (this._texture.width < width || this._texture.height < height)
|
||||
{
|
||||
this.createRepeatingTexture();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _texture;
|
||||
private _dynamicTexture: DynamicTexture = null;
|
||||
|
||||
private _A: Quad;
|
||||
private _B: Quad;
|
||||
private _C: Quad;
|
||||
private _D: Quad;
|
||||
|
||||
private _scroll: MicroPoint;
|
||||
private _sourceWidth: number;
|
||||
private _sourceHeight: number;
|
||||
|
||||
// local rendering related temp vars to help avoid gc spikes
|
||||
private _dx: number = 0;
|
||||
private _dy: number = 0;
|
||||
private _dw: number = 0;
|
||||
private _dh: number = 0;
|
||||
private _anchorWidth: number = 0;
|
||||
private _anchorHeight: number = 0;
|
||||
private _inverseWidth: number = 0;
|
||||
private _inverseHeight: number = 0;
|
||||
|
||||
public scrollSpeed: MicroPoint;
|
||||
public offset: MicroPoint;
|
||||
|
||||
public flipped: bool = false;
|
||||
|
||||
public update() {
|
||||
|
||||
this._scroll.x = Math.round(this._scroll.x + this.scrollSpeed.x);
|
||||
this._scroll.y = Math.round(this._scroll.y + this.scrollSpeed.y);
|
||||
|
||||
if (this._scroll.x > this._sourceWidth)
|
||||
{
|
||||
this._scroll.x = 0;
|
||||
}
|
||||
|
||||
if (this._scroll.x < 0)
|
||||
{
|
||||
this._scroll.x = this._sourceWidth;
|
||||
}
|
||||
|
||||
if (this._scroll.y > this._sourceHeight)
|
||||
{
|
||||
this._scroll.y = 0;
|
||||
}
|
||||
|
||||
if (this._scroll.y < 0)
|
||||
{
|
||||
this._scroll.y = this._sourceHeight;
|
||||
}
|
||||
|
||||
// Anchor Dimensions
|
||||
this._anchorWidth = this._sourceWidth - this._scroll.x;
|
||||
this._anchorHeight = this._sourceHeight - this._scroll.y;
|
||||
|
||||
if (this._anchorWidth > this.width)
|
||||
{
|
||||
this._anchorWidth = this.width;
|
||||
}
|
||||
|
||||
if (this._anchorHeight > this.height)
|
||||
{
|
||||
this._anchorHeight = this.height;
|
||||
}
|
||||
|
||||
this._inverseWidth = this.width - this._anchorWidth;
|
||||
this._inverseHeight = this.height - this._anchorHeight;
|
||||
|
||||
// Quad A
|
||||
this._A.setTo(this._scroll.x, this._scroll.y, this._anchorWidth, this._anchorHeight);
|
||||
|
||||
// Quad B
|
||||
this._B.y = this._scroll.y;
|
||||
this._B.width = this._inverseWidth;
|
||||
this._B.height = this._anchorHeight;
|
||||
|
||||
// Quad C
|
||||
this._C.x = this._scroll.x;
|
||||
this._C.width = this._anchorWidth;
|
||||
this._C.height = this._inverseHeight;
|
||||
|
||||
// Quad D
|
||||
this._D.width = this._inverseWidth;
|
||||
this._D.height = this._inverseHeight;
|
||||
|
||||
}
|
||||
|
||||
public inCamera(camera: Rectangle): bool {
|
||||
|
||||
if (this.scrollFactor.x !== 1.0 || this.scrollFactor.y !== 1.0)
|
||||
{
|
||||
this._dx = this.bounds.x - (camera.x * this.scrollFactor.x);
|
||||
this._dy = this.bounds.y - (camera.y * this.scrollFactor.x);
|
||||
this._dw = this.bounds.width * this.scale.x;
|
||||
this._dh = this.bounds.height * this.scale.y;
|
||||
|
||||
return (camera.right > this._dx) && (camera.x < this._dx + this._dw) && (camera.bottom > this._dy) && (camera.y < this._dy + this._dh);
|
||||
}
|
||||
else
|
||||
{
|
||||
return camera.intersects(this.bounds, this.bounds.length);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public render(camera: Camera, cameraOffsetX: number, cameraOffsetY: number) {
|
||||
|
||||
// Render checks
|
||||
if (this.visible === false || this.scale.x == 0 || this.scale.y == 0 || this.alpha < 0.1 || this.inCamera(camera.worldView) == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Alpha
|
||||
if (this.alpha !== 1)
|
||||
{
|
||||
var globalAlpha = this._game.stage.context.globalAlpha;
|
||||
this._game.stage.context.globalAlpha = this.alpha;
|
||||
}
|
||||
|
||||
this._dx = cameraOffsetX + (this.bounds.topLeft.x - camera.worldView.x);
|
||||
this._dy = cameraOffsetY + (this.bounds.topLeft.y - camera.worldView.y);
|
||||
this._dw = this.bounds.width * this.scale.x;
|
||||
this._dh = this.bounds.height * this.scale.y;
|
||||
|
||||
// Apply camera difference
|
||||
if (this.scrollFactor.x !== 1.0 || this.scrollFactor.y !== 1.0)
|
||||
{
|
||||
this._dx -= (camera.worldView.x * this.scrollFactor.x);
|
||||
this._dy -= (camera.worldView.y * this.scrollFactor.y);
|
||||
}
|
||||
|
||||
// Rotation - needs to work from origin point really, but for now from center
|
||||
if (this.angle !== 0 || this.flipped == true)
|
||||
{
|
||||
this._game.stage.context.save();
|
||||
this._game.stage.context.translate(this._dx + (this._dw / 2), this._dy + (this._dh / 2));
|
||||
|
||||
if (this.angle !== 0)
|
||||
{
|
||||
this._game.stage.context.rotate(this.angle * (Math.PI / 180));
|
||||
}
|
||||
|
||||
this._dx = -(this._dw / 2);
|
||||
this._dy = -(this._dh / 2);
|
||||
|
||||
if (this.flipped == true)
|
||||
{
|
||||
this._game.stage.context.scale(-1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this._dx = Math.round(this._dx);
|
||||
this._dy = Math.round(this._dy);
|
||||
|
||||
if (this._dynamicTexture)
|
||||
{
|
||||
if (this._A.width !== 0 && this._A.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._dynamicTexture.canvas, this._A.x, this._A.y, this._A.width, this._A.height, this._dx, this._dy, this._A.width, this._A.height);
|
||||
}
|
||||
|
||||
if (this._B.width !== 0 && this._B.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._dynamicTexture.canvas, this._B.x, this._B.y, this._B.width, this._B.height, this._dx + this._A.width, this._dy, this._B.width, this._B.height);
|
||||
}
|
||||
|
||||
if (this._C.width !== 0 && this._C.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._dynamicTexture.canvas, this._C.x, this._C.y, this._C.width, this._C.height, this._dx, this._dy + this._A.height, this._C.width, this._C.height);
|
||||
}
|
||||
|
||||
if (this._D.width !== 0 && this._D.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._dynamicTexture.canvas, this._D.x, this._D.y, this._D.width, this._D.height, this._dx + this._C.width, this._dy + this._A.height, this._D.width, this._D.height);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this._A.width !== 0 && this._A.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._texture, this._A.x, this._A.y, this._A.width, this._A.height, this._dx, this._dy, this._A.width, this._A.height);
|
||||
}
|
||||
|
||||
if (this._B.width !== 0 && this._B.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._texture, this._B.x, this._B.y, this._B.width, this._B.height, this._dx + this._A.width, this._dy, this._B.width, this._B.height);
|
||||
}
|
||||
|
||||
if (this._C.width !== 0 && this._C.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._texture, this._C.x, this._C.y, this._C.width, this._C.height, this._dx, this._dy + this._A.height, this._C.width, this._C.height);
|
||||
}
|
||||
|
||||
if (this._D.width !== 0 && this._D.height !== 0)
|
||||
{
|
||||
this._game.stage.context.drawImage(this._texture, this._D.x, this._D.y, this._D.width, this._D.height, this._dx + this._C.width, this._dy + this._A.height, this._D.width, this._D.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (globalAlpha > -1)
|
||||
{
|
||||
this._game.stage.context.globalAlpha = globalAlpha;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private createRepeatingTexture() {
|
||||
|
||||
// Work out how many we'll need of the source image to make it tile properly
|
||||
var tileWidth = Math.ceil(this.width / this._sourceWidth) * this._sourceWidth;
|
||||
var tileHeight = Math.ceil(this.height / this._sourceHeight) * this._sourceHeight;
|
||||
|
||||
this._dynamicTexture = new DynamicTexture(this._game, tileWidth, tileHeight);
|
||||
|
||||
this._dynamicTexture.context.rect(0, 0, tileWidth, tileHeight);
|
||||
this._dynamicTexture.context.fillStyle = this._dynamicTexture.context.createPattern(this._texture, "repeat");
|
||||
this._dynamicTexture.context.fill();
|
||||
|
||||
this._sourceWidth = tileWidth;
|
||||
this._sourceHeight = tileHeight;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+67
-182
@@ -1,270 +1,155 @@
|
||||
/// <reference path="../Game.ts" />
|
||||
/// <reference path="GameObject.ts" />
|
||||
/// <reference path="../system/Tile.ts" />
|
||||
/// <reference path="../system/TilemapBuffer.ts" />
|
||||
/// <reference path="../system/TilemapLayer.ts" />
|
||||
|
||||
/**
|
||||
* Phaser - Tilemap
|
||||
*
|
||||
* This GameObject allows for the display of a tilemap within the game world. Tile maps consist of an image, tile data and a size.
|
||||
* Internally it creates a TilemapBuffer for each camera in the world.
|
||||
* Internally it creates a TilemapLayer for each layer in the tilemap.
|
||||
*/
|
||||
|
||||
module Phaser {
|
||||
|
||||
export class Tilemap extends GameObject {
|
||||
|
||||
constructor(game: Game, key: string, mapData: string, format: number, tileWidth?: number = 0, tileHeight?: number = 0) {
|
||||
constructor(game: Game, key: string, mapData: string, format: number, resizeWorld: bool = true, tileWidth?: number = 0, tileHeight?: number = 0) {
|
||||
|
||||
super(game);
|
||||
|
||||
this._texture = this._game.cache.getImage(key);
|
||||
this._tilemapBuffers = [];
|
||||
|
||||
this.isGroup = false;
|
||||
|
||||
this.tileWidth = tileWidth;
|
||||
this.tileHeight = tileHeight;
|
||||
this.boundsInTiles = new Rectangle();
|
||||
this._layers = [];
|
||||
|
||||
this.mapFormat = format;
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case Tilemap.FORMAT_CSV:
|
||||
this.parseCSV(game.cache.getText(mapData));
|
||||
this.parseCSV(game.cache.getText(mapData), key, tileWidth, tileHeight);
|
||||
break;
|
||||
|
||||
case Tilemap.FORMAT_TILED_JSON:
|
||||
this.parseTiledJSON(game.cache.getText(mapData));
|
||||
this.parseTiledJSON(game.cache.getText(mapData), key);
|
||||
break;
|
||||
}
|
||||
|
||||
this.parseTileOffsets();
|
||||
this.createTilemapBuffers();
|
||||
if (this.currentLayer && resizeWorld)
|
||||
{
|
||||
this._game.world.setSize(this.currentLayer.widthInPixels, this.currentLayer.heightInPixels, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _texture;
|
||||
private _tileOffsets;
|
||||
private _tilemapBuffers: TilemapBuffer[];
|
||||
private _dx: number = 0;
|
||||
private _dy: number = 0;
|
||||
private _layers : TilemapLayer[];
|
||||
|
||||
public static FORMAT_CSV: number = 0;
|
||||
public static FORMAT_TILED_JSON: number = 1;
|
||||
|
||||
public mapData;
|
||||
public currentLayer: TilemapLayer;
|
||||
public mapFormat: number;
|
||||
public boundsInTiles: Rectangle;
|
||||
|
||||
public tileWidth: number;
|
||||
public tileHeight: number;
|
||||
public update() {
|
||||
}
|
||||
|
||||
public widthInTiles: number = 0;
|
||||
public heightInTiles: number = 0;
|
||||
public render(camera: Camera, cameraOffsetX: number, cameraOffsetY: number) {
|
||||
|
||||
public widthInPixels: number = 0;
|
||||
public heightInPixels: number = 0;
|
||||
// Loop through the layers
|
||||
for (var i = 0; i < this._layers.length; i++)
|
||||
{
|
||||
this._layers[i].render(camera, cameraOffsetX, cameraOffsetY);
|
||||
}
|
||||
|
||||
// How many extra tiles to draw around the edge of the screen (for fast scrolling games, or to optimise mobile performance try increasing this)
|
||||
// The number is the amount of extra tiles PER SIDE, so a value of 10 would be (10 tiles + screen size + 10 tiles)
|
||||
public tileBoundary: number = 10;
|
||||
}
|
||||
|
||||
private parseCSV(data: string) {
|
||||
private parseCSV(data: string, key: string, tileWidth: number, tileHeight: number) {
|
||||
|
||||
//console.log('parseMapData');
|
||||
|
||||
this.mapData = [];
|
||||
var layer: TilemapLayer = new TilemapLayer(this._game, key, Tilemap.FORMAT_CSV, 'TileLayerCSV' + this._layers.length.toString(), tileWidth, tileHeight);
|
||||
|
||||
// Trim any rogue whitespace from the data
|
||||
data = data.trim();
|
||||
|
||||
var rows = data.split("\n");
|
||||
//console.log('rows', rows);
|
||||
|
||||
for (var i = 0; i < rows.length; i++)
|
||||
{
|
||||
var column = rows[i].split(",");
|
||||
//console.log('column', column);
|
||||
var output = [];
|
||||
|
||||
if (column.length > 0)
|
||||
{
|
||||
// Set the width based on the first row
|
||||
if (this.widthInTiles == 0)
|
||||
{
|
||||
// Maybe -1?
|
||||
this.widthInTiles = column.length;
|
||||
}
|
||||
|
||||
// We have a new row of tiles
|
||||
this.heightInTiles++;
|
||||
|
||||
// Parse it
|
||||
for (var c = 0; c < column.length; c++)
|
||||
{
|
||||
output[c] = parseInt(column[c]);
|
||||
}
|
||||
|
||||
this.mapData.push(output);
|
||||
layer.addColumn(column);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//console.log('final map array');
|
||||
//console.log(this.mapData);
|
||||
layer.updateBounds();
|
||||
|
||||
if (this.widthInTiles > 0)
|
||||
{
|
||||
this.widthInPixels = this.tileWidth * this.widthInTiles;
|
||||
}
|
||||
this.currentLayer = layer;
|
||||
|
||||
if (this.heightInTiles > 0)
|
||||
{
|
||||
this.heightInPixels = this.tileHeight * this.heightInTiles;
|
||||
}
|
||||
|
||||
this.boundsInTiles.setTo(0, 0, this.widthInTiles, this.heightInTiles);
|
||||
this._layers.push(layer);
|
||||
|
||||
}
|
||||
|
||||
private parseTiledJSON(data: string) {
|
||||
|
||||
//console.log('parseTiledJSON');
|
||||
|
||||
this.mapData = [];
|
||||
private parseTiledJSON(data: string, key: string) {
|
||||
|
||||
// Trim any rogue whitespace from the data
|
||||
data = data.trim();
|
||||
|
||||
// We ought to change this soon, so we have layer support, but for now let's just get it working
|
||||
var json = JSON.parse(data);
|
||||
|
||||
// Right now we assume no errors at all with the parsing (safe I know)
|
||||
this.tileWidth = json.tilewidth;
|
||||
this.tileHeight = json.tileheight;
|
||||
|
||||
// Parse the first layer only
|
||||
this.widthInTiles = json.layers[0].width;
|
||||
this.heightInTiles = json.layers[0].height;
|
||||
this.widthInPixels = this.widthInTiles * this.tileWidth;
|
||||
this.heightInPixels = this.heightInTiles * this.tileHeight;
|
||||
this.boundsInTiles.setTo(0, 0, this.widthInTiles, this.heightInTiles);
|
||||
|
||||
//console.log('width in tiles', this.widthInTiles);
|
||||
//console.log('height in tiles', this.heightInTiles);
|
||||
//console.log('width in px', this.widthInPixels);
|
||||
//console.log('height in px', this.heightInPixels);
|
||||
|
||||
// Now let's get the data
|
||||
|
||||
var c = 0;
|
||||
var row;
|
||||
|
||||
for (var i = 0; i < json.layers[0].data.length; i++)
|
||||
for (var i = 0; i < json.layers.length; i++)
|
||||
{
|
||||
if (c == 0)
|
||||
var layer: TilemapLayer = new TilemapLayer(this._game, key, Tilemap.FORMAT_TILED_JSON, json.layers[i].name, json.tilewidth, json.tileheight);
|
||||
|
||||
layer.alpha = json.layers[i].opacity;
|
||||
layer.visible = json.layers[i].visible;
|
||||
|
||||
var c = 0;
|
||||
var row;
|
||||
|
||||
for (var t = 0; t < json.layers[i].data.length; t++)
|
||||
{
|
||||
row = [];
|
||||
if (c == 0)
|
||||
{
|
||||
row = [];
|
||||
}
|
||||
|
||||
row.push(json.layers[i].data[t]);
|
||||
|
||||
c++;
|
||||
|
||||
if (c == json.layers[i].width)
|
||||
{
|
||||
layer.addColumn(row);
|
||||
c = 0;
|
||||
}
|
||||
}
|
||||
|
||||
row.push(json.layers[0].data[i]);
|
||||
layer.updateBounds();
|
||||
|
||||
c++;
|
||||
this.currentLayer = layer;
|
||||
|
||||
if (c == this.widthInTiles)
|
||||
{
|
||||
this.mapData.push(row);
|
||||
c = 0;
|
||||
}
|
||||
}
|
||||
this._layers.push(layer);
|
||||
|
||||
//console.log('mapData');
|
||||
//console.log(this.mapData);
|
||||
|
||||
}
|
||||
|
||||
public getMapSegment(area: Rectangle) {
|
||||
}
|
||||
|
||||
private createTilemapBuffers() {
|
||||
|
||||
var cams = this._game.world.getAllCameras();
|
||||
|
||||
for (var i = 0; i < cams.length; i++)
|
||||
{
|
||||
this._tilemapBuffers[cams[i].ID] = new TilemapBuffer(this._game, cams[i], this, this._texture, this._tileOffsets);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private parseTileOffsets() {
|
||||
|
||||
this._tileOffsets = [];
|
||||
|
||||
var i = 0;
|
||||
|
||||
if (this.mapFormat == Tilemap.FORMAT_TILED_JSON)
|
||||
{
|
||||
// For some reason Tiled counts from 1 not 0
|
||||
this._tileOffsets[0] = null;
|
||||
i = 1;
|
||||
}
|
||||
|
||||
for (var ty = 0; ty < this._texture.height; ty += this.tileHeight)
|
||||
{
|
||||
for (var tx = 0; tx < this._texture.width; tx += this.tileWidth)
|
||||
{
|
||||
this._tileOffsets[i] = { x: tx, y: ty };
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public get widthInPixels(): number {
|
||||
return this.currentLayer.widthInPixels;
|
||||
}
|
||||
|
||||
/*
|
||||
// Use a Signal?
|
||||
public addTilemapBuffers(camera:Camera) {
|
||||
|
||||
console.log('added new camera to tilemap');
|
||||
this._tilemapBuffers[camera.ID] = new TilemapBuffer(this._game, camera, this, this._texture, this._tileOffsets);
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
public update() {
|
||||
|
||||
// Check if any of the cameras have scrolled far enough for us to need to refresh a TilemapBuffer
|
||||
this._tilemapBuffers[0].update();
|
||||
|
||||
public get heightInPixels(): number {
|
||||
return this.currentLayer.heightInPixels;
|
||||
}
|
||||
|
||||
public renderDebugInfo(x: number, y: number, color?: string = 'rgb(255,255,255)') {
|
||||
this._tilemapBuffers[0].renderDebugInfo(x, y, color);
|
||||
}
|
||||
// Set current layer
|
||||
// Set layer order?
|
||||
// Get tile from x/y
|
||||
// Get block of tiles
|
||||
// Swap tiles around
|
||||
// Delete tiles of certain type
|
||||
// Erase tiles
|
||||
|
||||
public render(camera: Camera, cameraOffsetX: number, cameraOffsetY: number): bool {
|
||||
|
||||
if (this.visible === false || this.scale.x == 0 || this.scale.y == 0 || this.alpha < 0.1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this._dx = cameraOffsetX + (this.bounds.x - camera.worldView.x);
|
||||
this._dy = cameraOffsetY + (this.bounds.y - camera.worldView.y);
|
||||
|
||||
this._dx = Math.round(this._dx);
|
||||
this._dy = Math.round(this._dy);
|
||||
|
||||
if (this._tilemapBuffers[camera.ID])
|
||||
{
|
||||
//this._tilemapBuffers[camera.ID].render(this._dx, this._dy);
|
||||
this._tilemapBuffers[camera.ID].render(cameraOffsetX, cameraOffsetY);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user