New 16px textures, player sprite, entity system.

v5stable
Ondřej Hruška 11 years ago
parent 3e69505787
commit 6761b69b63
  1. BIN
      res/img/dudes.png
  2. BIN
      res/img/dudes.xcf
  3. BIN
      res/img/tiles16.png
  4. BIN
      res/img/tiles16.xcf
  5. 100
      src/mightypork/gamecore/render/Render.java
  6. 1
      src/mightypork/gamecore/render/Screenshot.java
  7. 39
      src/mightypork/gamecore/render/textures/TxQuad.java
  8. 19
      src/mightypork/gamecore/render/textures/TxSheet.java
  9. 51
      src/mightypork/rogue/Res.java
  10. 139
      src/mightypork/rogue/screens/ingame/WorldLayer.java
  11. 2
      src/mightypork/rogue/screens/ingame/WorldRenderer.java
  12. 4
      src/mightypork/rogue/world/MapGenerator.java
  13. 37
      src/mightypork/rogue/world/PathStep.java
  14. 46
      src/mightypork/rogue/world/PlayerControl.java
  15. 23
      src/mightypork/rogue/world/World.java
  16. 12
      src/mightypork/rogue/world/WorldPos.java
  17. 113
      src/mightypork/rogue/world/entity/Entity.java
  18. 13
      src/mightypork/rogue/world/entity/models/EntityModel.java
  19. 28
      src/mightypork/rogue/world/entity/models/EntityMoveListener.java
  20. 8
      src/mightypork/rogue/world/entity/models/PlayerModel.java
  21. 9
      src/mightypork/rogue/world/entity/renderers/EntityRenderer.java
  22. 10
      src/mightypork/rogue/world/entity/renderers/NullEntityRenderer.java
  23. 43
      src/mightypork/rogue/world/entity/renderers/PlayerRenderer.java
  24. 64
      src/mightypork/rogue/world/level/Level.java
  25. 2
      src/mightypork/rogue/world/level/render/TileRenderContext.java
  26. 21
      src/mightypork/rogue/world/tile/Tile.java
  27. 33
      src/mightypork/rogue/world/tile/Tiles.java
  28. 6
      src/mightypork/rogue/world/tile/models/AbstractNullTile.java
  29. 6
      src/mightypork/rogue/world/tile/models/Floor.java
  30. 3
      src/mightypork/rogue/world/tile/models/TileModel.java
  31. 7
      src/mightypork/rogue/world/tile/models/Wall.java
  32. 62
      src/mightypork/rogue/world/tile/renderers/BasicTileRenderer.java

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

@ -5,11 +5,9 @@ import static org.lwjgl.opengl.GL11.*;
import java.io.IOException; import java.io.IOException;
import mightypork.gamecore.audio.players.EffectPlayer;
import mightypork.gamecore.render.textures.FilterMode; import mightypork.gamecore.render.textures.FilterMode;
import mightypork.gamecore.render.textures.GLTexture; import mightypork.gamecore.render.textures.GLTexture;
import mightypork.gamecore.render.textures.TxQuad; import mightypork.gamecore.render.textures.TxQuad;
import mightypork.rogue.Res;
import mightypork.util.constraints.rect.Rect; import mightypork.util.constraints.rect.Rect;
import mightypork.util.constraints.rect.caching.RectDigest; import mightypork.util.constraints.rect.caching.RectDigest;
import mightypork.util.constraints.vect.Vect; import mightypork.util.constraints.vect.Vect;
@ -430,93 +428,69 @@ public class Render {
* Render textured rect * Render textured rect
* *
* @param quad rectangle (px) * @param quad rectangle (px)
* @param uvs texture coords rectangle (0-1) * @param txquad texture quad
* @param texture texture instance */
public static void quadTextured(Rect quad, TxQuad txquad)
{
quadTextured(quad, txquad, Color.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param txquad texture instance
* @param tint color tint * @param tint color tint
*/ */
public static void quadTextured(Rect quad, Rect uvs, GLTexture texture, Color tint) public static void quadTextured(Rect quad, TxQuad txquad, Color tint)
{ {
if (!batchTexturedQuadMode) { if (!batchTexturedQuadMode) {
glEnable(GL_TEXTURE_2D); glEnable(GL_TEXTURE_2D);
texture.bind(); txquad.tx.bind();
glBegin(GL_QUADS); glBegin(GL_QUADS);
} }
setColor(tint); setColor(tint);
final RectDigest q = quad.digest(); final RectDigest q = quad.digest();
final RectDigest u = uvs.digest(); final RectDigest u = txquad.uvs.digest();
final double w = texture.getWidth01(); double tL = u.left, tR = u.right, tT = u.top, tB = u.bottom;
final double h = texture.getHeight01();
// handle flip
if (txquad.isFlippedY()) {
final double swap = tT;
tT = tB;
tB = swap;
}
if (txquad.isFlippedX()) {
final double swap = tL;
tL = tR;
tR = swap;
}
final double w = txquad.tx.getWidth01();
final double h = txquad.tx.getHeight01();
// quad with texture // quad with texture
glTexCoord2d(u.x * w, u.bottom * h); glTexCoord2d(tL * w, tB * h);
glVertex2d(q.left, q.bottom); glVertex2d(q.left, q.bottom);
glTexCoord2d(u.right * w, u.bottom * h); glTexCoord2d(tR * w, tB * h);
glVertex2d(q.right, q.bottom); glVertex2d(q.right, q.bottom);
glTexCoord2d(u.right * w, u.top * h); glTexCoord2d(tR * w, tT * h);
glVertex2d(q.right, q.top); glVertex2d(q.right, q.top);
glTexCoord2d(u.left * w, u.top * h); glTexCoord2d(tL * w, tT * h);
glVertex2d(q.left, q.top); glVertex2d(q.left, q.top);
if (!batchTexturedQuadMode) glEnd(); if (!batchTexturedQuadMode) glEnd();
} }
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param uvs texture coords rectangle (px)
* @param texture texture instance
*/
public static void quadTextured(Rect quad, Rect uvs, GLTexture texture)
{
quadTextured(quad, uvs, texture, Color.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param texture texture instance
*/
public static void quadTextured(Rect quad, GLTexture texture)
{
quadTextured(quad, Rect.ONE, texture, Color.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param txquad texture quad
*/
public static void quadTextured(Rect quad, TxQuad txquad)
{
quadTextured(quad, txquad, Color.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param txquad texture instance
* @param tint color tint
*/
public static void quadTextured(Rect quad, TxQuad txquad, Color tint)
{
quadTextured(quad, txquad.uvs, txquad.tx, tint);
}
/** /**
* Setup Ortho projection for 2D graphics * Setup Ortho projection for 2D graphics
* *

@ -75,6 +75,7 @@ public class Screenshot {
*/ */
public void save(File file) throws IOException public void save(File file) throws IOException
{ {
file.getParentFile().mkdirs();
ImageIO.write(getImage(), "PNG", file); ImageIO.write(getImage(), "PNG", file);
} }
} }

@ -17,6 +17,9 @@ public class TxQuad {
/** Coords in texture (0-1) */ /** Coords in texture (0-1) */
public final RectConst uvs; public final RectConst uvs;
private boolean flipX;
private boolean flipY;
/** /**
* TxQuad from origin and size in pixels * TxQuad from origin and size in pixels
@ -88,6 +91,8 @@ public class TxQuad {
{ {
this.tx = txQuad.tx; this.tx = txQuad.tx;
this.uvs = txQuad.uvs; this.uvs = txQuad.uvs;
this.flipX = txQuad.flipX;
this.flipY = txQuad.flipY;
} }
@ -113,4 +118,38 @@ public class TxQuad {
{ {
return new TxSheet(this, width, height); return new TxSheet(this, width, height);
} }
/**
* @return copy flipped X
*/
public TxQuad flipX()
{
final TxQuad copy = new TxQuad(this);
copy.flipX ^= true;
return copy;
}
/**
* @return copy flipped Y
*/
public TxQuad flipY()
{
final TxQuad copy = new TxQuad(this);
copy.flipY ^= true;
return copy;
}
public boolean isFlippedY()
{
return flipY;
}
public boolean isFlippedX()
{
return flipX;
}
} }

@ -3,6 +3,8 @@ package mightypork.gamecore.render.textures;
import java.util.Random; import java.util.Random;
import mightypork.util.logging.Log;
/** /**
* Basic sprite sheet * Basic sprite sheet
@ -39,6 +41,18 @@ public class TxSheet {
} }
/**
* Get a quad based on ratio 0-1 (0: first, 1: last)
*
* @param ratio ratio
* @return quad
*/
public TxQuad getQuad(double ratio)
{
return getQuad((int) Math.round((count - 1) * ratio));
}
/** /**
* Get quad of index * Get quad of index
* *
@ -47,8 +61,9 @@ public class TxSheet {
*/ */
public TxQuad getQuad(int index) public TxQuad getQuad(int index)
{ {
if (index < 0 || index > count) { if (index < 0 || index >= count) {
throw new IndexOutOfBoundsException("Index out of bounds: " + index + ", allowed: 0.." + count); Log.w("Index out of bounds: " + index + ", allowed: 0.." + count);
index = index % count;
} }
// lazy - init only when needed // lazy - init only when needed

@ -61,10 +61,13 @@ public final class Res {
private static void loadTextures() private static void loadTextures()
{ {
GLTexture texture; GLTexture texture;
QuadGrid tiles;
// tests
texture = textures.loadTexture("test.kitten", "/res/img/kitten.png", FilterMode.LINEAR, WrapMode.CLAMP); texture = textures.loadTexture("test.kitten", "/res/img/kitten.png", FilterMode.LINEAR, WrapMode.CLAMP);
texture = textures.loadTexture("test.kitten2", "/res/img/kitten_npot.png", FilterMode.LINEAR, WrapMode.CLAMP); texture = textures.loadTexture("test.kitten2", "/res/img/kitten_npot.png", FilterMode.LINEAR, WrapMode.CLAMP);
// gui
texture = textures.loadTexture("gui1", "/res/img/gui1.png", FilterMode.NEAREST, WrapMode.CLAMP); texture = textures.loadTexture("gui1", "/res/img/gui1.png", FilterMode.NEAREST, WrapMode.CLAMP);
final QuadGrid gui = texture.grid(4, 4); final QuadGrid gui = texture.grid(4, 4);
textures.addQuad("item_frame", gui.makeQuad(0, 0)); textures.addQuad("item_frame", gui.makeQuad(0, 0));
@ -76,19 +79,41 @@ public final class Res {
textures.addQuad("xp_off", gui.makeQuad(.5, 1.5, .5, .5)); textures.addQuad("xp_off", gui.makeQuad(.5, 1.5, .5, .5));
textures.addQuad("panel", gui.makeQuad(0, 3.75, 4, .25)); textures.addQuad("panel", gui.makeQuad(0, 3.75, 4, .25));
texture = textures.loadTexture("tiles", "/res/img/tiles.png", FilterMode.NEAREST, WrapMode.CLAMP); // huge sheet
final QuadGrid tiles = texture.grid(32, 32); // texture = textures.loadTexture("tiles", "/res/img/tiles.png", FilterMode.NEAREST, WrapMode.CLAMP);
// tiles = texture.grid(32, 32);
textures.addSheet("tile.wall.mossy_bricks", tiles.makeSheet(4, 0, 7, 1)); // textures.addSheet("tile.wall.mossy_bricks", tiles.makeSheet(4, 0, 7, 1));
textures.addSheet("tile.wall.small_bricks", tiles.makeSheet(0, 0, 4, 1)); // textures.addSheet("tile.wall.small_bricks", tiles.makeSheet(0, 0, 4, 1));
textures.addSheet("tile.floor.mossy_bricks", tiles.makeSheet(16, 5, 7, 1)); // textures.addSheet("tile.floor.mossy_bricks", tiles.makeSheet(16, 5, 7, 1));
textures.addSheet("tile.floor.rect_bricks", tiles.makeSheet(23, 5, 4, 1)); // textures.addSheet("tile.floor.rect_bricks", tiles.makeSheet(23, 5, 4, 1));
textures.addSheet("tile.wall.sandstone", tiles.makeSheet(0, 3, 10, 1)); // textures.addSheet("tile.wall.sandstone", tiles.makeSheet(0, 3, 10, 1));
textures.addSheet("tile.floor.sandstone", tiles.makeSheet(0, 6, 10, 1)); // textures.addSheet("tile.floor.sandstone", tiles.makeSheet(0, 6, 10, 1));
textures.addSheet("tile.wall.brown_cobble", tiles.makeSheet(0, 8, 8, 1)); // textures.addSheet("tile.wall.brown_cobble", tiles.makeSheet(0, 8, 8, 1));
textures.addSheet("tile.floor.brown_cobble", tiles.makeSheet(0, 11, 9, 1)); // textures.addSheet("tile.floor.brown_cobble", tiles.makeSheet(0, 11, 9, 1));
textures.addSheet("tile.floor.crystal", tiles.makeSheet(4, 5, 6, 1)); // textures.addSheet("tile.floor.crystal", tiles.makeSheet(4, 5, 6, 1));
textures.addSheet("tile.wall.crystal", tiles.makeSheet(12, 2, 14, 1)); // textures.addSheet("tile.wall.crystal", tiles.makeSheet(12, 2, 14, 1));
// sprites
texture = textures.loadTexture("mob", "/res/img/dudes.png", FilterMode.NEAREST, WrapMode.CLAMP);
tiles = texture.grid(8, 8);
textures.addSheet("player", tiles.makeSheet(0, 0, 4, 1));
// small sheet
texture = textures.loadTexture("tiles16", "/res/img/tiles16.png", FilterMode.NEAREST, WrapMode.CLAMP);
tiles = texture.grid(8, 8);
textures.addSheet("tile16.floor.dark", tiles.makeSheet(0, 1, 5, 1));
textures.addSheet("tile16.wall.brick", tiles.makeSheet(0, 0, 5, 1));
textures.addQuad("tile16.shadow.n", tiles.makeQuad(0, 7));
textures.addQuad("tile16.shadow.s", tiles.makeQuad(0, 7).flipY());
textures.addQuad("tile16.shadow.w", tiles.makeQuad(2, 7));
textures.addQuad("tile16.shadow.e", tiles.makeQuad(2, 7).flipX());
textures.addQuad("tile16.shadow.nw", tiles.makeQuad(1, 7));
textures.addQuad("tile16.shadow.ne", tiles.makeQuad(1, 7).flipX());
textures.addQuad("tile16.shadow.sw", tiles.makeQuad(1, 7).flipY());
textures.addQuad("tile16.shadow.se", tiles.makeQuad(1, 7).flipY().flipX());
} }

@ -7,9 +7,16 @@ import java.util.Random;
import mightypork.gamecore.gui.screens.Screen; import mightypork.gamecore.gui.screens.Screen;
import mightypork.gamecore.gui.screens.ScreenLayer; import mightypork.gamecore.gui.screens.ScreenLayer;
import mightypork.gamecore.input.InputSystem;
import mightypork.gamecore.input.KeyStroke;
import mightypork.gamecore.input.Keys;
import mightypork.rogue.Paths; import mightypork.rogue.Paths;
import mightypork.rogue.world.MapGenerator; import mightypork.rogue.world.MapGenerator;
import mightypork.rogue.world.PlayerControl;
import mightypork.rogue.world.World; import mightypork.rogue.world.World;
import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.entity.models.EntityMoveListener;
import mightypork.rogue.world.level.Level;
import mightypork.util.ion.Ion; import mightypork.util.ion.Ion;
@ -46,63 +53,81 @@ public class WorldLayer extends ScreenLayer {
wr.setRect(root); wr.setRect(root);
root.add(wr); root.add(wr);
// bindKey(new KeyStroke(true, Keys.LEFT), new Runnable() { final PlayerControl c = w.getPlayerControl();
//
// @Override bindKey(new KeyStroke(true, Keys.LEFT), new Runnable() {
// public void run()
// { @Override
// w.getPlayer().walk(-1, 0); public void run()
// } {
// }); c.walkWest();
// bindKey(new KeyStroke(true, Keys.RIGHT), new Runnable() { }
// });
// @Override
// public void run() bindKey(new KeyStroke(true, Keys.RIGHT), new Runnable() {
// {
// w.getPlayer().walk(1, 0); @Override
// } public void run()
// }); {
// bindKey(new KeyStroke(true, Keys.UP), new Runnable() { c.walkEast();
// }
// @Override });
// public void run()
// { bindKey(new KeyStroke(true, Keys.UP), new Runnable() {
// w.getPlayer().walk(0, -1);
// } @Override
// }); public void run()
// bindKey(new KeyStroke(true, Keys.DOWN), new Runnable() { {
// c.walkNorth();
// @Override }
// public void run() });
// {
// w.getPlayer().walk(0, 1); bindKey(new KeyStroke(true, Keys.DOWN), new Runnable() {
// }
// }); @Override
// bindKey(new KeyStroke(true, Keys.SPACE), new Runnable() { public void run()
// {
// @Override c.walkSouth();
// public void run() }
// { });
// w.getPlayer().walk(5, 5);
// } c.addMoveListener(new EntityMoveListener() {
// });
// private void tryGo(Entity entity)
// w.getPlayer().setMoveListener(new Runnable() { {
// if (InputSystem.isKeyDown(Keys.LEFT)) {
// @Override c.walkWest();
// public void run() } else if (InputSystem.isKeyDown(Keys.RIGHT)) {
// { c.walkEast();
// if (InputSystem.isKeyDown(Keys.LEFT)) { } else if (InputSystem.isKeyDown(Keys.UP)) {
// w.getPlayer().walk(-1, 0); c.walkNorth();
// } else if (InputSystem.isKeyDown(Keys.RIGHT)) { } else if (InputSystem.isKeyDown(Keys.DOWN)) {
// w.getPlayer().walk(1, 0); c.walkSouth();
// } else if (InputSystem.isKeyDown(Keys.UP)) { }
// w.getPlayer().walk(0, -1); }
// } else if (InputSystem.isKeyDown(Keys.DOWN)) {
// w.getPlayer().walk(0, 1);
// } @Override
// } public void onStepFinished(Entity entity, World world, Level level)
// }); {
entity.cancelPath(); // halt
tryGo(entity);
}
@Override
public void onPathFinished(Entity entity, World world, Level level)
{
entity.cancelPath(); // halt
tryGo(entity);
}
@Override
public void onPathAborted(Entity entity, World world, Level level)
{
}
});
} }

@ -48,7 +48,7 @@ public class WorldRenderer extends InputComponent implements Updateable {
@Override @Override
protected void renderComponent() protected void renderComponent()
{ {
world.render(this, 8, 6, 64); world.render(this, 8, 6, 110);
Render.quadGradH(leftShadow, RGB.BLACK, RGB.NONE); Render.quadGradH(leftShadow, RGB.BLACK, RGB.NONE);
Render.quadGradH(rightShadow, RGB.NONE, RGB.BLACK); Render.quadGradH(rightShadow, RGB.NONE, RGB.BLACK);

@ -21,8 +21,8 @@ public class MapGenerator {
final World w = new World(); final World w = new World();
w.setSeed(seed); w.setSeed(seed);
w.addLevel(createLevel(rand.nextLong(), Tiles.CRYSTAL_FLOOR, Tiles.CRYSTAL_WALL)); w.addLevel(createLevel(rand.nextLong(), Tiles.FLOOR_DARK, Tiles.WALL_BRICK));
w.addLevel(createLevel(rand.nextLong(), Tiles.BRCOBBLE_FLOOR, Tiles.BRCOBBLE_WALL)); //w.addLevel(createLevel(rand.nextLong(), Tiles.BRCOBBLE_FLOOR, Tiles.BRCOBBLE_WALL));
// TODO place on start position // TODO place on start position
w.createPlayer(10, 10, 0); w.createPlayer(10, 10, 0);

@ -10,6 +10,37 @@ import mightypork.util.ion.IonOutput;
public class PathStep implements IonBinary { public class PathStep implements IonBinary {
public static final PathStep NORTH = new PathStep(0, -1);
public static final PathStep SOUTH = new PathStep(0, 1);
public static final PathStep EAST = new PathStep(1, 0);
public static final PathStep WEST = new PathStep(-1, 0);
public static final PathStep NORTH_EAST = new PathStep(1, -1);
public static final PathStep NORTH_WEST = new PathStep(-1, -1);
public static final PathStep SOUTH_EAST = new PathStep(1, 1);
public static final PathStep SOUTH_WEST = new PathStep(-1, 1);
public static final PathStep NONE = new PathStep(0, 0);
public static PathStep make(int x, int y)
{
x = x < 0 ? -1 : x > 0 ? 1 : 0;
y = y < 0 ? -1 : y > 0 ? 1 : 0;
if (x == 0 && y == -1) return NORTH;
if (x == 0 && y == 1) return SOUTH;
if (x == -1 && y == 0) return WEST;
if (x == 1 && y == 0) return EAST;
if (x == -1 && y == -1) return NORTH_WEST;
if (x == 1 && y == -1) return NORTH_EAST;
if (x == -1 && y == 1) return SOUTH_WEST;
if (x == 1 && y == 1) return SOUTH_EAST;
if (x == 0 && y == 0) return NONE;
return new PathStep(x, y);
}
public static final int ION_MARK = 0; public static final int ION_MARK = 0;
public int x; public int x;
@ -18,10 +49,8 @@ public class PathStep implements IonBinary {
public PathStep(int x, int y) public PathStep(int x, int y)
{ {
this.x = x < 1 ? -1 : x > 0 ? 1 : 0; this.x = x < 0 ? -1 : x > 0 ? 1 : 0;
this.y = y < 1 ? -1 : y > 0 ? 1 : 0; this.y = y < 0 ? -1 : y > 0 ? 1 : 0;
y = (int) Math.signum(x);
} }

@ -0,0 +1,46 @@
package mightypork.rogue.world;
import mightypork.rogue.world.entity.models.EntityMoveListener;
public class PlayerControl {
private final World world;
public PlayerControl(World w)
{
this.world = w;
}
public void walkNorth()
{
world.playerEntity.addStep(PathStep.NORTH);
}
public void walkSouth()
{
world.playerEntity.addStep(PathStep.SOUTH);
}
public void walkEast()
{
world.playerEntity.addStep(PathStep.EAST);
}
public void walkWest()
{
world.playerEntity.addStep(PathStep.WEST);
}
public void addMoveListener(EntityMoveListener eml)
{
world.playerEntity.addMoveListener(eml);
}
}

@ -22,7 +22,10 @@ public class World implements IonBundled, Updateable {
private final ArrayList<Level> levels = new ArrayList<>(); private final ArrayList<Level> levels = new ArrayList<>();
private final PlayerInfo player = new PlayerInfo(); final PlayerInfo player = new PlayerInfo();
Entity playerEntity;
private final PlayerControl control = new PlayerControl(this);
private long seed; // world seed private long seed; // world seed
private int eid; // next entity ID private int eid; // next entity ID
@ -57,7 +60,7 @@ public class World implements IonBundled, Updateable {
@Override @Override
public void update(double delta) public void update(double delta)
{ {
getCurrentLevel().update(delta); getCurrentLevel().update(this, delta);
} }
@ -89,14 +92,14 @@ public class World implements IonBundled, Updateable {
} }
// make entity // make entity
int playerEid = getNewEID(); final int playerEid = getNewEID();
final Entity entity = Entities.PLAYER.createEntity(playerEid, new WorldPos(x, y)); playerEntity = Entities.PLAYER.createEntity(playerEid, new WorldPos(x, y));
player.setLevel(level); player.setLevel(level);
player.setEID(playerEid); player.setEID(playerEid);
levels.get(level).addEntity(entity); levels.get(level).addEntity(playerEntity);
} }
@ -108,9 +111,9 @@ public class World implements IonBundled, Updateable {
* @param yTiles Desired nr of tiles vertically * @param yTiles Desired nr of tiles vertically
* @param minSize minimum tile size * @param minSize minimum tile size
*/ */
public void render(RectBound viewport, final int yTiles, final int xTiles, final int minSize) public void render(RectBound viewport, final int xTiles, final int yTiles, final int minSize)
{ {
getCurrentLevel().render(player, viewport, yTiles, xTiles, minSize); getCurrentLevel().render(playerEntity.getPosition(), viewport, xTiles, yTiles, minSize);
} }
@ -118,4 +121,10 @@ public class World implements IonBundled, Updateable {
{ {
return levels.get(player.getLevel()); return levels.get(player.getLevel());
} }
public PlayerControl getPlayerControl()
{
return control;
}
} }

@ -82,6 +82,18 @@ public class WorldPos implements IonBundled, Updateable {
} }
public double getVisualXOffset()
{
return walkOffset.x();
}
public double getVisualYOffset()
{
return walkOffset.y();
}
public void setTo(int x, int y) public void setTo(int x, int y)
{ {
this.x = x; this.x = x;

@ -2,14 +2,18 @@ package mightypork.rogue.world.entity;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.Queue; import java.util.Queue;
import mightypork.rogue.world.PathStep; import mightypork.rogue.world.PathStep;
import mightypork.rogue.world.World; import mightypork.rogue.world.World;
import mightypork.rogue.world.WorldPos; import mightypork.rogue.world.WorldPos;
import mightypork.rogue.world.entity.models.EntityModel; import mightypork.rogue.world.entity.models.EntityModel;
import mightypork.rogue.world.entity.models.EntityMoveListener;
import mightypork.rogue.world.level.Level; import mightypork.rogue.world.level.Level;
import mightypork.rogue.world.level.render.EntityRenderContext;
import mightypork.util.ion.IonBinary; import mightypork.util.ion.IonBinary;
import mightypork.util.ion.IonBundle; import mightypork.util.ion.IonBundle;
import mightypork.util.ion.IonBundled; import mightypork.util.ion.IonBundled;
@ -22,23 +26,32 @@ import mightypork.util.ion.IonOutput;
* *
* @author MightyPork * @author MightyPork
*/ */
public final class Entity implements IonBinary, IonBundled { public final class Entity implements IonBinary, IonBundled, EntityMoveListener {
// binary & bundled - binary stores via a bundle // binary & bundled - binary stores via a bundle
public static final int ION_MARK = 52; public static final int ION_MARK = 52;
private final WorldPos position = new WorldPos(); private final WorldPos position = new WorldPos(); // saved
/** Entity ID */ /** Entity ID */
private int eid = 0; private int eid = 0; // saved
/** Model ID */ /** Model ID */
private int id; private int id; // saved
private final Queue<PathStep> path = new LinkedList<>(); private final Queue<PathStep> path = new LinkedList<>(); // saved
private EntityModel model; private EntityModel model;
private final IonBundle metadata = new IonBundle(); public final IonBundle metadata = new IonBundle(); // saved
public final IonBundle tmpdata = new IonBundle(); // NOT saved
// used for rendering "facing" sprite
public int lastXMove = 1;
public int lastYMove = 1;
private final List<EntityMoveListener> moveListeners = new ArrayList<>();
private boolean walking = false;
public Entity(int eid, WorldPos pos, EntityModel entityModel) public Entity(int eid, WorldPos pos, EntityModel entityModel)
@ -52,6 +65,10 @@ public final class Entity implements IonBinary, IonBundled {
private void setModel(EntityModel entityModel) private void setModel(EntityModel entityModel)
{ {
// replace listener
if (model != null) moveListeners.remove(model);
moveListeners.add(entityModel);
this.id = entityModel.id; this.id = entityModel.id;
this.model = entityModel; this.model = entityModel;
} }
@ -93,8 +110,11 @@ public final class Entity implements IonBinary, IonBundled {
bundle.putBundled("pos", position); bundle.putBundled("pos", position);
bundle.putSequence("steps", path); bundle.putSequence("steps", path);
bundle.put("eid", eid); bundle.put("eid", eid);
if (model.hasMetadata()) {
bundle.put("metadata", metadata); bundle.put("metadata", metadata);
} }
}
@Override @Override
@ -110,11 +130,16 @@ public final class Entity implements IonBinary, IonBundled {
bundle.loadSequence("path", path); bundle.loadSequence("path", path);
eid = bundle.get("eid", eid); eid = bundle.get("eid", eid);
if (model.hasMetadata()) {
metadata.clear(); metadata.clear();
bundle.loadBundle("metadata", metadata); bundle.loadBundle("metadata", metadata);
} }
}
/**
* @return unique entity id
*/
public int getEID() public int getEID()
{ {
return eid; return eid;
@ -150,22 +175,52 @@ public final class Entity implements IonBinary, IonBundled {
position.update(delta); position.update(delta);
} }
if (position.isFinished()) { if (walking && position.isFinished()) {
walking = false;
onStepFinished(this, world, level);
if (path.isEmpty()) {
onPathFinished(this, world, level);
}
}
if (!walking && !path.isEmpty()) {
model.onStepFinished(this, world, level); walking = true;
if (!path.isEmpty()) {
// get next step to walk
final PathStep step = path.poll(); final PathStep step = path.poll();
position.walk(step.x, step.y, getStepTime());
final int projX = position.x + step.x, projY = position.y + step.y;
if (!level.canWalkInto(projX, projY)) {
cancelPath();
onPathAborted(this, world, level);
walking = false;
} else { } else {
// notify AI or whatever
model.onPathFinished(this, world, level); // tmp for renderer
if (step.x != 0) lastXMove = step.x;
if (step.y != 0) lastYMove = step.y;
position.walk(step.x, step.y, getStepTime());
level.occupyTile(projX, projY);
level.freeTile(position.x, position.y);
}
} }
if (!walking) {
model.update(this, level, delta);
} }
} }
public void render(EntityRenderContext context)
{
model.renderer.render(this, context);
}
public boolean isPathFinished() public boolean isPathFinished()
{ {
return position.isFinished() && path.isEmpty(); return position.isFinished() && path.isEmpty();
@ -189,4 +244,36 @@ public final class Entity implements IonBinary, IonBundled {
return model.getStepTime(this); return model.getStepTime(this);
} }
@Override
public void onStepFinished(Entity entity, World world, Level level)
{
for (final EntityMoveListener l : moveListeners) {
l.onStepFinished(entity, world, level);
}
}
@Override
public void onPathFinished(Entity entity, World world, Level level)
{
for (final EntityMoveListener l : moveListeners) {
l.onStepFinished(entity, world, level);
}
}
@Override
public void onPathAborted(Entity entity, World world, Level level)
{
for (final EntityMoveListener l : moveListeners) {
l.onPathAborted(entity, world, level);
}
}
public void addMoveListener(EntityMoveListener listener)
{
moveListeners.add(listener);
}
} }

@ -14,7 +14,7 @@ import mightypork.rogue.world.level.Level;
* *
* @author MightyPork * @author MightyPork
*/ */
public abstract class EntityModel { public abstract class EntityModel implements EntityMoveListener {
/** Model ID */ /** Model ID */
public final int id; public final int id;
@ -45,7 +45,7 @@ public abstract class EntityModel {
/** /**
* Update entity * Entity is idle, waiting for action.
*/ */
public abstract void update(Entity entity, Level level, double delta); public abstract void update(Entity entity, Level level, double delta);
@ -57,15 +57,20 @@ public abstract class EntityModel {
/** /**
* @param entity the value is valid for * Get one path step duration (in seconds)
* @return step time (seconds)
*/ */
public abstract double getStepTime(Entity entity); public abstract double getStepTime(Entity entity);
@Override
public abstract void onStepFinished(Entity entity, World world, Level level); public abstract void onStepFinished(Entity entity, World world, Level level);
@Override
public abstract void onPathFinished(Entity entity, World world, Level level); public abstract void onPathFinished(Entity entity, World world, Level level);
@Override
public abstract void onPathAborted(Entity entity, World world, Level level);
} }

@ -0,0 +1,28 @@
package mightypork.rogue.world.entity.models;
import mightypork.rogue.world.World;
import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.level.Level;
public interface EntityMoveListener {
/**
* One step of a path finished
*/
void onStepFinished(Entity entity, World world, Level level);
/**
* Scheduled path finished
*/
void onPathFinished(Entity entity, World world, Level level);
/**
* Path was aborted (bumped into a wall or entity)
*/
void onPathAborted(Entity entity, World world, Level level);
}

@ -4,6 +4,7 @@ package mightypork.rogue.world.entity.models;
import mightypork.rogue.world.World; import mightypork.rogue.world.World;
import mightypork.rogue.world.WorldPos; import mightypork.rogue.world.WorldPos;
import mightypork.rogue.world.entity.Entity; import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.entity.renderers.PlayerRenderer;
import mightypork.rogue.world.level.Level; import mightypork.rogue.world.level.Level;
@ -20,6 +21,7 @@ public class PlayerModel extends EntityModel {
public PlayerModel(int id) public PlayerModel(int id)
{ {
super(id); super(id);
setRenderer(new PlayerRenderer("player"));
} }
@ -64,4 +66,10 @@ public class PlayerModel extends EntityModel {
public void onPathFinished(Entity entity, World world, Level level) public void onPathFinished(Entity entity, World world, Level level)
{ {
} }
@Override
public void onPathAborted(Entity entity, World world, Level level)
{
}
} }

@ -1,8 +1,15 @@
package mightypork.rogue.world.entity.renderers; package mightypork.rogue.world.entity.renderers;
public class EntityRenderer { import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.level.render.EntityRenderContext;
public abstract class EntityRenderer {
public static final EntityRenderer NONE = new NullEntityRenderer(); public static final EntityRenderer NONE = new NullEntityRenderer();
public abstract void render(Entity entity, EntityRenderContext context);
} }

@ -1,6 +1,16 @@
package mightypork.rogue.world.entity.renderers; package mightypork.rogue.world.entity.renderers;
import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.level.render.EntityRenderContext;
public class NullEntityRenderer extends EntityRenderer { public class NullEntityRenderer extends EntityRenderer {
@Override
public void render(Entity entity, EntityRenderContext context)
{
// hell no
}
} }

@ -0,0 +1,43 @@
package mightypork.rogue.world.entity.renderers;
import mightypork.gamecore.render.Render;
import mightypork.gamecore.render.textures.TxQuad;
import mightypork.gamecore.render.textures.TxSheet;
import mightypork.rogue.Res;
import mightypork.rogue.world.WorldPos;
import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.level.render.EntityRenderContext;
import mightypork.util.constraints.rect.Rect;
import mightypork.util.math.Calc;
public class PlayerRenderer extends EntityRenderer {
TxSheet sheet;
public PlayerRenderer(String sheetKey)
{
this.sheet = Res.getTxSheet(sheetKey); // expects 1x4
}
@Override
public void render(Entity entity, EntityRenderContext context)
{
TxQuad q = sheet.getQuad(Calc.frag(entity.getPosition().getProgress()));
if (entity.lastXMove == -1) q = q.flipX();
final WorldPos pos = entity.getPosition();
final Rect tileRect = context.getRectForTile(pos.x, pos.y);
final double w = tileRect.width().value();
Rect spriteRect = tileRect.move(pos.getVisualXOffset() * w, pos.getVisualYOffset() * w + w * 0.1);
spriteRect = spriteRect.shrink(w * 0.1);
Render.quadTextured(spriteRect, q);
}
}

@ -7,13 +7,12 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.lwjgl.opengl.GL11;
import mightypork.gamecore.render.Render; import mightypork.gamecore.render.Render;
import mightypork.rogue.Res; import mightypork.rogue.Res;
import mightypork.rogue.world.PlayerInfo; import mightypork.rogue.world.World;
import mightypork.rogue.world.WorldPos; import mightypork.rogue.world.WorldPos;
import mightypork.rogue.world.entity.Entity; import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.level.render.EntityRenderContext;
import mightypork.rogue.world.level.render.TileRenderContext; import mightypork.rogue.world.level.render.TileRenderContext;
import mightypork.rogue.world.tile.Tile; import mightypork.rogue.world.tile.Tile;
import mightypork.rogue.world.tile.Tiles; import mightypork.rogue.world.tile.Tiles;
@ -170,6 +169,11 @@ public class Level implements MapAccess, IonBinary {
tiles[y][x].load(in); tiles[y][x].load(in);
} }
} }
// mark tiles as occupied
for (final Entity e : entity_set) {
occupyTile(e.getPosition().x, e.getPosition().y);
}
} }
@ -201,7 +205,7 @@ public class Level implements MapAccess, IonBinary {
} }
public void update(double delta) public void update(World w, double delta)
{ {
// just update them all // just update them all
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
@ -209,6 +213,10 @@ public class Level implements MapAccess, IonBinary {
getTile(x, y).update(this, delta); getTile(x, y).update(this, delta);
} }
} }
for (final Entity e : entity_set) {
e.update(w, this, delta);
}
} }
@ -232,16 +240,8 @@ public class Level implements MapAccess, IonBinary {
* @param yTiles Desired nr of tiles vertically * @param yTiles Desired nr of tiles vertically
* @param minSize minimum tile size * @param minSize minimum tile size
*/ */
public void render(PlayerInfo playerInfo, RectBound viewport, final int yTiles, final int xTiles, final int minSize) public void render(WorldPos pos, RectBound viewport, final int xTiles, final int yTiles, final int minSize)
{ {
final WorldPos pos;
try {
pos = getEntity(playerInfo.getEID()).getPosition();
} catch (NullPointerException e) {
throw new RuntimeException("Player entity not found in level.", e);
}
final Rect r = viewport.getRect(); final Rect r = viewport.getRect();
final double vpH = r.height().value(); final double vpH = r.height().value();
final double vpW = r.width().value(); final double vpW = r.width().value();
@ -250,9 +250,9 @@ public class Level implements MapAccess, IonBinary {
final double allowedSizeW = vpW / xTiles; final double allowedSizeW = vpW / xTiles;
final double allowedSizeH = vpH / yTiles; final double allowedSizeH = vpH / yTiles;
int tileSize = (int) Math.round(Math.max(Math.min(allowedSizeH, allowedSizeW), minSize)); final int tileSize = (int) Math.round(Math.max(Math.min(allowedSizeH, allowedSizeW), minSize));
tileSize -= tileSize % 16; //tileSize -= tileSize % 8;
final VectConst vpCenter = r.center().sub(tileSize * 0.5, tileSize).freeze(); // 0.5 to center, 1 to move up (down is teh navbar) final VectConst vpCenter = r.center().sub(tileSize * 0.5, tileSize).freeze(); // 0.5 to center, 1 to move up (down is teh navbar)
@ -280,7 +280,7 @@ public class Level implements MapAccess, IonBinary {
// batch rendering of the tiles // batch rendering of the tiles
if (USE_BATCH_RENDERING) { if (USE_BATCH_RENDERING) {
Render.enterBatchTexturedQuadMode(Res.getTexture("tiles")); Render.enterBatchTexturedQuadMode(Res.getTexture("tiles16"));
} }
for (trc.y = y1; trc.y <= y2; trc.y++) { for (trc.y = y1; trc.y <= y2; trc.y++) {
@ -299,6 +299,12 @@ public class Level implements MapAccess, IonBinary {
trc.renderItems(); trc.renderItems();
} }
} }
// render entities
final EntityRenderContext erc = new EntityRenderContext(this, mapRect);
for (final Entity e : entity_set) {
e.render(erc);
}
} }
@ -327,4 +333,30 @@ public class Level implements MapAccess, IonBinary {
final Entity removed = entity_map.remove(eid); final Entity removed = entity_map.remove(eid);
entity_set.remove(removed); entity_set.remove(removed);
} }
public boolean canWalkInto(int x, int y)
{
final Tile t = getTile(x, y);
return t.isWalkable() && !t.isOccupied();
}
/**
* Mark tile as occupied by an entity
*/
public void occupyTile(int x, int y)
{
getTile(x, y).setOccupied(true);
}
/**
* Mark tile as free (no longet occupied)
*/
public void freeTile(int x, int y)
{
getTile(x, y).setOccupied(false);
}
} }

@ -66,11 +66,13 @@ public final class TileRenderContext extends MapRenderContext implements RectBou
map.getTile(x, y).renderTile(this); map.getTile(x, y).renderTile(this);
} }
public void renderItems() public void renderItems()
{ {
map.getTile(x, y).renderItems(this); map.getTile(x, y).renderItems(this);
} }
/** /**
* Rect of the current tile to draw * Rect of the current tile to draw
*/ */

@ -34,6 +34,13 @@ public final class Tile implements IonBinary {
/** persistent field for model, reflected by renderer */ /** persistent field for model, reflected by renderer */
public final IonBundle metadata = new IonBundle(); public final IonBundle metadata = new IonBundle();
// temporary flag for map.
private boolean occupied;
// for renderer of AO shadows
public byte shadows;
public boolean shadowsComputed;
public Tile(int id) public Tile(int id)
{ {
@ -75,6 +82,7 @@ public final class Tile implements IonBinary {
/** /**
* Render items * Render items
*
* @param context * @param context
*/ */
public void renderItems(TileRenderContext context) public void renderItems(TileRenderContext context)
@ -85,7 +93,6 @@ public final class Tile implements IonBinary {
} }
@Override @Override
public void save(IonOutput out) throws IOException public void save(IonOutput out) throws IOException
{ {
@ -156,4 +163,16 @@ public final class Tile implements IonBinary {
{ {
return ION_MARK; return ION_MARK;
} }
public boolean isOccupied()
{
return occupied;
}
public void setOccupied(boolean occupied)
{
this.occupied = occupied;
}
} }

@ -20,21 +20,24 @@ public final class Tiles {
public static final TileModel NULL_SOLID = new NullWall(0); public static final TileModel NULL_SOLID = new NullWall(0);
public static final TileModel NULL_EMPTY = new NullFloor(1); public static final TileModel NULL_EMPTY = new NullFloor(1);
public static final TileModel BRICK_FLOOR_VINES = new Floor(2).setTexture("tile.floor.mossy_bricks"); public static final TileModel FLOOR_DARK = new Floor(2).setTexture("tile16.floor.dark");
public static final TileModel BRICK_WALL_VINES = new Wall(3).setTexture("tile.wall.mossy_bricks"); public static final TileModel WALL_BRICK = new Wall(3).setTexture("tile16.wall.brick");
public static final TileModel BRICK_FLOOR_RECT = new Floor(4).setTexture("tile.floor.rect_bricks");
public static final TileModel BRICK_WALL_SMALL = new Wall(5).setTexture("tile.wall.small_bricks"); // public static final TileModel BRICK_FLOOR_VINES = new Floor(2).setTexture("tile.floor.mossy_bricks");
// public static final TileModel BRICK_WALL_VINES = new Wall(3).setTexture("tile.wall.mossy_bricks");
public static final TileModel SANDSTONE_FLOOR = new Floor(6).setTexture("tile.floor.sandstone"); //
public static final TileModel SANDSTONE_WALL = new Wall(7).setTexture("tile.wall.sandstone"); // public static final TileModel BRICK_FLOOR_RECT = new Floor(4).setTexture("tile.floor.rect_bricks");
// public static final TileModel BRICK_WALL_SMALL = new Wall(5).setTexture("tile.wall.small_bricks");
public static final TileModel BRCOBBLE_FLOOR = new Floor(8).setTexture("tile.floor.brown_cobble"); //
public static final TileModel BRCOBBLE_WALL = new Wall(9).setTexture("tile.wall.brown_cobble"); // public static final TileModel SANDSTONE_FLOOR = new Floor(6).setTexture("tile.floor.sandstone");
// public static final TileModel SANDSTONE_WALL = new Wall(7).setTexture("tile.wall.sandstone");
public static final TileModel CRYSTAL_FLOOR = new Floor(10).setTexture("tile.floor.crystal"); //
public static final TileModel CRYSTAL_WALL = new Wall(11).setTexture("tile.wall.crystal"); // public static final TileModel BRCOBBLE_FLOOR = new Floor(8).setTexture("tile.floor.brown_cobble");
// public static final TileModel BRCOBBLE_WALL = new Wall(9).setTexture("tile.wall.brown_cobble");
//
// public static final TileModel CRYSTAL_FLOOR = new Floor(10).setTexture("tile.floor.crystal");
// public static final TileModel CRYSTAL_WALL = new Wall(11).setTexture("tile.wall.crystal");
public static void register(int id, TileModel model) public static void register(int id, TileModel model)
{ {

@ -55,4 +55,10 @@ public abstract class AbstractNullTile extends SimpleTile {
@Override @Override
public abstract boolean isWalkable(); public abstract boolean isWalkable();
@Override
public boolean doesCastShadow()
{
return false;
}
} }

@ -27,4 +27,10 @@ public class Floor extends SimpleTile {
return true; return true;
} }
@Override
public boolean doesCastShadow()
{
return false;
}
} }

@ -63,6 +63,9 @@ public abstract class TileModel {
public abstract boolean isWalkable(); public abstract boolean isWalkable();
public abstract boolean doesCastShadow();
public boolean isNullTile() public boolean isNullTile()
{ {
return false; return false;

@ -27,4 +27,11 @@ public class Wall extends SimpleTile {
return false; return false;
} }
@Override
public boolean doesCastShadow()
{
return true;
}
} }

@ -2,25 +2,85 @@ package mightypork.rogue.world.tile.renderers;
import mightypork.gamecore.render.Render; import mightypork.gamecore.render.Render;
import mightypork.gamecore.render.textures.TxQuad;
import mightypork.gamecore.render.textures.TxSheet; import mightypork.gamecore.render.textures.TxSheet;
import mightypork.rogue.Res; import mightypork.rogue.Res;
import mightypork.rogue.world.level.render.TileRenderContext; import mightypork.rogue.world.level.render.TileRenderContext;
import mightypork.rogue.world.tile.Tile;
import mightypork.util.constraints.rect.Rect;
public class BasicTileRenderer extends TileRenderer { public class BasicTileRenderer extends TileRenderer {
private final TxSheet sheet; private final TxSheet sheet;
private static boolean inited;
private static TxQuad SH_N, SH_S, SH_E, SH_W, SH_NW, SH_NE, SH_SW, SH_SE;
public BasicTileRenderer(String sheetKey) public BasicTileRenderer(String sheetKey)
{ {
this.sheet = Res.getTxSheet(sheetKey); this.sheet = Res.getTxSheet(sheetKey);
if (!inited) {
SH_N = Res.getTxQuad("tile16.shadow.n");
SH_S = Res.getTxQuad("tile16.shadow.s");
SH_E = Res.getTxQuad("tile16.shadow.e");
SH_W = Res.getTxQuad("tile16.shadow.w");
SH_NW = Res.getTxQuad("tile16.shadow.nw");
SH_NE = Res.getTxQuad("tile16.shadow.ne");
SH_SW = Res.getTxQuad("tile16.shadow.sw");
SH_SE = Res.getTxQuad("tile16.shadow.se");
}
} }
@Override @Override
public void render(TileRenderContext context) public void render(TileRenderContext context)
{ {
Render.quadTextured(context.getRect(), sheet.getRandomQuad(context.getTileNoise())); final Rect rect = context.getRect();
Render.quadTextured(rect, sheet.getRandomQuad(context.getTileNoise()));
final Tile t = context.getTile();
if (t.getModel().doesCastShadow()) return; // no shadows for wall
Tile t2;
if (!t.shadowsComputed) {
// no shadows computed yet
t.shadows = 0; // reset the mask
int move = 0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
if (x == 0 && y == 0) continue;
t2 = context.getAdjacentTile(x, y);
if (t2.getModel().doesCastShadow()) {
t.shadows |= 1 << move;
}
move++;
}
}
t.shadowsComputed = true;
}
if (t.shadows == 0) return;
if ((t.shadows & (1 << 0)) != 0) Render.quadTextured(rect, SH_NW);
if ((t.shadows & (1 << 1)) != 0) Render.quadTextured(rect, SH_N);
if ((t.shadows & (1 << 2)) != 0) Render.quadTextured(rect, SH_NE);
if ((t.shadows & (1 << 3)) != 0) Render.quadTextured(rect, SH_W);
if ((t.shadows & (1 << 4)) != 0) Render.quadTextured(rect, SH_E);
if ((t.shadows & (1 << 5)) != 0) Render.quadTextured(rect, SH_SW);
if ((t.shadows & (1 << 6)) != 0) Render.quadTextured(rect, SH_S);
if ((t.shadows & (1 << 7)) != 0) Render.quadTextured(rect, SH_SE);
} }
} }

Loading…
Cancel
Save