DEADSOFTWARE

Game: Use proper syntax of sets for game options instead of raw bitwise operations
[d2df-sdl.git] / src / game / g_items.pas
index 30de46830504be65fe4d365ac6c8278fc9734e73..b3d8522465bc8b6da4c182b4ef7586e2a1130020 100644 (file)
@@ -1,23 +1,53 @@
-{$MODE DELPHI}
+(* Copyright (C)  Doom 2D: Forever Developers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License ONLY.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *)
+{$INCLUDE ../shared/a_modes.inc}
 unit g_items;
 
 interface
 
 uses
-  g_textures, g_phys, g_saveload, BinEditor;
+  SysUtils, Classes,
+  MAPDEF, g_textures, g_phys, g_saveload;
 
 Type
+  PItem = ^TItem;
   TItem = record
-    ItemType:      Byte;
-    Respawnable:   Boolean;
-    InitX, InitY:  Integer;
-    RespawnTime:   Word;
-    Live:          Boolean;
-    Fall:          Boolean;
-    QuietRespawn:  Boolean;
-    SpawnTrigger:  Integer;
-    Obj:           TObj;
-    Animation:     TAnimation;
+  private
+    //treeNode: Integer;
+    slotIsUsed: Boolean;
+    arrIdx: Integer; // in ggItems
+
+  public
+    ItemType: Byte;
+    Respawnable: Boolean;
+    InitX, InitY: Integer;
+    RespawnTime: Word;
+    alive: Boolean;
+    Fall: Boolean;
+    QuietRespawn: Boolean;
+    SpawnTrigger: Integer;
+    Obj: TObj;
+    Animation: TAnimation;
+    dropped: Boolean; // dropped from the monster? drops should be rendered after corpses, so zombie corpse will not obscure ammo container, for example
+    NeedSend: Boolean;
+
+    procedure positionChanged (); //WARNING! call this after monster position was changed, or coldet will not work right!
+    procedure getMapBox (out x, y, w, h: Integer); inline;
+    procedure moveBy (dx, dy: Integer); inline;
+
+    property myid: Integer read arrIdx;
   end;
 
 procedure g_Items_LoadData();
@@ -26,26 +56,108 @@ procedure g_Items_Init();
 procedure g_Items_Free();
 function g_Items_Create(X, Y: Integer; ItemType: Byte;
            Fall, Respawnable: Boolean; AdjCoord: Boolean = False; ForcedID: Integer = -1): DWORD;
+procedure g_Items_SetDrop (ID: DWORD);
+procedure g_Items_PreUpdate();
 procedure g_Items_Update();
 procedure g_Items_Draw();
+procedure g_Items_DrawDrop();
 procedure g_Items_Pick(ID: DWORD);
 procedure g_Items_Remove(ID: DWORD);
-procedure g_Items_SaveState(var Mem: TBinMemoryWriter);
-procedure g_Items_LoadState(var Mem: TBinMemoryReader);
+procedure g_Items_SaveState (st: TStream);
+procedure g_Items_LoadState (st: TStream);
+
+procedure g_Items_RestartRound ();
+
+function g_Items_ValidId (idx: Integer): Boolean; inline;
+function g_Items_ByIdx (idx: Integer): PItem;
+function g_Items_ObjByIdx (idx: Integer): PObj;
+
+procedure g_Items_EmitPickupSound (idx: Integer); // at item position
+procedure g_Items_EmitPickupSoundAt (idx, x, y: Integer);
+
+procedure g_Items_AddDynLights();
+
+
+type
+  TItemEachAliveCB = function (it: PItem): Boolean is nested; // return `true` to stop
+
+function g_Items_ForEachAlive (cb: TItemEachAliveCB; backwards: Boolean=false): Boolean;
+function g_Items_NextAlive (startIdx: Integer): PItem;
 
 var
-  gItems: Array of TItem = nil;
-  gItemsTexturesID: Array [1..35] of DWORD;
-  gMaxDist: Integer = 1;
-  ITEM_RESPAWNTIME: Integer = 60 * 36;
+  gItemsTexturesID: Array [1..ITEM_MAX] of DWORD;
+  gMaxDist: Integer = 1; // for sounds
 
 implementation
 
 uses
+  Math,
   g_basic, e_graphics, g_sound, g_main, g_gfx, g_map,
-  Math, g_game, g_triggers, g_console, SysUtils, g_player, g_net, g_netmsg,
-  MAPDEF, e_log;
+  g_game, g_triggers, g_console, g_player, g_net, g_netmsg,
+  e_log,
+  g_grid, binheap, idpool, utils, xstreams;
+
+
+var
+  ggItems: Array of TItem = nil;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+var
+  freeIds: TIdPool = nil;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function g_Items_ValidId (idx: Integer): Boolean; inline;
+begin
+  result := false;
+  if (idx < 0) or (idx > High(ggItems)) then exit;
+  if not ggItems[idx].slotIsUsed then exit;
+  result := true;
+end;
+
 
+function g_Items_ByIdx (idx: Integer): PItem;
+begin
+  if (idx < 0) or (idx > High(ggItems)) then raise Exception.Create('g_ItemObjByIdx: invalid index');
+  result := @ggItems[idx];
+  if not result.slotIsUsed then raise Exception.Create('g_ItemObjByIdx: requested inexistent item');
+end;
+
+
+function g_Items_ObjByIdx (idx: Integer): PObj;
+begin
+  if (idx < 0) or (idx > High(ggItems)) then raise Exception.Create('g_ItemObjByIdx: invalid index');
+  if not ggItems[idx].slotIsUsed then raise Exception.Create('g_ItemObjByIdx: requested inexistent item');
+  result := @ggItems[idx].Obj;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+procedure TItem.positionChanged ();
+begin
+  NeedSend := NeedSend or (Obj.X <> Obj.oldX) or (Obj.Y <> Obj.oldY);
+end;
+
+procedure TItem.getMapBox (out x, y, w, h: Integer); inline;
+begin
+  x := Obj.X+Obj.Rect.X;
+  y := Obj.Y+Obj.Rect.Y;
+  w := Obj.Rect.Width;
+  h := Obj.Rect.Height;
+end;
+
+procedure TItem.moveBy (dx, dy: Integer); inline;
+begin
+  if (dx <> 0) or (dy <> 0) then
+  begin
+    Obj.X += dx;
+    Obj.Y += dy;
+    positionChanged();
+  end;
+end;
+
+// ////////////////////////////////////////////////////////////////////////// //
 const
   ITEM_SIGNATURE = $4D455449; // 'ITEM'
 
@@ -67,7 +179,7 @@ const
      ((62), (16)), // WEAPON_ROCKETLAUNCHER
      ((54), (16)), // WEAPON_PLASMA
      ((61), (36)), // WEAPON_BFG
-     ((54), (16)), // WEAPON_SUPERPULEMET
+     ((54), (16)), // WEAPON_SUPERCHAINGUN
      (( 9), (11)), // AMMO_BULLETS
      ((28), (16)), // AMMO_BULLETS_BOX
      ((15), ( 7)), // AMMO_SHELLS
@@ -80,28 +192,31 @@ const
      ((16), (16)), // KEY_RED
      ((16), (16)), // KEY_GREEN
      ((16), (16)), // KEY_BLUE
-     (( 1), ( 1)), // WEAPON_KASTET
+     (( 1), ( 1)), // WEAPON_IRONFIST
      ((43), (16)), // WEAPON_PISTOL
      ((14), (18)), // BOTTLE
      ((16), (15)), // HELMET
      ((32), (24)), // JETPACK
-     ((25), (25))); // INVIS
+     ((25), (25)), // INVIS
+     ((53), (20)), // WEAPON_FLAMETHROWER
+     ((13), (20))); // AMMO_FUELCAN
 
 procedure InitTextures();
 begin
   g_Texture_Get('ITEM_MEDKIT_SMALL',     gItemsTexturesID[ITEM_MEDKIT_SMALL]);
   g_Texture_Get('ITEM_MEDKIT_LARGE',     gItemsTexturesID[ITEM_MEDKIT_LARGE]);
   g_Texture_Get('ITEM_MEDKIT_BLACK',     gItemsTexturesID[ITEM_MEDKIT_BLACK]);
-  g_Texture_Get('ITEM_SUIT',             gItemsTexturesID[ITEM_SUIT]);
-  g_Texture_Get('ITEM_OXYGEN',           gItemsTexturesID[ITEM_OXYGEN]);
+  g_Texture_Get('ITEM_WEAPON_IRONFIST',  gItemsTexturesID[ITEM_WEAPON_IRONFIST]);
   g_Texture_Get('ITEM_WEAPON_SAW',       gItemsTexturesID[ITEM_WEAPON_SAW]);
+  g_Texture_Get('ITEM_WEAPON_PISTOL',    gItemsTexturesID[ITEM_WEAPON_PISTOL]);
   g_Texture_Get('ITEM_WEAPON_SHOTGUN1',  gItemsTexturesID[ITEM_WEAPON_SHOTGUN1]);
   g_Texture_Get('ITEM_WEAPON_SHOTGUN2',  gItemsTexturesID[ITEM_WEAPON_SHOTGUN2]);
   g_Texture_Get('ITEM_WEAPON_CHAINGUN',  gItemsTexturesID[ITEM_WEAPON_CHAINGUN]);
   g_Texture_Get('ITEM_WEAPON_ROCKETLAUNCHER', gItemsTexturesID[ITEM_WEAPON_ROCKETLAUNCHER]);
   g_Texture_Get('ITEM_WEAPON_PLASMA',    gItemsTexturesID[ITEM_WEAPON_PLASMA]);
   g_Texture_Get('ITEM_WEAPON_BFG',       gItemsTexturesID[ITEM_WEAPON_BFG]);
-  g_Texture_Get('ITEM_WEAPON_SUPERPULEMET', gItemsTexturesID[ITEM_WEAPON_SUPERPULEMET]);
+  g_Texture_Get('ITEM_WEAPON_SUPERCHAINGUN', gItemsTexturesID[ITEM_WEAPON_SUPERCHAINGUN]);
+  g_Texture_Get('ITEM_WEAPON_FLAMETHROWER', gItemsTexturesID[ITEM_WEAPON_FLAMETHROWER]);
   g_Texture_Get('ITEM_AMMO_BULLETS',     gItemsTexturesID[ITEM_AMMO_BULLETS]);
   g_Texture_Get('ITEM_AMMO_BULLETS_BOX', gItemsTexturesID[ITEM_AMMO_BULLETS_BOX]);
   g_Texture_Get('ITEM_AMMO_SHELLS',      gItemsTexturesID[ITEM_AMMO_SHELLS]);
@@ -110,21 +225,21 @@ begin
   g_Texture_Get('ITEM_AMMO_ROCKET_BOX',  gItemsTexturesID[ITEM_AMMO_ROCKET_BOX]);
   g_Texture_Get('ITEM_AMMO_CELL',        gItemsTexturesID[ITEM_AMMO_CELL]);
   g_Texture_Get('ITEM_AMMO_CELL_BIG',    gItemsTexturesID[ITEM_AMMO_CELL_BIG]);
+  g_Texture_Get('ITEM_AMMO_FUELCAN',     gItemsTexturesID[ITEM_AMMO_FUELCAN]);
   g_Texture_Get('ITEM_AMMO_BACKPACK',    gItemsTexturesID[ITEM_AMMO_BACKPACK]);
   g_Texture_Get('ITEM_KEY_RED',          gItemsTexturesID[ITEM_KEY_RED]);
   g_Texture_Get('ITEM_KEY_GREEN',        gItemsTexturesID[ITEM_KEY_GREEN]);
   g_Texture_Get('ITEM_KEY_BLUE',         gItemsTexturesID[ITEM_KEY_BLUE]);
-  g_Texture_Get('ITEM_WEAPON_KASTET',    gItemsTexturesID[ITEM_WEAPON_KASTET]);
-  g_Texture_Get('ITEM_WEAPON_PISTOL',    gItemsTexturesID[ITEM_WEAPON_PISTOL]);
-  g_Texture_Get('ITEM_JETPACK',          gItemsTexturesID[ITEM_JETPACK]);
+  g_Texture_Get('ITEM_OXYGEN',           gItemsTexturesID[ITEM_OXYGEN]);
+  g_Texture_Get('ITEM_SUIT',             gItemsTexturesID[ITEM_SUIT]);
 end;
 
 procedure g_Items_LoadData();
 begin
-  e_WriteLog('Loading items data...', MSG_NOTIFY);
+  e_WriteLog('Loading items data...', TMsgType.Notify);
 
   g_Sound_CreateWADEx('SOUND_ITEM_RESPAWNITEM', GameWAD+':SOUNDS\RESPAWNITEM');
-  g_Sound_CreateWADEx('SOUND_ITEM_GETRULEZ', GameWAD+':SOUNDS\GETRULEZ');
+  g_Sound_CreateWADEx('SOUND_ITEM_GETPOWERUP', GameWAD+':SOUNDS\GETPOWERUP');
   g_Sound_CreateWADEx('SOUND_ITEM_GETWEAPON', GameWAD+':SOUNDS\GETWEAPON');
   g_Sound_CreateWADEx('SOUND_ITEM_GETITEM', GameWAD+':SOUNDS\GETITEM');
 
@@ -132,6 +247,7 @@ begin
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_WHITESPHERE', GameWAD+':TEXTURES\SWHITE', 32, 32, 4, True);
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_ARMORGREEN', GameWAD+':TEXTURES\ARMORGREEN', 32, 16, 3, True);
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_ARMORBLUE', GameWAD+':TEXTURES\ARMORBLUE', 32, 16, 3, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_JETPACK', GameWAD+':TEXTURES\JETPACK', 32, 32, 3, True);
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_INVUL', GameWAD+':TEXTURES\INVUL', 32, 32, 4, True);
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_INVIS', GameWAD+':TEXTURES\INVIS', 32, 32, 4, True);
   g_Frames_CreateWAD(nil, 'FRAMES_ITEM_RESPAWN', GameWAD+':TEXTURES\ITEMRESPAWN', 32, 32, 5, True);
@@ -142,16 +258,18 @@ begin
   g_Frames_CreateWAD(nil, 'FRAMES_FLAG_DOM', GameWAD+':TEXTURES\FLAGDOM', 64, 64, 5, False);
   g_Texture_CreateWADEx('ITEM_MEDKIT_SMALL', GameWAD+':TEXTURES\MED1');
   g_Texture_CreateWADEx('ITEM_MEDKIT_LARGE', GameWAD+':TEXTURES\MED2');
+  g_Texture_CreateWADEx('ITEM_MEDKIT_BLACK', GameWAD+':TEXTURES\BMED');
+  g_Texture_CreateWADEx('ITEM_WEAPON_IRONFIST', GameWAD+':TEXTURES\IRONFIST');
   g_Texture_CreateWADEx('ITEM_WEAPON_SAW', GameWAD+':TEXTURES\SAW');
   g_Texture_CreateWADEx('ITEM_WEAPON_PISTOL', GameWAD+':TEXTURES\PISTOL');
-  g_Texture_CreateWADEx('ITEM_WEAPON_KASTET', GameWAD+':TEXTURES\KASTET');
   g_Texture_CreateWADEx('ITEM_WEAPON_SHOTGUN1', GameWAD+':TEXTURES\SHOTGUN1');
   g_Texture_CreateWADEx('ITEM_WEAPON_SHOTGUN2', GameWAD+':TEXTURES\SHOTGUN2');
   g_Texture_CreateWADEx('ITEM_WEAPON_CHAINGUN', GameWAD+':TEXTURES\MGUN');
   g_Texture_CreateWADEx('ITEM_WEAPON_ROCKETLAUNCHER', GameWAD+':TEXTURES\RLAUNCHER');
   g_Texture_CreateWADEx('ITEM_WEAPON_PLASMA', GameWAD+':TEXTURES\PGUN');
   g_Texture_CreateWADEx('ITEM_WEAPON_BFG', GameWAD+':TEXTURES\BFG');
-  g_Texture_CreateWADEx('ITEM_WEAPON_SUPERPULEMET', GameWAD+':TEXTURES\SPULEMET');
+  g_Texture_CreateWADEx('ITEM_WEAPON_SUPERCHAINGUN', GameWAD+':TEXTURES\SCHAINGUN');
+  g_Texture_CreateWADEx('ITEM_WEAPON_FLAMETHROWER', GameWAD+':TEXTURES\FLAMETHROWER');
   g_Texture_CreateWADEx('ITEM_AMMO_BULLETS', GameWAD+':TEXTURES\CLIP');
   g_Texture_CreateWADEx('ITEM_AMMO_BULLETS_BOX', GameWAD+':TEXTURES\AMMO');
   g_Texture_CreateWADEx('ITEM_AMMO_SHELLS', GameWAD+':TEXTURES\SHELL1');
@@ -160,25 +278,26 @@ begin
   g_Texture_CreateWADEx('ITEM_AMMO_ROCKET_BOX', GameWAD+':TEXTURES\ROCKETS');
   g_Texture_CreateWADEx('ITEM_AMMO_CELL', GameWAD+':TEXTURES\CELL');
   g_Texture_CreateWADEx('ITEM_AMMO_CELL_BIG', GameWAD+':TEXTURES\CELL2');
+  g_Texture_CreateWADEx('ITEM_AMMO_FUELCAN', GameWAD+':TEXTURES\FUELCAN');
   g_Texture_CreateWADEx('ITEM_AMMO_BACKPACK', GameWAD+':TEXTURES\BPACK');
   g_Texture_CreateWADEx('ITEM_KEY_RED', GameWAD+':TEXTURES\KEYR');
   g_Texture_CreateWADEx('ITEM_KEY_GREEN', GameWAD+':TEXTURES\KEYG');
   g_Texture_CreateWADEx('ITEM_KEY_BLUE', GameWAD+':TEXTURES\KEYB');
   g_Texture_CreateWADEx('ITEM_OXYGEN', GameWAD+':TEXTURES\OXYGEN');
   g_Texture_CreateWADEx('ITEM_SUIT', GameWAD+':TEXTURES\SUIT');
-  g_Texture_CreateWADEx('ITEM_WEAPON_KASTET', GameWAD+':TEXTURES\KASTET');
-  g_Texture_CreateWADEx('ITEM_MEDKIT_BLACK', GameWAD+':TEXTURES\BMED');
-  g_Texture_CreateWADEx('ITEM_JETPACK', GameWAD+':TEXTURES\JETPACK');
 
   InitTextures();
+
+  freeIds := TIdPool.Create();
 end;
 
+
 procedure g_Items_FreeData();
 begin
-  e_WriteLog('Releasing items data...', MSG_NOTIFY);
+  e_WriteLog('Releasing items data...', TMsgType.Notify);
 
   g_Sound_Delete('SOUND_ITEM_RESPAWNITEM');
-  g_Sound_Delete('SOUND_ITEM_GETRULEZ');
+  g_Sound_Delete('SOUND_ITEM_GETPOWERUP');
   g_Sound_Delete('SOUND_ITEM_GETWEAPON');
   g_Sound_Delete('SOUND_ITEM_GETITEM');
 
@@ -186,6 +305,7 @@ begin
   g_Frames_DeleteByName('FRAMES_ITEM_WHITESPHERE');
   g_Frames_DeleteByName('FRAMES_ITEM_ARMORGREEN');
   g_Frames_DeleteByName('FRAMES_ITEM_ARMORBLUE');
+  g_Frames_DeleteByName('FRAMES_ITEM_JETPACK');
   g_Frames_DeleteByName('FRAMES_ITEM_INVUL');
   g_Frames_DeleteByName('FRAMES_ITEM_INVIS');
   g_Frames_DeleteByName('FRAMES_ITEM_RESPAWN');
@@ -196,16 +316,18 @@ begin
   g_Frames_DeleteByName('FRAMES_FLAG_DOM');
   g_Texture_Delete('ITEM_MEDKIT_SMALL');
   g_Texture_Delete('ITEM_MEDKIT_LARGE');
+  g_Texture_Delete('ITEM_MEDKIT_BLACK');
+  g_Texture_Delete('ITEM_WEAPON_IRONFIST');
   g_Texture_Delete('ITEM_WEAPON_SAW');
   g_Texture_Delete('ITEM_WEAPON_PISTOL');
-  g_Texture_Delete('ITEM_WEAPON_KASTET');
   g_Texture_Delete('ITEM_WEAPON_SHOTGUN1');
   g_Texture_Delete('ITEM_WEAPON_SHOTGUN2');
   g_Texture_Delete('ITEM_WEAPON_CHAINGUN');
   g_Texture_Delete('ITEM_WEAPON_ROCKETLAUNCHER');
   g_Texture_Delete('ITEM_WEAPON_PLASMA');
   g_Texture_Delete('ITEM_WEAPON_BFG');
-  g_Texture_Delete('ITEM_WEAPON_SUPERPULEMET');
+  g_Texture_Delete('ITEM_WEAPON_SUPERCHAINGUN');
+  g_Texture_Delete('ITEM_WEAPON_FLAMETHROWER');
   g_Texture_Delete('ITEM_AMMO_BULLETS');
   g_Texture_Delete('ITEM_AMMO_BULLETS_BOX');
   g_Texture_Delete('ITEM_AMMO_SHELLS');
@@ -214,444 +336,745 @@ begin
   g_Texture_Delete('ITEM_AMMO_ROCKET_BOX');
   g_Texture_Delete('ITEM_AMMO_CELL');
   g_Texture_Delete('ITEM_AMMO_CELL_BIG');
+  g_Texture_Delete('ITEM_AMMO_FUELCAN');
   g_Texture_Delete('ITEM_AMMO_BACKPACK');
   g_Texture_Delete('ITEM_KEY_RED');
   g_Texture_Delete('ITEM_KEY_GREEN');
   g_Texture_Delete('ITEM_KEY_BLUE');
   g_Texture_Delete('ITEM_OXYGEN');
   g_Texture_Delete('ITEM_SUIT');
-  g_Texture_Delete('ITEM_WEAPON_KASTET');
-  g_Texture_Delete('ITEM_MEDKIT_BLACK');
-  g_Texture_Delete('ITEM_JETPACK');
+
+  freeIds.Free();
+  freeIds := nil;
 end;
 
-function FindItem(): DWORD;
+
+procedure releaseItem (idx: Integer);
 var
-  i: Integer;
+  it: PItem;
 begin
-  if gItems <> nil then
-    for i := 0 to High(gItems) do
-      if gItems[i].ItemType = ITEM_NONE then
-      begin
-        Result := i;
-        Exit;
-      end;
+  if (idx < 0) or (idx > High(ggItems)) then raise Exception.Create('releaseItem: invalid item id');
+  if not freeIds.hasAlloced[LongWord(idx)] then raise Exception.Create('releaseItem: trying to release unallocated item (0)');
+  it := @ggItems[idx];
+  if not it.slotIsUsed then raise Exception.Create('releaseItem: trying to release unallocated item (1)');
+  if (it.arrIdx <> idx) then raise Exception.Create('releaseItem: arrIdx inconsistency');
+  it.slotIsUsed := false;
+  if (it.Animation <> nil) then
+  begin
+    it.Animation.Free();
+    it.Animation := nil;
+  end;
+  it.alive := False;
+  it.SpawnTrigger := -1;
+  it.ItemType := ITEM_NONE;
+  it.NeedSend := false;
+  freeIds.release(LongWord(idx));
+end;
 
-  if gItems = nil then
-    begin
-      SetLength(gItems, 32);
-      Result := 0;
-    end
-  else
-    begin
-      Result := High(gItems) + 1;
-      SetLength(gItems, Length(gItems) + 32);
-    end;
+
+procedure growItemArrayTo (newsz: Integer);
+var
+  i, olen: Integer;
+  it: PItem;
+begin
+  if (newsz < Length(ggItems)) then exit;
+  // no free slots
+  olen := Length(ggItems);
+  SetLength(ggItems, newsz);
+  for i := olen to High(ggItems) do
+  begin
+    it := @ggItems[i];
+    it.slotIsUsed := false;
+    it.arrIdx := i;
+    it.ItemType := ITEM_NONE;
+    it.Animation := nil;
+    it.alive := false;
+    it.SpawnTrigger := -1;
+    it.Respawnable := false;
+    it.NeedSend := false;
+    //if not freeIds.hasFree[LongWord(i)] then raise Exception.Create('internal error in item idx manager');
+  end;
 end;
 
-procedure g_Items_Init();
+
+function allocItem (): DWORD;
+begin
+  result := freeIds.alloc();
+  if (result >= Length(ggItems)) then growItemArrayTo(Integer(result)+64);
+  if (Integer(result) > High(ggItems)) then raise Exception.Create('allocItem: freeid list corrupted');
+  if (ggItems[result].arrIdx <> Integer(result)) then raise Exception.Create('allocItem: arrIdx inconsistency');
+end;
+
+
+// it will be slow if the slot is free (we have to rebuild the heap)
+function wantItemSlot (slot: Integer): Integer;
 var
-  a, b: Integer;
+  olen: Integer;
+  it: PItem;
 begin
-  if gMapInfo.Height > gPlayerScreenSize.Y then
-    a := gMapInfo.Height - gPlayerScreenSize.Y
-  else
-    a := gMapInfo.Height;
+  if (slot < 0) or (slot > $0fffffff) then raise Exception.Create('wantItemSlot: bad item slot request');
+  // do we need to grow item storate?
+  olen := Length(ggItems);
+  if (slot >= olen) then growItemArrayTo(slot+64);
 
-  if gMapInfo.Width > gPlayerScreenSize.X then
-    b := gMapInfo.Width - gPlayerScreenSize.X
+  it := @ggItems[slot];
+  if not it.slotIsUsed then
+  begin
+    freeIds.alloc(LongWord(slot));
+  end
   else
-    b := gMapInfo.Width;
+  begin
+    if not freeIds.hasAlloced[slot] then raise Exception.Create('wantItemSlot: internal error in item idx manager');
+  end;
+  it.slotIsUsed := false;
+
+  result := slot;
+end;
 
+
+// ////////////////////////////////////////////////////////////////////////// //
+procedure g_Items_Init ();
+var
+  a, b: Integer;
+begin
+  if gMapInfo.Height > gPlayerScreenSize.Y then a := gMapInfo.Height-gPlayerScreenSize.Y else a := gMapInfo.Height;
+  if gMapInfo.Width > gPlayerScreenSize.X then b := gMapInfo.Width-gPlayerScreenSize.X else b := gMapInfo.Width;
   gMaxDist := Trunc(Hypot(a, b));
 end;
 
-procedure g_Items_Free();
+
+procedure g_Items_Free ();
 var
   i: Integer;
 begin
-  if gItems <> nil then
+  if (ggItems <> nil) then
   begin
-    for i := 0 to High(gItems) do
-      gItems[i].Animation.Free();
-    gItems := nil;
+    for i := 0 to High(ggItems) do ggItems[i].Animation.Free();
+    ggItems := nil;
   end;
+  freeIds.clear();
 end;
 
-function g_Items_Create(X, Y: Integer; ItemType: Byte;
+
+function g_Items_Create (X, Y: Integer; ItemType: Byte;
            Fall, Respawnable: Boolean; AdjCoord: Boolean = False; ForcedID: Integer = -1): DWORD;
 var
   find_id: DWORD;
   ID: DWORD;
+  it: PItem;
 begin
-  if ForcedID < 0 then
-    find_id := FindItem()
-  else
-  begin
-    find_id := ForcedID;
-    if Integer(find_id) > High(gItems) then
-      SetLength(gItems, find_id + 32);
-  end;
+  if ForcedID < 0 then find_id := allocItem() else find_id := wantItemSlot(ForcedID);
+
+  //{$IF DEFINED(D2F_DEBUG)}e_WriteLog(Format('allocated item #%d', [Integer(find_id)]), MSG_NOTIFY);{$ENDIF}
+
+  it := @ggItems[find_id];
+
+  if (it.arrIdx <> Integer(find_id)) then raise Exception.Create('g_Items_Create: arrIdx inconsistency');
+  //it.arrIdx := find_id;
+  it.slotIsUsed := true;
 
-  gItems[find_id].ItemType := ItemType;
-  gItems[find_id].Respawnable := Respawnable;
-  if g_Game_IsServer and (ITEM_RESPAWNTIME = 0) then
-    gItems[find_id].Respawnable := False;
-  gItems[find_id].InitX := X;
-  gItems[find_id].InitY := Y;
-  gItems[find_id].RespawnTime := 0;
-  gItems[find_id].Fall := Fall;
-  gItems[find_id].Live := True;
-  gItems[find_id].QuietRespawn := False;
-
-  g_Obj_Init(@gItems[find_id].Obj);
-  gItems[find_id].Obj.X := X;
-  gItems[find_id].Obj.Y := Y;
-  gItems[find_id].Obj.Rect.Width := ITEMSIZE[ItemType][0];
-  gItems[find_id].Obj.Rect.Height := ITEMSIZE[ItemType][1];
-
-  gItems[find_id].Animation := nil;
-  gItems[find_id].SpawnTrigger := -1;
-
-// Êîîðäèíàòû îòíîñèòåëüíî öåíòðà íèæíåãî ðåáðà:
+  it.ItemType := ItemType;
+  it.Respawnable := Respawnable;
+  it.InitX := X;
+  it.InitY := Y;
+  it.RespawnTime := 0;
+  it.Fall := Fall;
+  it.alive := True;
+  it.QuietRespawn := False;
+  it.dropped := false;
+  it.NeedSend := false;
+
+  g_Obj_Init(@it.Obj);
+  it.Obj.X := X;
+  it.Obj.Y := Y;
+  it.Obj.Rect.Width := ITEMSIZE[ItemType][0];
+  it.Obj.Rect.Height := ITEMSIZE[ItemType][1];
+
+  it.Animation := nil;
+  it.SpawnTrigger := -1;
+
+  // Êîîðäèíàòû îòíîñèòåëüíî öåíòðà íèæíåãî ðåáðà
   if AdjCoord then
-    with gItems[find_id] do
+  begin
+    with it^ do
     begin
       Obj.X := X - (Obj.Rect.Width div 2);
       Obj.Y := Y - Obj.Rect.Height;
       InitX := Obj.X;
       InitY := Obj.Y;
     end;
+  end;
 
-// Óñòàíîâêà àíèìàöèè:
-  with gItems[find_id] do
-  begin
-    case ItemType of
-      ITEM_ARMOR_GREEN:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORGREEN') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_ARMOR_BLUE:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORBLUE') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_SPHERE_BLUE:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_BLUESPHERE') then
-          Animation := TAnimation.Create(ID, True, 15);
-      ITEM_SPHERE_WHITE:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_WHITESPHERE') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_INVUL:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_INVUL') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_INVIS:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_INVIS') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_BOTTLE:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_BOTTLE') then
-          Animation := TAnimation.Create(ID, True, 20);
-      ITEM_HELMET:
-        if g_Frames_Get(ID, 'FRAMES_ITEM_HELMET') then
-          Animation := TAnimation.Create(ID, True, 20);
-    end;
+  it.Obj.oldX := it.Obj.X;
+  it.Obj.oldY := it.Obj.Y;
+
+  // Óñòàíîâêà àíèìàöèè
+  case it.ItemType of
+    ITEM_ARMOR_GREEN: if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORGREEN') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_ARMOR_BLUE: if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORBLUE') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_JETPACK: if g_Frames_Get(ID, 'FRAMES_ITEM_JETPACK') then it.Animation := TAnimation.Create(ID, True, 15);
+    ITEM_SPHERE_BLUE: if g_Frames_Get(ID, 'FRAMES_ITEM_BLUESPHERE') then it.Animation := TAnimation.Create(ID, True, 15);
+    ITEM_SPHERE_WHITE: if g_Frames_Get(ID, 'FRAMES_ITEM_WHITESPHERE') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_INVUL: if g_Frames_Get(ID, 'FRAMES_ITEM_INVUL') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_INVIS: if g_Frames_Get(ID, 'FRAMES_ITEM_INVIS') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_BOTTLE: if g_Frames_Get(ID, 'FRAMES_ITEM_BOTTLE') then it.Animation := TAnimation.Create(ID, True, 20);
+    ITEM_HELMET: if g_Frames_Get(ID, 'FRAMES_ITEM_HELMET') then it.Animation := TAnimation.Create(ID, True, 20);
   end;
 
-  Result := find_id;
+  it.positionChanged();
+
+  result := find_id;
 end;
 
-procedure g_Items_Update();
+procedure g_Items_PreUpdate ();
+var
+  i: Integer;
+begin
+  if (ggItems = nil) then Exit;
+  for i := 0 to High(ggItems) do
+    if (ggItems[i].ItemType <> ITEM_NONE) and ggItems[i].slotIsUsed then
+    begin
+      ggItems[i].Obj.oldX := ggItems[i].Obj.X;
+      ggItems[i].Obj.oldY := ggItems[i].Obj.Y;
+    end;
+end;
+
+procedure g_Items_Update ();
 var
   i, j, k: Integer;
-  ID: DWORD;
+  ID: DWord;
   Anim: TAnimation;
-  m: Word;
+  m, ItemRespawnTime{, PowerupRespawnTime}: Word;
   r, nxt: Boolean;
 begin
-  if gItems <> nil then
-    for i := 0 to High(gItems) do
-      if gItems[i].ItemType <> ITEM_NONE then
-        with gItems[i] do
+  if (ggItems = nil) then exit;
+
+  // respawn items in 15 seconds regardless of settings during warmup
+  ItemRespawnTime := IfThen(gLMSRespawn = LMS_RESPAWN_NONE, gGameSettings.ItemRespawnTime, 15);
+  //PowerupRespawnTime := IfThen(gLMSRespawn = LMS_RESPAWN_NONE, gGameSettings.PowerupRespawnTime, 15);
+
+  for i := 0 to High(ggItems) do
+  begin
+    if (ggItems[i].ItemType = ITEM_NONE) then continue;
+    if not ggItems[i].slotIsUsed then continue; // just in case
+
+    with ggItems[i] do
+    begin
+      nxt := False;
+
+      if alive then
+      begin
+        if Fall then
         begin
-          nxt := False;
+          m := g_Obj_Move(@Obj, True, True);
+          positionChanged(); // this updates spatial accelerators
 
-          if Live then
+          // Ñîïðîòèâëåíèå âîçäóõà
+          if gTime mod (GAME_TICK*2) = 0 then Obj.Vel.X := z_dec(Obj.Vel.X, 1);
+
+          // Åñëè âûïàë çà êàðòó
+          if WordBool(m and MOVE_FALLOUT) then
           begin
-            if Fall then
+            if SpawnTrigger = -1 then
             begin
-              m := g_Obj_Move(@Obj, True, True);
-
-            // Ñîïðîòèâëåíèå âîçäóõà:
-              if gTime mod (GAME_TICK*2) = 0 then
-                Obj.Vel.X := z_dec(Obj.Vel.X, 1);
-            // Åñëè âûïàë çà êàðòó:
-              if WordBool(m and MOVE_FALLOUT) then
-              begin
-                if SpawnTrigger = -1 then
-                  g_Items_Pick(i)
-                else begin
-                  g_Items_Remove(i);
-                  if g_Game_IsServer and g_Game_IsNet then MH_SEND_ItemDestroy(True, i);
-                end;
-                Continue;
-              end;
-            end;
-
-          // Åñëè èãðîêè ïîáëèçîñòè:
-            if gPlayers <> nil then
+              g_Items_Pick(i);
+            end
+            else
             begin
-              j := Random(Length(gPlayers)) - 1;
-
-              for k := 0 to High(gPlayers) do
-              begin
-                Inc(j);
-                if j > High(gPlayers) then
-                  j := 0;
-
-                if (gPlayers[j] <> nil) and gPlayers[j].Live and
-                   g_Obj_Collide(@gPlayers[j].Obj, @Obj) then
-                begin
-                  if g_Game_IsClient then Continue;
-
-                  if not gPlayers[j].PickItem(ItemType, Respawnable, r) then
-                    Continue;
-
-                  if g_Game_IsNet then MH_SEND_PlayerStats(gPlayers[j].UID);
-
-{
-  Doom 2D: Original:
-  1. I_NONE,I_CLIP,I_SHEL,I_ROCKET,I_CELL,I_AMMO,I_SBOX,I_RBOX,I_CELP,I_BPACK,I_CSAW,I_SGUN,I_SGUN2,I_MGUN,I_LAUN,I_PLAS,I_BFG,I_GUN2
-  +2. I_MEGA,I_INVL,I_SUPER
-  3. I_STIM,I_MEDI,I_ARM1,I_ARM2,I_AQUA,I_KEYR,I_KEYG,I_KEYB,I_SUIT,I_RTORCH,I_GTORCH,I_BTORCH,I_GOR1,I_FCAN
-}
-
-                  if gSoundEffectsDF then
-                  begin
-                    if ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL,
-                                    ITEM_INVIS, ITEM_MEDKIT_BLACK, ITEM_JETPACK] then
-                      g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
-                        gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
-                    else
-                      if ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
-                                      ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
-                                      ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET, ITEM_AMMO_BACKPACK] then
-                        g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
-                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
-                      else
-                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
-                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y);
-                  end
-                  else
-                  begin
-                    if ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_SUIT,
-                                    ITEM_MEDKIT_BLACK, ITEM_INVUL, ITEM_INVIS, ITEM_JETPACK] then
-                      g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
-                        gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
-                    else
-                      if ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
-                                      ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
-                                      ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET] then
-                        g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
-                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
-                      else
-                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
-                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y);
-                  end;
-
-                // Íàäî óáðàòü ñ êàðòû, åñëè ýòî íå êëþ÷, êîòîðûì íóæíî ïîäåëèòüñÿ ñ äðóãèì èãðîêîì:
-                  if r then
-                  begin
-                    if not Respawnable then
-                      g_Items_Remove(i)
-                    else
-                      g_Items_Pick(i);
-
-                    if g_Game_IsNet then MH_SEND_ItemDestroy(False, i);
-                    nxt := True;
-                    Break;
-                  end;
-                end;
-              end;
+              g_Items_Remove(i);
+              if g_Game_IsServer and g_Game_IsNet then MH_SEND_ItemDestroy(True, i);
             end;
-
-            if nxt then
-              Continue;
+            continue;
           end;
+        end;
+
+        // Åñëè èãðîêè ïîáëèçîñòè
+        if (gPlayers <> nil) then
+        begin
+          j := Random(Length(gPlayers))-1;
 
-          if Respawnable and g_Game_IsServer then
+          for k := 0 to High(gPlayers) do
           begin
-            DecMin(RespawnTime, 0);
-            if (RespawnTime = 0) and (not Live) then
+            Inc(j);
+            if j > High(gPlayers) then j := 0;
+
+            if (gPlayers[j] <> nil) and gPlayers[j].alive and g_Obj_Collide(@gPlayers[j].Obj, @Obj) then
             begin
-              if not QuietRespawn then
-                g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', InitX, InitY);
+              if g_Game_IsClient then continue;
 
-              if g_Frames_Get(ID, 'FRAMES_ITEM_RESPAWN') then
+              if not gPlayers[j].PickItem(ItemType, Respawnable, r) then continue;
+
+              if g_Game_IsNet then MH_SEND_PlayerStats(gPlayers[j].UID);
+
+              {
+                Doom 2D: Original:
+                1. I_NONE,I_CLIP,I_SHEL,I_ROCKET,I_CELL,I_AMMO,I_SBOX,I_RBOX,I_CELP,I_BPACK,I_CSAW,I_SGUN,I_SGUN2,I_MGUN,I_LAUN,I_PLAS,I_BFG,I_GUN2
+                +2. I_MEGA,I_INVL,I_SUPER
+                3. I_STIM,I_MEDI,I_ARM1,I_ARM2,I_AQUA,I_KEYR,I_KEYG,I_KEYB,I_SUIT,I_RTORCH,I_GTORCH,I_BTORCH,I_GOR1,I_FCAN
+              }
+              g_Items_EmitPickupSoundAt(i, gPlayers[j].Obj.X, gPlayers[j].Obj.Y);
+
+              // Íàäî óáðàòü ñ êàðòû, åñëè ýòî íå êëþ÷, êîòîðûì íóæíî ïîäåëèòüñÿ ñ äðóãèì èãðîêîì
+              if r then
               begin
-                Anim := TAnimation.Create(ID, False, 4);
-                g_GFX_OnceAnim(InitX+(Obj.Rect.Width div 2)-16, InitY+(Obj.Rect.Height div 2)-16, Anim);
-                Anim.Free();
+                if not (Respawnable and (ItemRespawnTime > 0)) then
+                  g_Items_Remove(i)
+                else 
+                  g_Items_Pick(i);
+                if g_Game_IsNet then MH_SEND_ItemDestroy(False, i);
+                nxt := True;
+                break;
               end;
+            end;
+          end;
+        end;
 
-              Obj.X := InitX;
-              Obj.Y := InitY;
-              Obj.Vel.X := 0;
-              Obj.Vel.Y := 0;
-              Obj.Accel.X := 0;
-              Obj.Accel.Y := 0;
+        if nxt then continue;
+      end;
 
-              Live := True;
+      if Respawnable and g_Game_IsServer then
+      begin
+        DecMin(RespawnTime, 0);
+        if (RespawnTime = 0) and (not alive) then
+        begin
+          if not QuietRespawn then g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', InitX, InitY);
 
-              if g_Game_IsNet then MH_SEND_ItemSpawn(QuietRespawn, i);
-              QuietRespawn := False;
-            end;
+          if g_Frames_Get(ID, 'FRAMES_ITEM_RESPAWN') then
+          begin
+            Anim := TAnimation.Create(ID, False, 4);
+            g_GFX_OnceAnim(InitX+(Obj.Rect.Width div 2)-16, InitY+(Obj.Rect.Height div 2)-16, Anim);
+            Anim.Free();
           end;
 
-          if Animation <> nil then
-            Animation.Update();
+          Obj.oldX := InitX;
+          Obj.oldY := InitY;
+          Obj.X := InitX;
+          Obj.Y := InitY;
+          Obj.Vel.X := 0;
+          Obj.Vel.Y := 0;
+          Obj.Accel.X := 0;
+          Obj.Accel.Y := 0;
+          positionChanged(); // this updates spatial accelerators
+
+          alive := true;
+
+          if g_Game_IsNet then MH_SEND_ItemSpawn(QuietRespawn, i);
+          QuietRespawn := false;
         end;
+      end;
+
+      if (Animation <> nil) then Animation.Update();
+    end;
+  end;
 end;
 
-procedure g_Items_Draw();
+
+procedure itemsDrawInternal (dropflag: Boolean);
 var
-  i: Integer;
+  i, fX, fY: Integer;
+  it: PItem;
 begin
-  if gItems <> nil then
-    for i := 0 to High(gItems) do
-      if gItems[i].Live then
-        with gItems[i] do
-          if g_Collide(Obj.X, Obj.Y, Obj.Rect.Width, Obj.Rect.Height,
-                       sX, sY, sWidth, sHeight) then
-          begin
-            if Animation = nil then
-              e_Draw(gItemsTexturesID[ItemType], Obj.X, Obj.Y, 0, True, False)
-            else
-              Animation.Draw(Obj.X, Obj.Y, M_NONE);
+  if (ggItems = nil) then exit;
 
-            if g_debug_Frames then
-            begin
-              e_DrawQuad(Obj.X+Obj.Rect.X,
-                         Obj.Y+Obj.Rect.Y,
-                         Obj.X+Obj.Rect.X+Obj.Rect.Width-1,
-                         Obj.Y+Obj.Rect.Y+Obj.Rect.Height-1,
-                         0, 255, 0);
-            end;
-          end;
+  for i := 0 to High(ggItems) do
+  begin
+    it := @ggItems[i];
+    if (not it.slotIsUsed) or (it.ItemType = ITEM_NONE) then continue; // just in case
+    if not it.alive then continue;
+    if (it.dropped <> dropflag) then continue;
+
+    with it^ do
+    begin
+      if g_Collide(Obj.X, Obj.Y, Obj.Rect.Width, Obj.Rect.Height, sX, sY, sWidth, sHeight) then
+      begin
+        Obj.lerp(gLerpFactor, fX, fY);
+        if (Animation = nil) then
+        begin
+          e_Draw(gItemsTexturesID[ItemType], fX, fY, 0, true, false);
+        end
+        else
+        begin
+          Animation.Draw(fX, fY, TMirrorType.None);
+        end;
+
+        if g_debug_Frames then
+        begin
+          e_DrawQuad(Obj.X+Obj.Rect.X,
+                     Obj.Y+Obj.Rect.Y,
+                     Obj.X+Obj.Rect.X+Obj.Rect.Width-1,
+                     Obj.Y+Obj.Rect.Y+Obj.Rect.Height-1,
+                     0, 255, 0);
+        end;
+      end;
+    end;
+  end;
 end;
 
-procedure g_Items_Pick(ID: DWORD);
+
+procedure g_Items_Draw ();
 begin
-  gItems[ID].Live := False;
-  gItems[ID].RespawnTime := ITEM_RESPAWNTIME;
+  itemsDrawInternal(false);
 end;
 
-procedure g_Items_Remove(ID: DWORD);
+procedure g_Items_DrawDrop ();
 begin
-  gItems[ID].ItemType := ITEM_NONE;
+  itemsDrawInternal(true);
+end;
 
-  if gItems[ID].Animation <> nil then
+
+procedure g_Items_SetDrop (ID: DWORD);
+begin
+  if (ID < Length(ggItems)) then
   begin
-    gItems[ID].Animation.Free();
-    gItems[ID].Animation := nil;
+    ggItems[ID].dropped := true;
+  end;
+end;
+
+
+procedure g_Items_Pick (ID: DWORD);
+begin
+  if ID >= Length(ggItems) then exit;
+
+  ggItems[ID].Obj.oldX := ggItems[ID].Obj.X;
+  ggItems[ID].Obj.oldY := ggItems[ID].Obj.Y;
+  ggItems[ID].alive := false;
+
+  // Items respawn timer
+  ggItems[ID].RespawnTime := IfThen(gLMSRespawn = LMS_RESPAWN_NONE, gGameSettings.ItemRespawnTime, 15) * 36;
+
+  // Powerup respawn timer
+  if ggItems[ID].ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL, ITEM_INVIS,
+    ITEM_MEDKIT_BLACK, ITEM_JETPACK, ITEM_SUIT] then
+  begin
+    ggItems[ID].RespawnTime := IfThen(gLMSRespawn = LMS_RESPAWN_NONE, gGameSettings.PowerupRespawnTime, 15) * 36;
   end;
 
-  gItems[ID].Live := False;
+  // #### Random powerup respawn ####
+  if (TGameOption.POWERUP_RANDOM in gGameSettings.Options) and (ggItems[ID].ItemType in [
+    ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL, ITEM_INVIS, ITEM_MEDKIT_BLACK, ITEM_JETPACK,
+    ITEM_SUIT
+  ]) then
+  begin
+    ggItems[ID].RespawnTime := Max(1, (gGameSettings.PowerupRespawnTime +
+      RandomRange(-gGameSettings.PowerupRespawnRandom, gGameSettings.PowerupRespawnRandom + 1)) * 36);
+    //e_logwritefln ('Randomized powerup %s time: %s', [ggItems[ID].ItemType, ggItems[ID].RespawnTime]);
+  end;
+
+  // #### Random item respawn ####
+  // Randomize respawn for all items (excluding powerups)
+  if (TGameOption.ITEM_ALL_RANDOM in gGameSettings.Options) and (ggItems[ID].ItemType in [
+    ITEM_MEDKIT_SMALL, ITEM_MEDKIT_LARGE, ITEM_ARMOR_GREEN, ITEM_ARMOR_BLUE, ITEM_BOTTLE,
+    ITEM_HELMET, ITEM_OXYGEN, ITEM_AMMO_BULLETS, ITEM_AMMO_BULLETS_BOX, ITEM_AMMO_SHELLS,
+    ITEM_AMMO_SHELLS_BOX, ITEM_AMMO_ROCKET, ITEM_AMMO_ROCKET_BOX, ITEM_AMMO_CELL,
+    ITEM_AMMO_CELL_BIG, ITEM_AMMO_FUELCAN, ITEM_AMMO_BACKPACK, ITEM_WEAPON_SAW,
+    ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2, ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER,
+    ITEM_WEAPON_PLASMA, ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERCHAINGUN, ITEM_WEAPON_FLAMETHROWER
+  ]) then
+  begin
+    ggItems[ID].RespawnTime := Max(1, (gGameSettings.ItemRespawnTime +
+      RandomRange(-gGameSettings.ItemRespawnRandom, gGameSettings.ItemRespawnRandom + 1)) * 36);
+    //e_logwritefln ('Randomized item %s time: %s', [ggItems[ID].ItemType, ggItems[ID].RespawnTime]);
+  end;
 
-  if gItems[ID].SpawnTrigger > -1 then
+  // Randomize respawn for heal/armor
+  if (TGameOption.ITEM_LIFE_RANDOM in gGameSettings.Options) and (ggItems[ID].ItemType in [
+    ITEM_MEDKIT_SMALL, ITEM_MEDKIT_LARGE, ITEM_ARMOR_GREEN, ITEM_ARMOR_BLUE, ITEM_BOTTLE,
+    ITEM_HELMET, ITEM_OXYGEN, ITEM_AMMO_BACKPACK
+  ]) then
   begin
-    g_Triggers_DecreaseSpawner(gItems[ID].SpawnTrigger);
-    gItems[ID].SpawnTrigger := -1;
+    ggItems[ID].RespawnTime := Max(1, (gGameSettings.ItemRespawnTime +
+      RandomRange(-gGameSettings.ItemRespawnRandom, gGameSettings.ItemRespawnRandom + 1)) * 36);
+    //e_logwritefln ('Randomized help item %s time: %s', [ggItems[ID].ItemType, ggItems[ID].RespawnTime]);
+  end;
+
+  // Randomize respawn for ammo
+  if (TGameOption.ITEM_AMMO_RANDOM in gGameSettings.Options) and (ggItems[ID].ItemType in [
+    ITEM_AMMO_BULLETS, ITEM_AMMO_BULLETS_BOX, ITEM_AMMO_SHELLS, ITEM_AMMO_SHELLS_BOX,
+    ITEM_AMMO_ROCKET, ITEM_AMMO_ROCKET_BOX, ITEM_AMMO_CELL, ITEM_AMMO_CELL_BIG, ITEM_AMMO_FUELCAN
+  ]) then
+  begin
+    ggItems[ID].RespawnTime := Max(1, (gGameSettings.ItemRespawnTime +
+      RandomRange(-gGameSettings.ItemRespawnRandom, gGameSettings.ItemRespawnRandom + 1)) * 36);
+    //e_logwritefln ('Randomized ammo %s time: %s', [ggItems[ID].ItemType, ggItems[ID].RespawnTime]);
+  end;
+
+  // Randomize respawn for weapons
+  if (TGameOption.ITEM_WEAPON_RANDOM in gGameSettings.Options) and (ggItems[ID].ItemType in [
+    ITEM_WEAPON_SAW, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2, ITEM_WEAPON_CHAINGUN,
+    ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA, ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERCHAINGUN,
+    ITEM_WEAPON_FLAMETHROWER
+  ]) then
+  begin
+    ggItems[ID].RespawnTime := Max(1, (gGameSettings.ItemRespawnTime +
+      RandomRange(-gGameSettings.ItemRespawnRandom, gGameSettings.ItemRespawnRandom + 1)) * 36);
+    //e_logwritefln ('Randomized weapon %s time: %s', [ggItems[ID].ItemType, ggItems[ID].RespawnTime]);
   end;
 end;
 
-procedure g_Items_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Items_Remove (ID: DWORD);
 var
-  count, i: Integer;
-  sig: DWORD;
+  it: PItem;
+  trig: Integer;
 begin
-// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ ïðåäìåòîâ:
-  count := 0;
-  if gItems <> nil then
-    for i := 0 to High(gItems) do
-      if gItems[i].ItemType <> ITEM_NONE then
-        count := count + 1;
+  if not g_Items_ValidId(ID) then
+  begin
+    //writeln('g_Items_Remove: invalid item id: ', ID);
+    raise Exception.Create('g_Items_Remove: invalid item id');
+    //exit;
+  end;
+
+  it := @ggItems[ID];
+  if (it.arrIdx <> Integer(ID)) then raise Exception.Create('g_Items_Remove: arrIdx desync');
 
-  Mem := TBinMemoryWriter.Create((count+1) * 60);
+  it.Obj.oldX := it.Obj.X;
+  it.Obj.oldY := it.Obj.Y;
+  trig := it.SpawnTrigger;
 
-// Êîëè÷åñòâî ïðåäìåòîâ:
-  Mem.WriteInt(count);
+  releaseItem(ID);
+
+  if (trig > -1) then g_Triggers_DecreaseSpawner(trig);
+end;
 
-  if count = 0 then
-    Exit;
 
-  for i := 0 to High(gItems) do
-    if gItems[i].ItemType <> ITEM_NONE then
+procedure g_Items_SaveState (st: TStream);
+var
+  count, i: Integer;
+  tt: Byte;
+begin
+  // Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ ïðåäìåòîâ
+  count := 0;
+  for i := 0 to High(ggItems) do if (ggItems[i].ItemType <> ITEM_NONE) and (ggItems[i].slotIsUsed) then Inc(count);
+
+  // Êîëè÷åñòâî ïðåäìåòîâ
+  utils.writeInt(st, LongInt(count));
+  if (count = 0) then exit;
+
+  for i := 0 to High(ggItems) do
+  begin
+    if (ggItems[i].ItemType <> ITEM_NONE) and (ggItems[i].slotIsUsed) then
     begin
-    // Ñèãíàòóðà ïðåäìåòà:
-      sig := ITEM_SIGNATURE; // 'ITEM'
-      Mem.WriteDWORD(sig);
-    // Òèï ïðåäìåòà:
-      Mem.WriteByte(gItems[i].ItemType);
-    // Åñòü ëè ðåñïàóí:
-      Mem.WriteBoolean(gItems[i].Respawnable);
-    // Êîîðäèíàòû ðåñïóíà:
-      Mem.WriteInt(gItems[i].InitX);
-      Mem.WriteInt(gItems[i].InitY);
-    // Âðåìÿ äî ðåñïàóíà:
-      Mem.WriteWord(gItems[i].RespawnTime);
-    // Ñóùåñòâóåò ëè ýòîò ïðåäìåò:
-      Mem.WriteBoolean(gItems[i].Live);
-    // Ìîæåò ëè îí ïàäàòü:
-      Mem.WriteBoolean(gItems[i].Fall);
-    // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò:
-      Mem.WriteInt(gItems[i].SpawnTrigger);
-    // Îáúåêò ïðåäìåòà:
-      Obj_SaveState(@gItems[i].Obj, Mem);
+      // Ñèãíàòóðà ïðåäìåòà
+      utils.writeSign(st, 'ITEM');
+      utils.writeInt(st, Byte(0));
+      // Òèï ïðåäìåòà
+      tt := ggItems[i].ItemType;
+      if ggItems[i].dropped then tt := tt or $80;
+      utils.writeInt(st, Byte(tt));
+      // Åñòü ëè ðåñïàóí
+      utils.writeBool(st, ggItems[i].Respawnable);
+      // Êîîðäèíàòû ðåñïóíà
+      utils.writeInt(st, LongInt(ggItems[i].InitX));
+      utils.writeInt(st, LongInt(ggItems[i].InitY));
+      // Âðåìÿ äî ðåñïàóíà
+      utils.writeInt(st, Word(ggItems[i].RespawnTime));
+      // Ñóùåñòâóåò ëè ýòîò ïðåäìåò
+      utils.writeBool(st, ggItems[i].alive);
+      // Ìîæåò ëè îí ïàäàòü
+      utils.writeBool(st, ggItems[i].Fall);
+      // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò
+      utils.writeInt(st, LongInt(ggItems[i].SpawnTrigger));
+      // Îáúåêò ïðåäìåòà
+      Obj_SaveState(st, @ggItems[i].Obj);
     end;
+  end;
 end;
 
-procedure g_Items_LoadState(var Mem: TBinMemoryReader);
+
+procedure g_Items_LoadState (st: TStream);
 var
   count, i, a: Integer;
-  sig: DWORD;
   b: Byte;
 begin
-  if Mem = nil then
-    Exit;
+  assert(st <> nil);
 
   g_Items_Free();
 
-// Êîëè÷åñòâî ïðåäìåòîâ:
-  Mem.ReadInt(count);
-
-  if count = 0 then
-    Exit;
+  // Êîëè÷åñòâî ïðåäìåòîâ
+  count := utils.readLongInt(st);
+  if (count = 0) then exit;
+  if (count < 0) or (count > 1024*1024) then raise XStreamError.Create('invalid number of items');
 
   for a := 0 to count-1 do
   begin
-  // Ñèãíàòóðà ïðåäìåòà:
-    Mem.ReadDWORD(sig);
-    if sig <> ITEM_SIGNATURE then // 'ITEM'
+    // Ñèãíàòóðà ïðåäìåòà
+    if not utils.checkSign(st, 'ITEM') then raise XStreamError.Create('invalid item signature');
+    if (utils.readByte(st) <> 0) then raise XStreamError.Create('invalid item version');
+    // Òèï ïðåäìåòà
+    b := utils.readByte(st); // bit7=1: monster drop
+    // Ñîçäàåì ïðåäìåò
+    i := g_Items_Create(0, 0, b and $7F, False, False);
+    if ((b and $80) <> 0) then g_Items_SetDrop(i);
+    // Åñòü ëè ðåñïàóí
+    ggItems[i].Respawnable := utils.readBool(st);
+    // Êîîðäèíàòû ðåñïóíà
+    ggItems[i].InitX := utils.readLongInt(st);
+    ggItems[i].InitY := utils.readLongInt(st);
+    // Âðåìÿ äî ðåñïàóíà
+    ggItems[i].RespawnTime := utils.readWord(st);
+    // Ñóùåñòâóåò ëè ýòîò ïðåäìåò
+    ggItems[i].alive := utils.readBool(st);
+    // Ìîæåò ëè îí ïàäàòü
+    ggItems[i].Fall := utils.readBool(st);
+    // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò
+    ggItems[i].SpawnTrigger := utils.readLongInt(st);
+    // Îáúåêò ïðåäìåòà
+    Obj_LoadState(@ggItems[i].Obj, st);
+  end;
+end;
+
+
+procedure g_Items_RestartRound ();
+var
+  i: Integer;
+  it: PItem;
+begin
+  for i := 0 to High(ggItems) do
+  begin
+    it := @ggItems[i];
+    it.Obj.oldX := it.Obj.X;
+    it.Obj.oldY := it.Obj.Y;
+    if not it.slotIsUsed then continue;
+    if it.Respawnable and (it.ItemType <> ITEM_NONE) then
+    begin
+      it.QuietRespawn := True;
+      it.RespawnTime := 0;
+    end
+    else
+    begin
+      g_Items_Remove(i);
+      if g_Game_IsNet then MH_SEND_ItemDestroy(True, i);
+    end;
+  end;
+end;
+
+
+function g_Items_ForEachAlive (cb: TItemEachAliveCB; backwards: Boolean=false): Boolean;
+var
+  idx: Integer;
+begin
+  result := false;
+  if (ggItems = nil) or not assigned(cb) then exit;
+
+  if backwards then
+  begin
+    for idx := High(ggItems) downto 0 do
+    begin
+      if ggItems[idx].alive and ggItems[idx].slotIsUsed then
+      begin
+        result := cb(@ggItems[idx]);
+        if result then exit;
+      end;
+    end;
+  end
+  else
+  begin
+    for idx := 0 to High(ggItems) do
+    begin
+      if ggItems[idx].alive and ggItems[idx].slotIsUsed then
+      begin
+        result := cb(@ggItems[idx]);
+        if result then exit;
+      end;
+    end;
+  end;
+end;
+
+function g_Items_NextAlive (startIdx: Integer): PItem;
+var
+  idx: Integer;
+begin
+  result := nil;
+  if (ggItems = nil) or (startIdx >= High(ggItems)) then exit;
+  for idx := startIdx + 1 to High(ggItems) do
+    if ggItems[idx].alive and ggItems[idx].slotIsUsed then
+    begin
+      result := @ggItems[idx];
+      exit;
+    end;
+end;
+
+// ////////////////////////////////////////////////////////////////////////// //
+procedure g_Items_EmitPickupSound (idx: Integer);
+var
+  it: PItem;
+begin
+  if not g_Items_ValidId(idx) then exit;
+  it := @ggItems[idx];
+  g_Items_EmitPickupSoundAt(idx, it.Obj.X, it.Obj.Y);
+end;
+
+procedure g_Items_EmitPickupSoundAt (idx, x, y: Integer);
+var
+  it: PItem;
+begin
+  if not g_Items_ValidId(idx) then exit;
+
+  it := @ggItems[idx];
+  if gSoundEffectsDF then
+  begin
+    if it.ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL, ITEM_INVIS,
+                       ITEM_JETPACK, ITEM_MEDKIT_BLACK] then
+    begin
+      g_Sound_PlayExAt('SOUND_ITEM_GETPOWERUP', x, y);
+    end
+    else if it.ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                            ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                            ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERCHAINGUN, ITEM_WEAPON_FLAMETHROWER,
+                            ITEM_AMMO_BACKPACK] then
+    begin
+      g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON', x, y);
+    end
+    else
     begin
-      raise EBinSizeError.Create('g_Items_LoadState: Wrong Item Signature');
+      g_Sound_PlayExAt('SOUND_ITEM_GETITEM', x, y);
+    end;
+  end
+  else
+  begin
+    if it.ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL, ITEM_INVIS,
+                       ITEM_SUIT, ITEM_JETPACK, ITEM_MEDKIT_BLACK] then
+    begin
+      g_Sound_PlayExAt('SOUND_ITEM_GETPOWERUP', x, y);
+    end
+    else if it.ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                            ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                            ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERCHAINGUN, ITEM_WEAPON_FLAMETHROWER] then
+    begin
+      g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON', x, y);
+    end
+    else
+    begin
+      g_Sound_PlayExAt('SOUND_ITEM_GETITEM', x, y);
     end;
-  // Òèï ïðåäìåòà:
-    Mem.ReadByte(b);
-  // Ñîçäàåì ïðåäìåò:
-    i := g_Items_Create(0, 0, b, False, False);
-  // Åñòü ëè ðåñïàóí:
-    Mem.ReadBoolean(gItems[i].Respawnable);
-  // Êîîðäèíàòû ðåñïóíà:
-    Mem.ReadInt(gItems[i].InitX);
-    Mem.ReadInt(gItems[i].InitY);
-  // Âðåìÿ äî ðåñïàóíà:
-    Mem.ReadWord(gItems[i].RespawnTime);
-  // Ñóùåñòâóåò ëè ýòîò ïðåäìåò:
-    Mem.ReadBoolean(gItems[i].Live);
-  // Ìîæåò ëè îí ïàäàòü:
-    Mem.ReadBoolean(gItems[i].Fall);
-  // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò:
-    Mem.ReadInt(gItems[i].SpawnTrigger);
-  // Îáúåêò ïðåäìåòà:
-    Obj_LoadState(@gItems[i].Obj, Mem);
   end;
 end;
 
+
+procedure g_Items_AddDynLights();
+var
+  f: Integer;
+  it: PItem;
+begin
+  for f := 0 to High(ggItems) do
+  begin
+    it := @ggItems[f];
+    if not it.alive then continue;
+    case it.ItemType of
+      ITEM_KEY_RED: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 24,  1.0, 0.0, 0.0, 0.6);
+      ITEM_KEY_GREEN: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 24,  0.0, 1.0, 0.0, 0.6);
+      ITEM_KEY_BLUE: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 24,  0.0, 0.0, 1.0, 0.6);
+      ITEM_ARMOR_GREEN: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 42,  0.0, 1.0, 0.0, 0.6);
+      ITEM_ARMOR_BLUE: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 42,  0.0, 0.0, 1.0, 0.6);
+      ITEM_JETPACK: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 32,  1.0, 1.0, 1.0, 0.6);
+      ITEM_SPHERE_BLUE: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 32,  0.0, 1.0, 0.0, 0.6);
+      ITEM_SPHERE_WHITE: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 32,  1.0, 1.0, 1.0, 0.6);
+      ITEM_INVUL: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 32,  1.0, 0.0, 0.0, 0.6);
+      ITEM_INVIS: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 32,  1.0, 1.0, 0.0, 0.6);
+      ITEM_BOTTLE: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 16,  0.0, 0.0, 0.8, 0.6);
+      ITEM_HELMET: g_AddDynLight(it.Obj.X+(it.Obj.Rect.Width div 2), it.Obj.Y+(it.Obj.Rect.Height div 2), 16,  0.0, 0.8, 0.0, 0.6);
+    end;
+  end;
+end;
+
+
 end.