diff --git a/AGENTS.md b/AGENTS.md index fdd0d31..431170a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -248,3 +248,14 @@ EblanStudio has evolved from a pure level editor into a **Full Game Engine Studi - Canvas Zoom buttons (`[-]`, `[100%]`, `[+]`) and pan controls. - Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map. - "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme. + +5. **Gameplay Scripting Subsystem & Interactive Modals (`include/engine_gameplay.inc`)**: + - `ShowGameToast(pszText, durationTicks, color)`: floating framed notification banners (auto-centered, custom CP1251 string, colored outline, auto-dismiss). + - `ShowGameDialog(pszSpeaker, pszText)`: modal dialogue & lore note reader freezing player movement until dismissed with `[E]` / `[ENTER]`. + - Extensible engine lifecycle hooks: + - `Game_OnTick`: 60 Hz frame tick hook for custom game logic, timers, and puzzle triggers. + - `Game_OnPlayerInteract(entityIdx, entityType)`: intercepts `[E]` interactions before default behavior. + - `Game_OnEntityKilled(entityIdx, entityType)`: triggers on guard death or barrel explosion for boss encounters. + - `Game_OnCustomHUD`: custom 2D canvas drawing (health bars, gauges, quest markers). + - `PlayGameSFX(pszRelativePath)`: safe asynchronous WinMM audio player. + - Built-in automatic gameplay toasts for keys, medkits, ammo, switches, doors, and explosions. diff --git a/editor/editor.exe b/editor/editor.exe index b0bc4a7..af254ee 100644 Binary files a/editor/editor.exe and b/editor/editor.exe differ diff --git a/editor/src/editor_code.inl b/editor/src/editor_code.inl index c0abd82..ecb4f6e 100644 --- a/editor/src/editor_code.inl +++ b/editor/src/editor_code.inl @@ -18,6 +18,7 @@ struct CodeFileEntry { }; static const CodeFileEntry g_ProjectCodeFiles[] = { + { "engine_gameplay.inc (Events, Hooks & Dialogs)", "include/engine_gameplay.inc" }, { "engine_combat.inc (Weapons & Combat)", "include/engine_combat.inc" }, { "engine_entity.inc (Entities & AI)", "include/engine_entity.inc" }, { "main.asm (Engine Entry & Frame Loop)", "main.asm" }, @@ -37,6 +38,39 @@ struct ScriptTemplate { }; static const ScriptTemplate g_ScriptTemplates[] = { + { + "NPC Диалог и квест (ShowGameDialog)", + "; ==============================================================================\n" + "; NPC Quest Dialogue: Displays conversation modal and gives objective\n" + "; ==============================================================================\n" + ".data\n" + "szNpcSpeaker db \"СТАРИК\", 0\n" + "szNpcText db \"ВОЗЬМИ СИНИЙ КЛЮЧ И ОТКРОЙ ЗАПЕРТУЮ ДВЕРЬ НА СЕВЕРЕ!\", 0\n" + ".code\n" + " invoke ShowGameDialog, OFFSET szNpcSpeaker, OFFSET szNpcText\n" + " invoke ShowGameToast, OFFSET szKeyPickedToast, 90, COLOR_CYAN\n" + }, + { + "Записка / Терминал с лором (Lore Note)", + "; ==============================================================================\n" + "; Readable Note / Terminal: Player reads lore note on [E]\n" + "; ==============================================================================\n" + ".data\n" + "szNoteTitle db \"ДНЕВНИК УЧЕНОГО\", 0\n" + "szNoteBody db \"ЭКСПЕРИМЕНТ ВЫШЕЛ ИЗ-ПОД КОНТРОЛЯ. КОД ОТ ЛИФТА: 4 9 2 1.\", 0\n" + ".code\n" + " invoke ShowGameDialog, OFFSET szNoteTitle, OFFSET szNoteBody\n" + }, + { + "Всплывающее уведомление (Game Toast)", + "; ==============================================================================\n" + "; Floating Toast Banner (auto-centered with colored outline and fade)\n" + "; ==============================================================================\n" + ".data\n" + "szSecretToast db \"СЕКРЕТНАЯ КОМНАТА ОБНАРУЖЕНА!\", 0\n" + ".code\n" + " invoke ShowGameToast, OFFSET szSecretToast, 120, COLOR_YELLOW\n" + }, { "Патрулирующий охранник (Patrol Guard AI)", "; ==============================================================================\n" @@ -110,6 +144,14 @@ struct EngineApiItem { }; static const EngineApiItem g_EngineApiCatalog[] = { + { "Геймплей и Скрипты", "ShowGameToast", "Всплывающий баннер с цветной рамкой и затуханием", "invoke ShowGameToast, OFFSET szMyMessage, 90, COLOR_YELLOW\n" }, + { "Геймплей и Скрипты", "ShowGameDialog", "Диалоговое окно NPC или чтение записки", "invoke ShowGameDialog, OFFSET szSpeaker, OFFSET szText\n" }, + { "Геймплей и Скрипты", "CloseGameDialog", "Принудительно закрыть активное диалоговое окно", "call CloseGameDialog\n" }, + { "Геймплей и Скрипты", "PlayGameSFX", "Асинхронное воспроизведение звука WAV", "invoke PlayGameSFX, OFFSET szMySoundWav\n" }, + { "Геймплей и Скрипты", "Game_OnPlayerInteract", "Хук взаимодействия [E] (1=обработано, 0=дефолт)", "Game_OnPlayerInteract PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Ваша логика по нажатию [E]\n xor eax, eax\n ret\nGame_OnPlayerInteract ENDP\n" }, + { "Геймплей и Скрипты", "Game_OnEntityKilled", "Хук уничтожения врага/бочки (триггеры боссов)", "Game_OnEntityKilled PROC uses ebx ecx, entityIdx:DWORD, entityTypeVal:DWORD\n ; Смерть сущности\n ret\nGame_OnEntityKilled ENDP\n" }, + { "Геймплей и Скрипты", "Game_OnTick", "Покадровый хук 60 Hz (таймеры, зоны, квесты)", "Game_OnTick PROC uses ebx ecx\n ; Ваша покадровая логика\n ret\nGame_OnTick ENDP\n" }, + { "Бой и оружие", "UpdateCombat", "Обновляет прицел, стрельбу и расчет урона", "call UpdateCombat\n" }, { "Бой и оружие", "RenderCombatHUD", "Рисует оружие, прицел и статус игрока", "call RenderCombatHUD\n" }, { "Бой и оружие", "Смена оружия", "Устанавливает активное оружие игрока", "mov playerWeapon, WEAPON_SHOTGUN ; 0=Knife, 1=Pistol, 2=Shotgun\n" }, diff --git a/engine.exe b/engine.exe index 34d4c7c..e9f985d 100644 Binary files a/engine.exe and b/engine.exe differ diff --git a/include/engine_combat.inc b/include/engine_combat.inc index 1542c9a..4a18045 100644 --- a/include/engine_combat.inc +++ b/include/engine_combat.inc @@ -9,6 +9,8 @@ DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD +ShowGameToast PROTO :DWORD, :DWORD, :DWORD +Game_OnEntityKilled PROTO :DWORD, :DWORD ; ------------------------------------------------------------------------------ ; UpdateCombat: Process weapon firing, hitscan raycasting, cooldowns & damage @@ -249,6 +251,7 @@ hitscan_eval: mov [entityState + ecx*4], ENTITY_STATE_DEAD mov DWORD PTR [entityTimer + ecx*4], 0 add playerScore, 100 + invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_GUARD ; Drop ammo reward mov eax, playerAmmo @@ -271,6 +274,8 @@ explode_barrel: ; Fiery orange-red screen flash mov playerHitFlash, 8 add playerScore, 200 + invoke ShowGameToast, OFFSET szBarrelExplodedToast, 60, 00FFA020h + invoke Game_OnEntityKilled, closestIdx, ENTITY_TYPE_BARREL ; AOE damage to all surrounding guards within 128 units (2 cells) push esi diff --git a/include/engine_data.inc b/include/engine_data.inc index 86db906..a3e6618 100644 --- a/include/engine_data.inc +++ b/include/engine_data.inc @@ -240,3 +240,25 @@ entityState dd MAX_SPRITES dup(?) entityTimer dd MAX_SPRITES dup(?) entitySpeed dd MAX_SPRITES dup(?) entityAggro dd MAX_SPRITES dup(?) + +; Gameplay Variables & Built-in Toast Strings (CP1251) +.data +ALIGN 4 +gameToastTimer dd 0 +gameToastColor dd 0 +gameToastText db 64 dup(0) + +ALIGN 4 +gameDialogActive dd 0 +gameDialogSpeaker db 32 dup(0) +gameDialogText db 128 dup(0) + +szDialogCloseHint db "[ENTER / E] - ЗАКРЫТЬ", 0 +szKeyPickedToast db "ПОЛУЧЕН КЛЮЧ", 0 +szMedkitPickedToast db "АПТЕЧКА +25 ЗДОРОВЬЯ", 0 +szAmmoPickedToast db "БОЕПРИПАСЫ +20 ПАТРОНОВ", 0 +szSwitchOnToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВКЛ", 0 +szSwitchOffToast db "ПЕРЕКЛЮЧАТЕЛЬ: ВЫКЛ", 0 +szDoorOpenedToast db "ДВЕРЬ ОТКРЫТА", 0 +szDoorClosedToast db "ДВЕРЬ ЗАКРЫТА", 0 +szBarrelExplodedToast db "ВЗРЫВ БОЧКИ!", 0 diff --git a/include/engine_entity.inc b/include/engine_entity.inc index dfbc58c..d760f47 100644 --- a/include/engine_entity.inc +++ b/include/engine_entity.inc @@ -4,6 +4,9 @@ .code +ShowGameToast PROTO :DWORD, :DWORD, :DWORD +Game_OnPlayerInteract PROTO :DWORD, :DWORD + ; ------------------------------------------------------------------------------ ; InitEntities: Initialize entity runtime state from spriteList flags/defaults ; ------------------------------------------------------------------------------ @@ -214,6 +217,7 @@ entity_loop: mov ecx, i mov [entityType + ecx*4], ENTITY_TYPE_PROP add playerScore, 25 + invoke ShowGameToast, OFFSET szMedkitPickedToast, 75, COLOR_GREEN jmp next_entity ; -------------------------------------------------------------------------- @@ -237,6 +241,7 @@ check_ammo_pickup: mov ecx, i mov [entityType + ecx*4], ENTITY_TYPE_PROP add playerScore, 10 + invoke ShowGameToast, OFFSET szAmmoPickedToast, 75, COLOR_YELLOW jmp next_entity ; -------------------------------------------------------------------------- @@ -244,7 +249,7 @@ check_ammo_pickup: ; -------------------------------------------------------------------------- check_key_pickup: cmp edx, ENTITY_TYPE_KEY - jne check_guard_entity + jne check_switch_entity cmp absDX, 32 jge next_entity cmp absDY, 32 @@ -254,6 +259,39 @@ check_key_pickup: mov ecx, i mov [entityType + ecx*4], ENTITY_TYPE_PROP add playerScore, 50 + invoke ShowGameToast, OFFSET szKeyPickedToast, 90, COLOR_CYAN + jmp next_entity + + ; -------------------------------------------------------------------------- + ; Switch Trigger (Toggle on [E]) + ; -------------------------------------------------------------------------- +check_switch_entity: + cmp edx, ENTITY_TYPE_SWITCH + jne check_guard_entity + cmp absDX, 48 + jge next_entity + cmp absDY, 48 + jge next_entity + cmp keyAction, 0 + je next_entity + mov keyAction, 0 + + ; Check if handled by user gameplay script hook + invoke Game_OnPlayerInteract, i, edx + test eax, eax + jnz next_entity + + ; Default switch behavior: toggle state + mov ecx, i + mov eax, [entityState + ecx*4] + xor eax, 1 + mov [entityState + ecx*4], eax + test eax, eax + jz switch_off + invoke ShowGameToast, OFFSET szSwitchOnToast, 60, COLOR_GREEN + jmp next_entity +switch_off: + invoke ShowGameToast, OFFSET szSwitchOffToast, 60, COLOR_YELLOW jmp next_entity ; -------------------------------------------------------------------------- diff --git a/include/engine_gameplay.inc b/include/engine_gameplay.inc new file mode 100644 index 0000000..b017af2 --- /dev/null +++ b/include/engine_gameplay.inc @@ -0,0 +1,242 @@ +; ============================================================================== +; EBLAN ENGINE - Gameplay Events, Scripting Hooks, HUD Toasts & Dialog Modals +; ============================================================================== + +.code + +DrawPixel PROTO :DWORD, :DWORD, :DWORD +DrawRect PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD +DrawRectOutline PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD +DrawString8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD +DrawNumber8x8 PROTO :DWORD, :DWORD, :DWORD, :DWORD +PlaySoundA PROTO :DWORD, :DWORD, :DWORD + +; ------------------------------------------------------------------------------ +; ShowGameToast: Displays a floating toast notification banner on screen +; ------------------------------------------------------------------------------ +ShowGameToast PROC uses esi edi ecx eax, pszText:DWORD, duration:DWORD, textColor:DWORD + mov esi, pszText + test esi, esi + jz toast_skip_copy + + mov edi, OFFSET gameToastText + mov ecx, 63 +toast_copy_loop: + lodsb + stosb + test al, al + jz toast_copy_done + dec ecx + jnz toast_copy_loop + xor al, al + stosb +toast_copy_done: + +toast_skip_copy: + mov eax, duration + mov gameToastTimer, eax + mov eax, textColor + mov gameToastColor, eax + ret +ShowGameToast ENDP + +; ------------------------------------------------------------------------------ +; RenderGameToast: Draws active toast notification at top center of screen +; ------------------------------------------------------------------------------ +RenderGameToast PROC uses ebx ecx edx esi edi + cmp gameToastTimer, 0 + jle rt_done + dec gameToastTimer + + ; 1. Calculate text character length + mov esi, OFFSET gameToastText + xor ecx, ecx +count_len: + mov al, [esi+ecx] + test al, al + jz count_done + inc ecx + cmp ecx, 63 + jb count_len +count_done: + test ecx, ecx + jz rt_done + + ; Width in pixels = length * 8 + mov eax, ecx + shl eax, 3 + mov edx, eax + + ; Center X = (SCREEN_W - width) / 2 + mov ebx, SCREEN_W + sub ebx, edx + sar ebx, 1 + + ; Box coordinates: X = ebx - 6, Y = 16, W = edx + 12, H = 14 + mov eax, ebx + sub eax, 6 + mov esi, edx + add esi, 12 + + invoke DrawRect, eax, 16, esi, 14, 00101010h + invoke DrawRectOutline, eax, 16, esi, 14, gameToastColor + invoke DrawString8x8, ebx, 19, OFFSET gameToastText, gameToastColor + +rt_done: + ret +RenderGameToast ENDP + +; ------------------------------------------------------------------------------ +; ShowGameDialog: Opens an interactive retro dialogue modal with speaker and text +; ------------------------------------------------------------------------------ +ShowGameDialog PROC uses esi edi ecx eax, pszSpeaker:DWORD, pszText:DWORD + mov esi, pszSpeaker + mov edi, OFFSET gameDialogSpeaker + mov ecx, 31 + test esi, esi + jz no_speaker +copy_spk: + lodsb + stosb + test al, al + jz spk_done + dec ecx + jnz copy_spk + xor al, al + stosb +spk_done: + jmp setup_dlg_text +no_speaker: + mov BYTE PTR [edi], 0 + +setup_dlg_text: + mov esi, pszText + mov edi, OFFSET gameDialogText + mov ecx, 127 + test esi, esi + jz no_text +copy_txt: + lodsb + stosb + test al, al + jz txt_done + dec ecx + jnz copy_txt + xor al, al + stosb +txt_done: + jmp activate_dlg +no_text: + mov BYTE PTR [edi], 0 + +activate_dlg: + mov gameDialogActive, 1 + ret +ShowGameDialog ENDP + +; ------------------------------------------------------------------------------ +; CloseGameDialog: Closes active dialogue box +; ------------------------------------------------------------------------------ +CloseGameDialog PROC + mov gameDialogActive, 0 + ret +CloseGameDialog ENDP + +; ------------------------------------------------------------------------------ +; RenderGameDialog: Draws active dialogue box at bottom of screen +; ------------------------------------------------------------------------------ +RenderGameDialog PROC uses ebx ecx edx esi edi + cmp gameDialogActive, 0 + je rd_done + + ; Dialog Box background: X = 16, Y = 170, W = 288, H = 42 + invoke DrawRect, 16, 170, 288, 42, 00121212h + invoke DrawRectOutline, 16, 170, 288, 42, COLOR_CYAN + + ; Speaker name (if not empty) + cmp BYTE PTR [gameDialogSpeaker], 0 + je draw_dlg_body + invoke DrawString8x8, 24, 174, OFFSET gameDialogSpeaker, COLOR_YELLOW + +draw_dlg_body: + ; Text + invoke DrawString8x8, 24, 186, OFFSET gameDialogText, COLOR_WHITE + + ; Close hint in bottom-right + invoke DrawString8x8, 140, 200, OFFSET szDialogCloseHint, COLOR_GRAY + + ; Check if player pressed Action key or Attack to dismiss + cmp keyAction, 0 + jne do_dismiss + cmp keyAttack, 0 + jne do_dismiss + jmp rd_done + +do_dismiss: + mov keyAction, 0 + mov keyAttack, 0 + mov gameDialogActive, 0 + +rd_done: + ret +RenderGameDialog ENDP + +; ------------------------------------------------------------------------------ +; PlayGameSFX: Asynchronously plays a sound effect via WinMM PlaySoundA +; ------------------------------------------------------------------------------ +PlayGameSFX PROC uses eax, pszRelativePath:DWORD + cmp isCutsceneActive, 0 + jne sfx_done + mov eax, pszRelativePath + test eax, eax + jz sfx_done + cmp BYTE PTR [eax], 0 + jz sfx_done + invoke PlaySoundA, eax, 0, 9 ; SND_FILENAME (1) | SND_ASYNC (8) = 9 +sfx_done: + ret +PlayGameSFX ENDP + +; ------------------------------------------------------------------------------ +; Game_OnTick: Hook called every 60 Hz frame during active gameplay +; ------------------------------------------------------------------------------ +Game_OnTick PROC uses eax ebx ecx edx + ; Extensible hook for custom game logic (quests, timers, zones) + ret +Game_OnTick ENDP + +; ------------------------------------------------------------------------------ +; Game_OnPlayerInteract: Hook called when player presses [E] in range of an entity +; Returns: EAX = 1 if handled by custom script, 0 if default engine behavior +; ------------------------------------------------------------------------------ +Game_OnPlayerInteract PROC uses ebx ecx edx esi edi, entityIdx:DWORD, entityTypeVal:DWORD + ; Extensible hook for custom interactions + xor eax, eax + ret +Game_OnPlayerInteract ENDP + +; ------------------------------------------------------------------------------ +; Game_OnEntityKilled: Hook called when enemy guard or barrel is destroyed +; ------------------------------------------------------------------------------ +Game_OnEntityKilled PROC uses eax ebx ecx edx, entityIdx:DWORD, entityTypeVal:DWORD + ; Extensible hook for boss fights, doors opening on kill, score multipliers + ret +Game_OnEntityKilled ENDP + +; ------------------------------------------------------------------------------ +; Game_OnCustomHUD: Hook called during HUD rendering +; ------------------------------------------------------------------------------ +Game_OnCustomHUD PROC uses eax ebx ecx edx + ; Extensible hook for custom UI bars, quest markers, mini-dialogues + ret +Game_OnCustomHUD ENDP + +; ------------------------------------------------------------------------------ +; RenderGameHUDOverlays: Renders active toast banner, dialogue modal, and custom HUD +; ------------------------------------------------------------------------------ +RenderGameHUDOverlays PROC + call RenderGameToast + call RenderGameDialog + call Game_OnCustomHUD + ret +RenderGameHUDOverlays ENDP diff --git a/include/engine_map.inc b/include/engine_map.inc index a6d1d54..c1e3f2d 100644 --- a/include/engine_map.inc +++ b/include/engine_map.inc @@ -13,6 +13,7 @@ ValidateMapFilename PROTO :DWORD ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD CheckPortalTriggers PROTO InitEntities PROTO +ShowGameToast PROTO :DWORD, :DWORD, :DWORD ; ------------------------------------------------------------------------------ ; InitMap: Creates a rich test level with rooms, hallways, pillars & doors @@ -203,6 +204,7 @@ InteractDoor PROC uses ebx ecx edx esi edi ; Open the door! invoke SetMapCell, targetX, targetY, CELL_DOOR_OPEN + invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN ret check_door_close: @@ -211,6 +213,7 @@ check_door_close: ; Close the door! invoke SetMapCell, targetX, targetY, CELL_DOOR + invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW ret check_portal_interact: diff --git a/main.asm b/main.asm index 6fc6cda..dd0fb01 100644 --- a/main.asm +++ b/main.asm @@ -115,6 +115,7 @@ include include/engine_entity.inc include include/engine_raycaster.inc include include/engine_render2d.inc include include/engine_combat.inc +include include/engine_gameplay.inc include include/engine_cutscene.inc include include/engine_preview.inc @@ -254,6 +255,9 @@ hud_no_map_error: ; 5. Minimap Radar (Toggle with 'M') call DrawMinimap + ; 6. Gameplay HUD Overlays (Floating Toasts, Dialog Modals, Custom HUD) + call RenderGameHUDOverlays + ret Engine_RenderHUD ENDP @@ -269,9 +273,13 @@ no_error_tick: call PollInput cmp isGamePaused, 0 jne @F + cmp gameDialogActive, 0 + jne skip_movement_in_dialog call UpdatePlayer call UpdateEntities call UpdateCombat +skip_movement_in_dialog: + call Game_OnTick @@: ; 2. 3D Raycasting & 2D HUD Render diff --git a/main.obj b/main.obj index 7ca99a6..edf5649 100644 Binary files a/main.obj and b/main.obj differ diff --git a/tests/engine_regressions.asm b/tests/engine_regressions.asm index e530de5..f64d95a 100644 --- a/tests/engine_regressions.asm +++ b/tests/engine_regressions.asm @@ -20,6 +20,7 @@ include include/engine_entity.inc include include/engine_raycaster.inc include include/engine_render2d.inc include include/engine_combat.inc +include include/engine_gameplay.inc include include/engine_cutscene.inc include startup_parser.inc include project_paths.inc