1 (* Copyright (C) DooM 2D:Forever Developers
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 {$INCLUDE ../shared/a_modes.inc}
40 MARK_BLOCKED
= MARK_WALL
or MARK_DOOR
;
41 MARK_LIQUID
= MARK_WATER
or MARK_ACID
;
42 MARK_LIFT
= MARK_LIFTDOWN
or MARK_LIFTUP
or MARK_LIFTLEFT
or MARK_LIFTRIGHT
;
45 procedure g_GFX_Init ();
46 procedure g_GFX_Free ();
48 procedure g_GFX_Blood (fX
, fY
: Integer; count
: Word; vx
, vy
: Integer;
49 devX
, devY
: Word; cr
, cg
, cb
: Byte; kind
: Byte=BLOOD_NORMAL
);
50 procedure g_GFX_Spark (fX
, fY
: Integer; count
: Word; angle
: SmallInt; devX
, devY
: Byte);
51 procedure g_GFX_Water (fX
, fY
: Integer; count
: Word; fVelX
, fVelY
: Single; devX
, devY
, color
: Byte;
52 simple
: Boolean=false; cr
: Byte=0; cg
: Byte=0; cb
: Byte=0);
53 procedure g_GFX_SimpleWater (fX
, fY
: Integer; count
: Word; fVelX
, fVelY
: Single; defColor
, cr
, cg
, cb
: Byte);
54 procedure g_GFX_Bubbles (fX
, fY
: Integer; count
: Word; devX
, devY
: Byte);
56 procedure g_GFX_SetMax (count
: Integer);
57 function g_GFX_GetMax (): Integer;
59 procedure g_GFX_OnceAnim (X
, Y
: Integer; Anim
: TAnimation
; AnimType
: Byte = 0);
61 procedure g_Mark (x
, y
, Width
, Height
: Integer; t
: Byte; st
: Boolean=true);
63 procedure g_GFX_Update ();
64 procedure g_GFX_Draw ();
68 gpart_dbg_enabled
: Boolean = true;
69 gpart_dbg_phys_enabled
: Boolean = true;
75 g_map
, g_panel
, g_basic
, Math
, e_graphics
, GL
, GLExt
,
76 g_options
, g_console
, SysUtils
, g_triggers
, MAPDEF
,
77 g_game
, g_language
, g_net
, utils
, xprofiler
;
81 Unknown
= Integer($7fffffff);
85 TPartType
= (Blood
, Spark
, Bubbles
, Water
);
86 TPartState
= (Free
, Normal
, Stuck
, Sleeping
);
87 TFloorType
= (Wall
, LiquidIn
, LiquidOut
);
88 // Wall: floorY is just before floor
89 // LiquidIn: floorY is liquid *start* (i.e. just in a liquid)
90 // LiquidOut: floorY is liquid *end* (i.e. just out of a liquid)
91 TEnvType
= (EAir
, ELiquid
, EWall
); // where particle is now
93 // note: this MUST be record, so we can keep it in
94 // dynamic array and has sequential memory access pattern
95 PParticle
= ^TParticle
;
99 accelX
, accelY
: Single;
101 particleType
: TPartType
;
102 red
, green
, blue
: Byte;
104 time
, liveTime
: Word;
105 stickDX
: Integer; // STATE_STICK: -1,1: stuck to a wall; 0: stuck to ceiling
106 justSticked
: Boolean; // not used
107 floorY
: Integer; // actually, floor-1; `Unknown`: unknown
108 floorType
: TFloorType
;
109 env
: TEnvType
; // where particle is now
110 ceilingY
: Integer; // actually, ceiling+1; `Unknown`: unknown
111 wallEndY
: Integer; // if we stuck to a wall, this is where wall ends
113 //k8: sorry, i have to emulate virtual methods this way, 'cause i haet `Object`
114 procedure thinkerBloodAndWater ();
115 procedure thinkerSpark ();
116 procedure thinkerBubble ();
118 procedure findFloor (force
: Boolean=false); // this updates `floorY` if forced or Unknown
119 procedure findCeiling (force
: Boolean=false); // this updates `ceilingY` if forced or Unknown
121 procedure freeze (); inline; // remove velocities and acceleration
122 procedure sleep (); inline; // switch to sleep mode
124 function checkAirStreams (): Boolean; // `true`: affected by air stream
126 function alive (): Boolean; inline;
127 procedure die (); inline;
128 procedure think (); inline;
134 Animation
: TAnimation
;
139 Particles
: array of TParticle
= nil;
140 OnceAnims
: array of TOnceAnim
= nil;
141 MaxParticles
: Integer = 0;
142 CurrentParticle
: Integer = 0;
143 // awakeMap has one bit for each map grid cell; on g_Mark,
144 // corresponding bits will be set, and in `think()` all particles
145 // in marked cells will be awaken
146 awakeMap
: packed array of LongWord = nil;
147 awakeMapH
: Integer = -1;
148 awakeMapW
: Integer = -1;
149 awakeMinX
, awakeMinY
: Integer;
150 awakeDirty
: Boolean = false;
153 // ////////////////////////////////////////////////////////////////////////// //
154 // HACK! using mapgrid
155 procedure awmClear (); inline;
157 if awakeDirty
and (awakeMapW
> 0) then
159 FillDWord(awakeMap
[0], Length(awakeMap
), 0);
165 procedure awmSetup ();
167 assert(mapGrid
<> nil);
168 awakeMapW
:= (mapGrid
.gridWidth
+mapGrid
.tileSize
-1) div mapGrid
.tileSize
;
169 awakeMapW
:= (awakeMapW
+31) div 32; // LongWord has 32 bits ;-)
170 awakeMapH
:= (mapGrid
.gridHeight
+mapGrid
.tileSize
-1) div mapGrid
.tileSize
;
171 awakeMinX
:= mapGrid
.gridX0
;
172 awakeMinY
:= mapGrid
.gridY0
;
173 SetLength(awakeMap
, awakeMapW
*awakeMapH
);
174 {$IF DEFINED(D2F_DEBUG)}
175 e_LogWritefln('particle awake map: %sx%s (for grid of size %sx%s)', [awakeMapW
, awakeMapH
, mapGrid
.gridWidth
, mapGrid
.gridHeight
]);
182 function awmIsSet (x
, y
: Integer): Boolean; inline;
184 x
:= (x
-awakeMinX
) div mapGrid
.tileSize
;
185 y
:= (y
-awakeMinY
) div mapGrid
.tileSize
;
186 if (x
>= 0) and (y
>= 0) and (x
div 32 < awakeMapW
) and (y
< awakeMapH
) then
188 {$IF DEFINED(D2F_DEBUG)}
189 assert(y
*awakeMapW
+x
div 32 < Length(awakeMap
));
191 result
:= ((awakeMap
[y
*awakeMapW
+x
div 32] and (LongWord(1) shl (x
mod 32))) <> 0);
200 procedure awmSet (x
, y
: Integer); inline;
204 x
:= (x
-awakeMinX
) div mapGrid
.tileSize
;
205 y
:= (y
-awakeMinY
) div mapGrid
.tileSize
;
206 if (x
>= 0) and (y
>= 0) and (x
div 32 < awakeMapW
) and (y
< awakeMapH
) then
208 {$IF DEFINED(D2F_DEBUG)}
209 assert(y
*awakeMapW
+x
div 32 < Length(awakeMap
));
211 v
:= @awakeMap
[y
*awakeMapW
+x
div 32];
212 v
^ := v
^ or (LongWord(1) shl (x
mod 32));
218 // ////////////////////////////////////////////////////////////////////////// //
219 function TParticle
.alive (): Boolean; inline; begin result
:= (state
<> TPartState
.Free
); end;
220 procedure TParticle
.die (); inline; begin state
:= TPartState
.Free
; end;
222 // remove velocities and acceleration
223 procedure TParticle
.freeze (); inline;
225 // stop right there, you criminal scum!
233 // `true`: affected by air stream
234 function TParticle
.checkAirStreams (): Boolean;
238 pan
:= g_Map_PanelAtPoint(x
, y
, GridTagLift
);
239 result
:= (pan
<> nil);
242 if ((pan
.PanelType
and PANEL_LIFTUP
) <> 0) then
244 if (velY
> -4-Random(3)) then velY
-= 0.8;
245 if (abs(velX
) > 0.1) then velX
-= velX
/10.0;
246 velX
+= (Random
-Random
)*0.2;
249 else if ((pan
.PanelType
and PANEL_LIFTLEFT
) <> 0) then
251 if (velX
> -8-Random(3)) then velX
-= 0.8;
254 else if ((pan
.PanelType
and PANEL_LIFTRIGHT
) <> 0) then
256 if (velX
< 8+Random(3)) then velX
+= 0.8;
264 if result
and (state
= TPartState
.Sleeping
) then state
:= TPartState
.Normal
;
269 // switch to sleep mode
270 procedure TParticle
.sleep (); inline;
272 if not checkAirStreams() then
274 state
:= TPartState
.Sleeping
;
280 procedure TParticle
.findFloor (force
: Boolean=false);
285 if (not force
) and (floorY
<> Unknown
) then exit
;
286 // stuck in the wall? rescan, 'cause it can be mplat
287 if (env
= TEnvType
.EWall
) then
289 pan
:= g_Map_PanelAtPoint(x
, y
, (GridTagObstacle
or GridTagLiquid
));
292 // either in a wall, or in a liquid
293 if ((pan
.tag
and GridTagObstacle
) <> 0) then
295 // we are in the wall, wtf?!
297 env
:= TEnvType
.EWall
;
298 floorType
:= TFloorType
.Wall
;
299 state
:= TPartState
.Sleeping
; // anyway
302 // we are in liquid, trace to liquid end
303 env
:= TEnvType
.ELiquid
;
306 // are we in a liquid?
307 if (env
= TEnvType
.ELiquid
) then
309 // trace out of the liquid
310 //env := TEnvType.ELiquid;
311 floorType
:= TFloorType
.LiquidOut
;
312 //e_LogWritefln('tracing out of a liquid; floorY=%s; y=%s', [floorY, y]);
313 mapGrid
.traceOrthoRayWhileIn(ex
, floorY
, x
, y
, x
, g_Map_MaxY
, GridTagLiquid
);
314 floorY
+= 1; // so `floorY` is just out of a liquid
315 //e_LogWritefln(' traced out of a liquid; floorY=%s; y=%s', [floorY, y]);
320 assert(env
= TEnvType
.EAir
);
321 //env := TEnvType.EAir;
322 pan
:= g_Map_traceToNearest(x
, y
, x
, g_Map_MaxY
, (GridTagObstacle
or GridTagLiquid
), @ex
, @floorY
);
326 if ((pan
.tag
and GridTagObstacle
) <> 0) then
329 floorType
:= TFloorType
.Wall
;
334 floorType
:= TFloorType
.LiquidIn
; // entering liquid
335 floorY
+= 1; // so `floorY` is just in a liquid
340 // out of the level; assume wall, but it doesn't really matter
341 floorType
:= TFloorType
.Wall
;
342 floorY
:= g_Map_MaxY
+2;
348 procedure TParticle
.findCeiling (force
: Boolean=false);
352 if (not force
) and (ceilingY
<> Unknown
) then exit
;
353 if (nil = g_Map_traceToNearest(x
, y
, x
, g_Map_MinY
, GridTagObstacle
, @ex
, @ceilingY
)) then
355 ceilingY
:= g_Map_MinY
-2;
360 procedure TParticle
.think (); inline;
363 state
:= TPartState
.Normal
;
367 if (velY
= 0) then velY
:= 0.1;
368 if (accelY
= 0) then accelY
:= 0.5;
372 // awake sleeping particle, if necessary
376 TPartState
.Sleeping
, TPartState
.Stuck
:
377 if awmIsSet(x
, y
) then awake();
379 if (env
= TEnvType
.EWall
) and awmIsSet(x
, y
) then awake();
383 TPartType
.Blood
, TPartType
.Water
: thinkerBloodAndWater();
384 TPartType
.Spark
: thinkerSpark();
385 TPartType
.Bubbles
: thinkerBubble();
390 // ////////////////////////////////////////////////////////////////////////// //
391 procedure TParticle
.thinkerBloodAndWater ();
392 procedure stickToCeiling ();
394 state
:= TPartState
.Stuck
;
397 ceilingY
:= y
; // yep
400 procedure stickToWall (dx
: Integer);
404 state
:= TPartState
.Stuck
;
405 if (dX
> 0) then stickDX
:= 1 else stickDX
:= -1;
407 // find next floor transition
410 mapGrid
.traceOrthoRayWhileIn(ex
, wallEndY
, x
+stickDX
, y
, x
+stickDX
, floorY
+1, (GridTagWall
or GridTagDoor
or GridTagStep
));
413 procedure hitAFloor ();
415 state
:= TPartState
.Sleeping
; // we aren't moving anymore
418 floorType
:= TFloorType
.Wall
; // yep
421 // `true`: didn't, get outa thinker
422 function drip (): Boolean;
425 TPartType
.Blood
: result
:= (Random(200) = 100);
426 TPartType
.Water
: result
:= (Random(30) = 15);
427 else raise Exception
.Create('internal error in particle engine: drip');
433 // if we're falling from ceiling, switch to normal mode
434 if (state
= TPartState
.Stuck
) and (stickDX
= 0) then state
:= TPartState
.Normal
;
438 // switch to freefall mode
439 procedure freefall ();
441 state
:= TPartState
.Normal
;
446 procedure applyGravity (inLiquid
: Boolean);
448 state
:= TPartState
.Normal
;
462 _done
, _gravityagain
, _stuckagain
;
468 floorJustTraced
: Boolean;
470 if not gpart_dbg_phys_enabled
then goto _done
;
474 // still check for air streams when sleeping (no)
475 if (state
= TPartState
.Sleeping
) then begin {checkAirStreams();} goto _done
; end; // so blood will dissolve
477 // process stuck particles
478 if (state
= TPartState
.Stuck
) then
480 // stuck to a ceiling?
481 if (stickDX
= 0) then
483 // yeah, stuck to a ceiling
484 assert(ceilingY
<> Unknown
);
485 // dropped from a ceiling?
486 if (y
> ceilingY
) then
491 state
:= TPartState
.Normal
;
495 // otherwise, try to drip
496 if drip() then goto _done
;
502 if (wallEndY
= Unknown
) then
504 // this can happen if mplat was moved out; find new `wallEndY`
505 findFloor(true); // force trace, just in case
506 if (floorType
= TFloorType
.LiquidOut
) then env
:= TEnvType
.ELiquid
else env
:= TEnvType
.EAir
;
507 mapGrid
.traceOrthoRayWhileIn(ex
, wallEndY
, x
+stickDX
, y
, x
+stickDX
, floorY
+1, (GridTagWall
or GridTagDoor
or GridTagStep
));
511 if (wallEndY
<= floorY
) and (y
>= floorY
) then
515 TFloorType
.Wall
: // hit the ground
517 // check if our ground wasn't moved since the last scan
518 findFloor(true); // force trace
522 goto _done
; // nothing to do anymore
524 // otherwise, do it again
527 TFloorType
.LiquidIn
: // entering the liquid
529 // rescan, so we'll know when we'll exit the liquid
530 findFloor(true); // force rescan
532 TFloorType
.LiquidOut
: // exiting the liquid
534 // rescan, so we'll know when we'll enter something interesting
535 findFloor(true); // force rescan
536 if (floorType
= TFloorType
.Wall
) and (floorY
= y
) then begin sleep(); goto _done
; end;
541 if (floorY
<= wallEndY
) and (y
>= wallEndY
) then
543 // just unstuck from the wall, switch to freefall mode
549 // otherwise, try to drip
550 if drip() then goto _done
;
553 // nope, process as usual
556 // it is important to have it here
560 if (state
= TPartState
.Normal
) then checkAirStreams();
562 // gravity, if not stuck
563 if (state
<> TPartState
.Stuck
) and (abs(velX
) < 0.1) and (abs(velY
) < 0.1) then
565 floorJustTraced
:= (floorY
= Unknown
);
566 if floorJustTraced
then findFloor();
572 TFloorType
.Wall
: // hit the ground
574 // check if our ground wasn't moved since the last scan
575 if not floorJustTraced
then
577 findFloor(true); // force trace
578 if (floorType
= TFloorType
.LiquidOut
) then env
:= TEnvType
.ELiquid
else env
:= TEnvType
.EAir
;
579 if (y
<> floorY
) then goto _gravityagain
;
581 // otherwise, nothing to do
583 TFloorType
.LiquidIn
: // entering the liquid
585 // rescan, so we'll know when we'll exit the liquid
586 findFloor(true); // force rescan
589 TFloorType
.LiquidOut
: // exiting the liquid
591 // rescan, so we'll know when we'll enter something interesting
592 findFloor(true); // force rescan
593 if (floorType
<> TFloorType
.Wall
) or (floorY
<> y
) then applyGravity(floorType
= TFloorType
.LiquidIn
);
599 // looks like we're in the air
607 // has some horizontal velocity
608 pan
:= g_Map_traceToNearest(x
, y
, x
+dX
, y
+dY
, GridTagObstacle
, @ex
, @ey
);
609 checkEnv
:= (x
<> ex
);
617 // check environment (air/liquid)
618 if (g_Map_PanelAtPoint(x
, y
, GridTagLiquid
) <> nil) then env
:= TEnvType
.ELiquid
else env
:= TEnvType
.EAir
;
623 // the only case when we can have both ceiling and wall is corner; stick to wall in this case
624 // check if we stuck to a wall
625 if (dX
< 0) then dX
:= -1 else dX
:= 1;
626 if (g_Map_PanelAtPoint(x
+dX
, y
, GridTagObstacle
) <> nil) then
633 // stuck to a ceiling
638 else if (dY
<> 0) then
640 // has only vertical velocity
644 if (ceilingY
= Unknown
) then findCeiling(); // need to do this anyway
646 if (y
<= ceilingY
) then begin y
:= ceilingY
; stickToCeiling(); end; // oops, hit a ceiling
647 // environment didn't changed
654 floorJustTraced
:= (floorY
= Unknown
);
655 if floorJustTraced
then findFloor();
656 if (floorType
= TFloorType
.LiquidOut
) then env
:= TEnvType
.ELiquid
else env
:= TEnvType
.EAir
;
658 //e_LogWritefln('floorY=%s; newy=%s; dY=%s; floorType=%s', [floorY, y, dY, floorType]);
659 if (y
>= floorY
) then
664 //e_LogWritefln(' HIT FLOORY: floorY=%s; newy=%s; dY=%s; floorType=%s', [floorY, y, dY, floorType]);
666 TFloorType
.Wall
: // hit the ground
668 // check if our ground wasn't moved since the last scan
669 if not floorJustTraced
then
671 e_LogWritefln('force rescanning vpart at (%d,%d); floorY=%d', [x
, y
, floorY
]);
672 findFloor(true); // force trace
673 e_LogWritefln(' rescanned vpart at (%d,%d); floorY=%d', [x
, y
, floorY
]);
674 if (floorType
= TFloorType
.LiquidOut
) then env
:= TEnvType
.ELiquid
else env
:= TEnvType
.EAir
;
675 if (y
<> floorY
) then continue
;
677 // environment didn't changed
679 break
; // done with vertical movement
681 TFloorType
.LiquidIn
: // entering the liquid
683 // we're entered the liquid
684 env
:= TEnvType
.ELiquid
;
685 // rescan, so we'll know when we'll exit the liquid
686 findFloor(true); // force rescan
688 TFloorType
.LiquidOut
: // exiting the liquid
690 // we're exited the liquid
691 env
:= TEnvType
.EAir
;
692 // rescan, so we'll know when we'll enter something interesting
693 findFloor(true); // force rescan
694 if (floorType
= TFloorType
.Wall
) and (floorY
= y
) then
697 break
; // done with vertical movement
704 break
; // done with vertical movement
717 if (g_Map_PanelAtPoint(x
, y
, GridTagObstacle
) <> nil) then begin die(); exit
; end;
721 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then begin die(); end;
726 // blood will dissolve in other liquids
727 if (particleType
= TPartType
.Blood
) then
729 if (env
= TEnvType
.ELiquid
) then
732 if (liveTime
<= 0) then begin die(); exit
; end;
733 ex
:= 255-trunc(255.0*time
/liveTime
);
734 if (ex
<= 10) then begin die(); exit
; end;
735 if (ex
> 250) then ex
:= 255;
741 // water will disappear in any liquid
742 if (env
= TEnvType
.ELiquid
) then begin die(); exit
; end;
745 if (liveTime
<= 0) then begin die(); exit
; end;
746 ex
:= 255-trunc(255.0*time
/liveTime
);
747 if (ex
<= 10) then begin die(); exit
; end;
748 if (ex
> 250) then ex
:= 255;
754 // ////////////////////////////////////////////////////////////////////////// //
755 procedure g_GFX_SparkVel (fX
, fY
: Integer; count
: Word; vx
, vy
: Integer; devX
, devY
: Byte); forward;
757 procedure g_GFX_Blood (fX
, fY
: Integer; count
: Word; vx
, vy
: Integer;
758 devX
, devY
: Word; cr
, cg
, cb
: Byte; kind
: Byte = BLOOD_NORMAL
);
760 function genColor (cbase
, crnd
: Integer; def
: Byte=0): Byte;
765 if (cbase
< 0) then result
:= 0
766 else if (cbase
> 255) then result
:= 255
767 else result
:= Byte(cbase
);
777 devX1
, devX2
, devY1
, devY2
: Integer;
782 if not gpart_dbg_enabled
then exit
;
784 if (kind
= BLOOD_SPARKS
) then
786 g_GFX_SparkVel(fX
, fY
, 2+Random(2), -vx
div 2, -vy
div 2, devX
, devY
);
790 l
:= Length(Particles
);
791 if (l
= 0) then exit
;
792 if (count
> l
) then count
:= l
;
799 for a
:= 1 to count
do
801 with Particles
[CurrentParticle
] do
803 x
:= fX
-devX1
+Random(devX2
);
804 y
:= fY
-devY1
+Random(devY2
);
806 // check for level bounds
807 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then continue
;
809 // in what environment we are starting in?
810 pan
:= g_Map_PanelAtPoint(x
, y
, (GridTagObstacle
or GridTagLiquid
));
813 // either in a wall, or in a liquid
814 if ((pan
.tag
and GridTagObstacle
) <> 0) then continue
; // don't spawn in walls
815 env
:= TEnvType
.ELiquid
;
819 env
:= TEnvType
.EAir
;
822 velX
:= vx
+(Random
-Random
)*3;
823 velY
:= vy
+(Random
-Random
)*3;
827 if (velY
-4 < -4) then velY
:= -4 else velY
:= velY
-4;
830 accelX
:= -sign(velX
)*Random
/100;
833 crnd
:= 20*Random(6)-50;
835 red
:= genColor(cr
, CRnd
, 0);
836 green
:= genColor(cg
, CRnd
, 0);
837 blue
:= genColor(cb
, CRnd
, 0);
840 particleType
:= TPartType
.Blood
;
841 state
:= TPartState
.Normal
;
843 liveTime
:= 120+Random(40);
848 if (CurrentParticle
>= MaxParticles
-1) then CurrentParticle
:= 0 else CurrentParticle
+= 1;
853 procedure g_GFX_Water (fX
, fY
: Integer; count
: Word; fVelX
, fVelY
: Single; devX
, devY
, color
: Byte;
854 simple
: Boolean=false; cr
: Byte=0; cg
: Byte=0; cb
: Byte=0);
857 devX1
, devX2
, devY1
, devY2
: Integer;
861 if not gpart_dbg_enabled
then exit
;
863 l
:= Length(Particles
);
864 if (l
= 0) then exit
;
865 if (count
> l
) then count
:= l
;
867 if (abs(fVelX
) < 3.0) then fVelX
:= 3.0-6.0*Random
;
874 if (not simple
) and (color
> 3) then color
:= 0;
876 for a
:= 1 to count
do
878 with Particles
[CurrentParticle
] do
882 x
:= fX
-devX1
+Random(devX2
);
883 y
:= fY
-devY1
+Random(devY2
);
885 if (abs(fVelX
) < 0.5) then velX
:= 1.0-2.0*Random
else velX
:= fVelX
*Random
;
886 if (Random(10) < 7) then velX
:= -velX
;
887 velY
:= fVelY
*Random
;
902 // check for level bounds
903 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then continue
;
905 // this hack will allow water spawned in water to fly out
906 // it can happen when player fell from a huge height (see "DOOM2D.WAD:\MAP03", for example)
909 // in what environment we are starting in?
910 pan
:= g_Map_PanelAtPoint(x
, y
, (GridTagObstacle
or GridTagLiquid
));
914 pan
:= g_Map_PanelAtPoint(x
, y
, GridTagObstacle
);
916 if (pan
<> nil) then continue
;
917 env
:= TEnvType
.EAir
;
923 red
:= 155+Random(9)*10;
924 green
:= trunc(150*Random
);
929 red
:= trunc(150*Random
);
930 green
:= 175+Random(9)*10;
935 red
:= trunc(200*Random
);
937 blue
:= 175+Random(9)*10;
939 4: // Ñâîé öâåò, ñâåòëåå
941 red
:= 20+Random(19)*10;
944 red
:= nmin(red
+cr
, 255);
945 green
:= nmin(green
+cg
, 255);
946 blue
:= nmin(blue
+cb
, 255);
948 5: // Ñâîé öâåò, òåìÃåå
950 red
:= 20+Random(19)*10;
953 red
:= nmax(cr
-red
, 0);
954 green
:= nmax(cg
-green
, 0);
955 blue
:= nmax(cb
-blue
, 0);
959 red
:= 90+random(12)*10;
966 particleType
:= TPartType
.Water
;
967 state
:= TPartState
.Normal
;
969 liveTime
:= 60+Random(60);
974 if (CurrentParticle
>= MaxParticles
-1) then CurrentParticle
:= 0 else CurrentParticle
+= 1;
979 procedure g_GFX_SimpleWater (fX
, fY
: Integer; count
: Word; fVelX
, fVelY
: Single; defColor
, cr
, cg
, cb
: Byte);
981 g_GFX_Water(fX
, fY
, count
, 0, 0, 0, 0, defColor
, true, cr
, cg
, cb
);
985 // ////////////////////////////////////////////////////////////////////////// //
986 procedure TParticle
.thinkerBubble ();
997 if (y
<= ceilingY
) then begin die(); exit
; end;
1001 if (y
>= floorY
) then begin die(); exit
; end;
1003 if (y
< g_Map_MinY
) or (y
> g_Map_MaxY
) then begin die(); exit
; end;
1006 if (velY
> -4) then velY
+= accelY
;
1012 {.$DEFINE D2F_DEBUG_BUBBLES}
1013 procedure g_GFX_Bubbles (fX
, fY
: Integer; count
: Word; devX
, devY
: Byte);
1015 a
, liquidx
: Integer;
1016 devX1
, devX2
, devY1
, devY2
: Integer;
1018 {$IF DEFINED(D2F_DEBUG_BUBBLES)}
1023 if not gpart_dbg_enabled
then exit
;
1025 l
:= Length(Particles
);
1026 if (l
= 0) then exit
;
1027 if (count
> l
) then count
:= l
;
1029 devX1
:= devX
div 2;
1031 devY1
:= devY
div 2;
1034 for a
:= 1 to count
do
1036 with Particles
[CurrentParticle
] do
1038 x
:= fX
-devX1
+Random(devX2
);
1039 y
:= fY
-devY1
+Random(devY2
);
1041 // check for level bounds
1042 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then continue
;
1045 // don't spawn bubbles outside of the liquid
1046 if not isLiquidAt(X, Y) {ByteBool(gCollideMap[Y, X] and MARK_LIQUID)} then
1050 // trace liquid, so we'll know where it ends; do it in 8px steps for speed
1051 // tracer will return `false` if we started outside of the liquid
1053 {$IF DEFINED(D2F_DEBUG_BUBBLES)}
1054 stt
:= curTimeMicro();
1055 ptr
:= mapGrid
.traceOrthoRayWhileIn(liquidx
, liquidTopY
, x
, y
, x
, 0, GridTagWater
or GridTagAcid1
or GridTagAcid2
);
1056 stt
:= curTimeMicro()-stt
;
1057 e_LogWritefln('traceOrthoRayWhileIn: time=%s (%s); liquidTopY=%s', [Integer(stt
), ptr
, liquidTopY
]);
1059 stt
:= curTimeMicro();
1060 nptr
:= g_Map_TraceLiquidNonPrecise(x
, y
, 0, -8, liquidx
, liquidTopY
);
1061 stt
:= curTimeMicro()-stt
;
1062 e_LogWritefln('g_Map_TraceLiquidNonPrecise: time=%s (%s); liquidTopY=%s', [Integer(stt
), nptr
, liquidTopY
]);
1063 if not nptr
then continue
;
1065 if not g_Map_TraceLiquidNonPrecise(x
, y
, 0, -8, liquidx
, ceilingY
) then continue
;
1066 if not g_Map_TraceLiquidNonPrecise(x
, y
, 0, +8, liquidx
, floorY
) then continue
;
1079 state
:= TPartState
.Normal
;
1080 particleType
:= TPartType
.Bubbles
;
1085 if (CurrentParticle
>= MaxParticles
-1) then CurrentParticle
:= 0 else CurrentParticle
+= 1;
1090 // ////////////////////////////////////////////////////////////////////////// //
1091 procedure TParticle
.thinkerSpark ();
1099 if not gpart_dbg_phys_enabled
then goto _done
;
1105 if (abs(velX
) < 0.1) and (abs(velY
) < 0.1) then
1114 // has some horizontal velocity
1115 pan
:= g_Map_traceToNearest(x
, y
, x
+dX
, y
+dY
, (GridTagObstacle
or GridTagLiquid
), @ex
, @ey
);
1116 if (x
<> ex
) then begin floorY
:= Unknown
; ceilingY
:= Unknown
; end; // dunno yet
1119 if (pan
<> nil) then
1121 if ((pan
.tag
and GridTagLiquid
) <> 0) then begin die(); exit
; end; // die in liquid
1122 // hit the wall; falling down vertically
1127 else if (dY
<> 0) then
1129 // has some vertical velocity
1133 if (ceilingY
= Unknown
) then findCeiling(); // need to do this anyway
1135 if (y
<= ceilingY
) then
1137 // oops, hit a ceiling
1140 accelY
:= abs(accelY
);
1142 // environment didn't changed
1147 if (floorY
= Unknown
) then findFloor(); // need to do this anyway
1149 if (y
>= floorY
) then
1151 // hit something except a floor?
1152 if (floorType
<> TFloorType
.Wall
) then begin die(); exit
; end; // yep: just die
1153 // otherwise, go to sleep
1156 // environment didn't changed
1162 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then begin die(); end;
1164 if (velX
<> 0.0) then velX
+= accelX
;
1166 if (velY
<> 0.0) then
1168 if (accelY
< 10) then accelY
+= 0.08;
1176 // ////////////////////////////////////////////////////////////////////////// //
1177 procedure g_GFX_SparkVel (fX
, fY
: Integer; count
: Word; vx
, vy
: Integer; devX
, devY
: Byte);
1180 devX1
, devX2
, devY1
, devY2
: Integer;
1184 if not gpart_dbg_enabled
then exit
;
1186 l
:= Length(Particles
);
1187 if (l
= 0) then exit
;
1188 if (count
> l
) then count
:= l
;
1190 devX1
:= devX
div 2;
1192 devY1
:= devY
div 2;
1195 for a
:= 1 to count
do
1197 with Particles
[CurrentParticle
] do
1199 x
:= fX
-devX1
+Random(devX2
);
1200 y
:= fY
-devY1
+Random(devY2
);
1202 // check for level bounds
1203 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then continue
;
1205 // in what environment we are starting in?
1206 pan
:= g_Map_PanelAtPoint(x
, y
, (GridTagObstacle
or GridTagLiquid
));
1207 if (pan
<> nil) then
1209 // either in a wall, or in a liquid
1210 //if ((pan.tag and GridTagObstacle) <> 0) then continue; // don't spawn in walls
1211 //env := TEnvType.ELiquid;
1216 env
:= TEnvType
.EAir
;
1219 velX
:= vx
+(Random
-Random
)*3;
1220 velY
:= vy
+(Random
-Random
)*3;
1224 if (velY
-4 < -4) then velY
:= -4 else velY
:= velY
-4;
1227 accelX
:= -sign(velX
)*Random
/100;
1231 green
:= 100+Random(155);
1235 particleType
:= TPartType
.Spark
;
1236 state
:= TPartState
.Normal
;
1238 liveTime
:= 30+Random(60);
1240 ceilingY
:= Unknown
;
1243 if (CurrentParticle
>= MaxParticles
-1) then CurrentParticle
:= 0 else CurrentParticle
+= 1;
1248 procedure g_GFX_Spark (fX
, fY
: Integer; count
: Word; angle
: SmallInt; devX
, devY
: Byte);
1252 devX1
, devX2
, devY1
, devY2
: Integer;
1253 baseVelX
, baseVelY
: Single;
1257 if not gpart_dbg_enabled
then exit
;
1259 l
:= Length(Particles
);
1260 if (l
= 0) then exit
;
1261 if (count
> l
) then count
:= l
;
1265 devX1
:= devX
div 2;
1267 devY1
:= devY
div 2;
1270 b
:= DegToRad(angle
);
1272 baseVelY
:= 1.6*sin(b
);
1273 if (abs(baseVelX
) < 0.01) then baseVelX
:= 0.0;
1274 if (abs(baseVelY
) < 0.01) then baseVelY
:= 0.0;
1276 for a
:= 1 to count
do
1278 with Particles
[CurrentParticle
] do
1280 x
:= fX
-devX1
+Random(devX2
);
1281 y
:= fY
-devY1
+Random(devY2
);
1283 // check for level bounds
1284 if (x
< g_Map_MinX
) or (y
< g_Map_MinY
) or (x
> g_Map_MaxX
) or (y
> g_Map_MaxY
) then continue
;
1286 // in what environment we are starting in?
1287 pan
:= g_Map_PanelAtPoint(x
, y
, (GridTagObstacle
or GridTagLiquid
));
1288 if (pan
<> nil) then
1290 // either in a wall, or in a liquid
1291 //if ((pan.tag and GridTagObstacle) <> 0) then continue; // don't spawn in walls
1292 //env := TEnvType.ELiquid;
1297 env
:= TEnvType
.EAir
;
1300 velX
:= baseVelX
*Random
;
1301 velY
:= baseVelY
-Random
;
1306 green
:= 100+Random(155);
1310 particleType
:= TPartType
.Spark
;
1311 state
:= TPartState
.Normal
;
1313 liveTime
:= 30+Random(60);
1315 ceilingY
:= Unknown
;
1318 if (CurrentParticle
>= MaxParticles
-1) then CurrentParticle
:= 0 else CurrentParticle
+= 1;
1323 // ////////////////////////////////////////////////////////////////////////// //
1324 procedure g_GFX_SetMax (count
: Integer);
1328 if count
> 50000 then count
:= 50000;
1329 if (count
< 1) then count
:= 1;
1330 SetLength(Particles
, count
);
1331 for a
:= 0 to High(Particles
) do Particles
[a
].die();
1332 MaxParticles
:= count
;
1333 CurrentParticle
:= 0;
1337 function g_GFX_GetMax (): Integer;
1339 result
:= MaxParticles
;
1343 function FindOnceAnim (): DWORD
;
1347 if OnceAnims
<> nil then
1348 for i
:= 0 to High(OnceAnims
) do
1349 if OnceAnims
[i
].Animation
= nil then
1355 if OnceAnims
= nil then
1357 SetLength(OnceAnims
, 16);
1362 Result
:= High(OnceAnims
) + 1;
1363 SetLength(OnceAnims
, Length(OnceAnims
) + 16);
1368 procedure g_GFX_OnceAnim (x
, y
: Integer; Anim
: TAnimation
; AnimType
: Byte = 0);
1372 if not gpart_dbg_enabled
then exit
;
1374 if (Anim
= nil) then exit
;
1376 find_id
:= FindOnceAnim();
1378 OnceAnims
[find_id
].AnimType
:= AnimType
;
1379 OnceAnims
[find_id
].Animation
:= TAnimation
.Create(Anim
.FramesID
, Anim
.Loop
, Anim
.Speed
);
1380 OnceAnims
[find_id
].Animation
.Blending
:= Anim
.Blending
;
1381 OnceAnims
[find_id
].Animation
.alpha
:= Anim
.alpha
;
1382 OnceAnims
[find_id
].x
:= x
;
1383 OnceAnims
[find_id
].y
:= y
;
1387 // ////////////////////////////////////////////////////////////////////////// //
1391 procedure g_Mark (x
, y
, Width
, Height
: Integer; t
: Byte; st
: Boolean=true);
1393 cx
, ex
, ey
: Integer;
1396 if not gpart_dbg_enabled
then exit
;
1398 if (Width
< 1) or (Height
< 1) then exit
;
1399 // make some border, so we'll hit particles lying around the panel
1401 y
-= 1; Height
+= 2;
1404 ts
:= mapGrid
.tileSize
;
1418 // ////////////////////////////////////////////////////////////////////////// //
1419 procedure g_GFX_Init ();
1421 //g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 1/6', 0, False);
1422 //SetLength(gCollideMap, gMapInfo.Height+1);
1423 //for a := 0 to High(gCollideMap) do SetLength(gCollideMap[a], gMapInfo.Width+1);
1426 gpart_dbg_enabled
:= false;
1431 procedure g_GFX_Free ();
1436 SetLength(Particles
, MaxParticles
);
1437 for a
:= 0 to High(Particles
) do Particles
[a
].die();
1438 CurrentParticle
:= 0;
1440 if (OnceAnims
<> nil) then
1442 for a
:= 0 to High(OnceAnims
) do OnceAnims
[a
].Animation
.Free();
1453 // ////////////////////////////////////////////////////////////////////////// //
1454 procedure g_GFX_Update ();
1460 if not gpart_dbg_enabled
then exit
;
1462 if (Particles
<> nil) then
1464 w
:= gMapInfo
.Width
;
1465 h
:= gMapInfo
.Height
;
1467 len
:= High(Particles
);
1469 for a
:= 0 to len
do
1471 if Particles
[a
].alive
then
1473 with Particles
[a
] do
1475 if (time
= liveTime
) then begin die(); continue
; end;
1476 if (x
+1 >= w
) or (y
+1 >= h
) or (x
<= 0) or (y
<= 0) then begin die(); end;
1481 end; // Particles <> nil
1486 if OnceAnims
<> nil then
1488 for a
:= 0 to High(OnceAnims
) do
1489 if OnceAnims
[a
].Animation
<> nil then
1491 case OnceAnims
[a
].AnimType
of
1494 if Random(3) = 0 then
1495 OnceAnims
[a
].x
:= OnceAnims
[a
].x
-1+Random(3);
1496 if Random(2) = 0 then
1497 OnceAnims
[a
].y
:= OnceAnims
[a
].y
-Random(2);
1501 if OnceAnims
[a
].Animation
.Played
then
1503 OnceAnims
[a
].Animation
.Free();
1504 OnceAnims
[a
].Animation
:= nil;
1507 OnceAnims
[a
].Animation
.Update();
1513 procedure g_GFX_Draw ();
1518 if not gpart_dbg_enabled
then exit
;
1520 if (Particles
<> nil) then
1522 glDisable(GL_TEXTURE_2D
);
1526 glBlendFunc(GL_SRC_ALPHA
, GL_ONE_MINUS_SRC_ALPHA
);
1530 scaled
:= (g_dbg_scale
<> 1.0);
1532 len
:= High(Particles
);
1533 for a
:= 0 to len
do
1535 with Particles
[a
] do
1537 if not alive
then continue
;
1538 if scaled
or ((x
>= sX
) and (y
>= sY
) and (x
<= sX
+sWidth
) and (sY
<= sY
+sHeight
)) then
1540 glColor4ub(red
, green
, blue
, alpha
);
1541 glVertex2f(x
+0.37, y
+0.37);
1548 glDisable(GL_BLEND
);
1551 if (OnceAnims
<> nil) then
1553 len
:= High(OnceAnims
);
1554 for a
:= 0 to len
do
1556 if (OnceAnims
[a
].Animation
<> nil) then
1558 with OnceAnims
[a
] do Animation
.Draw(x
, y
, M_NONE
);