package com.gebauz.PonPonChun.game.entities;
import com.gebauz.Bauzoid.graphics.sprite.AtlasSprite;
import com.gebauz.Bauzoid.graphics.sprite.AtlasSpriteInstance;
import com.gebauz.Bauzoid.graphics.sprite.Sprite;
import com.gebauz.Bauzoid.math.MathUtil;
import com.gebauz.PonPonChun.game.GameLogic;
import com.gebauz.PonPonChun.game.GameSounds;
import com.gebauz.PonPonChun.game.entities.PlayField.Cell;
/** Represents a single Block.
* Each Block has a certain type and pixel position in the PlayField. The cell raster position can be calculated
* from this using convenience methods in PlayField.
*
* Every Block also belongs to exactly a PlayField to which they are spawned. The PlayField keeps the Blocks
* in a list, and each Block also references the parent PlayField.
*
* A Block can occupy more than one cell of the PlayField (for instance, while falling downwards). Hence
* Cell occupancy information is stored in the Cell data structures of the PlayField - each Cell can
* only have one Block at a time.
*
* @author chiu
*
*/
public class Block
{
// Constants========================================================================================
public static final int TYPE_EMPTY = -
1;
public static final int TYPE_BLACK =
0;
public static final int TYPE_WHITE =
1;
public static final int TYPE_RED =
2;
public static final int TYPE_GREEN =
3;
public static final int TYPE_BLUE =
4;
public static final int TYPE_YELLOW =
5;
public static final int TYPE_BOMB =
6;
public static final int TYPE_CRYSTAL =
7;
public static final int TYPE_JOKER =
8;
public static final int TYPE_CLOCK =
9;
public static final int TYPE_CHERRY =
10;
public static final int TYPE_BANANA =
11;
public static final int TYPE_APPLE =
12;
public static final int TYPE_GRAPES =
13;
public static final int NUM_TYPES =
14;
public static final int BLOCK_SIZE =
52;
public static float MAX_DRAG_BLOCK_SIZE = BLOCK_SIZE +
8;
public static final float BREAKBLOCK_SIZE =
56;
/** Time in which a Block falls by a single Cell. */
public static final float FALLING_TIME = 0.05f
;
/** Time for the clearing animation. */
public static final float CLEAR_TIME = 1.0f
;
/** Time between each block when exploding during clearing. */
public static final float INCREMENTAL_CLEAR_TIME = 0.25f
;
/** Time for a chance for a chain. */
public static final float CHAINABLE_TIME = 0.25f
;
/** Squash and stretch time. */
public static final float SQUASH_TIME = 0.2f
;
/** Time per frame in animation. */
public static final float ANIM_PHASE_TIME = 0.1f
;
/** Time for continuous animation. */
public static final float ANIM_TIME_CONTINOUS = 0.5f
;
/** Anim frame indices for color blocks. */
public static final int ANIM_INDICES_BLOCK
[] =
{ 0,
1,
2,
1,
0 };
/** Anim frame indices for bombs. */
public static final int ANIM_INDICES_BOMB
[] =
{ 1,
2,
1,
0,
1 };
/** Anim frame indices for clock, fruit. */
public static final int ANIM_INDICES_CONTINUOUS
[] =
{ 1,
2,
1,
0 };
/** Anim frame indices for crystalized blocks. */
public static final int ANIM_INDICES_CRYSTALIZED
[] =
{ 0,
1,
2,
3,
2,
1,
0 };
/** Period for glossy shine. */
public static final float GLOSSY_SHINE_PERIOD = 3.5f
;
/** Period for bombs. */
public static final float BOMB_ANIM_PERIOD = 0.9f
;
/** Total time for no move indicator. */
public static final float NO_MOVE_INDICATOR_TIME = 1.0f
;
/** Time to crystalize. */
public static final float CRYSTALIZATION_TIME = 0.25f
;
// Embedded Types===================================================================================
public enum State
{
NORMAL,
// Normal, resting.
FALLING,
// Block is falling.
SWAPPING,
// Block is being swapped.
CLEARING,
// Block is being cleared.
CLEARED,
// Block is already cleared, ready to be removed from list.
}
// Members==========================================================================================
private PlayField mPlayField =
null;
private int mType = TYPE_EMPTY
;
/** Primary cell occupied by this block. Note that falling blocks might partially occupy the slot above too. */
public int cellX =
0;
public int cellY =
0;
/** Current state of the block. */
private State mCurrentState =
State.
NORMAL;
/** Countdown Timer for current state (if timed). */
private float mStateTimer = 0.0f
;
/** Store the total clear time for a group of cleared blocks. */
private float mTotalClearTime = 0.0f
;
/** Total time allotted for the state (currently only used for clearing). */
private float mTotalStateTime = 0.0f
;
/** When the chain countdown timer should start counting down, set to true. */
private boolean mDoChainCountdown =
false;
/** If this block is part of a chainable group, this value is > 0.0f. */
private float mChainableTimer = 0.0f
;
/** Extra timer for squash and stretch. */
private float mSquashTimer = 0.0f
;
/** Timer for the animation. */
private float mAnimTimer = 0.0f
;
/** Current animation index. */
private int mAnimPhase =
0;
/** Time to display no move indicator. */
private float mNoMoveTimer = 0.0f
;
/** Block is crystalized and can only be destroyed by a bomb. */
private boolean mIsCrystalized =
false;
private float mCrystalizationTimer = 0.0f
;
private int mCrystalAnimPhase =
0;
// Methods==========================================================================================
public Block
(PlayField parent,
int type
)
{
mPlayField = parent
;
mType = type
;
/*if ((getType() == TYPE_CLOCK) ||
(getType() == TYPE_CHERRY) ||
(getType() == TYPE_BANANA) ||
(getType() == TYPE_APPLE) ||
(getType() == TYPE_GRAPES))
{
mAnimTimer = mPlayField.getGameLogic().getGame().getTime() % ANIM_PHASE_TIME_CONTINOUS;
}*/
}
public void update
(float deltaTime
)
{
if (isCrystalized
())
{
if (mCrystalizationTimer
> 0.0f
)
{
mCrystalizationTimer -= deltaTime
;
if (mCrystalizationTimer
< 0.0f
)
{
mCrystalizationTimer = 0.0f
;
}
}
}
if (mNoMoveTimer
> 0.0f
)
{
mNoMoveTimer -= deltaTime
;
if (mNoMoveTimer
<= 0.0f
)
mNoMoveTimer = 0.0f
;
}
if (mAnimTimer
> 0.0f
)
{
mAnimTimer -= deltaTime
;
if (mAnimTimer
< 0.0f
)
{
mAnimTimer = 0.0f
;
}
}
// update animation
int num = ANIM_INDICES_BLOCK.
length; // default color block case
float period = GLOSSY_SHINE_PERIOD
;
if ((getType
() == TYPE_BOMB
))
{
period = BOMB_ANIM_PERIOD
;
num = ANIM_INDICES_BOMB.
length;
}
else if ((getType
() == TYPE_CLOCK
) ||
(getType
() == TYPE_CHERRY
) ||
(getType
() == TYPE_BANANA
) ||
(getType
() == TYPE_APPLE
) ||
(getType
() == TYPE_GRAPES
))
{
// continous animation
//period = CONTINUOUS_ANIM_PERIOD;
//num = ANIM_INDICES_CONTINUOUS.length;
float t = mPlayField.
getGameLogic().
getGame().
getTime() % ANIM_TIME_CONTINOUS
;
t /= ANIM_TIME_CONTINOUS
;
mAnimPhase =
(int)(t
* (ANIM_INDICES_CONTINUOUS.
length));
return;
}
// calculate animation phase
float s = 1.0f - MathUtil.
clamp(mAnimTimer /
(ANIM_PHASE_TIME
* num
),
0,
1);
mAnimPhase = MathUtil.
clamp((int)(s
* num
),
0, num-
1);
//float s2 = 1.0f - MathUtil.clamp(mAnimTimer / (ANIM_PHASE_TIME * ANIM_INDICES_CRYSTALIZED.length), 0, 1);
mCrystalAnimPhase = MathUtil.
clamp((int)(s
* ANIM_INDICES_CRYSTALIZED.
length),
0, ANIM_INDICES_CRYSTALIZED.
length-
1);
// synchronize to global timer
float tPrev = mPlayField.
getGameLogic().
getGame().
getTime() % period
;
float tPost =
(mPlayField.
getGameLogic().
getGame().
getTime() + deltaTime
) % period
;
// If the latter time is earlier, then we had a carry-over
if (tPost
< tPrev
)
{
// trigger animation
mAnimTimer = ANIM_PHASE_TIME
* num
;
}
}
public void updateStates
(float deltaTime
)
{
switch (mCurrentState
)
{
case NORMAL:
updateNormal
(deltaTime
);
break;
case FALLING:
// handled by PlayFieldLogic
//updateFalling(deltaTime);
break;
case SWAPPING:
updateSwapping
(deltaTime
);
break;
case CLEARING:
updateClearing
(deltaTime
);
break;
case CLEARED:
break;
}
}
public void updateNormal
(float deltaTime
)
{
// count down chainable time
if ((mDoChainCountdown
) && (mChainableTimer
> 0.0f
))
{
mChainableTimer -= deltaTime
;
if (mChainableTimer
< 0.0f
)
mDoChainCountdown =
false;
}
if (mSquashTimer
> 0.0f
)
{
mSquashTimer -= deltaTime
;
if (mSquashTimer
< 0.0f
)
mSquashTimer = 0.0f
;
}
}
public void updateFalling
(float deltaTime
)
{
if (mStateTimer
> 0.0f
)
{
// move downwards
mStateTimer -= deltaTime
;
}
if (mStateTimer
<= 0.0f
)
{
// check cell below if empty
Cell cell = getContainingCell
();
Cell below = cell.
below;
if ((below ==
null) ||
(!below.
isEmpty()))
{
// if cell below is falling, do nothing and wait for next frame
if ((below
!=
null) && (!below.
isEmpty()) && (below.
containedBlock.
getState() ==
State.
FALLING))
{
return;
}
// No Cell below, or Cell below is not empty, so stop falling state
mDoChainCountdown =
true;
switchState
(State.
NORMAL);
mStateTimer = 0.0f
;
mSquashTimer = SQUASH_TIME
;
mPlayField.
playFallingSound();
}
else
{
// Empty Cell below so move to next Cell and reset state timer
cell.
containedBlock =
null;
below.
containedBlock =
this;
cellY++
;
mStateTimer += FALLING_TIME
;
}
}
}
public void updateSwapping
(float deltaTime
)
{
// while swapping
}
public void updateClearing
(float deltaTime
)
{
if (mStateTimer
> 0.0f
)
{
mStateTimer -= deltaTime
;
mTotalClearTime -= deltaTime
;
if (mStateTimer
< 0.0f
)
{
mStateTimer = 0.0f
;
// spawn particles
// if bomb, remove all surrounding blocks and spawn explosion
if (getType
() == TYPE_BOMB
)
{
mPlayField.
getBombParticles().
spawnExplosion(getCenterX
(), getCenterY
(), BombParticles.
BIG_SIZE, BombParticles.
EXPLO_LIFE_TIME_BIG);
mPlayField.
getGameLogic().
getSounds().
playSound(GameSounds.
SFX_EXPLOSION);
// get quicker to actual destruction
mTotalClearTime = 0.15f
;
}
else
mPlayField.
getBlockBreakParticles().
spawnParticles(getCenterX
(), getCenterY
());
// add score
if (isFruitType
())
{
// spawn fruit type animation and add fruit score
mPlayField.
getGameLogic().
getSounds().
playSound(GameSounds.
SFX_PICKUP_FRUIT);
mPlayField.
getGameLogic().
getFruitParticles().
spawnFruit(getCenterX
(), getCenterY
(), getType
());
mPlayField.
getGameLogic().
addScore(GameLogic.
getFruitScore(getType
()));
// TODO: add fruit meter for mascot instead of score!
}
else if (getType
() == TYPE_CLOCK
)
{
mPlayField.
getGameLogic().
getHeadsUpDisplay().
startFreezeTimeDisplay(GameLogic.
TIME_FREEZE_AMOUNT);
}
else
mPlayField.
getGameLogic().
addScore(GameLogic.
BLOCK_CLEAR_SCORE);
// play sound
mPlayField.
getGameLogic().
getSounds().
playSound(GameSounds.
SFX_CLEAR_BREAK);
}
}
else
{
// after personal clear time
mTotalClearTime -= deltaTime
;
if (mTotalClearTime
< 0.0f
)
{
mTotalClearTime = 0.0f
;
// clear all around if bomb
if (getType
() == TYPE_BOMB
)
{
for (int ix =
0; ix
< 3; ix++
)
{
for (int iy =
0; iy
< 3; iy++
)
{
int dx = cellX -
1 + ix
;
int dy = cellY -
1 + iy
;
if ((dx == cellX
) && (dy == cellY
))
continue;
if ((dx
< 0) ||
(dx
>= PlayField.
NUM_CELLS_X))
continue;
if ((dy
< 0) ||
(dy
>= PlayField.
NUM_CELLS_Y))
continue;
Cell cell = mPlayField.
getCell(dx, dy
);
if (!cell.
isEmpty())
{
//cell.containedBlock.switchState(State.CLEARED);
cell.
containedBlock.
switchState(State.
CLEARING, 0.1f
);
cell.
containedBlock.
setTotalClearTime(0.2f
);
mPlayField.
getBombParticles().
spawnExplosion(cell.
containedBlock.
getCenterX(), cell.
containedBlock.
getCenterY(), BombParticles.
SMALL_SIZE, BombParticles.
EXPLO_LIFE_TIME_SMALL);
}
}
}
}
// remove block
switchState
(State.
CLEARED);
}
}
}
/** Switch the current state. */
public void switchState
(State newState
)
{
switchState
(newState, 0.0f
);
}
/** Switch the current state. */
public void switchState
(State newState,
float initialStateTimerValue
)
{
mStateTimer = initialStateTimerValue
;
mTotalStateTime = initialStateTimerValue
;
mCurrentState = newState
;
}
/** Clearing has two values - total clear time and a personal clear time (stored in mStateTimer) per block. */
public void setTotalClearTime
(float totalClearTime
)
{
mTotalClearTime = totalClearTime
;
}
public void render
(boolean selected
)
{
// swapping blocks are rendered by PlayFieldLogic
if (mCurrentState ==
State.
SWAPPING)
return;
// update animation
int index = getAnimIndex
();
// apply local atttributes
int spriteIndex = getSpriteIndex
();
AtlasSpriteInstance sprite = mPlayField.
getSprites().
getBlockSprite(spriteIndex + index
* BlockSprites.
NUM_BLOCK_SPRITES);
if (sprite ==
null)
return;
float scale = 1.0f
;
if (selected
)
scale = MAX_DRAG_BLOCK_SIZE/BLOCK_SIZE
;
// for Rumbling
float offsetX = 0.0f
;
float offsetY = 0.0f
;
if (mPlayField.
isGameOver() && !mPlayField.
getGameLogic().
getGameOverScreen().
isShowingMenu())
{
offsetX = mPlayField.
getGameLogic().
getGame().
getRandomFloat(-PlayField.
CELL_SIZE/
32, PlayField.
CELL_SIZE/
32);
offsetY = mPlayField.
getGameLogic().
getGame().
getRandomFloat(-PlayField.
CELL_SIZE/
32, PlayField.
CELL_SIZE/
32);
}
sprite.
x = getCenterX
() + offsetX
;
sprite.
y = getCenterY
() - mPlayField.
getAdvanceOffsetY() + offsetY
;
sprite.
w = BLOCK_SIZE
* scale
;
sprite.
h = BLOCK_SIZE
* scale
;
sprite.
pivotX = sprite.
w/
2;
sprite.
pivotY = sprite.
h/
2;
// modify for squashing if in squashing state
if ((mCurrentState ==
State.
NORMAL) && (mSquashTimer
> 0.0f
) && (!isCrystalized
()))
{
float f =
1 - mSquashTimer / SQUASH_TIME
;
float squashFactor = f
*f
;
sprite.
w *= 1.2f - 0.2f
* squashFactor
;
sprite.
h *= 0.8f + 0.2f
* squashFactor
;
sprite.
pivotX = sprite.
w/
2;
sprite.
pivotY = sprite.
h;
sprite.
y += Block.
BLOCK_SIZE/
2;
}
if (mCurrentState ==
State.
CLEARING)
{
//sprite.alpha = mStateTimer / mTotalStateTime;
sprite.
alpha = 1.0f
;
if (mStateTimer
< (0.1f
))
sprite.
alpha = 0.0f
;
}
else
sprite.
alpha = 1.0f
;
sprite.
render();
// on clearing, overlay the breaking block
if (getState
() ==
State.
CLEARING)
{
AtlasSprite breakSprite = mPlayField.
getSprites().
getBreakSprite();
breakSprite.
x = getCenterX
();
breakSprite.
y = getCenterY
() - mPlayField.
getAdvanceOffsetY();;
breakSprite.
w = BREAKBLOCK_SIZE
;
breakSprite.
h = BREAKBLOCK_SIZE
;
breakSprite.
pivotX = breakSprite.
w/
2;
breakSprite.
pivotY = breakSprite.
h/
2;
float s =
(mTotalStateTime - mStateTimer
);
if (s
< 0.2f
)
{
// fade in
breakSprite.
alpha = s / 0.2f
;
}
else if (mStateTimer
<= 0.0f
)
{
breakSprite.
alpha = 0.0f
;
}
else
{
breakSprite.
alpha = 1.0f
;
}
//breakSprite.alpha *= 0.5f;
/* if (s > 0.8f)
breakSprite.alpha = (1.0f-s) / 0.2f;
else if (s < 0.2f)
breakSprite.alpha = (s) / 0.2f;
else
breakSprite.alpha = 1.0f;*/
//s = (s*10.0f) % 1.0f;
//Gdx.app.log("BLA", "t:" + mPlayField.getGameLogic().getGame().getCurrentTimeMs());
float animate =
((mPlayField.
getGameLogic().
getGame().
getTime()) * 10.0f
) % 1.0f
;
int n =
0;
if (animate
< 0.33f
)
n =
1;
else if (animate
> 0.66f
)
n =
2;
breakSprite.
render(n
);
}
if (isCrystalized
())
{
//float animate = ((mPlayField.getGameLogic().getGame().getTime()) * 0.1f) % 1.0f;
//int n = MathUtil.clamp((int)(animate * 4), 0, 3);
int n = ANIM_INDICES_CRYSTALIZED
[mCrystalAnimPhase
];
AtlasSpriteInstance s = mPlayField.
getSprites().
getCrystalizedFrame(n
);
s.
x = getCenterX
();
s.
y = getCenterY
() - mPlayField.
getAdvanceOffsetY();;
s.
w = BLOCK_SIZE
*2;
s.
h = BLOCK_SIZE
*2;
s.
pivotX = s.
w/
2;
s.
pivotY = s.
h/
2;
s.
alpha =
1-
(mCrystalizationTimer / CRYSTALIZATION_TIME
);
s.
render();
}
if (mNoMoveTimer
> 0.0f
)
{
float a = mNoMoveTimer / NO_MOVE_INDICATOR_TIME
;
Sprite noMoveSprite = mPlayField.
getSprites().
getNoMove();
noMoveSprite.
x = getCenterX
();
noMoveSprite.
y = getCenterY
() - mPlayField.
getAdvanceOffsetY();;
noMoveSprite.
alpha = a
;
noMoveSprite.
render();
}
}
public void renderSwapping
(float offsetX,
float scale
)
{
if (mCurrentState
!=
State.
SWAPPING)
return;
// apply local atttributes
// update animation
int index = getAnimIndex
();
int spriteIndex = getSpriteIndex
();
AtlasSpriteInstance sprite = mPlayField.
getSprites().
getBlockSprite(spriteIndex + index
* BlockSprites.
NUM_BLOCK_SPRITES);
if (sprite ==
null)
return;
sprite.
x = getCenterX
() + offsetX
;
sprite.
y = getCenterY
() - mPlayField.
getAdvanceOffsetY();;
sprite.
w = BLOCK_SIZE
* scale
;
sprite.
h = BLOCK_SIZE
* scale
;
sprite.
pivotX = sprite.
w/
2;
sprite.
pivotY = sprite.
h/
2;
sprite.
alpha = 1.0f
;
sprite.
render();
}
public int getAnimIndex
()
{
if ((getType
() == TYPE_BOMB
))
{
return ANIM_INDICES_BOMB
[mAnimPhase
];
}
else if ((getType
() == TYPE_CLOCK
) ||
(getType
() == TYPE_CHERRY
) ||
(getType
() == TYPE_BANANA
) ||
(getType
() == TYPE_APPLE
) ||
(getType
() == TYPE_GRAPES
))
{
return ANIM_INDICES_CONTINUOUS
[mAnimPhase
];
}
return ANIM_INDICES_BLOCK
[mAnimPhase
];
}
/** Calculate sprite index based on type. */
public int getSpriteIndex
()
{
return mType
;
}
public boolean isImmovableBlockType
()
{
return (isCrystalized
() || isImmovableBlockType
(mType
));
}
public boolean isSpecialBlockType
()
{
return isSpecialBlockType
(mType
);
}
public boolean isJokerBlockType
()
{
return isJokerBlockType
(mType
);
}
public boolean isColorBlockType
()
{
return isColorBlockType
(mType
);
}
public boolean isFruitType
()
{
return isFruitType
(mType
);
}
/** Check if the type is not movable. */
public static boolean isImmovableBlockType
(int type
)
{
if ((type == TYPE_BOMB
) ||
(type == TYPE_CRYSTAL
) ||
(type == TYPE_CLOCK
) ||
(type == TYPE_CHERRY
) ||
(type == TYPE_BANANA
) ||
(type == TYPE_APPLE
) ||
(type == TYPE_GRAPES
))
return true;
return false;
}
/** Check if the type is compatible with a matching color set (only one allowed per clearing row). */
public static boolean isSpecialBlockType
(int type
)
{
if ((type == TYPE_BOMB
) ||
//(type == TYPE_JOKER) ||
(type == TYPE_CRYSTAL
) ||
(type == TYPE_CLOCK
) ||
(type == TYPE_CHERRY
) ||
(type == TYPE_BANANA
) ||
(type == TYPE_APPLE
) ||
(type == TYPE_GRAPES
))
return true;
return false;
}
/** Check if the type is a joker color block that counts as any color. */
public static boolean isJokerBlockType
(int type
)
{
if (type == TYPE_JOKER
)
return true;
return false;
}
/** Check if the type is one of the 6 color block types. */
public static boolean isColorBlockType
(int type
)
{
if ((type == TYPE_BLACK
) ||
(type == TYPE_WHITE
) ||
(type == TYPE_RED
) ||
(type == TYPE_GREEN
) ||
(type == TYPE_BLUE
) ||
(type == TYPE_YELLOW
) ||
(type == TYPE_JOKER
))
return true;
return false;
}
/** Check if the type is one of the 4 fruit types. */
public static boolean isFruitType
(int type
)
{
if ((type == TYPE_CHERRY
) ||
(type == TYPE_BANANA
) ||
(type == TYPE_APPLE
) ||
(type == TYPE_GRAPES
))
return true;
return false;
}
public void showNoMoveIndicator
()
{
mNoMoveTimer = NO_MOVE_INDICATOR_TIME
;
}
public void changeToCrystal
()
{
mIsCrystalized =
true;
mCrystalizationTimer = CRYSTALIZATION_TIME
;
}
// Getters/Setters==================================================================================
/** Get the cell the block is in. */
public final Cell getCell
()
{
return mPlayField.
getCell(cellX, cellY
);
}
public final int getType
()
{
return mType
;
}
/** Get the current state. */
public final State getState
()
{
return mCurrentState
;
}
/** Get the center X position in pixels. */
public final float getCenterX
()
{
return (cellX
* PlayField.
CELL_SIZE);
}
/** Get the center Y position in pixels. */
public final float getCenterY
()
{
float offset = 0.0f
;
// account for falling
if (mCurrentState ==
State.
FALLING)
{
offset =
(mStateTimer
* (float)PlayField.
CELL_SIZE) /
(float)FALLING_TIME
;
}
return (cellY
* PlayField.
CELL_SIZE) - offset
;
}
/** Get the Cell that contains this Block. */
public final Cell getContainingCell
()
{
return mPlayField.
getCell(cellX, cellY
);
}
/** Returns true when still chainable. */
public final boolean isChainable
()
{
return (mChainableTimer
> 0.0f
);
}
/** Set chainable. */
public void setChainable
()
{
// this value is set to true once the transition from FALLING->NORMAL occurs
mDoChainCountdown =
false;
mChainableTimer = CHAINABLE_TIME
;
}
public boolean isCrystalized
()
{
return mIsCrystalized
;
}
}