fruit-popper/ENTITIES.PAS

1122 lines
35 KiB
Plaintext
Raw Normal View History

{$A+,B-,E+,F-,G+,I-,N+,P-,Q-,R-,S-,T-,V-,X+}
unit Entities;
interface
uses FixedP, MathFP;
const
PLAYER_WALK_SPEED = FP_0_6;
PLAYER_TACK_PUSH_FORCE = FP_8;
ENTITY_FRICTION = FP_0_2;
FORCE_FRICTION = FP_0_7;
PRICK_RADIUS = 4;
SCORE_UP_PARTICLE_SPEED = -trunc(0.075 * FP_FLOAT_SHIFT);
OW_PARTICLE_SPEED = -trunc(0.1 * FP_FLOAT_SHIFT);
{ cooldowns/times are all specified in terms of frame ticks }
STAB_COOLDOWN = 80;
STABBED_DEBUFF_TIME = 5000;
SPLASHED_DEBUFF_TIME = 5000;
type
Direction = (South, North, East, West);
{ used to statically define an animation sequence }
AnimationDesc = record
frames : array[0..5] of word; { 6 frames total ever ... }
count : word; { number of frames (max of 6) }
delay : word; { frame timer/delay between switching to next frame }
time : word; { total time. = count * delay }
loops : bytebool; { true = animation loops }
base : word; { spritesheet index of first frame. if
multi-directional, this should be the index of the
first frame for the south direction }
dirLength : word; { number of frames from the start of one direction
to the start of the next direction. if not a
multi-directional animation, this must be set to 0 }
end;
PAnimationDesc = ^AnimationDesc;
{ state needed for a currently running animation sequence. will be used
in conjunction with an AnimationDesc that defines the animation sequence
itself }
AnimationState = record
complete : bytebool; { true = current animation sequence is complete }
frameIndex : word; { index of the current animation sequence frame }
time : word; { current frame timer }
end;
PAnimationState = ^AnimationState;
{ general entity properties. an instance of this should be included
in the actual entity's specific record-type. not all entity types
actually use all of these properties. }
Entity = record
position : Vec2FP;
velocity : Vec2FP;
force : Vec2FP;
direction : Direction;
animation : AnimationState;
noCollision : bytebool;
end;
{ particle entity properties. sprite-animation-based particles
have their lifetime directly linked to the length of the animation.
TODO: non-sprite-animation-based particles. }
Particle = record
active : bytebool;
entity : Entity;
animation : PAnimationDesc; { if non-nil, particle is a
sprite-animation-based particle }
end;
FruitKind = (Tomato, Grapes);
FruitState = (Plant, Growing, Grown, Popped);
{ fruit entity properties }
Fruit = record
entity : Entity;
kind : FruitKind;
state : FruitState;
{ the meaning of these two values depends on the 'state' value }
counter : word;
value : word;
isGold : bytebool;
end;
PlayerState = (Idle, Walking, Stabbing, Victory, Defeat);
{ holds player state. duh. }
Player = record
entity : Entity;
fruitPref : FruitKind;
state : PlayerState;
stabCooldown : word;
stabbedDebuffTime : word;
splashedDebuffTime : word;
skipRenderFlag : bytebool;
score : word;
end;
PPlayer = ^Player;
procedure ResetAnimationState(var animation : AnimationState);
function GetAnimationFrame(const state : AnimationState;
const desc : AnimationDesc;
direction : Direction) : word;
procedure UpdateAnimation(var entity : Entity;
const animation : AnimationDesc);
function DoEntitiesOverlap(const a, b : Entity) : boolean;
function DoesEntityOverlap(const entity : Entity;
x1, y1, x2, y2 : integer) : boolean;
function IsEntityPositionValid(const entity : Entity) : boolean;
function MoveEntity(var entity : Entity) : boolean;
function IsEntityStopped(const entity : Entity) : boolean;
procedure UpdateEntity(var entity : Entity);
procedure SetPlayerState(var player : Player; state : PlayerState);
procedure GetThumbTackPointCoords(const player : Player; var out_x, out_y : integer);
procedure GetThumbTackRenderCoords(const player : Player; var out_x, out_y : integer);
procedure UpdatePlayer(var player : Player);
procedure InitPlayer(var player : Player; x, y : integer; fruit: FruitKind);
procedure MovePlayer(var player : Player; dir : Direction);
procedure StabPlayer(var player : Player);
procedure DoThumbTackStabAt(px, py : integer; player : PPlayer);
function SpawnRandomFruit : integer;
procedure PopAllFruit(kind : FruitKind; player : PPlayer);
procedure PopFruitAt(x, y : integer; player : PPlayer);
procedure UpdateAllFruit;
function GetUnusedParticleIndex : integer;
function SpawnTomatoSplash(x, y : integer) : integer;
function SpawnGrapesSplash(x, y : integer) : integer;
function SpawnPlantSplash(x, y : integer) : integer;
function SpawnStabFlash(x, y : integer) : integer;
function SpawnScoreUp(x, y : integer; kind : FruitKind) : integer;
function SpawnOw(x, y : integer) : integer;
procedure UpdateAllParticles;
implementation
uses Math, Toolbox, Maps, Assets, Shared;
procedure ResetAnimationState(var animation : AnimationState);
{ resets the given animation state, so it can be used to start an
animation sequence from the very beginning }
begin
with animation do begin
complete := false;
frameIndex := 0;
time := 0;
end;
end;
function GetAnimationFrame(const state : AnimationState;
const desc : AnimationDesc;
direction : Direction) : word;
{ returns the current spritesheet bitmap index that should be blitted
to draw an entity based on it's animation state and facing direction }
begin
with desc do begin
GetAnimationFrame := frames[state.frameIndex]
+ (ord(direction) * dirLength)
+ base;
end;
end;
procedure UpdateAnimation(var entity : Entity;
const animation : AnimationDesc);
{ cycles the entity's animation state. the passed AnimationDesc should be
the corresponding animation descriptor/definition for the animation state
that the entity is currently in }
begin
with entity.animation do begin
if not complete then begin
inc(time, frameTicks);
if time >= animation.delay then begin
{ move to next frame in the current animation sequence }
time := 0;
if frameIndex = (animation.count-1) then begin
{ we're at the last frame in the current animation sequence }
if not animation.loops then begin
complete := true;
end else
frameIndex := 0;
end else
inc(frameIndex);
end;
end;
end;
end;
{ ------------------------------------------------------------------------ }
function DoEntitiesOverlap(const a, b : Entity) : boolean;
{ returns true if the given entities overlap fully or partially }
const
EDGE = 2;
var
ax1, ay1, ax2, ay2 : integer;
bx1, by1, bx2, by2 : integer;
begin
DoEntitiesOverlap := false;
with a.position do begin
ax1 := FixToInt(x)+EDGE;
ay1 := FixToInt(y)+EDGE;
ax2 := ax1 + (ENTITY_SIZE-1)-EDGE;
ay2 := ay1 + (ENTITY_SIZE-1)-EDGE;
end;
with b.position do begin
bx1 := FixToInt(x)+EDGE;
by1 := FixToInt(y)+EDGE;
bx2 := bx1 + (ENTITY_SIZE-1)-EDGE;
by2 := by1 + (ENTITY_SIZE-1)-EDGE;
end;
if (ay1 < by1) and (ay2 < by1) then
exit;
if (ay1 > by2) and (ay2 > by2) then
exit;
if (ax1 < bx1) and (ax2 < bx1) then
exit;
if (ax1 > bx2) and (ax2 > bx2) then
exit;
DoEntitiesOverlap := true;
end;
function DoesEntityOverlap(const entity : Entity;
x1, y1, x2, y2 : integer) : boolean;
{ returns true if the entity partially or fully overlaps with the given
area (specified in pixel coordinates) }
const
EDGE = 2;
var
ex1, ey1, ex2, ey2 : integer;
begin
DoesEntityOverlap := false;
with entity.position do begin
ex1 := FixToInt(x)+EDGE;
ey1 := FixToInt(y)+EDGE;
ex2 := ex1 + (ENTITY_SIZE-1)-EDGE;
ey2 := ey1 + (ENTITY_SIZE-1)-EDGE;
end;
if (ey1 < y1) and (ey2 < y1) then
exit;
if (ey1 > y2) and (ey2 > y2) then
exit;
if (ex1 < x1) and (ex2 < x1) then
exit;
if (ex1 > x2) and (ex2 > x2) then
exit;
DoesEntityOverlap := true;
end;
function IsEntityPositionValid(const entity : Entity) : boolean;
{ returns true if the given entity is currently located in a position
that is "valid", meaning completely free of collisions with anything. }
begin
IsEntityPositionValid := true;
with entity.position do begin
{ any collision with the map? either collidable tiles or fruit ... }
if IsMapCollision(FixToInt(x), FixToInt(y)) then
IsEntityPositionValid := false
else begin
{ TODO: this seems a bit clumsy ...? }
{ make sure we don't compare against ourself. since this function
operates on generic Entity's, we kinda have to check this way }
if @entity = @player1.entity then begin
IsEntityPositionValid := (not DoEntitiesOverlap(entity,
player2.entity));
end else begin
IsEntityPositionValid := (not DoEntitiesOverlap(entity,
player1.entity));
end;
end;
end;
end;
function MoveEntity(var entity : Entity) : boolean;
{ updates the entity's X and Y position based on their current movement
velocity and any force velocity. checks for map collisions and prevents
movement along the X and/or Y axis if any collisions are found. returns
true if the entity collided against something. }
const
{ number of movement+collision sub-steps to divide this movement into }
NUM_STEPS = 2;
{ reciprocal (also as fixed point) just so that we can use Vec2FP_Scale }
STEP_SCALE = trunc((1 / NUM_STEPS) * FP_FLOAT_SHIFT);
var
stepVelocity : Vec2FP;
i : integer;
begin
MoveEntity := false;
with entity do begin
{ calculate the sub-step velocity for the below loop ... }
Vec2FP_Add(stepVelocity, velocity, force);
if (stepVelocity.x = 0) and (stepVelocity.y = 0) then exit;
{ if this entity skips collision checks, just move it and return }
if noCollision then begin
Vec2FP_AddTo(position, stepVelocity);
exit;
end;
{ we're dividing the movement and collision checks into sub-steps!
this is a possibly-hacky solution to fix issues with any frame-timings
that might result in us (without using sub-steps) moving entities
more than 1 pixel per loop. this could cause problems! e.g. the player
might not be able to move into a 1-tile-wide gap because their
movement keeps skipping over 1 pixel too much ... }
Vec2FP_ScaleThis(stepVelocity, STEP_SCALE);
for i := 1 to NUM_STEPS do begin
{ add velocity X to player's position, then test for collisions using
this new position. if a collision occurs, we cannot move the player
in the X direction by this amount, so we back it out }
inc(position.x, stepVelocity.x);
if not IsEntityPositionValid(entity) then begin
MoveEntity := true;
dec(position.x, stepVelocity.x);
end;
{ same thing for the velocity Y now. note that, if no collision occured
in the X direction, the position that is tested for here will also
include the velocity X component ... }
inc(position.y, stepVelocity.y);
if not IsEntityPositionValid(entity) then begin
MoveEntity := true;
dec(position.y, stepVelocity.y);
end;
end;
end;
end;
function IsEntityStopped(const entity : Entity) : boolean;
{ returns true if the entity's movement velocity is slow enough that they
could be considered stopped. this does not take into account the entity's
force velocity }
const
THRESHOLD = trunc(0.05 * FP_FLOAT_SHIFT);
begin
with entity.velocity do begin
IsEntityStopped := (abs(x) < THRESHOLD) and (abs(y) < THRESHOLD);
end;
end;
procedure UpdateEntity(var entity : Entity);
{ updates general entity state. this includes applying velocity/force
vectors to the entity's position and also applying friction to those
velocity/force vecotrs too }
begin
{ move entity in the direction of their velocity and any combined force
that is currently being applied to them. also handles collision. }
MoveEntity(entity);
with entity do begin
{ slow both the velocity and force down by friction }
{ TODO: probably for the force vector, we should use something other
than friction ... ? some per-force specific value maybe? }
Vec2FP_ScaleThis(velocity, ENTITY_FRICTION);
Vec2FP_ScaleThis(force, FORCE_FRICTION);
end;
end;
{ ------------------------------------------------------------------------ }
procedure SetPlayerState(var player : Player; state : PlayerState);
{ switches the player state to the one specified, also resetting the
player's current animation state }
begin
if state = player.state then exit;
player.state := state;
ResetAnimationState(player.entity.animation);
end;
procedure GetThumbTackPointCoords(const player : Player;
var out_x, out_y : integer);
{ computes the pixel coordinate of where the player's thumb tack's point
should be (assuming they are currently stabbing). }
var
dir : Direction;
begin
{ lol, perhaps just computing, say, 8-16 pixels out in a line directly
centered on the player and outward in their facing direction and using
that as the point coordinate would be best ... ?
this all seems silly now that i've written it ... }
with player.entity do begin
dir := direction;
with position do begin
out_x := FixToInt(x)
+ thumbTackRenderOffsetsX[ord(dir)]
+ thumbTackPointOffsetsX[ord(dir)];
out_y := FixToInt(y)
+ thumbTackRenderOffsetsY[ord(dir)]
+ thumbTackPointOffsetsY[ord(dir)];
end;
end;
end;
procedure GetThumbTackRenderCoords(const player : Player;
var out_x, out_y : integer);
{ computes the pixel coordinate of where the player's thumb tack sprite
should be (assuming they are currently stabbing). }
var
dir : Direction;
begin
with player.entity do begin
dir := direction;
with position do begin
out_x := FixToInt(x) + thumbTackRenderOffsetsX[ord(dir)];
out_y := FixToInt(y) + thumbTackRenderOffsetsY[ord(dir)];
end;
end;
end;
procedure UpdatePlayer(var player : Player);
{ updates player (and general entity) state. this includes entity
movement, as well as player animation state }
var
animation : ^AnimationDesc;
i : integer;
px, py : integer;
dir : Direction;
begin
with player do begin
{ do general entity updates first ... this will handle movement via
any velocity/force vectors }
UpdateEntity(entity);
if state = Stabbing then begin
if (entity.animation.frameIndex = 0)
and (entity.animation.time = 0)
and (not entity.animation.complete) then begin
{ only for the very first frame of the stabbing animation,
check for any fruit that collide with the thumb tack's pointy
end and should be popped }
dir := entity.direction;
with entity.position do begin
GetThumbTackPointCoords(player, px, py);
DoThumbTackStabAt(px, py, @player);
end;
end;
if entity.animation.complete then begin
{ keep player in the stabbing state until that animation has
completed. }
SetPlayerState(player, Idle);
stabCooldown := STAB_COOLDOWN;
{ stab/attack cooldown time quadrupled when afflicted by either
the 'stabbed' or 'splashed' cooldown }
if (stabbedDebuffTime > 0) or (splashedDebuffTime > 0) then
stabCooldown := stabCooldown * 4;
end;
end else if (state <> Victory) and (state <> Defeat) then begin
{ set player idle/walking based on their velocity.
note that this check ignores their force velocity! }
if IsEntityStopped(entity) then
SetPlayerState(player, Idle)
else
SetPlayerState(player, Walking);
end;
UpdateAnimation(entity, playerAnimations[ord(state)]);
{ update cooldowns / debuff timers }
if stabCooldown > 0 then
if stabCooldown > frameTicks then
dec(stabCooldown, frameTicks)
else
stabCooldown := 0;
if stabbedDebuffTime > 0 then
if stabbedDebuffTime > frameTicks then
dec(stabbedDebuffTime, frameTicks)
else
stabbedDebuffTime := 0;
if splashedDebuffTime > 0 then
if splashedDebuffTime > frameTicks then
dec(splashedDebuffTime, frameTicks)
else
splashedDebuffTime := 0;
if (stabbedDebuffTime > 0) or (splashedDebuffTime > 0) then
skipRenderFlag := not skipRenderFlag;
end;
end;
procedure InitPlayer(var player : Player; x, y : integer; fruit: FruitKind);
begin
MemFill(@player, 0, SizeOf(Player));
player.entity.position.x := IntToFix(x);
player.entity.position.y := IntToFix(y);
player.entity.direction := South;
player.fruitPref := fruit;
case fruit of
Tomato: tomatoPlayer := @player;
Grapes: grapesPlayer := @player;
end;
SetPlayerState(player, Idle);
end;
procedure MovePlayer(var player : Player; dir : Direction);
{ sets the given player in motion in the given direction. this function
does not actually adjust the players position in any way. it only sets
their velocity }
var
speed : fixed;
begin
with player do begin
if stabCooldown > 0 then exit;
if (state <> Idle) and (state <> Walking) then exit;
{ movement speed is halved when afflicted by 'splashed' }
if splashedDebuffTime > 0 then
speed := FixMul(PLAYER_WALK_SPEED, FP_0_5)
{ movement speed is cut by 30% when afflicted by 'stabbed' }
else if stabbedDebuffTime > 0 then
speed := FixMul(PLAYER_WALK_SPEED, FP_0_7)
else
speed := PLAYER_WALK_SPEED;
case dir of
North: begin
with entity do begin
dec(velocity.y, speed);
direction := North;
end;
end;
South: begin
with entity do begin
inc(velocity.Y, speed);
direction := South;
end;
end;
West: begin
with entity do begin
dec(velocity.x, speed);
direction := West;
end;
end;
East: begin
with entity do begin
inc(velocity.x, speed);
direction := East;
end;
end;
end;
end;
end;
procedure StabPlayer(var player : Player);
{ switches the player into the 'stabbing' state, which will start the
animation as well as bring out the player's thumb tack (during the
next player update anyway) }
begin
with player do begin
if stabCooldown > 0 then exit;
if (state <> Idle) and (state <> Walking) then exit;
SetPlayerState(player, Stabbing);
end;
end;
procedure DoThumbTackStabAt(px, py : integer; player : PPlayer);
{ determines what, if anything, a thumb tack stab with the pixel coordinates
of the point provided, collided with and what should happen. the player
passed should be the player who owns the thumb tack. }
var
otherPlayer : PPlayer;
dir : Direction;
begin
{ always pop any fruit / destroy any plants at this position }
PopFruitAt(px, py, player);
{ did we also hit the other player? }
{ determine which player is which ... }
if player = @player1 then
otherPlayer := @player2
else
otherPlayer := @player1;
{ now check if this thumb tack point collided with that other player }
if DoesEntityOverlap(otherPlayer^.entity,
px - PRICK_RADIUS,
py - PRICK_RADIUS,
px + PRICK_RADIUS,
py + PRICK_RADIUS) then begin
{ we hit the other player. push the other player in the direction
that this player is facing }
dir := player^.entity.direction;
with otherPlayer^.entity do begin
case dir of
North: AngleToVec2DFP(BIN_ANGLE_270, force);
South: AngleToVec2DFP(BIN_ANGLE_90, force);
West: AngleToVec2DFP(BIN_ANGLE_180, force);
East: AngleToVec2DFP(0, force);
end;
Vec2FP_ScaleThis(force, PLAYER_TACK_PUSH_FORCE);
end;
{ also apply the 'stabbed' debuff to the other player }
otherPlayer^.stabbedDebuffTime := STABBED_DEBUFF_TIME;
{ finally, spawn a 'ow' particle as another indication that a player
was stabbed }
with otherPlayer^.entity.position do begin
SpawnOw(FixToInt(x), FixToInt(y));
end;
end;
end;
{ ------------------------------------------------------------------------ }
procedure SetFruitState(var fruit : Fruit; state : FruitState);
begin
if state = fruit.state then exit;
fruit.state := state;
fruit.counter := 0;
fruit.value := 0;
ResetAnimationState(fruit.entity.animation);
end;
function GetPlantRandomLifeTime : word;
const
MINIMUM_TIME = 3000;
STEP_SIZE = 2000;
begin
GetPlantRandomLifeTime := MINIMUM_TIME
+ ((1+random(5)) * STEP_SIZE)
- random(STEP_SIZE);
end;
function SpawnRandomFruit : integer;
{ spawns a new fruit (starting it off as a plant) in any random available
dirt tile on the map. spawning may fail (if a free dirt tile could not
be found randomly). returns the dirtTiles index of the new fruit, or
-1 if spawning failed }
var
idx : integer;
begin
SpawnRandomFruit := -1;
{ find a random spot to spawn a new fruit in }
idx := GetRandomUnusedDirtTileIndex;
if idx = -1 then exit;
SpawnRandomFruit := idx;
with dirtTiles[idx] do begin
{ important! this marks the dirt tile as being 'used' }
hasFruit := true;
inc(numActiveDirtTiles);
{ zero out the fruit, and then fill in its starter properties }
MemFill(@fruit, 0, SizeOf(fruit));
SetFruitState(fruit, Plant);
if random(2) = 0 then
fruit.kind := Tomato
else
fruit.kind := Grapes;
fruit.value := GetPlantRandomLifeTime;
fruit.isGold := (random(100) < GOLD_FRUIT_SPAWN_CHANCE);
fruit.entity.position.x := IntToFix(x*16);
fruit.entity.position.y := IntToFix(y*16);
end;
end;
procedure PopFruitIn(var tile : DirtTile; player : PPlayer);
{ switches any fruit entity located within the given dirt tile into the
'popped' state, as well as starting up any relevant animations. if
there is no fruit in this tile, or the fruit is not in the 'grown'
state yet, nothing happens. the player passed in here will be the one
given 'credit' for popping the fruit (or if nil, no credit is given to
any player). }
var
fx, fy : integer;
begin
with tile do begin
if not hasFruit then exit;
case fruit.state of
Plant, Growing: begin
{ no score credit for stabbing a fruit plant, or a growing
but not yet full grown fruit. just despawn it. }
hasFruit := false;
dec(numActiveDirtTiles);
with fruit.entity.position do begin
SpawnPlantSplash(FixToInt(x), FixToInt(y));
end;
end;
Grown: begin
{ stabbing fully grown fruit }
SetFruitState(fruit, Popped);
with fruit.entity.position do begin
fx := FixToInt(x);
fy := FixToInt(y);
{ if the stabbing player's fruit choice matches the popped
fruit, then give the player score credit }
if fruit.kind = player^.fruitPref then begin
SpawnScoreUp(fx, fy, fruit.kind);
inc(player^.score);
end;
if fruit.isGold then
PopAllFruit(fruit.kind, player);
SpawnStabFlash(fx, fy);
end;
end;
end;
end;
end;
procedure PopAllFruit(kind : FruitKind; player : PPlayer);
var
idx : integer;
begin
for idx := 0 to numDirtTiles-1 do begin
with dirtTiles[idx] do begin
if not hasFruit then continue;
if (fruit.kind = kind) and (fruit.state = Grown) then begin
PopFruitIn(dirtTiles[idx], player);
end;
end;
end;
end;
procedure PopFruitAt(x, y : integer; player : PPlayer);
{ switches any fruit located near the given x/y coordinates into the
'popped' state, assuming that the fruit are already in the 'grown'
state (otherwise, they will not be changed). these x/y coordinates
would normally be the pixel coordinates corresponding to a player's
thumb tack point. the player passed in here will be the one given
'credit' for popping the fruit (or if nil, no credit is given to
any player). }
var
left, right, top, bottom : integer;
cx, cy : integer;
dirtTile : PDirtTile;
begin
{ get the map tile x/y region to check for dirt tiles within }
left := (x - PRICK_RADIUS) div TILE_SIZE;
right := (x + PRICK_RADIUS) div TILE_SIZE;
top := (y - PRICK_RADIUS) div TILE_SIZE;
bottom := (y + PRICK_RADIUS) div TILE_SIZE;
if left < 0 then left := 0;
if right > MAP_RIGHT then right := MAP_RIGHT;
if top < 0 then top := 0;
if bottom > MAP_BOTTOM then bottom := MAP_BOTTOM;
{ for all dirt tiles located within this region, pop the fruit
in them }
for cy := top to bottom do begin
for cx := left to right do begin
dirtTile := dirtTileMapping[(cy * SCREEN_MAP_WIDTH) + cx];
if dirtTile <> nil then begin
PopFruitIn(dirtTile^, player);
end;
end;
end;
end;
procedure UpdateAllFruit;
{ updates the state of all fruit currently active within dirtTiles }
const
GROW_STEP_TIME = 40;
SPRITE_MAX_SIZE = 16;
var
i, fx, fy : integer;
begin
{ periodically spawn more fruit }
if (fruitSpawnTimer >= 1000)
and (numActiveDirtTiles < map.header.maxFruit) then begin
SpawnRandomFruit;
fruitSpawnTimer := 0;
end;
for i := 0 to numDirtTiles-1 do begin
with dirtTiles[i] do begin
if not hasFruit then continue;
case fruit.state of
Plant: begin
{ count time into the plant has been a plant for 'value' time
at which point it should 'grow' into a fruit }
with fruit do begin
inc(counter, frameTicks);
if counter >= value then begin
SetFruitState(fruit, Growing);
value := 2;
end;
end;
end;
Growing: begin
{ the fruit "grows" by scaling it's size up from zero to it's
normal pixel sprite size. the size increments by 1 every
so often }
with fruit do begin
if value < SPRITE_MAX_SIZE then begin
inc(counter, frameTicks);
if counter >= GROW_STEP_TIME then begin
inc(value);
counter := 0;
end;
end else begin
SetFruitState(fruit, Grown);
end;
end;
end;
Grown: begin
end;
Popped: begin
{ when the popped "animation" (not really an animation, just
abusing a 1-frame sequence with long delay as a timer)
completes, we can deactivate this dirt tile and fruit }
if fruit.entity.animation.complete then begin
hasFruit := false;
dec(numActiveDirtTiles);
with fruit.entity.position do begin
fx := FixToInt(x);
fy := FixToInt(y);
end;
if fruit.kind = Tomato then begin
SpawnTomatoSplash(fx, fy);
{ if the grapes-preference player is nearby this tomato
splash, then spawn an extra tomato splash at their exact
position and afflict them with the 'splashed' debuff }
with grapesPlayer^ do begin
if DoesEntityOverlap(entity,
fx-32,
fy-32,
fx+48,
fy+48) then begin
with entity.position do
SpawnTomatoSplash(FixToInt(x), FixToInt(y));
splashedDebuffTime := SPLASHED_DEBUFF_TIME;
end;
end;
end else begin
SpawnGrapesSplash(fx, fy);
{ if the tomato-preference player is nearby this grapes
splash, then spawn an extra grapes splash at their exact
position and afflict them with the 'splashed' debuff }
with tomatoPlayer^ do begin
if DoesEntityOverlap(entity,
fx-32,
fy-32,
fx+48,
fy+48) then begin
with entity.position do
SpawnGrapesSplash(FixToInt(x), FixToInt(y));
splashedDebuffTime := SPLASHED_DEBUFF_TIME;
end;
end;
end;
continue;
end;
end;
end;
with fruit do begin
UpdateAnimation(entity, fruitAnimations[ord(state)]);
end;
end;
end;
end;
{ ------------------------------------------------------------------------ }
function GetUnusedParticleIndex : integer;
{ returns the index of the next unused/inactive particle. returns -1 if
there is no free index }
var
i : integer;
begin
GetUnusedParticleIndex := -1;
for i := 0 to MAX_PARTICLES-1 do begin
if not particles[i].active then begin
GetUnusedParticleIndex := i;
exit;
end;
end;
end;
function InitNewParticle(x, y : integer) : integer;
var
i : integer;
begin
InitNewParticle := -1;
i := GetUnusedParticleIndex;
if i = -1 then exit;
MemFill(@particles[i], 0, SizeOf(Particle));
with particles[i] do begin
active := true;
with entity do begin
noCollision := true;
position.x := IntToFix(x);
position.y := IntToFix(y);
end;
end;
InitNewParticle := i;
end;
function SpawnTomatoSplash(x, y : integer) : integer;
{ spawns a new 'tomato splash' particle at the given coordinates.
returns the index of the spawned particle if successful, or -1 if there
was no free particle index }
var
i : integer;
begin
SpawnTomatoSplash := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
animation := @tomatoSplashAnimation;
end;
SpawnTomatoSplash := i;
end;
function SpawnGrapesSplash(x, y : integer) : integer;
{ spawns a new 'grapes splash' particle at the given coordinates.
returns the index of the spawned particle if successful, or -1 if there
was no free particle index }
var
i : integer;
begin
SpawnGrapesSplash := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
animation := @grapesSplashAnimation;
end;
SpawnGrapesSplash := i;
end;
function SpawnPlantSplash(x, y : integer) : integer;
{ spawns a new 'plant splash' (for when a plant is destroyed) particle at
the given coordinates. returns the index of the spawned particle if
successful, or -1 if there was no free particle index }
var
i : integer;
begin
SpawnPlantSplash := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
animation := @plantDestroyAnimation;
end;
SpawnPlantSplash := i;
end;
function SpawnStabFlash(x, y : integer) : integer;
{ spawns a new 'stab flash' particle at the given coordinates.
returns the index of the spawned particle if successful, or -1 if there
was no free particle index }
var
i : integer;
begin
SpawnStabFlash := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
animation := @stabFlashAnimation;
end;
SpawnStabFlash := i;
end;
function SpawnScoreUp(x, y : integer; kind : FruitKind) : integer;
{ spawns a new '+1' score particle at the given coordinates, for the
specified fruit (affects how it is displayed).
returns the index of the spawned particle if successful, or -1 if there
was no free particle index }
var
i : integer;
begin
SpawnScoreUp := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
if kind = Tomato then
animation := @tomatoScoreUpAnimation
else
animation := @grapesScoreUpAnimation;
with entity do begin
velocity.y := SCORE_UP_PARTICLE_SPEED;
end;
end;
SpawnScoreUp := i;
end;
function SpawnOw(x, y : integer) : integer;
{ spawns a new 'ow' particle (to indicate a player was stabbed) at the given
coordinates.
returns the index of the spawned particle if successful, or -1 if there
was no free particle index }
var
i : integer;
begin
SpawnOw := -1;
i := InitNewParticle(x, y);
if i = -1 then exit;
with particles[i] do begin
animation := @owAnimation;
with entity do begin
velocity.y := OW_PARTICLE_SPEED;
end;
end;
SpawnOw := i;
end;
procedure UpdateAllParticles;
var
i : word;
begin
for i := 0 to MAX_PARTICLES-1 do begin
with particles[i] do begin
if not active then continue;
MoveEntity(entity);
if animation <> nil then begin
{ particle is a "sprite-animated" particle type.
this means its lifetime is tied to the animation. update it's
animation, and when it is complete, kill the particle }
if entity.animation.complete then
active := false
else
UpdateAnimation(entity, animation^);
end else begin
{ TODO: "pixel" particle types ... }
end;
end;
end;
end;
end.