1122 lines
35 KiB
Plaintext
1122 lines
35 KiB
Plaintext
{$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.
|
|
|
|
|