Compare commits

..
Author SHA1 Message Date
Vladislav Khorev 804e34ce12 Linux 2026-07-14 20:14:30 +06:00
Vladislav Khorev 34a55611d5 Some clean up 2026-07-14 14:23:00 +03:00
Vladislav Khorev 35a2a91178 Working on taxi car 2026-07-14 14:07:53 +03:00
Vladislav Khorev 38e45d67b8 Working on viewing distance for enemies 2026-07-12 19:09:26 +03:00
Vladislav Khorev 33c2b313af Improving the battle, fixing bugs 2026-07-12 18:35:46 +03:00
Vladislav Khorev 92929118d5 Fixing some stuff related to attacks 2026-07-12 16:55:23 +03:00
Vladislav Khorev 9d03b52c2a Fixing localization bugs, working with graphics settings 2026-07-12 16:02:09 +03:00
Vladislav Khorev d4365728fe Fixing minor bugs in texts, encoding, UI 2026-07-12 12:07:58 +03:00
Vladislav Khorev 79413e7319 Fixing lots of UI bugs 2026-07-10 21:30:59 +03:00
Vladislav Khorev 8e745ce0cd Working on fixing scripting bugs' 2026-07-10 17:00:10 +03:00
Vladislav Khorev 7dadfd8ed8 Fixing dorm script bugs 2026-07-10 15:08:38 +03:00
Vladislav Khorev 07f45ad1e1 Working on stuff 2026-07-10 14:09:31 +03:00
Vladislav Khorev 17d903fd52 Rewrite callbacks from scripts 2026-07-10 13:24:23 +03:00
Vladislav Khorev 2d3aa22521 Working on icons and pics 2026-07-10 12:26:08 +03:00
Vladislav Khorev 5cde7078f9 Fixing books rotation and books appearance in darklands 2026-07-09 22:12:24 +03:00
Vladislav Khorev 44f5136f1d Working on loading screen 2026-07-09 20:32:57 +03:00
Vladislav Khorev 1c2c80f0ea Working on reset state and the end game 2026-07-08 16:24:06 +03:00
Vladislav Khorev 124208f2a4 Added sounds 2026-07-07 23:05:01 +03:00
Vladislav Khorev c6ba8bd008 Working on translations and minor bugs fix 2026-07-07 20:27:46 +03:00
Vladislav Khorev a40a582b5a Working on translations in portrait mode 2026-07-07 19:56:08 +03:00
Vladislav Khorev cdaebc3c0c Working more on translations, fixing tree bugs 2026-07-07 19:03:58 +03:00
Vladislav Khorev e7dc3279ae Adding translations for English for UI' 2026-07-07 16:39:04 +03:00
Vladislav Khorev 08eac60657 Started working on UI localization 2026-07-07 13:46:28 +03:00
Vladislav Khorev ef5b142425 Added simple localizations for locations 2026-07-07 12:11:34 +03:00
Vladislav Khorev e2d9105b02 Working on translations 2026-07-07 11:24:21 +03:00
Vladislav Khorev 9d8ca865c7 Minor refactoring 2026-07-07 09:46:15 +03:00
Vladislav Khorev 794a820be9 Working on translations for dialogs and cutscenes 2026-07-05 21:52:37 +03:00
Vladislav Khorev a98f71676d Preparing for translations 2026-07-05 17:53:13 +03:00
Vladislav Khorev bd994a8690 Mainly dialogues are completed 2026-07-05 17:01:56 +03:00
Vladislav Khorev 2d8f80b675 Fixing dialogues 2026-07-05 14:48:46 +03:00
Vladislav Khorev 8a6366098c Working on new dialogues 2026-07-04 22:45:47 +03:00
Vladislav Khorev f7e63f933b Working on minor stuff, arts, settings, cutscenes 2026-07-04 17:33:01 +03:00
Vladislav Khorev 5516d02e2c Fixing ui bugs, added cutscenes 2026-06-27 22:05:19 +03:00
Vladislav Khorev 9aeb0b1194 Working on hints on portrait mode 2026-06-27 17:21:31 +03:00
Vladislav Khorev 40e7e0ee44 Fixing hints for portrait mode 2026-06-27 15:14:03 +03:00
Vladislav Khorev 763ba9eeac Fixing clicking on phone 2026-06-27 13:50:39 +03:00
Vladislav Khorev bdad5fa107 Portrait mode for inventory screen 2026-06-27 13:07:16 +03:00
Vladislav Khorev 1b172460e0 Working on journal portrait mode 2026-06-27 11:50:09 +03:00
Vladislav Khorev b41e312511 Working on portrait mode 2026-06-26 21:25:53 +03:00
Vladislav Khorev 8c42179576 Working on messenger in portrait mode 2026-06-26 20:04:08 +03:00
Vladislav Khorev 0b4df8ab2d Working on portrait mode 2026-06-25 17:30:58 +03:00
Vladislav Khorev 6b39cfca3b Working on portrait mode 2026-06-25 12:13:02 +03:00
Vladislav Khorev b8d79c7b7b Working on music cross fade and settings 2026-06-24 20:58:31 +03:00
Vladislav Khorev 84f9fa37a8 Added music 2026-06-21 21:27:27 +03:00
Vladislav Khorev 7db9ca8f70 3d model optimization 2026-06-21 20:25:27 +03:00
Vladislav Khorev 51977a9eea Fixing bugs, navigations, adding cutscenes 2026-06-21 20:00:04 +03:00
Vladislav Khorev 130e4411cb New meshes and new trees 2026-06-21 15:15:40 +03:00
Vladislav Khorev d7db0faea3 Working on save-load system and UI 2026-06-20 21:41:07 +03:00
Vladislav Khorev 901142385d Working on new UI 2026-06-20 12:42:10 +03:00
Vladislav Khorev 2807d82d51 Working on game state 2026-06-19 18:39:10 +03:00
Vladislav Khorev b01e970554 Working on state for location 2026-06-18 21:48:27 +03:00
Vladislav Khorev e0968b5522 Working on game states 2026-06-18 20:51:44 +03:00
Vladislav Khorev 0ca00f965d Minor code change for CharacterState 2026-06-18 20:22:25 +03:00
Vladislav Khorev 0eb9d0be25 Adapt for web, make CharacterState class 2026-06-18 20:02:59 +03:00
Vladislav Khorev c891161d69 Working on fixing dorm bugs and some uni 2026-06-16 14:05:18 +03:00
Vladislav Khorev 939136c705 Fixing bug with night light in the dorms 2026-06-15 22:42:53 +03:00
Vladislav Khorev bba493ee7c Fixing lights and shadows 2026-06-15 21:25:54 +03:00
Vladislav Khorev 9f4d60036d Working on camera positioning 2026-06-15 12:06:03 +03:00
Vladislav Khorev 020f31e1e4 Working on cutscenes and pathfinding 2026-06-14 22:31:11 +03:00
Vladislav Khorev 80825e3220 Working on 3d models in uni interior location 2026-06-14 21:29:33 +03:00
Vladislav Khorev b80e3fdc5e Working on web version and also improving dorm 2026-06-14 19:02:06 +03:00
Vladislav Khorev 5d427af9f7 Working on script to reduce bones 2026-06-14 14:11:44 +03:00
Vladislav Khorev db456bde09 Minor dialogue and UI bug fixing 2026-06-13 20:58:44 +03:00
Vladislav Khorev fff5a9d4aa Fixing dialogues and cutscenes, minor bug fixing 2026-06-13 15:07:09 +03:00
Vladislav Khorev 2b32fce5e0 Working on UI fixing 2026-06-12 21:04:19 +03:00
Vladislav Khorev 7031a21922 Fixing UI bugs, updating web UI 2026-06-12 20:18:43 +03:00
Vladislav Khorev dee9615483 Working on UI fixing 2026-06-12 14:40:41 +03:00
Vladislav Khorev a4032d769e Adapting for web 2026-06-12 10:28:12 +03:00
Vladislav Khorev a9f4025333 Fixing inconsistency between day and nights on uni_int location 2026-06-11 21:19:39 +03:00
Vladislav Khorev f52b6872b0 Working on navigation 2026-06-10 12:21:49 +03:00
Vladislav Khorev 87fb6d4655 Fixing minor bugs in scripts 2026-06-09 23:57:27 +03:00
Vladislav Khorev 80133aa8c8 Working on main menu and tutorials 2026-06-09 17:20:03 +03:00
Vladislav Khorev 0ed7021e90 Working on teleports 2026-06-09 14:43:04 +03:00
Vladislav Khorev b94f02bd77 Working on taxi logic 2026-06-09 14:06:43 +03:00
Vladislav Khorev 77c8471636 Fixing navigation 2026-06-09 12:51:11 +03:00
Vladislav Khorev dcdeae75bc fixing minor bug 2026-06-08 22:30:33 +03:00
Vladislav Khorev 619de45221 Clean up UI, work on tutorial 2026-06-08 22:09:24 +03:00
Vladislav Khorev cfbd29d5b1 Working on cutscenes, minor fixing 2026-06-07 15:50:14 +03:00
Vladislav Khorev 6349859e66 working on cutscenes 2026-06-07 14:40:15 +03:00
Vladislav Khorev 54cc118df7 Some base for cutscene 2026-06-06 23:00:22 +03:00
Vladislav Khorev 3ba8a2f44b Cleanup 2026-06-06 22:20:08 +03:00
Vladislav Khorev 2e5988f8f4 Refactoring cutscenes 2026-06-06 22:04:42 +03:00
Vladislav Khorev 7011bbb754 Working on cutscene skip 2026-06-06 21:21:56 +03:00
Vladislav Khorev c991b5dbc1 Added shadow, working on UI adapting 2026-06-06 20:32:09 +03:00
Vladislav Khorev f61a12f874 Fixing bug in inventory 2026-06-06 18:40:40 +03:00
Vladislav Khorev 0cc384028f Working on web version 2026-06-06 16:37:11 +03:00
Vladislav Khorev 597ed04fdc Working on optimizations and navigation 2026-06-06 14:24:16 +03:00
Vladislav Khorev e96c9f171f Web version clean up 2026-06-06 12:44:06 +03:00
Vladislav Khorev 1a00e3d4a2 Working on scripts, make game finished 2026-06-05 22:02:40 +03:00
Vladislav Khorev 8f7797789d Working on dialog editor, on dialogs, on phone 2026-06-05 21:17:51 +03:00
Vladislav Khorev 0e27521bea Minor bug fixing 2026-06-05 12:31:00 +03:00
Vladislav Khorev 1fc8120ee0 Working on messenger 2026-06-05 12:13:02 +03:00
Vladislav Khorev 9221ab68ac Working on night mode and how player goes in 2026-06-04 21:41:32 +03:00
Vladislav Khorev 1d3bea452c Added some dialogs for transitions 2026-06-04 19:12:18 +03:00
Vladislav Khorev 59e0b2d77a Working with phone and time 2026-06-04 18:21:58 +03:00
Vladislav Khorev c8d743b932 Added dawn 2026-06-04 18:01:51 +03:00
Vladislav Khorev 83b92e9881 Working on night and day transition 2026-06-04 17:14:10 +03:00
Vladislav Khorev ed8d6f2e92 Working with lights and shadows 2026-06-04 16:35:01 +03:00
Vladislav Khorev e3542b59f7 Working on lights 2026-06-04 15:14:18 +03:00
Vladislav Khorev 442d3a7945 Working on the night mode 2026-06-04 13:27:52 +03:00
Vladislav Khorev e6bfe85e2e Working on phone and bank account 2026-06-03 22:53:47 +03:00
Vladislav Khorev 1b1ceac2fe Working on phone 2026-06-03 22:27:18 +03:00
Vladislav Khorev 7a02665305 Working on phone screens 2026-06-03 21:38:12 +03:00
Vladislav Khorev 8694b0087d Journal completed 2026-06-03 16:42:38 +03:00
Vladislav Khorev e7ae148ead Working on quest system 2026-06-03 14:37:09 +03:00
Vladislav Khorev aaae368a07 Working on doors interactive objects in dorm 2026-06-02 21:25:10 +03:00
Vladislav Khorev 65d19c577a Working on minor object fixing 2026-06-02 20:55:45 +03:00
Vladislav Khorev 89cbc293fc Working on objects 2026-06-02 15:47:05 +03:00
Vladislav Khorev 60b0ba69ba Working on updating positions for dorm location 2026-06-02 13:29:47 +03:00
Vladislav Khorev 4b868f47c2 Added editor functionality to edit bound boxes 2026-06-02 12:11:37 +03:00
Vladislav Khorev 22b0e1992f Working on health bar 2026-06-01 22:03:05 +03:00
Vladislav Khorev 8552725dfc Working on UI hints 2026-06-01 19:02:03 +03:00
Vladislav Khorev 3d4560bb51 Working on darklands mode 2026-05-31 21:57:18 +03:00
Vladislav Khorev 5f7e66f765 Fixing inventory and HUD screen 2026-05-31 21:11:58 +03:00
Vladislav Khorev 03681bedaf Working on UI, navigation, minor fixing 2026-05-31 18:33:39 +03:00
Vladislav Khorev a0550daf4b Working on UI 2026-05-30 22:27:09 +03:00
Vladislav Khorev 9425f9b19a Working on phone messages 2026-05-30 21:27:25 +03:00
Vladislav Khorev cdab525858 Working on chart 2026-05-30 15:43:53 +03:00
Vladislav Khorev 3f5c4a381b Working on phone ui 2026-05-29 21:18:44 +03:00
Vladislav Khorev 31130ec248 Working on stuff 2026-05-29 14:05:16 +03:00
Vladislav Khorev c659293bf8 Working on UI 2026-05-29 12:44:55 +03:00
Vladislav Khorev 9adcde5c05 Working on UI journal 2026-05-28 20:30:36 +03:00
Vladislav Khorev 90c57932ec Working on inventory 2026-05-28 17:16:10 +03:00
Vladislav Khorev dda9f34277 Working on inventory 2026-05-28 14:43:05 +03:00
Vladislav Khorev 586ffc99b6 Working on ui 2026-05-27 22:35:05 +03:00
Vladislav Khorev 1eee415b41 Working on new character 2026-05-27 18:14:21 +03:00
Vladislav Khorev 80af0e61ae Working on UI improvement 2026-05-27 17:30:41 +03:00
Vladislav Khorev 0da1ae124c Working on UI part 2026-05-26 22:21:05 +03:00
Vladislav Khorev 6c8fcf17b6 Updated dorm interior 2026-05-26 15:39:04 +03:00
Vladislav Khorev cfafcfc944 Working on dorm env 2026-05-24 19:11:56 +03:00
Vladislav Khorev 26f596a149 Working on navigation 2026-05-24 14:08:52 +03:00
Vladislav Khorev e1a191634c Adding furn to the uni interior 2026-05-23 22:44:43 +03:00
Vladislav Khorev 84c8a7a27a Dialog fixing 2026-05-22 22:32:05 +03:00
Vladislav Khorev 8c200974c2 Working on dorm navigation 2026-05-22 21:06:09 +03:00
Vladislav Khorev 715f2e8c31 Working on navigation 2026-05-21 22:00:11 +03:00
Vladislav Khorev ffd5bd01a4 Working on location exterior 2026-05-21 20:30:49 +03:00
Vladislav Khorev 3a73f3fbde Improved pathfinding logic 2026-05-21 11:48:18 +03:00
Vladislav Khorev 2659951d8e Working on the player respawn correctly 2026-05-18 21:10:55 +03:00
Vladislav Khorev 27b4b19df4 Updating scripts 2026-05-18 15:21:42 +03:00
Vladislav Khorev 21bf3f0710 Fixing hp bugs 2026-05-18 09:17:05 +03:00
Vladislav Khorev 49501e32ce Working with scripts, testing new chair 2026-05-17 20:28:18 +03:00
Vladislav Khorev 4044acefb2 Working on scripts 2026-05-17 14:39:02 +03:00
Vladislav Khorev c086fe1ef0 Added darklands 2026-05-16 18:32:23 +03:00
Vladislav Khorev 4e9eef1863 Fixing minor bugs 2026-05-15 21:32:09 +03:00
Vladislav Khorev b973d714cc Navigation adjusting 2026-05-15 20:50:19 +03:00
Vladislav Khorev e618c91415 Added covers, cross-fade 2026-05-15 14:59:58 +03:00
Vladislav Khorev 0f2b2e55af Working on doors and scripting 2026-05-14 21:28:41 +03:00
Vladislav Khorev 774397f690 adding doors 2026-05-14 19:54:58 +03:00
Vladislav Khorev d6417eba1b Some new scripts, dialogs and animations 2026-05-14 16:43:06 +03:00
Vladislav Khorev 2cae9998a0 Major refactoring for game objects 2026-05-12 21:19:29 +03:00
Vladislav Khorev efb85f7328 Some minor fixing 2026-05-12 19:32:57 +03:00
Vladislav Khorev 001077a1d6 More items 2026-05-11 22:29:26 +03:00
Vladislav Khorev be05643b70 Some improvements, added cutscene stub 2026-05-09 23:31:49 +03:00
Vladislav Khorev 72e05cdc28 little cleanup 2026-05-09 20:28:25 +03:00
Vladislav Khorev 13ef92160d Added startup scripts 2026-05-09 14:14:04 +03:00
Vladislav Khorev a479fa4ac1 Working on navigation system 2026-05-08 20:26:15 +03:00
Vladislav Khorev d4bc73f732 Initial dialog, updated girl model 2026-05-08 19:17:47 +03:00
Vladislav Khorev 8214342248 Working on teleports 2026-05-07 20:53:20 +03:00
Vladislav Khorev 7e94009217 Working on new locations 2026-05-07 19:49:41 +03:00
Vladislav Khorev fb7b42ddf9 Some clean up 2026-05-02 20:40:29 +03:00
Vladislav Khorev 6149182f02 Fixing bugs, added girl 2026-05-02 20:34:43 +03:00
Vladislav Khorev d24748df1e Model optimization 2026-05-01 21:47:54 +03:00
Vladislav Khorev be6a8cca9e working on texture manager 2026-05-01 21:00:51 +03:00
Vladislav Khorev e04fcbb9bd Clean up 2026-05-01 20:26:50 +03:00
Vladislav Khorev 519d780b3c Clean up 2026-05-01 20:01:06 +03:00
Vladislav Khorev 16d250a51d merge 2026-05-01 19:03:13 +03:00
Vladislav Khorev 5231b50aab Merge branch 'witcher001-path' into witcher001 2026-05-01 16:53:25 +03:00
anime 8b0a18a6cf corrected collision of tree 2026-05-01 16:01:32 +06:00
Vlad 4de8f78eca merge 2026-04-30 15:15:11 +06:00
Vlad 5dd964c740 speed 2026-04-30 14:20:10 +06:00
Vladislav Khorev b98631a8c0 Fixing stuff 2026-04-30 11:08:06 +03:00
vottozi d78286a9c2 add quest journal (QuestJournal + split UI view) 2026-04-30 03:20:52 +06:00
Vlad 635960007c added slow-mo 2026-04-29 20:02:35 +06:00
vottozi dbf0af2cb2 implement dynamic text wrapping based on pixel width 2026-04-29 13:30:02 +06:00
anime cebbb8bb5d Added collision 2026-04-26 22:18:42 +06:00
Vladislav Khorev 2ee288cbe0 Added full screen mode 2026-04-25 21:17:40 +03:00
Vladislav Khorev 348aa632dd Code works more or less good 2026-04-25 21:11:07 +03:00
Vladislav Khorev 3495d28edf Fixing minor bugs 2026-04-25 20:30:27 +03:00
Vladislav Khorev c7b3f04cd4 Working on fog, 2nd level, locations, dialogs 2026-04-25 19:56:00 +03:00
Vladislav Khorev eac914e073 minor fixing, adapting for mobile web 2026-04-25 14:39:27 +03:00
Vladislav Khorev 44cc0fba67 Added hp and attack actions, and labels 2026-04-25 12:07:52 +03:00
vottozi 0a073243ad safe LMB hold-to-skip cutscenes and multiple images with crossfade 2026-04-25 01:32:16 +06:00
Vladislav Khorev 393ebcd831 Added sparks 2026-04-24 19:53:18 +03:00
Vladislav Khorev 2bb7da2e37 Added hp 2026-04-24 18:40:51 +03:00
Vladislav Khorev b4b41e8620 Added more animations 2026-04-24 17:39:32 +03:00
Vladislav Khorev 3241ab3735 Working on more animations 2026-04-23 22:24:33 +03:00
Vladislav Khorev c655678486 Some strange shit happens 2026-04-22 22:38:20 +03:00
Vladislav Khorev 4d9a5b0a6e Set new type of animation 2026-04-22 20:45:40 +03:00
Vladislav Khorev 52c4ad8ee0 Working on new animations 2026-04-22 18:53:27 +03:00
Vladislav Khorev fe01a5d1b2 Working on audio in web part 2 2026-04-17 17:14:44 +03:00
Vladislav Khorev b6e0422d71 Working on audio in web part 1 2026-04-17 17:13:55 +03:00
Vladislav Khorev 3778a603f2 merge 2026-04-17 16:30:09 +03:00
Vladislav Khorev e310822462 Working on web version 2026-04-17 16:13:56 +03:00
Vladislav Khorev 3d3c3b2a36 merge 2026-04-17 13:42:45 +03:00
Vlad b5bcd3a812 added music 2026-04-17 14:23:56 +06:00
vottozi 16e7580418 Merge branch 'witcher001-cutscene' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into witcher001-cutscene 2026-04-17 02:33:56 +06:00
vottozi be4dcd4cf9 fix: make choice options the only clickable area in dialogue choices 2026-04-17 02:04:36 +06:00
Vladislav Khorev a1dda3fd50 Major refactoring 2026-04-16 20:24:27 +03:00
Vladislav Khorev 92ba3f2b60 Some refactoring 2026-04-16 20:06:03 +03:00
Vladislav Khorev f2f56125f0 Refactoring 2026-04-16 18:07:22 +03:00
Vladislav Khorev 2ef2e456f7 GPU skinning refactoring 2026-04-16 17:48:23 +03:00
Vladislav Khorev 83cceecccd Refactoring 2026-04-16 15:37:48 +03:00
Vladislav Khorev 24851a89e8 Merge 2026-04-16 15:16:45 +03:00
Vladislav Khorev d47cfd1b6b Minor refactoring 2026-04-16 15:07:59 +03:00
Vladislav Khorev 71346b4227 Remove unused files 2026-04-16 14:03:52 +03:00
Vladislav Khorev 08e1174eaf Fixing minor bugs 2026-04-16 13:44:40 +03:00
Vladislav Khorev cc2eba1352 merge 2026-04-16 13:24:04 +03:00
Vladislav Khorev cfee60d71a merge 2026-04-16 13:11:01 +03:00
Vladislav Khorev dc76986cd3 Added path showing 2026-04-16 11:39:14 +03:00
Vladislav Khorev 61665434ab Fixing minor bugs 2026-04-16 09:37:36 +03:00
vottozi 2494a44e4a refactor: switch cutscene camera to viewport/UV-based system 2026-04-16 01:29:01 +06:00
Vladislav Khorev d8795ec076 Added shadow maps 2026-04-15 19:54:21 +03:00
vottozi ceebb13719 improve dialogue advance and add flexible cutscene camera system 2026-04-15 20:42:41 +06:00
Vladislav Khorev a47306533e Working on stuff 2026-04-15 16:51:55 +03:00
Vladislav Khorev f708843747 Working on optimization: GPU skinning and binary animations 2026-04-15 16:39:33 +03:00
Vladislav Khorev a589765b7e prepare to add anims 2026-04-15 16:38:30 +03:00
anime 4eaefd25d4 added finding path with A* and json file to know where character can walk 2026-04-15 18:36:27 +06:00
Vladislav Khorev eff7322140 Working on dialog portraits 2026-04-15 11:56:38 +03:00
Vlad 44dd09fcb5 fix inventory 2026-04-15 14:50:46 +06:00
Vladislav Khorev de2e18e38f Fixing rotation issues in battle mode 2026-04-15 10:34:55 +03:00
Vladislav Khorev 984d7489d5 Working on enemies and attack 2026-04-15 10:00:16 +03:00
Vlad f7e6f063d1 added dialog start radius and background by time in cutscenes 2026-04-14 18:07:46 +06:00
Vlad db8b9b7a18 fix timetyper and dialog start by click on npc 2026-04-14 17:41:28 +06:00
Vladislav Khorev 493eb5a5d7 Working on battle animation 2026-04-13 23:19:03 +03:00
Vlad 422ff1fbe3 merge 2026-04-13 15:08:38 +06:00
vottozi 6465549c3b fix: dialogue text now displays correctly 2026-04-13 02:16:25 +06:00
Vlad 34759326b9 added npc's gift 2026-04-12 14:52:37 +06:00
vottozi 7125674308 add dialogue and cutscene system 2026-04-12 01:46:33 +06:00
Vlad bc2997d86f added activeFunc for active obj 2026-04-11 16:18:59 +06:00
Vlad 943b6b4d21 added load npcs from json 2026-04-11 15:30:29 +06:00
Vlad b3fda88784 added inventory & active object 2026-04-09 15:33:55 +06:00
Vlad e1b491be73 added load from json game objects 2026-04-07 20:17:04 +06:00
Vladislav Khorev 178f69967b Working on web, change python to lua 2026-04-04 14:29:20 +03:00
Vladislav Khorev 90e2a369f4 Added attack animation for ghots 2026-04-03 21:54:40 +03:00
Vladislav Khorev 403b93e7f5 Added more scripts 2026-04-03 20:06:33 +03:00
Vladislav Khorev 63c9d7b35b Adding scripts 2026-04-03 19:46:43 +03:00
Vladislav Khorev 995e2c8e5b NPC code 2026-04-02 21:03:55 +03:00
Vladislav Khorev 2e4a7ab467 Some cleanup 2026-04-02 19:24:15 +03:00
Vladislav Khorev a96d0b207a Added ai generated animation 2026-04-01 19:07:21 +03:00
Vladislav Khorev 7840bfdb4e Added viola 2026-04-01 13:02:39 +03:00
Vladislav Khorev 56ce826205 Remove unused references 2026-04-01 12:03:35 +03:00
Vladislav Khorev 5364b97235 Working on new scene 2026-04-01 12:03:16 +03:00
Vladislav Khorev beac06fd45 Production-ready, fixing server bugs 2026-03-12 11:36:47 +03:00
Vladislav Khorev ca98fea2bd Fixing stupid bug 2026-03-10 22:09:07 +03:00
Vladislav Khorev a4d5017270 Remove log 2026-03-10 21:40:58 +03:00
Vladislav Khorev 45826ab5b5 Minor changes in config 2026-03-10 21:39:05 +03:00
Vladislav Khorev e5296a41b9 Added help and about, minor improvements 2026-03-10 21:16:58 +03:00
Vladislav Khorev 18674efd8e Working on minor bug fixing 2026-03-10 18:03:16 +03:00
Vladislav Khorev 5e7c050c68 Merge branch 'spark' 2026-03-10 11:39:02 +03:00
Vladislav Khorev c88784a669 Added inverse, fixing server bug 2026-03-10 11:38:16 +03:00
Vlad 181132e092 merge 2026-03-10 14:36:35 +06:00
Vlad 6fe10eacfc Merge remote-tracking branch 'origin/main' into spark 2026-03-10 14:26:51 +06:00
Vlad 9e692c9ec3 added enter nicknamw 2026-03-10 14:25:00 +06:00
Vladislav Khorev 785b96ce82 Working on list of players 2026-03-09 23:04:48 +03:00
Vladislav Khorev a8ded217df Fixing ui bugs 2026-03-09 20:52:16 +03:00
Vladislav Khorev 3e8dba36a4 fixing minor bugs, fixing velocity not changed after death, fixing server crash 2026-03-09 19:27:20 +03:00
Vladislav Khorev 84a5e888a0 Added pickup boxes 2026-03-09 18:36:04 +03:00
Vladislav Khorev 86a9f38c3b Some UI changes 2026-03-09 13:50:59 +03:00
Vladislav Khorev 24aa7007bb Added trails to other ships toos 2026-03-08 21:27:45 +03:00
Vladislav Khorev 59b5bea540 Fixing particles 2026-03-08 19:07:02 +03:00
Vladislav Khorev 9da3cc4401 Fixing bug in particle emitter 2026-03-08 00:22:13 +03:00
Vladislav Khorev 4a542fd6c8 Added multi-touch, added on press down 2026-03-07 22:41:25 +03:00
Vladislav Khorev a06cb77a66 Fixing minor stuff, refactoring 2026-03-07 20:51:34 +03:00
Vladislav Khorev 3c2c06ca40 Working on refactoring 2026-03-07 20:03:16 +03:00
Vladislav Khorev 928600acd4 Refactoring 2026-03-07 19:41:15 +03:00
Vladislav Khorev 7ccda081b0 Fixing bugs 2026-03-06 23:15:56 +03:00
Vladislav Khorev 41f370f2ee Minor fixing 2026-03-06 22:34:03 +03:00
Vladislav Khorev 4af20c65e6 Make menu to state machine 2026-03-06 21:52:00 +03:00
Vladislav Khorev ac551122d9 Added force disconnect from server 2026-03-06 20:05:55 +03:00
Vladislav Khorev 8528b4dc90 Clean up server 2026-03-06 19:08:55 +03:00
Vladislav Khorev c35e093cc5 Working on encoding 2026-03-06 17:51:46 +03:00
Vladislav Khorev a8c76dee3c Working on fixing minor bugs 2026-03-05 23:58:35 +03:00
Vladislav Khorev e84a094ad9 Merge branch 'spark' 2026-03-05 19:54:24 +03:00
Vladislav Khorev b0b7391939 Fixed low quality 2026-03-05 19:53:27 +03:00
Vlad 3e2632c5b5 added textures 2026-03-05 20:14:02 +06:00
Vladislav Khorev 70ef730e86 Merge branch 'main' into ShipSpawn 2026-03-05 10:24:17 +03:00
Vladislav Khorev 92b288d978 fixing bugs 2026-03-05 10:15:55 +03:00
Vladislav Khorev cca1d0ece2 Merge branch 'spark' 2026-03-05 09:10:38 +03:00
Vladislav Khorev 8effdd5dfa Merge branch 'sergey' 2026-03-05 09:10:00 +03:00
Vladislav Khorev e78429b600 Working on running the game even in Telegram 2026-03-04 21:41:51 +03:00
anime 05a13a40e7 fixed: Box destroition 2026-03-04 16:37:00 +06:00
vottozi ef7617701e fix server collision radius mismatch 2026-03-04 03:37:58 +06:00
vottozi 2f50dc1522 fix Lead Indicator rendering when target off-screen 2026-03-04 01:42:57 +06:00
vottozi 9eafcd27fb Fix lead indicator prediction & align projectile lifetime (client/server) 2026-03-03 19:42:08 +06:00
Vlad 3879eb1d75 fixed web ship types boxes / local boxes collision and projectiles 2026-03-03 19:17:07 +06:00
anime 78046e5e2d Merge remote-tracking branch 'origin/main' into ShipSpawn 2026-03-02 17:49:09 +06:00
anime 26233f934f fixed: spawning ships 2026-03-02 17:47:46 +06:00
Vlad 167c04b107 install 2026-03-02 14:05:23 +06:00
Vladislav Khorev 5fcb1d1234 server changes 2026-03-01 22:35:17 +03:00
Vladislav Khorev 70a617b688 More changes 2026-03-01 22:34:12 +03:00
Vladislav Khorev bb0f584bf6 Working on UI and web game 2026-03-01 21:22:57 +03:00
Vladislav Khorev 6b4b549b3c Working on UI for web 2026-02-28 23:01:26 +03:00
Vladislav Khorev ffbecbbcde Working on ui 2026-02-28 22:21:08 +03:00
Vladislav Khorev 2728ca9646 Adapting web version 2026-02-28 20:37:47 +03:00
Vladislav Khorev 7e5aa22fee merge 2026-02-28 13:33:33 +03:00
Vlad c20e82634a fix game over 2026-02-28 12:07:40 +06:00
Vladislav Khorev 9f82e7a2e6 Adapt for web 2026-02-27 22:31:07 +03:00
Vlad 7418bbbe27 fix spaceship rotation fix sync boxes 2026-02-27 17:37:00 +06:00
Vladislav Khorev be2aa76f8b Working on server optimization 2026-02-26 22:26:43 +03:00
Vladislav Khorev e8f1786a2a merge 2026-02-26 21:22:38 +03:00
Vladislav Khorev e30f8a4c70 merge 2026-02-26 21:10:27 +03:00
Vladislav Khorev f8642efa23 merge 2026-02-26 21:02:16 +03:00
vottozi 5da5a754fe add configurable HUD crosshair 2026-02-26 01:49:49 +06:00
vottozi fb1b0a1c2e Lead Indicator fix 2026-02-25 22:18:47 +06:00
Vlad 74c2f786a1 added choising spaceship type for multiplayer and some fix in respawn 2026-02-25 19:17:42 +06:00
Vlad 5216965496 added choosing spaceship in singleplay 2026-02-24 20:34:01 +06:00
Vlad ad7294ceea added random spaceship type 2026-02-24 19:51:33 +06:00
Vlad 3893038d9a Merge remote-tracking branch 'origin/main' into spark 2026-02-23 15:30:01 +06:00
Vlad cb2e8318e7 added multi_menu 2026-02-23 15:29:05 +06:00
Vladislav Khorev 5cca2413fe changes 2026-02-23 11:46:57 +03:00
Vladislav Khorev db4254e205 working on web 2026-02-23 11:45:55 +03:00
vottozi fa3a4c66c9 add lead indicator 2026-02-23 14:43:23 +06:00
Vladislav Khorev 84006380db Added loading screen 2026-02-23 09:55:01 +03:00
Vladislav Khorev f6cc30a30c Try out Cursor 2026-02-22 21:20:39 +03:00
Vladislav Khorev d883b58260 Adapt for web 2026-02-22 20:04:18 +03:00
Vladislav Khorev b59a10b7e6 Major refactoring for game menu 2026-02-22 19:50:13 +03:00
Vladislav Khorev 5b57696acf Refactoring major 2026-02-22 19:15:25 +03:00
Vladislav Khorev 1265d87bc5 Updated UI 2026-02-22 18:23:27 +03:00
Vladislav Khorev 46d7a2a25d Merge branch 'spark' 2026-02-22 17:02:40 +03:00
Vlad 0babfff28b added multiplayer menu 2026-02-22 19:21:12 +06:00
Vladislav Khorev ed6e1bacc7 merge 2026-02-22 16:03:11 +03:00
Vladislav Khorev 9c39782729 Fixing bug with collisions 2026-02-22 15:53:04 +03:00
Vladislav Khorev efad2dde3e Restore as it was before 2026-02-22 14:08:12 +03:00
Vladislav Khorev 84d8ee7eee Merge branch 'spark' 2026-02-22 13:30:33 +03:00
Vladislav Khorev 7693237aa5 Added websocket 2026-02-22 13:29:06 +03:00
vottozi 528c94e921 Add enemy target brackets + off-screen arrows 2026-02-19 01:13:08 +06:00
Vlad a8700af1d0 added npc destroy 2026-02-13 15:40:40 +06:00
Vlad 9793c5bf06 fix npc 2026-02-12 18:13:19 +06:00
vottozi b56fafa0e0 Add mesh caching for unchanging text labels 2026-02-12 13:23:16 +06:00
Vladislav Khorev 22ee99418d Working on web version 2026-02-11 21:48:25 +03:00
vottozi 4eda57b4e4 single texture atlas for all font glyphs in TextRenderer 2026-02-12 00:08:52 +06:00
Vladislav Khorev 2ffd8124f2 Change the way fire happens 2026-02-11 20:02:15 +03:00
Vladislav Khorev 210c191d41 Added different hash func 2026-02-10 21:06:21 +03:00
Vladislav Khorev 251a59ddbe added hash 2026-02-10 11:42:23 +03:00
Vlad 5df2216da6 added npc 2026-02-10 12:42:19 +06:00
Vladislav Khorev 2c1c077611 Added time offset sync, added web version multiplayer 2026-02-09 22:56:48 +03:00
Vlad 629c9ba7b1 add local server 2026-02-09 18:42:07 +06:00
Vladislav Khorev a59bcc0c4b Adapting for web 2026-02-08 22:38:24 +03:00
Vladislav Khorev cf1ec1e76f merge 2026-02-08 15:53:55 +03:00
Vladislav Khorev 8a374a4231 Merge with main 2026-02-08 15:31:25 +03:00
Vlad da2b6e5577 some fix/added sync boxes 2026-02-06 19:16:17 +06:00
vottozi aa9590e2f4 Add velocity HUD display (Velocity: 0) in top-left corner 2026-02-06 18:04:33 +06:00
vottozi a1d54b01b0 Add TextView UI component 2026-02-06 15:11:27 +06:00
Vlad 65a2066727 size 2026-02-05 14:00:20 +06:00
Vladislav Khorev c49c4626d0 merge 2026-02-05 10:43:24 +03:00
Vlad ec156bd679 add sync projectiles 2026-02-05 13:29:21 +06:00
Vladislav Khorev 7b1d9363a0 Fixing lag compensation and make more smooth 2026-02-05 09:00:54 +03:00
Vladislav Khorev 82ca994fac merge 2026-02-05 07:33:13 +03:00
Vladislav Khorev 0d2f705d42 merge 2026-02-05 07:30:12 +03:00
vottozi e799b4ec79 conflict resolution 2026-02-05 01:43:45 +06:00
vottozi c8076b6018 adjust label scale: constant far, smooth growth near 2026-02-05 01:34:14 +06:00
Vlad 88f68b20ea some fix 2026-02-02 14:45:36 +06:00
vottozi d249f17d22 everything uselles 2026-02-02 13:50:43 +06:00
vottozi bee3545196 change to permanent visibility of ship labels 2026-02-02 13:48:28 +06:00
vottozi e050d7d0ec Improve ship nickname rendering 2026-02-02 04:44:18 +06:00
vottozi e18484ea62 add player ID labels for ships 2026-02-01 23:34:21 +06:00
Vlad 204b79bf06 add main menu 2026-01-31 19:25:40 +06:00
Vlad 50232c0816 fix shooting 2026-01-31 16:44:00 +06:00
Vlad e8fb14b809 added sync projectiles/boxes 2026-01-27 19:38:18 +06:00
Vladislav Khorev d425167dce Added planet physics and simple base 2026-01-25 20:55:36 +03:00
vottozi f41228acf8 add freetype-based text rendering and box labels 2026-01-22 15:54:21 +06:00
Vlad cacc18dc7e added boxes from server 2026-01-19 22:04:33 +06:00
Vladislav Khorev 96d879076a Merge branch 'main' into sergey 2026-01-18 13:35:06 +03:00
Vladislav Khorev 8f3528f9a1 merge 2026-01-18 13:29:48 +03:00
Vladislav Khorev 67c0696061 By default make offline 2026-01-18 13:18:59 +03:00
Vladislav Khorev 1a7e424085 Major clean up 2026-01-18 13:13:25 +03:00
Vladislav Khorev 6aff22ec53 Working on cleanup 2026-01-18 11:29:15 +03:00
Vladislav Khorev 579f4886d1 Finally fixed bugs, now need to do cleanup 2026-01-17 20:06:57 +03:00
Vladislav Khorev 64385ba15c Fixing bug, implementing delays, remove WORLD_UPDATE 2026-01-17 19:16:06 +03:00
Vladislav Khorev 3c55b59c8d Fixing bugs, more freq updated 2026-01-17 15:03:37 +03:00
Vladislav Khorev e0a60dcd6f Change clock to system clock, fix bugs 2026-01-17 13:59:02 +03:00
Vladislav Khorev f967807296 Fixing bugs, unify velocity 2026-01-17 12:43:41 +03:00
Vladislav Khorev fbc420e894 Propagate ROT and VEL 2026-01-17 11:05:04 +03:00
Vladislav Khorev ff95163bcd Yee render works 2026-01-16 18:11:52 +03:00
Vladislav Khorev e0453db063 Working on implementing server side physics on gam 2026-01-16 00:01:44 +03:00
vottozi 0f415b6ed6 added SDL_ttf and FreeType libs 2026-01-16 00:58:22 +06:00
Vladislav Khorev 1348efb5fe Working on server 2026-01-15 21:54:37 +03:00
Vladislav Khorev ea26d6fb23 Working on local and websocket client 2026-01-15 13:41:00 +03:00
Vlad 52a52ef960 added projectile collision and some fixed 2026-01-15 15:49:32 +06:00
Vladislav Khorev 90ad3392cb Merge branch 'main' into spark 2026-01-15 09:32:35 +03:00
Vladislav Khorev dc5c76f888 Merge branch 'spark' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into spark 2026-01-15 09:32:29 +03:00
Vladislav Khorev 483757f8ca first version 2026-01-14 23:17:52 +03:00
Vlad bdce619e68 fix 2026-01-14 17:06:53 +06:00
Vladislav Khorev dca38c986d Minor refactoring 2026-01-14 13:24:07 +03:00
Vlad 13af968a04 added collision for stones 2026-01-14 16:06:32 +06:00
Vladislav Khorev 4cc6971542 Merge, adding ship vertical shift 2026-01-14 12:32:42 +03:00
Vladislav Khorev 8d58e41459 Merge branch 'spark' 2026-01-14 11:13:29 +03:00
Vladislav Khorev dffbc6029d Merge branch 'spark' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into spark 2026-01-14 11:12:50 +03:00
Vladislav Khorev 988e0fefa8 Adding new ship to test, temporary remove ui 2026-01-14 11:12:04 +03:00
Vladislav Khorev f0ad32476e some move 2026-01-14 11:08:27 +03:00
Vladislav Khorev deedec7833 merge 2026-01-14 10:53:05 +03:00
Vlad cda1c0de20 added collision box/planet 2026-01-13 19:09:06 +06:00
Vlad ec30cbe810 merge 2026-01-12 14:55:16 +06:00
Vladislav Khorev d610f1dea1 Merge branch 'spark' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into spark 2026-01-12 09:44:54 +03:00
Vladislav Khorev c347545d0e Major refactoring to multi-platform code 2026-01-11 19:59:58 +03:00
Vladislav Khorev a08ee412a6 Added android version too 2026-01-11 12:08:48 +03:00
Vlad 528e7ae5a6 add animation buttons 2026-01-10 15:06:12 +06:00
Vladislav Khorev 8089bcc0f3 porting web to emcmake 2026-01-09 23:19:39 +03:00
Vladislav Khorev d1cab1f3b3 refactoring 2026-01-09 22:33:46 +03:00
Vladislav Khorev 9fc0adf210 Major refactoring + web adaptation 2026-01-09 20:49:03 +03:00
Vladislav Khorev 197423b572 Merge branch 'main' into spark 2026-01-09 17:17:11 +03:00
Vlad 112684b40a add read from zip 2026-01-09 20:08:09 +06:00
Vladislav Khorev 648acf8938 Working on multi-threading, including web 2026-01-09 15:24:01 +03:00
Vladislav Khorev b055faf882 Working on web version 2026-01-08 23:23:39 +03:00
Vladislav Khorev 723b973c7f Porting to web 2026-01-07 15:14:13 +03:00
Vladislav Khorev 6961152d34 Added mipmapping partially 2026-01-07 13:30:21 +03:00
Vladislav Khorev 34fee1005a Lighting and clouds are somehow working 2026-01-04 18:11:10 +03:00
Vladislav Khorev 8168734fbb fixing bug with stones baking 2026-01-03 20:58:18 +03:00
Vladislav Khorev 144978bfa2 Added Eigen, replacing ZMath 2026-01-03 20:38:36 +03:00
Vladislav Khorev 9bfe2bef89 Working on adding Eigen 2026-01-03 19:52:44 +03:00
Vladislav Khorev c553780b6e Make IcoSphere 2026-01-03 17:29:40 +03:00
Vladislav Khorev 570c96a628 working on colors on the planet 2026-01-02 22:01:33 +03:00
Vladislav Khorev c9178cff3a Working on fog shader 2026-01-02 21:16:44 +03:00
Vladislav Khorev 2b40c13c9d Finally somehow planet is done 2025-12-31 23:02:13 +03:00
Vladislav Khorev 592e008914 Minor refactoring 2025-12-30 16:16:50 +03:00
Vladislav Khorev 0a5a3b653d merge 2025-12-30 15:52:26 +03:00
Vlad ff3946a019 added projectile 2025-12-30 16:47:32 +06:00
Vladislav Khorev f197dfcf02 Clean up some 2025-12-29 20:40:13 +03:00
Vladislav Khorev 1af829bf49 Finally fog is working 2025-12-29 19:05:10 +03:00
Vladislav Khorev 7287f4b619 Working on stuff 2025-12-28 20:11:17 +03:00
Vladislav Khorev 14b43239b5 Parallax mapping working on single triangle 2025-12-28 18:29:25 +03:00
Vlad d57112be35 add UiSlider and some fix 2025-12-23 13:37:54 +06:00
Vladislav Khorev 7fc46d36a1 Lod working 2025-12-22 15:43:50 +03:00
Vlad bee2fff538 add menu 2025-12-22 15:40:45 +06:00
Vladislav Khorev 29834d528a finally frame buffer rendering is working 2025-12-20 23:08:26 +03:00
Vlad 068c319890 add ui.json and del default values from SparkEmitter 2025-12-19 17:35:34 +06:00
Vlad 53ea1f768c merge 2025-12-18 12:33:37 +06:00
Vlad b369c52270 error 2025-12-18 12:26:41 +06:00
Vlad 01d362615f add loading from json 2025-12-17 17:05:40 +06:00
Vladislav Khorev e690d0f8e2 Working on stones 2025-12-16 22:21:20 +03:00
Vladislav Khorev 854a425d85 Added some rocks 2025-12-15 22:30:33 +03:00
Vladislav Khorev 0464bed4cf Refactoring 2025-12-15 21:51:43 +03:00
Vladislav Khorev 7c272a2ee6 Refactoring 2025-12-15 21:47:45 +03:00
Vladislav Khorev db25abc305 Somehow it is working 2025-12-14 19:52:55 +03:00
Vladislav Khorev 57ff27b0f6 working 2025-12-14 18:39:45 +03:00
Vladislav Khorev a91d7c04e8 Neighbours finding well 2025-12-14 17:45:29 +03:00
Vladislav Khorev 16ba536481 lod 0 and 1 2025-12-14 15:32:39 +03:00
Vladislav Khorev 7a1961b5d1 Somehow planet is working 2025-12-14 13:00:39 +03:00
Vladislav Khorev af550f56a7 Cool working 2025-12-13 23:17:03 +03:00
Vladislav Khorev 27fb7a448c planet is closed 2025-12-13 20:34:47 +03:00
Vladislav Khorev 2b6af91992 Good constants 2025-12-13 20:16:40 +03:00
Vladislav Khorev 4afb9d6f4f Somehow planet is working 2025-12-13 20:11:48 +03:00
Vladislav Khorev fe361885c0 Working on planet 2025-12-13 15:02:03 +03:00
Vladislav Khorev 3887208b8d Working on planet 2025-12-12 23:53:34 +03:00
Vladislav Khorev 4c837eff0a merge 2025-12-12 23:07:25 +03:00
Vladislav Khorev 8f9a18d960 Merge branch 'main' into spark 2025-12-12 22:45:57 +03:00
Vlad bc49540a95 add blend 2025-12-12 17:12:03 +06:00
Vlad d2605d9108 moved drawEffect() to SparkEmitter 2025-12-12 16:58:23 +06:00
Vlad 115cbbb7fa fix spark 2025-12-11 20:09:41 +06:00
vottozi ae1ca71a20 test 2025-12-11 06:42:02 +00:00
anime c02273d29d added ship velocity using dragging volume 2025-12-10 20:58:10 +06:00
anime fec7e08c2b added dragging scroll button for music volume 2025-12-10 20:39:09 +06:00
anime 0f937e2e02 added music volume bar, without logic 2025-12-09 21:12:54 +06:00
Vlad b855cff0e6 added sparks on the back of the spaceship 2025-12-08 19:28:47 +06:00
Vladislav Khorev b768b27d49 update readme 2025-12-08 00:15:11 +03:00
Vladislav Khorev c5044d42de merge 2025-12-08 00:08:37 +03:00
Vladislav Khorev 88bc764f08 Working 2025-12-07 19:14:33 +03:00
Vladislav Khorev 72a2b4c47c Fixing minor bugs 2025-12-07 17:48:23 +03:00
Vladislav Khorev 38f4f6b7fc Working on web 2025-12-07 16:46:43 +03:00
Vladislav Khorev af00e77dc2 Added model 2025-12-05 22:43:57 +03:00
Vladislav Khorev 0322098efa Completed touch interface 2025-12-05 22:27:45 +03:00
Vladislav Khorev 2b19a3eaba Working 2025-12-05 21:27:57 +03:00
anime 69941dd86d Created new CmakeList to download and install sdl2 zlib libpng 2025-12-04 14:54:19 +06:00
Elaman 0843ee4fa4 add readme description for windows run 2025-12-01 17:05:52 +06:00
Vladislav Khorev 05bf63d447 working 2025-12-01 08:28:27 +03:00
Vladislav Khorev b45d9300bd New version 2025-11-30 22:46:42 +03:00
Vladislav Khorev fb4e2e2491 Make INAI building 2025-11-11 13:23:42 +03:00
Vlad c088080be5 fix html 2025-04-11 17:55:30 +06:00
Vlad 52592bea16 add -sUSE_LIBPNG=1 2025-04-11 17:41:14 +06:00
Vlad 933c1ec10e clean html 2025-04-11 13:49:40 +06:00
Vladislav Khorev 02443c7974 Adapt for Windows 2025-04-10 15:33:15 +03:00
NurgazievichR dd0381a007 zip file browser 2025-04-08 16:48:55 +06:00
NurgazievichR 60b71317ce works on browser 2025-04-08 16:19:54 +06:00
NurgazievichR 946c5d784b working with shaders 2025-04-08 12:46:25 +06:00
NurgazievichR a4d2b732aa base works on browser and laptop 2025-04-07 17:58:33 +06:00
Vlad 17d3b0ce2c test emcription 2025-03-28 16:37:05 +06:00
NurgazievichR f37c0ee9e6 loadfromfile bonesystem zip 2025-03-18 16:49:46 +06:00
NurgazievichR 2797b9fd3a shader zip 2025-03-18 15:40:55 +06:00
NurgazievichR a8689e8b58 Чтение bmp из zip файла 2025-03-18 15:06:25 +06:00
NurgazievichR 4e9275fa88 -DAUDIO=1 параметр добавил 2025-03-18 13:48:51 +06:00
NurgazievichR 2f9ab72258 Добавил Cmakelist, удалил ненужные файлы, запуск без аудио 2025-03-17 21:09:52 +06:00
Vladislav Khorev 8e82e716c7 Update Readme.md 2025-03-04 09:43:32 +03:00
Vladislav Khorev da7805ce0c Final version 2025-03-03 07:58:27 +03:00
Vladislav Khorev f77a3fa4ad Fixing bugs 2025-03-03 06:38:47 +03:00
albrut 5db648a99f Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-03 08:30:46 +06:00
albrut 1938d987f9 fixed problem with texture 2025-03-03 08:30:17 +06:00
Pavel Makarov 56fee07553 Merge pull request #46 from mephi1984/pavel
have fixed screen texture
2025-03-03 08:08:14 +06:00
maka70vv 24762e4d16 have fixed screen texture 2025-03-03 08:07:34 +06:00
Pavel Makarov 4dd60fb61e Merge pull request #45 from mephi1984/pavel
Pavel
2025-03-03 08:04:20 +06:00
Pavel Makarov 4e1d5859e2 Merge branch 'salmon' into pavel 2025-03-03 08:04:10 +06:00
Vladislav Khorev 1632549b48 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-03 05:04:01 +03:00
Vladislav Khorev b092ceaf14 Viola texture 2025-03-03 05:03:42 +03:00
maka70vv a43b6ee04a super fucking final commit fulll nahui worked version 2025-03-03 08:03:11 +06:00
albrut 4aa7545a7f added some objects in 3 room nut teksturka ne rabotaet 2025-03-03 08:03:08 +06:00
maka70vv 4afecc431c Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-03 07:52:49 +06:00
maka70vv 0db76cb793 prepare final room logic 2025-03-03 07:51:10 +06:00
albrut 6784a29086 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-03 07:49:00 +06:00
albrut 7faccda488 added some objects in 3 room 2025-03-03 07:48:22 +06:00
Pavel Makarov 940d5d4339 Merge pull request #44 from mephi1984/pavel
switching to third room
2025-03-03 07:41:08 +06:00
maka70vv 19c2f62ba9 switching to third room 2025-03-03 07:39:12 +06:00
albrut df9615e42a Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-03 07:33:34 +06:00
albrut 6298761311 added 3 room and button to move to next rom 2025-03-03 07:33:19 +06:00
Pavel Makarov 7c9e75b629 Merge pull request #43 from mephi1984/pavel
Pavel
2025-03-03 07:22:37 +06:00
maka70vv 5e7c274645 trying to make battery dialog 2025-03-03 07:22:03 +06:00
maka70vv 112be9bb0c not worked battery dialog windows 2025-03-03 07:13:31 +06:00
albrut 7951e6ab80 added 3 room and button to move to next rom 2025-03-03 06:52:20 +06:00
Pavel Makarov 3453c9780e Merge pull request #42 from mephi1984/pavel
dialogs
2025-03-03 06:50:53 +06:00
maka70vv 0e862e7a61 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-03 06:49:24 +06:00
maka70vv 644983408e dialogs 2025-03-03 06:49:02 +06:00
Vladislav Khorev f7c9c0c31f Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-03 03:45:36 +03:00
Vladislav Khorev 2e257e8abe Windows Release settings 2025-03-03 03:45:07 +03:00
Pavel Makarov f93a8586ae Merge pull request #41 from mephi1984/pavel
Pavel
2025-03-03 06:29:18 +06:00
maka70vv c153f2fe61 added dialog screen 2025-03-03 06:28:24 +06:00
maka70vv 07364460c3 worked version of lamp 2025-03-03 06:07:34 +06:00
maka70vv 86523fa5bb prepared to integrate dialog 2025-03-03 05:51:14 +06:00
maka70vv 5c0b7f9761 unworked logic for lamp 2025-03-03 05:34:48 +06:00
maka70vv 7fa4a74ef0 resolved conflicts 2025-03-03 05:19:44 +06:00
Albrut 31353179e5 Merge pull request #38 from mephi1984/Albert
added lighter and scale
2025-03-03 05:14:25 +06:00
albrut 80d003e9b2 added lighter and scale 2025-03-03 05:12:28 +06:00
maka70vv 146644b3db adding battery to the lamp 2025-03-03 04:53:37 +06:00
Albrut 271ceadd66 Merge pull request #37 from mephi1984/Albert
Albert
2025-03-03 04:33:10 +06:00
albrut 15db2b3006 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-03 04:32:23 +06:00
albrut 836e58f634 added test object and scale car toy 2025-03-03 04:31:59 +06:00
maka70vv d0c49843af lampe obj 2025-03-03 04:27:57 +06:00
Albrut b9bd0648ab Merge pull request #36 from mephi1984/Albert
Albert
2025-03-03 04:27:43 +06:00
maka70vv cac687a2d2 lampe obj 2025-03-03 04:27:36 +06:00
albrut 928b87cb6e deleted music that is not useful 2025-03-03 04:27:03 +06:00
albrut 5f5fe2ade5 added musics for rooms 2025-03-03 04:25:36 +06:00
Albrut d062bf472c Merge pull request #35 from mephi1984/Albert
added collision for second room for car toy
2025-03-03 04:08:25 +06:00
albrut 1e030e9d3a added collision for second room for car toy 2025-03-03 04:07:20 +06:00
Albrut d6c01368d7 Merge pull request #34 from mephi1984/Albert
added collision for second room for basement entering
2025-03-03 03:57:03 +06:00
albrut c39c9e8a35 added collision for second room for basement entering 2025-03-03 03:56:18 +06:00
Pavel Makarov e89c70eddd Merge pull request #33 from mephi1984/pavel
Pavel
2025-03-03 03:48:38 +06:00
maka70vv 7c5ef50f9b script if name uncorrect 2025-03-03 03:47:59 +06:00
Albrut bbd29ea310 Merge pull request #32 from mephi1984/Albert
added collision for second room buit not enough
2025-03-03 03:44:27 +06:00
albrut 0ec18bac20 added collision for second room buit not enough 2025-03-03 03:43:34 +06:00
maka70vv 6b9b670373 updated highlited position 2025-03-03 03:33:55 +06:00
Pavel Makarov ebe58e5488 Merge pull request #31 from mephi1984/pavel
Pavel
2025-03-03 02:56:41 +06:00
Pavel Makarov b065669583 Merge branch 'salmon' into pavel 2025-03-03 02:56:16 +06:00
maka70vv 45334c77f7 removing object if added to inventory 2025-03-03 02:53:43 +06:00
Albrut 895a947a60 Merge pull request #30 from mephi1984/Albert
Albert
2025-03-03 02:49:38 +06:00
albrut de2ffec17d added all cubes for first room 2025-03-03 02:48:39 +06:00
maka70vv 2d21324743 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-03 01:45:47 +06:00
albrut 73eee0525c Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-03 01:45:47 +06:00
maka70vv 8d61db9558 second room object 2025-03-03 01:45:38 +06:00
Vladislav Khorev 6a4fb6390a Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-02 22:44:30 +03:00
Vladislav Khorev 769854ac84 Added monster 2025-03-02 22:44:12 +03:00
albrut 82307f56e4 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-03 01:41:52 +06:00
albrut f972b840a6 added one cube in first room 2025-03-03 01:41:36 +06:00
maka70vv 24eecbec3b added inventory icon to objects 2025-03-03 01:27:09 +06:00
maka70vv 2e73c41519 added inventory icon to objects 2025-03-03 01:25:41 +06:00
Pavel Makarov 29f4a43f09 Merge pull request #29 from mephi1984/pavel
added timer
2025-03-03 01:10:05 +06:00
maka70vv 6c576510b7 added timer 2025-03-03 01:08:59 +06:00
Vladislav Khorev 32b2acf739 added second room, fix bugs 2025-03-02 21:51:29 +03:00
Vladislav Khorev e91bf4b35f Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-02 21:29:45 +03:00
Vladislav Khorev 1bb131ecd2 fffff 2025-03-02 21:29:33 +03:00
Pavel Makarov 87caae43cc Merge pull request #28 from mephi1984/pavel
Pavel
2025-03-03 00:22:48 +06:00
maka70vv 650927498c fixed cubs sorting 2025-03-03 00:22:14 +06:00
Pavel Makarov b51ef1f983 Merge pull request #27 from mephi1984/pavel
Pavel
2025-03-03 00:01:27 +06:00
maka70vv 6d38a9e914 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-03 00:01:09 +06:00
maka70vv 5ebc68043e finished level 1 without timer 2025-03-03 00:00:26 +06:00
Albrut 2a9149c746 Merge pull request #25 from mephi1984/Albert
added collision for first room
2025-03-02 23:55:36 +06:00
albrut f7de370212 added collision for first room 2025-03-02 23:54:04 +06:00
maka70vv cc38d52f6c resolved conflicts 2025-03-02 23:38:07 +06:00
maka70vv 526efb217e cubes positioning 2025-03-02 23:32:39 +06:00
maka70vv e8e4dc9b28 draw cubes 2025-03-02 23:19:15 +06:00
Vladislav Khorev a3075433f0 Move collision box and model to the room itself 2025-03-02 19:58:38 +03:00
maka70vv c39eac4241 adding cubes in inventory map ifuncorrect name 2025-03-02 22:43:07 +06:00
Vladislav Khorev fe5d8c2f47 working with collisions 2025-03-02 19:32:10 +03:00
maka70vv 4f2741765b finished cubes logic 2025-03-02 22:30:13 +06:00
maka70vv ede7597acb finished cubes logic 2025-03-02 22:28:52 +06:00
albrut 5c571578dd da 2025-03-02 22:09:25 +06:00
maka70vv 91e9a3a3a8 resolvig conflicts 2025-03-02 21:57:30 +06:00
albrut 0aff255379 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-02 21:56:44 +06:00
maka70vv ebf292d993 started first level logic 2025-03-02 21:55:27 +06:00
albrut a580afb8b9 trying to resolve conflicts 2025-03-02 21:54:32 +06:00
Vladislav Khorev 9012986f9d Added loading 2025-03-02 18:53:27 +03:00
maka70vv cc5401201d Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-02 21:52:59 +06:00
Vladislav Khorev ed11b23d24 added file texture 2025-03-02 18:52:29 +03:00
albrut 60b610d111 kolliziya dobavlena 2 krovat 2025-03-02 21:46:38 +06:00
maka70vv daf0991f0b Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-02 21:38:34 +06:00
Vladislav Khorev 01e6b0e355 Working with music 2025-03-02 18:11:29 +03:00
Vladislav Khorev b3efd62229 Merge 2025-03-02 17:00:32 +03:00
Vladislav Khorev c281070ff6 Music to separated thread 2025-03-02 16:54:20 +03:00
maka70vv 1229e61702 scaling inventory obj 2025-03-02 19:42:03 +06:00
Albrut 568384658e Merge pull request #24 from mephi1984/Albert
kolliziya dobavlena
2025-03-02 19:20:42 +06:00
albrut c31139fdec kolliziya dobavlena 2025-03-02 19:19:15 +06:00
Vladislav Khorev 54d0af8afb fff 2025-03-02 15:46:53 +03:00
Vladislav Khorev 322d83de9d Shader 2025-03-02 15:46:20 +03:00
Pavel Makarov 4e070d3d04 Merge pull request #23 from mephi1984/pavel
fixed freezing in second scene
2025-03-02 18:43:31 +06:00
maka70vv 383b34f5d6 fixed freezing in second scene 2025-03-02 18:42:31 +06:00
Pavel Makarov a568d3d0da Merge pull request #22 from mephi1984/pavel
Pavel
2025-03-02 18:38:08 +06:00
maka70vv cfc96f645e fixed inventory choosing 2025-03-02 18:37:37 +06:00
maka70vv 512e912471 resolved conflicts 2025-03-02 18:25:55 +06:00
Albrut 7e448812e2 Merge pull request #21 from mephi1984/Albert
Albert
2025-03-02 18:24:00 +06:00
albrut 2ae62fbc07 Merge branch 'Albert' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-02 18:23:23 +06:00
albrut f8b25e581b linux shadres 2025-03-02 18:21:50 +06:00
maka70vv 103c43eba1 select inventory 1-9 2025-03-02 18:19:25 +06:00
maka70vv aa84fa0823 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-02 18:12:59 +06:00
Vladislav Khorev 9f75e3ad60 fixing hole bugs, merge 2025-03-02 14:51:24 +03:00
Vladislav Khorev 0fc6b483d1 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-02 14:34:11 +03:00
Vladislav Khorev d414505d15 Make hole 2025-03-02 14:33:54 +03:00
albrut 1ac4c51a5d hotkesy for items 2025-03-02 17:33:30 +06:00
maka70vv a6a275a934 moving active inventory object 2025-03-02 17:30:55 +06:00
maka70vv 4ff416757f getting selected inventory items 2025-03-02 17:16:22 +06:00
Albrut 37f0a6aa08 Merge pull request #19 from mephi1984/Albert
working code
2025-03-02 17:02:37 +06:00
albrut 929c5aade1 working code 2025-03-02 17:01:40 +06:00
Pavel Makarov ebc63e6663 Merge pull request #18 from mephi1984/pavel
Pavel
2025-03-02 16:49:50 +06:00
Pavel Makarov d35547560e Merge branch 'salmon' into pavel 2025-03-02 16:49:35 +06:00
maka70vv 70f30b8b3a not worked version of inventory choosing 2025-03-02 16:48:31 +06:00
Albrut bdf721ac15 Merge pull request #17 from mephi1984/Albert
fixed the staying at the air
2025-03-02 16:31:22 +06:00
albrut a15b74e7a9 fixed the staying at the air 2025-03-02 16:28:45 +06:00
Albrut 980b8133a8 Merge pull request #16 from mephi1984/Albert
Albert
2025-03-02 16:23:20 +06:00
albrut 527315e976 now she stays on the ground i think 2025-03-02 16:21:42 +06:00
maka70vv d79a684868 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into pavel 2025-03-02 15:41:05 +06:00
albrut 6fd0fc5868 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-02 15:34:00 +06:00
albrut fe88d27632 added first room 2025-03-02 15:33:45 +06:00
Vladislav Khorev 87032466ac Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-02 12:17:56 +03:00
Vladislav Khorev aac2e84ad6 Change mouse logic 2025-03-02 12:13:04 +03:00
maka70vv b94bebdf0d updated inventory scale 2025-03-02 15:11:45 +06:00
Pavel Makarov fa520b5df8 Merge pull request #15 from mephi1984/pavel
updated room mesh
2025-03-02 14:50:38 +06:00
maka70vv 8657301da0 updated room mesh 2025-03-02 14:49:26 +06:00
Pavel Makarov f816b5ada8 Merge pull request #13 from mephi1984/pavel
Pavel
2025-03-02 13:47:32 +06:00
Pavel Makarov 6ea9276495 Merge branch 'salmon' into pavel 2025-03-02 13:47:25 +06:00
maka70vv 8b07b6cc3f drawing hidden ui objects 2025-03-02 13:45:29 +06:00
maka70vv ac31c3bbf5 update rendering aictive objects as unordered_map 2025-03-02 13:40:12 +06:00
Vladislav Khorev a8a3b04b09 merge 2025-03-02 09:37:49 +03:00
Vladislav Khorev 15697ebe67 Adding walking animation, fixing minor bugs 2025-03-02 09:29:31 +03:00
Pavel Makarov 2c26c5c0b5 Merge pull request #12 from mephi1984/pavel
Pavel
2025-03-02 11:30:19 +06:00
maka70vv 8e5e96e3ee resolving conflicts by merge 2025-03-02 11:28:16 +06:00
maka70vv 6c25d1f06b active objects to unordered map 2025-03-02 11:25:42 +06:00
Vladislav Khorev d84e7904e6 missing file added 2025-03-02 08:23:47 +03:00
Vladislav Khorev dc01bdf12e mege 2025-03-02 07:41:47 +03:00
Vladislav Khorev 064d446aea Finally animation 2025-03-02 07:37:51 +03:00
maka70vv 4fd09212dd tested game logic 2025-03-02 10:01:05 +06:00
maka70vv 784da51a3f inventory to unordered map 2025-03-02 09:55:56 +06:00
maka70vv d46e5da520 added game logic class and lambda function for room 2025-03-02 09:41:54 +06:00
Pavel Makarov b7f8a0f67a Merge pull request #11 from mephi1984/pavel
Pavel
2025-03-02 03:10:39 +06:00
maka70vv 1e3be66495 removing object from map after adding to inventory 2025-03-02 03:10:01 +06:00
maka70vv eb0bae44aa adding inventory 2025-03-02 03:00:50 +06:00
maka70vv aef61e19b4 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-02 02:08:11 +06:00
maka70vv a4aea4163a music changing 2025-03-02 02:07:26 +06:00
Pavel Makarov 7616435e00 Merge pull request #10 from mephi1984/pavel
Pavel
2025-03-02 01:50:59 +06:00
Pavel Makarov 37e2c489a8 Merge branch 'salmon' into pavel 2025-03-02 01:50:52 +06:00
maka70vv 378c9c6d6d objects rendering by rooms 2025-03-02 01:49:16 +06:00
maka70vv 22ad0ebf98 tested room switching 2025-03-02 01:01:10 +06:00
maka70vv 5d31d8421f getting room texture by index 2025-03-02 00:51:39 +06:00
maka70vv 06cc639bb7 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into salmon 2025-03-01 23:53:54 +06:00
maka70vv 74a2dc78a6 Fixing rooms 2025-03-01 23:52:55 +06:00
Vladislav Khorev 68b31cb197 merge 2025-03-01 20:14:56 +03:00
Vladislav Khorev 2a16970cde Fixing animation bugs 2025-03-01 19:44:15 +03:00
maka70vv 243dd61209 fixed default room size 2025-03-01 22:30:39 +06:00
maka70vv 1bf6d3f0b1 bugged room from class 2025-03-01 22:30:39 +06:00
Albrut 5221b3a689 Merge pull request #9 from mephi1984/Albert
added mouse handling
2025-03-01 22:16:38 +06:00
albrut 2d0c7ec166 added mouse handling 2025-03-01 22:14:33 +06:00
Albrut 4c397eb3d0 Merge pull request #8 from mephi1984/Albert
Albert
2025-03-01 21:44:44 +06:00
albrut 4271ef556d Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-01 21:43:54 +06:00
albrut 55133bbd87 fully refactored main.cpp 2025-03-01 21:43:31 +06:00
albrut 51a1cc742c refacored some parts of main.cpp 2025-03-01 21:33:34 +06:00
Albrut 60380ee733 Merge pull request #7 from mephi1984/Albert
Albert
2025-03-01 21:19:28 +06:00
albrut b1954fd334 refacored some parts of main.cpp 2025-03-01 21:13:53 +06:00
albrut 641efaf582 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-01 20:54:15 +06:00
albrut dbffcf5f04 refacored some parts of main.sh 2025-03-01 20:53:11 +06:00
albrut b6684f7929 refactored 2025-03-01 19:22:19 +06:00
albrut 0c431f9c08 Merge branch 'Albert' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-01 19:19:09 +06:00
albrut e2af375aa0 starting refactoring main.cpp 2025-03-01 19:18:42 +06:00
albrut 666bfc1b37 added sh file for start in linux 2025-03-01 19:16:26 +06:00
Albrut 436cf015a1 Merge pull request #5 from mephi1984/Albert
Albert
2025-03-01 19:08:30 +06:00
albrut b27b4adab3 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Albert 2025-03-01 19:07:06 +06:00
albrut 613d809b43 deleted buttons for testing 2025-03-01 19:06:42 +06:00
albrut 880caecd97 added inventory testing buttons I to add items O to delete last items in inventory and p to get info about inventory 2025-03-01 19:04:49 +06:00
Albrut 7dd2816409 Merge pull request #4 from mephi1984/Albert
Albert
2025-03-01 18:40:53 +06:00
albrut a8ab5015b9 Merge branch 'salmon' of github.com:mephi1984/ZeptoLabTest1 into Abert 2025-03-01 18:34:36 +06:00
albrut 27ecda3124 added sounds and music in main cpp by cliking wasd or arrow will play sound of steps and music in setup start playing 2025-03-01 18:20:50 +06:00
Albrut 43dd93a802 Merge pull request #3 from mephi1984/Abert
added sounds by clicking on button and changed script to compile
2025-03-01 18:04:30 +06:00
albrut bed5ae7a40 added sounds by clicking on button and changed script to compile 2025-03-01 18:01:24 +06:00
Vladislav Khorev b5877a488d Project windows 2025-03-01 14:53:47 +03:00
Vladislav Khorev 3fa844ddb9 missing textures 2025-03-01 14:39:23 +03:00
Albrut 7249f30b71 Merge pull request #2 from mephi1984/Abert
Abert
2025-03-01 17:28:17 +06:00
albrut d9038fee44 added sounds library 2025-03-01 17:26:04 +06:00
albrut 098f2b4005 added sounds library 2025-03-01 17:19:38 +06:00
Pavel Makarov 69a03cf524 Merge pull request #1 from mephi1984/pavel
added inventory logic
2025-03-01 17:09:45 +06:00
Vladislav Khorev c6b8a52c6d Adding active objects 2025-03-01 14:09:28 +03:00
maka70vv f5cf579115 added inventory logic 2025-03-01 17:08:49 +06:00
Vladislav Khorev 64ec35415c Working with scene and projection 2025-03-01 13:13:58 +03:00
Vladislav Khorev 4a784036bd Working on model 2025-03-01 11:45:16 +03:00
Vladislav Khorev f5940c729c Working on status 2025-03-01 10:19:37 +03:00
Vladislav Khorev fa0a369357 Working with Windows also 2025-03-01 10:14:28 +03:00
albrut 8d8843cb11 linuc 2025-03-01 11:44:31 +06:00
albrut 0fd42529c7 Linux version 2025-03-01 11:22:32 +06:00
albrut 0a37ba49c3 fixing conflict 2025-03-01 11:16:52 +06:00
albrut b9e88547e6 linux part 2025-03-01 11:12:22 +06:00
Vladislav Khorev dfa5786f24 change path 2025-03-01 07:53:49 +03:00
Vladislav Khorev 407db412c7 added meshes with skeleton 2025-03-01 07:52:56 +03:00
Vladislav Khorev 4d2d35e34e Finally animation works 2025-03-01 01:39:19 +03:00
Vladislav Khorev 329f1b1663 Loading MD3 and now we need to place models properly 2025-02-23 21:24:15 +03:00
Vladislav Khorev e130c2ba7f Working with format md3 2025-02-23 15:44:28 +03:00
Vladislav Khorev 3c40d3c20d Adding models 2025-02-22 22:03:29 +03:00
Vladislav Khorev 476a1e23f7 Working with obj model format 2025-02-22 21:35:08 +03:00
Vladislav Khorev 21301652ce Minor refactoring 2025-02-22 21:04:23 +03:00
Vladislav Khorev c792e50f1b Added color cube rotating in 3d 2025-02-22 20:58:34 +03:00
Vladislav Khorev 2876446ec8 2d is working, now try 3d 2025-02-22 20:09:45 +03:00
Vladislav Khorev 826f8cbb23 Something compiles 2025-02-22 19:42:58 +03:00
Vladislav Khorev 0cc3dce9a8 Fixing bug 2023-12-03 22:39:08 +03:00
Vladislav Khorev 48510c3aa6 Compiling again 2023-12-03 22:36:03 +03:00
Vladislav Khorev 7517b5f336 Fixing bug 2023-12-03 22:35:41 +03:00
Vladislav Khorev 1500a7456d Reduce size of bundle 2023-12-03 22:29:29 +03:00
Vladislav Khorev c8e8903ef2 SDL for Emscripten is working 2023-12-03 22:18:33 +03:00
Vladislav Khorev cb9f6c6932 SDL compiling for Emscripten 2023-12-03 21:36:04 +03:00
Vladislav Khorev 3bfac34979 Switch to use SDL 2023-12-03 21:10:02 +03:00
vladislav.khorev 7dda6d6ffb Upload to github 2021-06-30 20:31:14 +05:00
1066 changed files with 195228 additions and 17927 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.anim filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
Regular → Executable
+399 -50
View File
@@ -1,62 +1,411 @@
# Built application files
*.apk
*.ap_
*.aab
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# Files for the ART/Dalvik VM
*.dex
.vscode/
# Java class files
*.class
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# Generated files
bin/
gen/
out/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Gradle files
.gradle/
build/
.idea/
# Mono auto generated files
mono_crash.*
.........................................................................
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Local configuration file (sdk path, etc)
local.properties
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# CMake build files
CMakeFiles/
cmake_install.cmake
CMakeCache.txt
Makefile
*.cmake
# Visual Studio 2017 auto generated files
Generated\ Files/
# Android Studio
*.iml
.idea/
.cxx/
.gradle/
.DS_Store
/captures/
.externalNativeBuild/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NDK
.obj/
*.o
*.so
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# SDL2 (если используешь)
libs/
obj/
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Временные файлы
*.swp
*.swo
*~
.thumbs.db
desktop.ini
# Benchmark Results
BenchmarkDotNet.Artifacts/
# Проектные специфичные
app/build/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
app/jni/libpng
app/jni/SDL
app/jni/zlib
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
.idea/
sdl_app
*.sln.iml
images.zip
script.py
jumpingbird.*
jumpingbird.data
build
build-emcmake
thirdparty
proj-web/build
proj-windows/build
public
web_resources/
pc_resources/
resources_hd/
web_resources_x2/
+351
View File
@@ -0,0 +1,351 @@
# Cutscene System
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
The file can contain multiple cutscenes:
```json
{
"cutscenes": [
{ "id": "intro", ... },
{ "id": "ending", ... }
]
}
```
Cutscenes and dialogues are loaded from **separate files**:
```cpp
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
```
---
## Cutscene object
```json
{
"id": "intro_cutscene",
"skippable": true,
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"onFadeInCallback": "",
"imageSegments": [ ... ],
"lines": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
### Timing model
The total cutscene duration is:
```
contentDuration = max(durationMs, max(segment.endMs for all segments))
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
```
The full timeline looks like this:
```
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
world→black black→images images→black black→world
```
---
## Image segments
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
```json
{
"path": "resources/cutscenes/bg_layer.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 300,
"fadeOutMs": 300,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `path` | string | — | Path to the PNG image (**required**) |
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
---
## Image pose
A pose defines how an image is framed on screen at a given moment.
```json
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
### Pose intuition
| Goal | Config |
|---|---|
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }` → `to: { "centerX": 0.7, "scale": 1.2 }` |
| Zoom out from close-up | `from: { "scale": 1.8 }` → `to: { "scale": 1.0 }` |
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
---
## Easing types
Controls the interpolation curve applied to pose animation between `from` and `to`.
| Value | Description |
|---|---|
| `"Linear"` | Constant speed (default) |
| `"EaseInSine"` | Slow start, fast end |
| `"EaseOutSine"` | Fast start, slow end |
| `"EaseInOutSine"` | Slow start and end, fast middle |
| `"EaseInQuad"` | Quadratic slow start |
| `"EaseOutQuad"` | Quadratic slow end |
| `"EaseInOutQuad"` | Quadratic slow start and end |
| `"EaseInCubic"` | Cubic slow start |
| `"EaseOutCubic"` | Cubic slow end |
| `"EaseInOutCubic"` | Cubic slow start and end |
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
---
## Subtitle lines
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
```json
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000,
"waitForConfirm": false,
"luaCallback": ""
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
---
## C++ API
### Starting a cutscene
```cpp
// Standalone cutscene (not part of a dialogue):
dialogueSystem.startCutscene("intro_cutscene");
// Skip the currently playing cutscene:
dialogueSystem.skipCutscene();
```
### Callbacks
```cpp
// Called when a cutscene begins:
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
// Called when a cutscene ends (receives the cutscene id):
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
// id == "intro_cutscene"
});
// Called when a subtitle line begins (receives luaCallback value):
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
// Called when the opening fade-in completes (receives onFadeInCallback value):
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
```
### Triggering from dialogue
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
```json
{
"id": "node_cutscene",
"type": "CutsceneStart",
"cutsceneId": "intro_cutscene",
"next": "node_after_cutscene"
}
```
---
## Full examples
### Minimal — static image, timed
```json
{
"id": "simple",
"durationMs": 4000,
"fadeOutMs": 300,
"fadeInMs": 300,
"endFadeOutMs": 300,
"endFadeInMs": 300,
"imageSegments": [
{
"path": "resources/cutscenes/city.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 4000
}
]
}
```
### Two-layer parallax pan
Background moves slowly left-to-right; foreground character moves faster, creating depth.
```json
{
"id": "classroom_intro",
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/cutscenes/classroom_bg.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 400,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
},
{
"path": "resources/cutscenes/classroom_teacher.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"easing": "EaseInOutSine",
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "Рассаживайтесь.",
"durationMs": 2500
}
]
}
```
### Zoom-in reveal with a second image appearing mid-way
```json
{
"id": "letter_reveal",
"durationMs": 7000,
"fadeOutMs": 400,
"fadeInMs": 600,
"endFadeOutMs": 600,
"endFadeInMs": 400,
"imageSegments": [
{
"path": "resources/cutscenes/desk_bg.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 7000
},
{
"path": "resources/cutscenes/letter_closeup.png",
"width": 1280,
"height": 720,
"startMs": 2000,
"endMs": 7000,
"fadeInMs": 800,
"easing": "EaseOutCubic",
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
}
],
"lines": [
{
"text": "Среди бумаг на столе лежит конверт.",
"durationMs": 2500
},
{
"speaker": "Главный герой",
"text": "«Явитесь в деканат немедленно».",
"durationMs": 3000
}
]
}
```
+260
View File
@@ -0,0 +1,260 @@
# Pathfinding System
This document describes the grid-based pathfinding used for the player and all NPCs, including the collision avoidance and movement quality improvements.
---
## Table of Contents
1. [Grid Representation](#1-grid-representation)
2. [Building the Walkable Grid](#2-building-the-walkable-grid)
3. [A\* Path Search](#3-a-path-search)
4. [Path Smoothing](#4-path-smoothing)
5. [Approaching Unreachable Destinations](#5-approaching-unreachable-destinations)
6. [Dynamic Obstacles](#6-dynamic-obstacles)
7. [Path Following](#7-path-following)
8. [Character Collision Resolution](#8-character-collision-resolution)
9. [Dynamic Replanning](#9-dynamic-replanning)
10. [Key Constants Reference](#10-key-constants-reference)
---
## 1. Grid Representation
The world is divided into a uniform 2D grid in the XZ plane (Y is ignored during pathfinding; all characters walk on a flat floor at `floorY`).
Each cell is either **walkable** (`1`) or **blocked** (`0`). The grid is stored as a flat `std::vector<unsigned char>` indexed by `z * gridWidth + x`.
**Parameters** (all configurable in the JSON config file):
| Parameter | Default | Description |
|---|---|---|
| `cellSize` | 0.4 m | Width and depth of one cell |
| `agentRadius` | 0.45 m | Half-width of a character — used to erode free space |
| `objectPadding` | 0.25 m | Extra clearance added around obstacle polygons |
| `boundaryPadding` | 0.0 m | Inward erosion from the edges of navigation areas |
| `floorY` | 0.0 | Y coordinate placed on every path waypoint |
**Grid bounds** are computed from the union of all navigation area polygons plus a padding margin of `cellSize * 2 + agentRadius + objectPadding` on every side.
**Cell coordinate conversion:**
```
cell.x = floor((worldX - minX) / cellSize)
cell.z = floor((worldZ - minZ) / cellSize)
cellCenter.x = minX + (cell.x + 0.5) * cellSize
cellCenter.z = minZ + (cell.z + 0.5) * cellSize
```
---
## 2. Building the Walkable Grid
The grid can be loaded in two ways.
### 2a. Pre-computed grid (`.txt` file)
A plain-text file with a small header followed by rows of `1`/`0` characters:
```
cellSize 0.4
agentRadius 0.45
floorY 0.0
...
minX -5.0
minZ -5.0
gridWidth 50
gridDepth 50
11111111...
10000001...
```
This format is generated by `PathFinder::saveGrid()` after building from polygons and can be loaded much faster than recomputing from geometry.
### 2b. Polygon-based config (`.json` file)
The JSON file lists **navigation areas** (convex or concave walkable regions) and **obstacle polygons** (impassable zones within those regions):
```json
{
"cellSize": 0.4,
"areas": [
{ "name": "main_room", "available": true, "polygon": [[x,z], ...] }
],
"obstacles": [
{ "name": "table", "polygon": [[x,z], ...] }
]
}
```
**Build steps:**
1. **Mark available areas walkable** — every cell whose center lies inside any `available` navigation area polygon gets `walkable = 1`. If `boundaryPadding > 0`, cells too close to the outer edge of the area are left blocked.
2. **Mark obstacle polygons blocked** — cells whose center lies inside an obstacle polygon, or within `agentRadius + objectPadding` of its edges, are set to `0`.
Navigation areas can be toggled at runtime via `PathFinder::setAreaAvailable()`, which rebuilds the entire grid. This is used to open or close doors, gated areas, etc.
---
## 3. A\* Path Search
`PathFinder::findPath(start, end)` runs a standard A\* on the walkable grid.
**Neighbor connectivity:** 8-directional (cardinal + diagonal). Diagonal moves are blocked if either of the two adjacent cardinal cells is unwalkable (no corner-cutting).
**Step costs:** `1.0` for cardinal, `√2 ≈ 1.414` for diagonal.
**Heuristic:** Euclidean distance in cell units to the end cell.
**Start/end snapping:** If the exact cell for `start` or `end` is not walkable, `findNearestWalkableCell` expands a square ring outward (up to radius 8 m) to find the nearest walkable cell. This makes clicking slightly outside the nav mesh still produce a valid path.
**Path reconstruction:** After A\* completes, the cell chain is walked via `cameFrom[]` from `end` back to `start`, reversed, then smoothed (see §4).
**First-waypoint trimming:** If the first waypoint is within `cellSize × 0.75` of `start`, it is dropped (the character is already close enough).
**Last-waypoint precision:** If the requested `end` maps to the same cell as the snapped end cell, the last waypoint is replaced with the exact `end` world position rather than the cell centre.
---
## 4. Path Smoothing
Raw A\* paths follow the grid diagonals and produce staircase-shaped routes. A **string-pulling** (line-of-sight) pass compresses them:
```
anchor = path[0]
result = [anchor]
while anchor is not the last cell:
find the furthest cell 'next' from anchor with unobstructed line of sight
result.append(next)
anchor = next
```
Line-of-sight is checked by stepping along the segment in increments of `cellSize / 2` and verifying that each sampled cell is walkable. The result is a minimal set of waypoints connected by straight, obstacle-free segments.
---
## 5. Approaching Unreachable Destinations
When a player clicks on a point in a disconnected region (e.g., across a thin wall), the original `findPath` returns an empty path and the character does not move. This is surprising — a click on a solid wall sensibly moves the character to the nearest reachable point, but a click into an inaccessible room does nothing.
**`findPathToNearest`** fixes this with a three-step cascade:
1. Try `findPath` with dynamic obstacles (stationary characters are avoided).
2. If empty, retry `findPath` without dynamic obstacles (an NPC blocking a doorway is ignored).
3. If still empty (destination genuinely unreachable), run **nearest-reachable A\***.
**Nearest-reachable A\***, implemented in `findNearestReachableImpl`:
- Runs the identical A\* loop against the static walkable grid.
- While processing cells, tracks `bestIndex` — the already-visited cell with the smallest Euclidean distance (in cell units) to the end cell.
- If A\* exhausts all reachable space without finding `end`, it reconstructs and returns a path to `bestIndex`.
- If `bestIndex` is still the start cell (character is completely isolated), an empty path is returned and the character stays put.
The net effect: clicking anywhere in the world always moves the character as close as possible to the target, matching the behaviour of clicking on a solid wall.
`findPathToNearest` replaces the direct `findPath` call in `Location::setupNavigation`'s path planner lambda, so it applies equally to the player and all NPCs.
---
## 6. Dynamic Obstacles
When a path is planned, other characters can temporarily mark cells as blocked to make the character walk around them rather than through them.
**How it works:**
In `Location::setupNavigation`, every character is given a `PathPlanner` closure. Before calling `findPath`, the closure builds a list of `PathFinder::DynamicObstacle` entries (position + radius) representing nearby characters. `findPath` copies the static walkable grid, stamps zeros in circles around each obstacle, then runs A\* on the modified copy. The static grid is never mutated.
**Which characters become obstacles:**
A character is added as a dynamic obstacle only when **all** of these are true:
- It is not the character currently planning the path (`self`).
- It is alive and enabled.
- **It is not moving** — a moving character is transparent to pathfinding, so it does not block narrow corridors that it is actively passing through.
- Its position lies within `kDynamicObstacleInfluenceDist = 6 m` of the direct line segment from `start` to `end` (distant characters do not affect the search).
**Obstacle radius:** `character.collisionRadius × 0.6`. Using 60 % of the physical collision radius makes path planning less conservative; physical separation at full radius is still enforced by collision resolution (§8).
**Fallback when dynamic obstacles block the only path:**
If step 1 of `findPathToNearest` (with dynamic obstacles) returns empty, step 2 retries without any dynamic obstacles. This handles the common case of an NPC standing in a doorway: the player paths through the NPC's position, and the nudge logic (§8) pushes the NPC aside as the player passes.
---
## 7. Path Following
`Character::setTarget(destination, onArrived)` sets a new walk target. It calls the path planner to generate a waypoint list. The result is stored in `pathWaypoints`; the final destination is also stored in `walkTarget` and `requestedWalkTarget`.
Each frame in `Character::update`:
1. **Active target** — if `pathWaypoints` is non-empty, the character moves toward `pathWaypoints[currentWaypointIndex]`; otherwise it moves toward `walkTarget`.
2. **Movement** — the character advances along the XZ direction at `walkSpeed` m/s and rotates smoothly toward the movement direction at `rotationSpeed` rad/s.
3. **Waypoint advance** — when the character is within `WALK_THRESHOLD = 0.05 m` of the current waypoint, it advances to the next one. When the last waypoint is reached, `pathWaypoints` is cleared and the optional `onArrived` callback is fired.
4. **State machine** — the animation state switches between `STAND` and `WALK` based on whether the character is moving.
`Character::isMoving()` returns `true` if `pathWaypoints` is non-empty or the distance to `walkTarget` exceeds `WALK_THRESHOLD`. This is used by dynamic obstacle filtering and collision nudging.
**Stopping in place:** `Character::stopInPlace()` sets `walkTarget` and `requestedWalkTarget` to the current position and clears `pathWaypoints`. It is called when an external force (collision resolution) displaces a stationary player so that the player does not walk back to their previous target position.
---
## 8. Character Collision Resolution
Pathfinding alone does not prevent two characters from occupying the same space — it only steers paths around stationary characters. Physical separation is handled separately each frame by `Location::resolveCharacterCollisions`.
**Algorithm** (3 iterations per frame):
For every pair `(A, B)` of living, enabled characters:
1. Compute the overlap: `penetration = (collisionRadius_A + collisionRadius_B) - distance(A, B)`.
2. If `penetration > 0`, compute a push direction (A-to-B normal) and a push magnitude of `penetration / 2` per character.
3. Compute candidate new positions `newA` and `newB`.
4. Validate against the navigation grid (`PathFinder::isWalkable`). If a pushed position is unwalkable, only the other character is moved.
5. **Player stays put:** if the player was not moving (`!isMoving()`) before the push, `stopInPlace()` is called after the push so the player does not walk back to the old target.
6. **NPC yielding:** if one character was moving and the other was standing, `nudgeCharacterAside` is called on the standing character.
**`nudgeCharacterAside(standing, awayFrom)`:**
Gives the standing NPC a short walk target so it steps out of the way:
1. Compute the direction from `awayFrom` to the NPC's current position.
2. Try four candidate targets at distance `1.2 m` in directions: straight away, +90°, −90°, 180°.
3. Use the first candidate that is walkable (per `PathFinder::isWalkable`).
4. Call `standing->setTarget(candidate)` — the NPC takes a small step aside, then stands at the new spot.
5. The player is never nudged; combat NPCs can be nudged, but their attack AI immediately overrides the yield target on the next tick.
---
## 9. Dynamic Replanning
When characters move they can displace each other or enter each other's planned paths. `Location::updateDynamicReplans` handles this:
**Every frame:**
1. Measure how much each character moved since the last frame. Characters that moved more than `kMovedEps = 0.05 m` are collected as **movers**.
2. For each mover, find other characters that are currently walking. If the mover's position is within `kReplanTriggerDist = 1.8 m` of the segment `[walker.position → walker.nextWaypoint]`, trigger a replan for the walker via `forceReplan()`.
3. A per-character cooldown of `kReplanCooldownMs = 500 ms` prevents the same character from replanning more often than twice per second.
**`Character::forceReplan()`** re-runs the path planner from the character's current position to its stored `requestedWalkTarget`, updating `pathWaypoints` in place. If the replanned path is empty, the character stops at its current position.
The relatively generous trigger distance (1.8 m vs the old 1.1 m) and cooldown (500 ms vs 300 ms) prevent micro-jitter: small position corrections from collision resolution no longer spam replanning events.
---
## 10. Key Constants Reference
| Constant | Location | Value | Description |
|---|---|---|---|
| `cellSize` | `PathFinder` config | 0.4 m | Grid cell size |
| `agentRadius` | `PathFinder` config | 0.45 m | Character half-width for grid erosion |
| `objectPadding` | `PathFinder` config | 0.25 m | Extra clearance around obstacles |
| `WALK_THRESHOLD` | `Character.h` | 0.05 m | Distance below which a waypoint is considered reached |
| `TARGET_REPLAN_THRESHOLD` | `Character.h` | 0.25 m | Deduplication threshold in `setTarget` |
| `kDynamicObstacleInfluenceDist` | `Location.cpp` | 6.0 m | Max distance from path for a character to become an obstacle |
| `kDynamicObstacleRadiusFraction` | `Location.cpp` | 0.6× | Fraction of collision radius used for dynamic obstacle footprint |
| `kNudgeDist` | `Location.cpp` | 1.2 m | Distance an NPC steps aside when yielding |
| `kReplanTriggerDist` | `Location.cpp` | 1.8 m | Mover must be this close to a walker's path to trigger replan |
| `kReplanCooldownMs` | `Location.cpp` | 500 ms | Minimum interval between replans for any one character |
| `NPC_TALK_DISTANCE` | `Location.cpp` | 1.35 m | Distance at which walking-to-NPC interaction fires |
| `kIterations` (collision) | `Location.cpp` | 3 | Push-apart iterations per frame |
+187
View File
@@ -0,0 +1,187 @@
# Windows
download from https://cmake.org/download/
Windows x64 Installer: cmake-4.2.0-windows-x86_64.msi
download from https://github.com/libsdl-org/SDL/releases/tag/release-2.32.10
SDL2-2.32.10-win32-x64.zip
SDL2-2.32.10:
```
cd C:\..\SDL-realese-2.32.10
mkdir build
cd build
cmake -G "Visual Studio 18 2026" -DCMAKE_INSTALL_PREFIX=install ..
cmake --build . --config Debug
cmake --install . --config Debug
```
download from https://www.zlib.net/
zlib source code, version 1.3.1, zipfile format (1616K, SHA-256 hash 72af66d44fcc14c22013b46b814d5d2514673dda3d115e64b690c1ad636e7b17):
US (zlib.net)
zlib-1.3.1:
```
cd C:\..\zlib-1.3.1
mkdir build
cd build
cmake -G "Visual Studio 18 2026" -DCMAKE_INSTALL_PREFIX=install ..
cmake --build . --config Debug
cmake --install . --config Debug
```
download from https://github.com/pnggroup/libpng/releases/tag/v1.6.51
Source code (zip)
libpng-1.6.51:
```
cd C:\..\libpng-1.6.51
mkdir build
cd build
```
To build libpng, you need to specify the path to the zlib installation directory as follows:
```
cmake -DCMAKE_PREFIX_PATH="../zlib-1.3.1/build/install" -DCMAKE_INSTALL_PREFIX=install -G "Visual Studio 18 2026" ..
cmake --build . --config Debug
cmake --install . --config Debug
```
Настройка проекта в Visual Studio:
Перейдите в Project Properties (правый клик на проект, "Properties").
C/C++ - ОБЩИЕ; Дополнительные каталоги включаемых файлов, проверить чтобы был добавлен путь к папке include:(пример)
..\SDL-release-2.32.10\include;..\libpng-1.6.51\build\install\include;C:\Work\OpenAL 1.1 SDK\include;..\Projects\libogg\include;..\vorbis\include
Компоновщик - ОБЩИЕ; Доподнительные каталоги библиотек (пример)
..\SDL-release-2.32.10\build\install\lib;..\libpng-1.6.51\build\install\lib;..\zlib-1.3.1\build\install\lib
Компоновщик - ВВОД; Дополнительные зависимости, добавить zlibstaticd.lib (пример)
zlibstaticd.lib;libpng16_staticd.lib;SDL2d.lib;SDL2maind.lib;opengl32.lib;glu32.lib;shell32.lib;opengl32.lib;glu32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib
в папку ..\projectGAME01\x64\Debug добавить файл SDL2d.dll
который можно скопировать из папки ..\SDL-release-2.32.10\build\Debug
# Script to run:
```
C:\Work\Projects\emsdk\emsdk.bat activate latest
C:\Work\Projects\emsdk\emsdk_env.bat
emcc main.cpp Game.cpp Math.cpp Physics.cpp Renderer.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -sTOTAL_MEMORY=33554432 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS="[""png""]" -sUSE_SDL=2 --preload-file background.bmp --preload-file bird.bmp32 --preload-file default.fragment --preload-file default.vertex --preload-file game_over.bmp32 --preload-file pipe.bmp32 -o space-game001.html
```
```
zlib-1.3.1:
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=install ..
then run ALL_BUILD and INSTALL in Visual Studio
lpng1645:
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=install -DZLIB_ROOT=C:\Work\Projects\zlib-1.3.1\build\install ..
then run ALL_BUILD and INSTALL in Visual Studio
```
https://github.com/Bly7/OBJ-Loader/blob/master/Source/OBJ_Loader.h
https://github.com/gametutorials/tutorials/blob/master/OpenGL/MD3%20Animation/Main.cpp
linux:
```
g++ Game.cpp main.cpp Math.cpp OpenGlExtensions.cpp Physics.cpp Renderer.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp BoneAnimatedModel.cpp ObjLoader.cpp cmakeaudioplayer/src/AudioPlayer.cpp TextModel.cpp Inventory.cpp -o sdl_app -O2 -std=c++17 \
-I cmakeaudioplayer/include \
$(pkg-config --cflags --libs sdl2 gl) \
$(pkg-config --cflags --libs vorbis vorbisfile ogg) \
-lopenal
```
Linux new:
sudo apt-get update
sudo apt-get install build-essential cmake pkg-config \
libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev \
libgl1-mesa-dev libpng-dev libz-dev libzip-dev \
libboost-dev libeigen3-dev liblua5.4-dev
sudo apt-get install libglu1-mesa-dev
# Emscripten new
```
cd build-emcmake/
emcmake cmake -DCMAKE_INSTALL_PREFIX=install ..
cmake --build .
cmake --install .
emcc main.cpp Game.cpp Environment.cpp GameObjectManager.cpp BoneAnimatedModel.cpp GameWorld.cpp InputManager.cpp Inventory.cpp ObjLoader.cpp QuestScripts.cpp RenderSystem.cpp Math.cpp Physics.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -IC:\Users\ASUS\Desktop\fishrungame2\ZeptoLabTest1\thirdparty\libzip-1.11.3\build-emcmake\install\include -LC:\Users\ASUS\Desktop\fishrungame2\ZeptoLabTest1\thirdparty\libzip-1.11.3\build-emcmake\install\lib -lzip -sTOTAL_MEMORY=33554432 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS="[""png""]" -sUSE_SDL=2 --preload-file background.bmp --preload-file bird.bmp32 --preload-file default.fragment --preload-file default.vertex --preload-file game_over.bmp32 --preload-file pipe.bmp32 -o jumpingbird.html
emcc main.cpp Game.cpp Environment.cpp GameObjectManager.cpp BoneAnimatedModel.cpp GameWorld.cpp InputManager.cpp Inventory.cpp ObjLoader.cpp QuestScripts.cpp RenderSystem.cpp Math.cpp Physics.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -I./thirdparty/zlib-1.3.1/install/include -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -L./thirdparty/zlib-1.3.1/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS='["png"]' -sUSE_SDL=2 --preload-file data.zip -o jumpingbird.html
# Windows:
emcc --clear-cache
embuilder build sdl2 sdl2_ttf sdl2_image sdl2_image_jpg sdl2_image_png
emcc main.cpp Game.cpp Environment.cpp BoneAnimatedModel.cpp ZLMath.cpp Renderer.cpp TextModel.cpp ShaderManager.cpp TextureManager.cpp Utils.cpp OpenGlExtensions.cpp -O2 -std=c++14 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sUSE_SDL=2 -sUSE_LIBPNG=1 --preload-file space-game001.zip -o space-game001.html
emrun --no_browser --port 8080 .
```
# Emscripten new
```
emcc src/main.cpp src/Game.cpp src/Environment.cpp src/BoneAnimatedModel.cpp src/TextModel.cpp src/Projectile.cpp src/SparkEmitter.cpp src/UiManager.cpp src/render/Renderer.cpp src/render/ShaderManager.cpp src/render/TextureManager.cpp src/render/FrameBuffer.cpp src/render/OpenGlExtensions.cpp src/utils/Utils.cpp src/utils/TaskManager.cpp src/utils/Perlin.cpp src/planet/PlanetData.cpp src/planet/PlanetObject.cpp src/planet/StoneObject.cpp -O2 -std=c++17 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -fexceptions -I./thirdparty1/eigen-5.0.0 -I./src -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -IC:/Boost/include/boost-1_84 -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sUSE_SDL=2 -sUSE_LIBPNG=1 --preload-file space-game001.zip -o space-game001.html
```
# License
Code: MIT
Art: CC-BY
# Cmake Run Linux
Run using cmakelist
make -j$(nproc) -C build #Компилируем
./build/sdl_app #Запускаем
Для постройки без звука
rm -rf build #Очищаем build папку
cmake -B build -DAUDIO=1 #Пересоздаём конфигурацию CMake
+527
View File
@@ -0,0 +1,527 @@
# UI System
UI layouts are defined in JSON files and loaded at runtime by `UiManager`. Each file has a single `"root"` node that is the top-level container.
The coordinate system has the origin at the **bottom-left** of the screen. Y increases upward.
The virtual canvas size is defined by `Environment::projectionWidth` × `Environment::projectionHeight`.
```json
{
"root": { ... }
}
```
---
## Common Node Properties
These properties are available on every node type.
| Property | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | Unique name used to find the node from C++ code |
| `x` | float | `0` | Horizontal offset from the parent's origin (or gravity-adjusted position) |
| `y` | float | `0` | Vertical offset |
| `width` | float \| `"match_parent"` | `0` | Width in virtual pixels. `"match_parent"` fills the parent |
| `height` | float \| `"match_parent"` | `0` | Height in virtual pixels |
| `horizontal_gravity` | `"left"` \| `"center"` \| `"right"` | `"left"` | Positions the node horizontally inside a **FrameLayout** parent |
| `vertical_gravity` | `"bottom"` \| `"center"` \| `"top"` | `"bottom"` | Positions the node vertically inside a **FrameLayout** parent |
| `visible` | bool | `true` | Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via `setNodeVisible` |
---
## Containers
### FrameLayout
Children are positioned using absolute `x`/`y` offsets and/or `horizontal_gravity` / `vertical_gravity`.
```json
{
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `children` | array | `[]` | Child nodes |
---
### LinearLayout
Children are stacked automatically in a row or column. Gravity and align properties control the layout of the block and its children.
```json
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"width": 400,
"height": 600,
"children": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Direction children are stacked |
| `spacing` | float | `0` | Gap in pixels between consecutive children |
| `vertical_align` | `"top"` \| `"center"` \| `"bottom"` | `"top"` | **Vertical** alignment of the child block inside this layout. For vertical orientation, controls how the whole stack is aligned; for horizontal orientation, controls each child's cross-axis alignment |
| `horizontal_align` | `"left"` \| `"center"` \| `"right"` | `"left"` | **Horizontal** alignment of the child block. For horizontal orientation, controls how the whole row is aligned; for vertical orientation, controls each child's cross-axis alignment |
| `children` | array | `[]` | Child nodes, laid out in order |
---
## Widgets
### Button
An image-only clickable button. Swaps textures on hover/press.
```json
{
"type": "Button",
"name": "closeButton",
"width": 90,
"height": 90,
"x": 580,
"y": 240,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"textures": {
"normal": "resources/w/ui/img/Close001_State=Default.png",
"hover": "resources/w/ui/img/Close001_State=Selected.png",
"pressed": "resources/w/ui/img/Close001_State=Tap.png",
"disabled": "resources/w/ui/img/Close001_State=Disabled.png"
},
"border": 4,
"clickZoneWidth": 80,
"clickZoneHeight": 80
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `textures.normal` | string | — | Texture path shown in the default state (**required**) |
| `textures.hover` | string | — | Texture path shown when the mouse hovers |
| `textures.pressed` | string | — | Texture path shown while pressed |
| `textures.disabled` | string | — | Texture path shown when the button is disabled |
| `border` | float | `0` | Inset (pixels) applied to the hit-test zone on all sides |
| `clickZoneWidth` | float | `0` | Explicit hit-test width; `0` uses the widget width |
| `clickZoneHeight` | float | `0` | Explicit hit-test height; `0` uses the widget height |
**C++ callbacks:**
```cpp
uiManager.setButtonCallback("closeButton", [](const std::string&) { /* click */ });
uiManager.setButtonPressCallback("closeButton", [](const std::string&) { /* press */ });
```
---
### TextButton
A button that renders a text label on top of an optional background texture.
```json
{
"type": "TextButton",
"name": "item1name",
"width": 270,
"height": 60,
"text": "Main Quest",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": false,
"textPaddingX": 12,
"textPaddingY": -8,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
},
"border": 0,
"clickZoneWidth": 0,
"clickZoneHeight": 0
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `text` | string | `""` | Label text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `textCentered` | bool | `true` | Horizontally centers the text within the widget. When `false`, text starts at `textPaddingX` from the left edge |
| `topAligned` | bool | `false` | When `true`, the first text line is placed near the top of the widget; when `false`, the text is vertically centered |
| `textPaddingX` | float | `12` | Left padding when `textCentered` is `false`; also used to compute the wrapping width |
| `textPaddingY` | float | `0` | Vertical offset applied to the text baseline |
| `wrap` | bool | `false` | Wraps text that exceeds `width - textPaddingX * 2` pixels |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color, each channel 0..1 |
| `textures.*` | string | — | Background textures (all optional — button can be text-only) |
| `border` | float | `0` | Hit-test inset |
| `clickZoneWidth` / `clickZoneHeight` | float | `0` | Explicit hit-test size; `0` uses the widget size |
**C++ callbacks:**
```cpp
uiManager.setTextButtonCallback("item1name", [](const std::string&) { /* click */ });
uiManager.setTextButtonPressCallback("item1name", [](const std::string&) { /* press */ });
// Programmatic updates
uiManager.setTextButtonText("item1name", "New Quest Name");
uiManager.setTextButtonColor("item1name", {1.f, 0.f, 0.f, 1.f});
```
---
### TextView
A non-interactive text display widget.
```json
{
"type": "TextView",
"name": "quest_description",
"x": 170,
"y": 390,
"width": 1000,
"height": 300,
"text": "Long description here.",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"paddingX": 0,
"paddingY": 4,
"maxLines": 10,
"color": [1.0, 1.0, 0.0, 1.0]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `text` | string | `""` | Display text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `textCentered` | bool | `true` | Horizontally centers the text when `true`; left-aligns from `paddingX` when `false` |
| `topAligned` | bool | `false` | When `true`, the first line is placed near the top edge; when `false`, text is vertically centered |
| `wrap` | bool | `false` | Wraps text at `width - paddingX * 2` pixels |
| `paddingX` | float | `0` | Left/right padding used for alignment and wrap width |
| `paddingY` | float | `0` | Vertical inset applied when `topAligned` is `true` |
| `maxLines` | int | `0` | Maximum number of lines to display; `0` means unlimited. Truncated text gets `...` |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color |
> **Legacy note:** If none of `wrap`, `topAligned`, `paddingX`, `paddingY`, or `maxLines` are set, the text is drawn centered on `(x + width/2, y + height/2)` for backward compatibility.
**C++ updates:**
```cpp
uiManager.setText("quest_description", "New text here.");
uiManager.setTextColor("quest_description", {1.f, 1.f, 0.f, 1.f});
```
---
### TextField
An interactive single-line text input field. Receives keyboard input when focused.
```json
{
"type": "TextField",
"name": "playerName",
"x": 100,
"y": 300,
"width": 400,
"height": 50,
"placeholder": "Enter name...",
"fontSize": 28,
"fontPath": "resources/fonts/DroidSans.ttf",
"maxLength": 64,
"color": [1.0, 1.0, 1.0, 1.0],
"placeholderColor": [0.5, 0.5, 0.5, 1.0],
"backgroundColor": [0.2, 0.2, 0.2, 1.0],
"borderColor": [0.5, 0.5, 0.5, 1.0]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `placeholder` | string | `""` | Text shown when the field is empty |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `maxLength` | int | `256` | Maximum number of characters |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Input text color |
| `placeholderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Placeholder text color |
| `backgroundColor` | [R, G, B, A] | `[0.2,0.2,0.2,1]` | Field background color |
| `borderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Border color |
**C++ callbacks and queries:**
```cpp
uiManager.setTextFieldCallback("playerName", [](const std::string& name, const std::string& value) {
// called on every keystroke
});
std::string current = uiManager.getTextFieldValue("playerName");
```
---
### Slider
A draggable slider that returns a normalized value in the range `[0, 1]`.
```json
{
"type": "Slider",
"name": "volumeSlider",
"x": 100,
"y": 200,
"width": 40,
"height": 300,
"orientation": "vertical",
"value": 0.75,
"textures": {
"track": "resources/w/ui/img/slider_track.png",
"knob": "resources/w/ui/img/slider_knob.png"
}
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `textures.track` | string | — | Texture for the slider track |
| `textures.knob` | string | — | Texture for the draggable knob |
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Drag direction |
| `value` | float | `0` | Initial normalized value `[0, 1]` |
**C++ callbacks:**
```cpp
uiManager.setSliderCallback("volumeSlider", [](const std::string& name, float value) {
// value is 0..1
});
uiManager.setSliderValue("volumeSlider", 0.5f);
```
---
### StaticImage
A non-interactive image. Supports optional fade-in and pulse-scale animations.
```json
{
"type": "StaticImage",
"name": "background",
"width": 1266,
"height": 585,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/journal/QuestJournal003.png",
"fadeIn": {
"durationMs": 600
},
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
}
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `texture` | string | — | Path to the PNG texture |
| `fadeIn.durationMs` | float | — | If present, the image fades in over this many milliseconds each time the UI is shown |
| `pulse.minScale` | float | `0.9` | Minimum scale during the pulse cycle |
| `pulse.maxScale` | float | `1.1` | Maximum scale during the pulse cycle |
| `pulse.periodMs` | float | `1000` | Duration of one full pulse cycle in milliseconds |
**C++ pop-in animation** (scales the node from 0 → 1, ease-out quad):
```cpp
uiManager.startPopIn("background", 300.0f); // duration in milliseconds
```
Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.
---
## Touch / Click Priority
All `Button` and `TextButton` nodes in a layout are collected into a single ordered list during `collectButtonsAndSliders` (depth-first traversal of the node tree, which matches JSON declaration order). When a touch or mouse-down event arrives, the list is scanned **in reverse** — later-declared nodes are checked first — and the **first hit wins**. At most one element (button or textButton, regardless of type) fires per touch.
**Practical rule:** place background "catch-all" elements (e.g. a full-screen transparent exit button) **early** in the JSON, and foreground interactive elements **later**. The later-declared element will always win when they overlap.
```json
{
"type": "FrameLayout",
"children": [
{
"type": "Button",
"name": "phoneExitButton", // declared first → lowest priority
"width": "match_parent",
"height": "match_parent",
"textures": { "normal": "resources/transparent.png", ... }
},
{
"type": "TextButton",
"name": "chat2button", // declared later → wins over phoneExitButton
...
}
]
}
```
This behaviour is consistent across `Button` and `TextButton` — there is no inherent type priority, only declaration order matters.
---
## Animations
Animations can be defined on **Button** and **TextButton** nodes and started from C++ code. Each animation is a named sequence of steps.
```json
{
"type": "Button",
"name": "myButton",
"width": 100,
"height": 100,
"textures": { "normal": "resources/w/btn.png" },
"animations": {
"bounce": {
"repeat": false,
"steps": [
{ "type": "move", "to": [0, 20], "duration": 0.15, "easing": "easeout" },
{ "type": "move", "to": [0, 0], "duration": 0.15, "easing": "easein" },
{ "type": "wait", "duration": 0.1 }
]
},
"pulse": {
"repeat": true,
"steps": [
{ "type": "scale", "to": [1.1, 1.1], "duration": 0.4, "easing": "easeout" },
{ "type": "scale", "to": [1.0, 1.0], "duration": 0.4, "easing": "easein" }
]
}
}
}
```
### Animation sequence properties
| Property | Type | Default | Description |
|---|---|---|---|
| `repeat` | bool | `false` | Whether the sequence loops after the last step |
| `steps` | array | — | Ordered list of animation steps |
### Step properties
| Property | Type | Description |
|---|---|---|
| `type` | `"move"` \| `"scale"` \| `"wait"` | Step kind |
| `to` | [x, y] | Target offset (`move`) or scale factors (`scale`) |
| `duration` | float (seconds) | Duration of the step. `0` applies the target instantly |
| `easing` | `"linear"` \| `"easein"` \| `"easeout"` | Interpolation curve (default `"linear"`) |
**C++ control:**
```cpp
uiManager.startAnimationOnNode("myButton", "bounce");
uiManager.stopAnimationOnNode("myButton", "bounce");
uiManager.setAnimationCallback("myButton", "bounce", []() {
// called when the non-repeating sequence finishes
});
```
---
## C++ API Quick Reference
### Loading and navigation
```cpp
uiManager.loadFromFile("resources/w/ui/screen.json", renderer);
uiManager.pushMenuFromFile("resources/w/ui/popup.json", renderer); // push on stack
uiManager.popMenu(); // restore previous UI
uiManager.clearMenuStack();
```
### Finding nodes
```cpp
auto node = uiManager.findNode("myNode");
auto btn = uiManager.findButton("myButton");
auto tbtn = uiManager.findTextButton("item1name");
auto tv = uiManager.findTextView("quest_description");
auto img = uiManager.findStaticImage("background");
auto slider = uiManager.findSlider("volumeSlider");
auto tf = uiManager.findTextField("playerName");
```
### Visibility
```cpp
uiManager.setNodeVisible("hint5", false);
bool visible = uiManager.getNodeVisible("hint5");
```
### Pop-in animation
Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.
```cpp
uiManager.startPopIn("messageBubble", 300.0f); // node name, duration ms
```
Set the node's `scaleX`/`scaleY` to `0` and call `setNodeVisible` before calling `startPopIn` to avoid a one-frame flash at full size.
### Dynamic node repositioning
`node->localY` (and `localX`) can be modified directly on a node pointer, then a layout recalculation applied:
```cpp
auto node = uiManager.findNode("messageBubble");
node->localY = 350.0f; // new bottom-Y (for vertical_gravity: bottom nodes)
uiManager.updateAllLayouts(); // recomputes screenRect and rebuilds meshes
```
This is how the phone chat manager repositions bubbles as new messages arrive.
### Per-frame update
```cpp
uiManager.update(deltaMs); // advance animations and fade-ins
uiManager.draw(renderer); // render everything
```
---
## Dialogue → UI integration (phone chat bubbles)
Dialogue nodes in JSON can carry a `"bubbleSlot"` field naming a `StaticImage` UI node. When the dialogue runtime presents that line, it fires the `onBubbleSlotReady` callback with the slot name, which the game uses to reveal the corresponding bubble image.
```json
{
"id": "line_1",
"type": "Line",
"speaker": "Айпери",
"text": "...",
"next": "line_2",
"bubbleSlot": "message01in"
}
```
Lines without `"bubbleSlot"` (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.
**C++ wiring:**
```cpp
dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
// slotName == "message01in" etc.
menuManager.revealPhoneChatBubble(slotName);
});
```
-41
View File
@@ -1,41 +0,0 @@
cmake_minimum_required(VERSION 3.6)
project(GAME)
if(POLICY CMP0079)
cmake_policy(SET CMP0079 NEW)
endif()
# Копируем pnglibconf.h.prebuilt
execute_process(
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/libpng/scripts/pnglibconf.h.prebuilt
${CMAKE_CURRENT_SOURCE_DIR}/libpng/pnglibconf.h
)
# Сначала zlib
add_subdirectory(zlib)
# ВАЖНО: Установите опции ДО add_subdirectory(libpng)
# Libpng создает ДВЕ цели: png_shared (shared) и png_static (static)
# Мы хотим статическую библиотеку для Android
set(PNG_STATIC ON CACHE BOOL "Build static library" FORCE)
set(PNG_SHARED OFF CACHE BOOL "Don't build shared library" FORCE)
set(PNG_TESTS OFF CACHE BOOL "Disable tests" FORCE)
set(PNG_TOOLS OFF CACHE BOOL "Disable tools" FORCE)
set(PNG_EXECUTABLES OFF CACHE BOOL "Disable executables" FORCE)
set(PNG_DEBUG OFF CACHE BOOL "Disable debug" FORCE)
set(SKIP_INSTALL_ALL ON CACHE BOOL "Skip installation" FORCE)
# Для Android отключаем оптимизации
set(PNG_HARDWARE_OPTIMIZATIONS OFF CACHE BOOL "Disable hardware optimizations" FORCE)
set(PNG_ARM_NEON "off" CACHE STRING "Disable ARM NEON" FORCE)
# Добавляем libpng
add_subdirectory(libpng)
# Затем SDL
add_subdirectory(SDL)
# И ваш код
add_subdirectory(src)
-719
View File
@@ -1,719 +0,0 @@
#include "BoneAnimatedModel.h"
#include <regex>
#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
namespace ZL
{
int getIndexByValue(const std::string& name, const std::vector<std::string>& words)
{
for (int i = 0; i < words.size(); i++)
{
if (words[i] == name)
{
return i;
}
}
return -1;
}
void BoneSystem::LoadFromFile(const std::string& fileName, const std::string& ZIPFileName)
{
std::ifstream filestream;
std::istringstream zipStream;
if (!ZIPFileName.empty())
{
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
}
else
{
filestream.open(fileName);
}
std::istream& f = (!ZIPFileName.empty()) ? static_cast<std::istream&>(zipStream) : static_cast<std::istream&>(filestream);
//Skip first 5 lines
std::string tempLine;
for (int i = 0; i < 5; i++)
{
std::getline(f, tempLine);
}
std::getline(f, tempLine);
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
static const std::regex pattern_int(R"([-]?\d+)");
static const std::regex pattern_boneChildren(R"(\'([^\']+)\')");
static const std::regex pattern_bone_weight(R"(\'([^\']+)\'.*?([-]?\d+\.\d+))");
std::smatch match;
int numberBones;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberBones = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<Bone> bones;
std::vector<std::string> boneNames;
std::vector<std::string> boneParentNames;
std::unordered_map<std::string, std::vector<std::string>> boneChildren;
bones.resize(numberBones);
boneNames.resize(numberBones);
boneParentNames.resize(numberBones);
for (int i = 0; i < numberBones; i++)
{
std::getline(f, tempLine);
std::string boneName = tempLine.substr(6);
boneNames[i] = boneName;
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
std::getline(f, tempLine); //skip tail
std::getline(f, tempLine); //len
if (std::regex_search(tempLine, match, pattern_float)) {
std::string len_str = match.str();
bones[i].boneLength = std::stof(len_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
//---------- matrix begin
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[0] = floatValues[0];
bones[i].boneMatrixWorld.m[0 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[0 + 2 * 3] = floatValues[2];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[1] = floatValues[0];
bones[i].boneMatrixWorld.m[1 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[1 + 2 * 3] = floatValues[2];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[2] = floatValues[0];
bones[i].boneMatrixWorld.m[2 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[2 + 2 * 3] = floatValues[2];
//----------- matrix end
std::getline(f, tempLine); //parent
if (tempLine == " Parent: None")
{
bones[i].parent = -1;
}
else
{
std::string boneParent = tempLine.substr(10);
boneParentNames[i] = boneParent;
}
std::getline(f, tempLine); //children
b = tempLine.cbegin();
e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_boneChildren)) {
boneChildren[boneName].push_back(match.str(1));
b = match.suffix().first;
}
}
//Now process all the bones:
for (int i = 0; i < numberBones; i++)
{
std::string boneName = boneNames[i];
std::string boneParent = boneParentNames[i];
if (boneParent == "")
{
bones[i].parent = -1;
}
else
{
bones[i].parent = getIndexByValue(boneParent, boneNames);
}
for (int j = 0; j < boneChildren[boneName].size(); j++)
{
bones[i].children.push_back(getIndexByValue(boneChildren[boneName][j], boneNames));
}
/*if (boneName == "Bone.020")
{
std::cout << i << std::endl;
}*/
}
startBones = bones;
currentBones = bones;
///std::cout << "Hello!" << std::endl;
std::getline(f, tempLine); //vertice count
int numberVertices;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberVertices = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<Vector3f> vertices;
vertices.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
vertices[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
}
//==== process uv and normals begin
std::cout << "Hello x1" << std::endl;
std::getline(f, tempLine); //===UV Coordinates:
std::getline(f, tempLine); //triangle count
int numberTriangles;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
// Now process UVs
std::vector<std::array<Vector2f, 3>> uvCoords;
uvCoords.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine); //Face 0
int uvCount;
std::getline(f, tempLine);
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
uvCount = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
if (uvCount != 3)
{
throw std::runtime_error("more than 3 uvs");
}
std::vector<float> floatValues;
for (int j = 0; j < 3; j++)
{
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
auto b = tempLine.cbegin();
auto e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
if (floatValues.size() != 2)
{
throw std::runtime_error("more than 2 uvs---");
}
uvCoords[i][j] = Vector2f{ floatValues[0],floatValues[1] };
}
}
std::cout << "Hello eee" << std::endl;
std::getline(f, tempLine); //===Normals:
std::vector<Vector3f> normals;
normals.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
normals[i] = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
}
//==== process uv and normals end
std::getline(f, tempLine); //triangle count.
//numberTriangles; //Need to check if new value is the same as was read before
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<std::array<int, 3>> triangles;
triangles.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine);
std::vector<int> intValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
intValues.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
triangles[i] = { intValues[0], intValues[1], intValues[2] };
}
std::getline(f, tempLine);//=== Vertex Weights ===
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> localVerticesBoneWeight;
localVerticesBoneWeight.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine); //skip Vertex 0:
std::getline(f, tempLine); //vertex group count
int boneCount;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
boneCount = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
if (boneCount > MAX_BONE_COUNT)
{
throw std::runtime_error("more than 5 bones");
}
float sumWeights = 0;
for (int j = 0; j < boneCount; j++)
{
std::getline(f, tempLine); //Group: 'Bone', Weight: 0.9929084181785583
if (std::regex_search(tempLine, match, pattern_bone_weight)) {
// ��������� ����� (��� �������)
std::string word = match.str(1);
double weight = std::stod(match.str(2));
int boneNumber = getIndexByValue(word, boneNames);
localVerticesBoneWeight[i][j].boneIndex = boneNumber;
localVerticesBoneWeight[i][j].weight = weight;
sumWeights += weight;
}
else {
throw std::runtime_error("No match found in the input string.");
}
}
//Normalize weights:
for (int j = 0; j < boneCount; j++)
{
localVerticesBoneWeight[i][j].weight = localVerticesBoneWeight[i][j].weight / sumWeights;
}
}
std::getline(f, tempLine);//=== Animation Keyframes ===
std::getline(f, tempLine);//=== Bone Transforms per Keyframe ===
std::getline(f, tempLine);
int numberKeyFrames;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberKeyFrames = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
animations.resize(1);
animations[0].keyFrames.resize(numberKeyFrames);
for (int i = 0; i < numberKeyFrames; i++)
{
std::getline(f, tempLine);
int numberFrame;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberFrame = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
animations[0].keyFrames[i].frame = numberFrame;
animations[0].keyFrames[i].bones.resize(numberBones);
for (int j = 0; j < numberBones; j++)
{
std::getline(f, tempLine);
std::string boneName = tempLine.substr(8);
int boneNumber = getIndexByValue(boneName, boneNames);
animations[0].keyFrames[i].bones[boneNumber] = startBones[boneNumber];
std::getline(f, tempLine); // Location: <Vector (0.0000, 0.0000, -0.0091)>
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
std::getline(f, tempLine); // Rotation
std::getline(f, tempLine); // Matrix
//=============== Matrix begin ==================
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 3 * 4] = floatValues[3];
//std::getline(f, tempLine);// ignore last matrix line
//=============== Matrix end ==================
}
}
// Now let's process bone weights and vertices
for (int i = 0; i < numberTriangles; i++)
{
mesh.PositionData.push_back(vertices[triangles[i][0]]);
mesh.PositionData.push_back(vertices[triangles[i][1]]);
mesh.PositionData.push_back(vertices[triangles[i][2]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][0]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][1]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][2]]);
mesh.TexCoordData.push_back(uvCoords[i][0]);
mesh.TexCoordData.push_back(uvCoords[i][1]);
mesh.TexCoordData.push_back(uvCoords[i][2]);
}
startMesh = mesh;
}
void BoneSystem::Interpolate(int frame)
{
int startingFrame = -1;
for (int i = 0; i < animations[0].keyFrames.size() - 1; i++)
{
int oldFrame = animations[0].keyFrames[i].frame;
int nextFrame = animations[0].keyFrames[i + 1].frame;
if (frame >= oldFrame && frame < nextFrame)
{
startingFrame = i;
break;
}
}
if (startingFrame == -1)
{
throw std::runtime_error("Exception here");
}
int modifiedFrameNumber = frame - animations[0].keyFrames[startingFrame].frame;
int diffFrames = animations[0].keyFrames[startingFrame + 1].frame - animations[0].keyFrames[startingFrame].frame;
float t = (modifiedFrameNumber + 0.f) / diffFrames;
std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingFrame].bones;
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingFrame+1].bones;
std::vector<Matrix4f> skinningMatrixForEachBone;
//std::vector<Matrix3f> skinningMatrixForEachBone;
skinningMatrixForEachBone.resize(currentBones.size());
for (int i = 0; i < currentBones.size(); i++)
{
currentBones[i].boneStartWorld.v[0] = oneFrameBones[i].boneStartWorld.v[0] + t * (nextFrameBones[i].boneStartWorld.v[0] - oneFrameBones[i].boneStartWorld.v[0]);
currentBones[i].boneStartWorld.v[1] = oneFrameBones[i].boneStartWorld.v[1] + t * (nextFrameBones[i].boneStartWorld.v[1] - oneFrameBones[i].boneStartWorld.v[1]);
currentBones[i].boneStartWorld.v[2] = oneFrameBones[i].boneStartWorld.v[2] + t * (nextFrameBones[i].boneStartWorld.v[2] - oneFrameBones[i].boneStartWorld.v[2]);
Matrix3f oneFrameBonesMatrix;
oneFrameBonesMatrix.m[0] = oneFrameBones[i].boneMatrixWorld.m[0];
oneFrameBonesMatrix.m[1] = oneFrameBones[i].boneMatrixWorld.m[1];
oneFrameBonesMatrix.m[2] = oneFrameBones[i].boneMatrixWorld.m[2];
oneFrameBonesMatrix.m[3] = oneFrameBones[i].boneMatrixWorld.m[0 + 1*4];
oneFrameBonesMatrix.m[4] = oneFrameBones[i].boneMatrixWorld.m[1 + 1*4];
oneFrameBonesMatrix.m[5] = oneFrameBones[i].boneMatrixWorld.m[2 + 1*4];
oneFrameBonesMatrix.m[6] = oneFrameBones[i].boneMatrixWorld.m[0 + 2*4];
oneFrameBonesMatrix.m[7] = oneFrameBones[i].boneMatrixWorld.m[1 + 2*4];
oneFrameBonesMatrix.m[8] = oneFrameBones[i].boneMatrixWorld.m[2 + 2*4];
Matrix3f nextFrameBonesMatrix;
nextFrameBonesMatrix.m[0] = nextFrameBones[i].boneMatrixWorld.m[0];
nextFrameBonesMatrix.m[1] = nextFrameBones[i].boneMatrixWorld.m[1];
nextFrameBonesMatrix.m[2] = nextFrameBones[i].boneMatrixWorld.m[2];
nextFrameBonesMatrix.m[3] = nextFrameBones[i].boneMatrixWorld.m[0 + 1 * 4];
nextFrameBonesMatrix.m[4] = nextFrameBones[i].boneMatrixWorld.m[1 + 1 * 4];
nextFrameBonesMatrix.m[5] = nextFrameBones[i].boneMatrixWorld.m[2 + 1 * 4];
nextFrameBonesMatrix.m[6] = nextFrameBones[i].boneMatrixWorld.m[0 + 2 * 4];
nextFrameBonesMatrix.m[7] = nextFrameBones[i].boneMatrixWorld.m[1 + 2 * 4];
nextFrameBonesMatrix.m[8] = nextFrameBones[i].boneMatrixWorld.m[2 + 2 * 4];
Vector4f q1 = MatrixToQuat(oneFrameBonesMatrix);
Vector4f q2 = MatrixToQuat(nextFrameBonesMatrix);
Vector4f q1_norm = q1.normalized();
Vector4f q2_norm = q2.normalized();
Vector4f result = slerp(q1_norm, q2_norm, t);
Matrix3f boneMatrixWorld3 = QuatToMatrix(result);
currentBones[i].boneMatrixWorld = MakeMatrix4x4(boneMatrixWorld3, currentBones[i].boneStartWorld);
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
}
/*
for (int i = 0; i < currentBones.size(); i++)
{
currentBones[i].boneStartWorld = oneFrameBones[i].boneStartWorld;
currentBones[i].boneMatrixWorld = oneFrameBones[i].boneMatrixWorld;
//Matrix4f currentBoneMatrixWorld4 = MakeMatrix4x4(currentBones[i].boneMatrixWorld, currentBones[i].boneStartWorld);
//Matrix4f startBoneMatrixWorld4 = MakeMatrix4x4(animations[0].keyFrames[0].bones[i].boneMatrixWorld, animations[0].keyFrames[0].bones[i].boneStartWorld);
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
if (i == 10)
{
std::cout << i << std::endl;
}
}*/
for (int i = 0; i < mesh.PositionData.size(); i++)
{
Vector4f originalPos = {
startMesh.PositionData[i].v[0],
startMesh.PositionData[i].v[1],
startMesh.PositionData[i].v[2], 1.0};
Vector4f finalPos = Vector4f{0.f, 0.f, 0.f, 0.f};
bool vMoved = false;
//Vector3f finalPos = Vector3f{ 0.f, 0.f, 0.f };
for (int j = 0; j < MAX_BONE_COUNT; j++)
{
if (verticesBoneWeight[i][j].weight != 0)
{
vMoved = true;
//finalPos = finalPos + MultVectorMatrix(originalPos, skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex]) * verticesBoneWeight[i][j].weight;
finalPos = finalPos + MultMatrixVector(skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex], originalPos) * verticesBoneWeight[i][j].weight;
}
}
if (abs(finalPos.v[0] - originalPos.v[0]) > 1 || abs(finalPos.v[1] - originalPos.v[1]) > 1 || abs(finalPos.v[2] - originalPos.v[2]) > 1)
{
}
if (!vMoved)
{
finalPos = originalPos;
}
mesh.PositionData[i].v[0] = finalPos.v[0];
mesh.PositionData[i].v[1] = finalPos.v[1];
mesh.PositionData[i].v[2] = finalPos.v[2];
}
}
}
-60
View File
@@ -1,60 +0,0 @@
#pragma once
#include "Math.h"
#include "Renderer.h"
#include <unordered_map>
namespace ZL
{
constexpr int MAX_BONE_COUNT = 6;
struct Bone
{
Vector3f boneStartWorld;
float boneLength;
Matrix4f boneMatrixWorld;
// boneVector = boneLength * (0, 1, 0) � ���� ��������
// Then multiply by boneMatrixWorld � �� �������� �������� �����
int parent;
std::vector<int> children;
};
struct BoneWeight
{
int boneIndex = -1;
float weight = 0;
};
struct AnimationKeyFrame
{
int frame;
std::vector<Bone> bones;
};
struct Animation
{
std::vector<AnimationKeyFrame> keyFrames;
};
struct BoneSystem
{
VertexDataStruct mesh;
VertexDataStruct startMesh;
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> verticesBoneWeight;
Matrix4f armatureMatrix;
std::vector<Bone> startBones;
std::vector<Bone> currentBones;
std::vector<Animation> animations;
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
void Interpolate(int frame);
};
};
-62
View File
@@ -1,62 +0,0 @@
cmake_minimum_required(VERSION 3.6)
project(MY_APP CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(main SHARED
SDL_android_main.c
main.cpp
Utils.h
Utils.cpp
TextureManager.h
TextureManager.cpp
TextModel.h
TextModel.cpp
ShaderManager.h
ShaderManager.cpp
Renderer.h
Renderer.cpp
Math.h
Math.cpp
Environment.h
Environment.cpp
BoneAnimatedModel.h
BoneAnimatedModel.cpp
AnimatedModel.h
Game.h
Game.cpp
)
# Подключаем заголовки
target_include_directories(main PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../SDL/include
${CMAKE_CURRENT_SOURCE_DIR}/../zlib
${CMAKE_CURRENT_SOURCE_DIR}/../libpng
)
# ВАЖНО: Линкуемся с png_static (статика) или png_shared (динамика)
# Так как мы установили PNG_STATIC=ON и PNG_SHARED=OFF,
# должна создаться цель png_static
target_link_libraries(main
png_static # ← ЭТО ПРАВИЛЬНОЕ ИМЯ ЦЕЛИ!
z
SDL2
)
if(ANDROID)
# OpenGL ES 2.0 или 3.0 для Android
find_library(OPENGLES2_LIB GLESv2)
find_library(OPENGLES1_LIB GLESv1_CM)
target_link_libraries(main
${OPENGLES2_LIB} # OpenGL ES 2.0/3.0
${OPENGLES1_LIB} # OpenGL ES 1.x (если нужно)
log
android
OpenSLES
dl
)
endif()
-35
View File
@@ -1,35 +0,0 @@
#include "Environment.h"
#include "Utils.h"
//#include <GL/gl.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
namespace ZL {
int Environment::windowHeaderHeight = 0;
int Environment::width = 0;
int Environment::height = 0;
float Environment::zoom = 20.f;
bool Environment::leftPressed = false;
bool Environment::rightPressed = false;
bool Environment::upPressed = false;
bool Environment::downPressed = false;
Vector3f Environment::cameraShift = {0, 0, 0};
Vector3f Environment::characterPos = {0, 0, 0};
float Environment::cameraPhi = 0.f;
float Environment::cameraAlpha = 0.3*M_PI / 2.0;
bool Environment::settings_inverseVertical = false;
SDL_Window* Environment::window = nullptr;
bool Environment::showMouse = false;
bool Environment::exitGameLoop = false;
} // namespace ZL
-40
View File
@@ -1,40 +0,0 @@
#pragma once
#include "Math.h"
#ifdef __linux__
#include <SDL2/SDL.h>
#endif
//#include "OpenGlExtensions.h"
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
namespace ZL {
class Environment {
public:
static int windowHeaderHeight;
static int width;
static int height;
static float zoom;
static bool leftPressed;
static bool rightPressed;
static bool upPressed;
static bool downPressed;
static Vector3f cameraShift;
static Vector3f characterPos;
static float cameraPhi;
static float cameraAlpha;
static bool settings_inverseVertical;
static SDL_Window* window;
static bool showMouse;
static bool exitGameLoop;
};
} // namespace ZL
-248
View File
@@ -1,248 +0,0 @@
#include "Game.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "Utils.h"
#include <iostream>
#include "TextureManager.h"
#include "TextModel.h"
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace ZL {
const char *CONST_ZIP_FILE = "";
Game::Game()
: window(nullptr), glContext(nullptr), newTickCount(0), lastTickCount(0),
resourcesLoaded(false), modelLoaded(false) {
}
Game::~Game() {
if (glContext) {
SDL_GL_DeleteContext(glContext);
}
if (window) {
SDL_DestroyWindow(window);
}
SDL_Quit();
}
void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window);
// Initialize renderer
renderer.InitOpenGL();
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "Game", "Start for Android");
const char* testFiles[] = {
"resources/spaceship004.txt",
"shaders/default.vertex",
"shaders/default_android.fragment",
"shaders/defaultColor.vertex",
"shaders/defaultColor_android.fragment",
"resources/sship001x.bmp",
nullptr
};
for (int i = 0; testFiles[i] != nullptr; i++) {
SDL_RWops* file = SDL_RWFromFile(testFiles[i], "rb");
if (file) {
Sint64 size = SDL_RWsize(file);
__android_log_print(ANDROID_LOG_INFO, "Game", "Found: %s (size: %lld)", testFiles[i], size);
SDL_RWclose(file);
} else {
__android_log_print(ANDROID_LOG_WARN, "Game", "Not found: %s (SDL error: %s)",
testFiles[i], SDL_GetError());
}
}
try {
__android_log_print(ANDROID_LOG_INFO, "Game", "Shaders...");
renderer.shaderManager.AddShaderFromFiles("default",
"shaders/default.vertex",
"shaders/default_android.fragment",
CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColor",
"shaders/defaultColor.vertex",
"shaders/defaultColor_android.fragment",
CONST_ZIP_FILE);
__android_log_print(ANDROID_LOG_INFO, "Game", "Textures...");
try {
spaceshipTexture = std::make_shared<Texture>(
CreateTextureDataFromBmp32("resources/sship001x.bmp")
);
} catch (const std::exception& e) {
spaceshipTexture = nullptr;
}
__android_log_print(ANDROID_LOG_INFO, "Game", "Model...");
std::string modelPaths[] = {
"resources/spaceship004.txt",
""
};
bool modelLoadSuccess = false;
for (int i = 0; !modelLoadSuccess && !modelPaths[i].empty(); i++) {
try {
spaceshipBase = LoadFromTextFile02(modelPaths[i]);
spaceshipBase.RotateByMatrix(QuatToMatrix(QuatFromRotateAroundY(M_PI / 2.0)));
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
modelLoaded = true;
modelLoadSuccess = true;
__android_log_print(ANDROID_LOG_INFO, "Game", "Model loaded successfully from: %s", modelPaths[i].c_str());
} catch (const std::exception& e) {
__android_log_print(ANDROID_LOG_WARN, "Game", "Failed to load model from %s: %s",
modelPaths[i].c_str(), e.what());
}
}
resourcesLoaded = (spaceshipTexture != nullptr && modelLoaded);
} catch (const std::exception& e) {
__android_log_print(ANDROID_LOG_ERROR, "Game", "Setup failed: %s", e.what());
resourcesLoaded = false;
}
#else
// Десктопная версия
renderer.shaderManager.AddShaderFromFiles("default",
"./shaders/default.vertex",
"./shaders/default_desktop.fragment",
CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColor",
"./shaders/defaultColor.vertex",
"./shaders/defaultColor_desktop.fragment",
CONST_ZIP_FILE);
//Load texture
spaceshipTexture = std::make_shared<Texture>(
CreateTextureDataFromPng("./resources/sship001x.png"));
spaceshipBase = LoadFromTextFile02("./resources/spaceship004.txt");
spaceshipBase.RotateByMatrix(QuatToMatrix(QuatFromRotateAroundY(M_PI / 2.0)));
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
resourcesLoaded = true;
modelLoaded = true;
#endif
}
void Game::drawScene() {
static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
glClearColor(0.0f, 0.5f, 1.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glViewport(0, 0, Environment::width, Environment::height);
if (!resourcesLoaded) {
glClearColor(0.2f, 0.3f, 0.8f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
return;
}
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) /
static_cast<float>(Environment::height),
1, 1000);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({0, 0, -1.0f * Environment::zoom});
renderer.RotateMatrix(QuatFromRotateAroundX(M_PI / 6.0));
//renderer.RotateMatrix(QuatFromRotateAroundX(Environment::cameraAlpha));
if (spaceshipTexture) {
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
} else {
renderer.DrawVertexRenderStruct(spaceship);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
}
void Game::processTickCount() {
if (lastTickCount == 0) {
lastTickCount = SDL_GetTicks64();
return;
}
newTickCount = SDL_GetTicks64();
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ?
CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount;
//gameObjects.updateScene(delta);
Environment::cameraAlpha = Environment::cameraAlpha + delta * M_PI / 10000.f;
lastTickCount = newTickCount;
}
}
void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene();
processTickCount();
SDL_GL_SwapWindow(ZL::Environment::window);
}
void Game::update() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true;
} else if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f;
if (event.wheel.y > 0) {
Environment::zoom -= zoomstep;
} else if (event.wheel.y < 0) {
Environment::zoom += zoomstep;
}
if (Environment::zoom < zoomstep) {
Environment::zoom = zoomstep;
}
/*if (Environment::zoom > 4) {
Environment::zoom = 4;
}*/
//this->modelMeshRender.data.Scale(0.5);
//this->modelMeshRender.RefreshVBO();
}
}
render();
}
} // namespace ZL
-48
View File
@@ -1,48 +0,0 @@
#pragma once
#include "Renderer.h"
#include "Environment.h"
#include "TextureManager.h"
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
namespace ZL {
class Game {
public:
Game();
~Game();
void setup();
void update();
void render();
bool shouldExit() const { return Environment::exitGameLoop; }
private:
void processTickCount();
void drawScene();
SDL_Window *window;
SDL_GLContext glContext;
Renderer renderer;
size_t newTickCount;
size_t lastTickCount;
bool resourcesLoaded;
bool modelLoaded;
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr <Texture> spaceshipTexture;
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
};
} // namespace ZL
-734
View File
@@ -1,734 +0,0 @@
#include "Math.h"
#include <exception>
#include <cmath>
namespace ZL {
Vector2f operator+(const Vector2f& x, const Vector2f& y)
{
Vector2f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
return result;
}
Vector2f operator-(const Vector2f& x, const Vector2f& y)
{
Vector2f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
return result;
}
Vector3f operator+(const Vector3f& x, const Vector3f& y)
{
Vector3f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
result.v[2] = x.v[2] + y.v[2];
return result;
}
Vector3f operator-(const Vector3f& x, const Vector3f& y)
{
Vector3f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
result.v[2] = x.v[2] - y.v[2];
return result;
}
Vector4f operator+(const Vector4f& x, const Vector4f& y)
{
Vector4f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
result.v[2] = x.v[2] + y.v[2];
result.v[3] = x.v[3] + y.v[3];
return result;
}
Vector4f operator-(const Vector4f& x, const Vector4f& y)
{
Vector4f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
result.v[2] = x.v[2] - y.v[2];
result.v[3] = x.v[3] - y.v[3];
return result;
}
Matrix3f Matrix3f::Identity()
{
Matrix3f r;
r.m[0] = 1.f;
r.m[1] = 0.f;
r.m[2] = 0.f;
r.m[3] = 0.f;
r.m[4] = 1.f;
r.m[5] = 0.f;
r.m[6] = 0.f;
r.m[7] = 0.f;
r.m[8] = 1.f;
return r;
}
Matrix4f Matrix4f::Identity()
{
Matrix4f r;
r.m[0] = 1.f;
r.m[1] = 0.f;
r.m[2] = 0.f;
r.m[3] = 0.f;
r.m[4] = 0.f;
r.m[5] = 1.f;
r.m[6] = 0.f;
r.m[7] = 0.f;
r.m[8] = 0.f;
r.m[9] = 0.f;
r.m[10] = 1.f;
r.m[11] = 0.f;
r.m[12] = 0.f;
r.m[13] = 0.f;
r.m[14] = 0.f;
r.m[15] = 1.f;
return r;
}
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2)
{
Matrix4f r;
r.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
r.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
r.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
r.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
r.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
r.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
r.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
r.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
r.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
r.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
r.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
r.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
r.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
r.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
r.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
r.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
return r;
}
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar)
{
float depthRange = zFar - zNear;
if (depthRange <= 0)
{
throw std::runtime_error("zFar must be greater than zNear");
}
Matrix4f r;
r.m[0] = 2.f / width;
r.m[1] = 0;
r.m[2] = 0;
r.m[3] = 0;
r.m[4] = 0;
r.m[5] = 2.f / height;
r.m[6] = 0;
r.m[7] = 0;
r.m[8] = 0;
r.m[9] = 0;
r.m[10] = -1 / depthRange;
r.m[11] = 0;
r.m[12] = -1;
r.m[13] = -1;
r.m[14] = zNear / depthRange;
r.m[15] = 1;
return r;
}
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar)
{
float tanHalfFovy = tan(fovY / 2.f);
Matrix4f r;
if (zNear >= zFar || aspectRatio == 0)
{
throw std::runtime_error("Invalid perspective parameters");
}
r.m[0] = 1.f / (aspectRatio * tanHalfFovy);
r.m[1] = 0;
r.m[2] = 0;
r.m[3] = 0;
r.m[4] = 0;
r.m[5] = 1.f / (tanHalfFovy);
r.m[6] = 0;
r.m[7] = 0;
r.m[8] = 0;
r.m[9] = 0;
r.m[10] = -(zFar + zNear) / (zFar - zNear);
r.m[11] = -1;
r.m[12] = 0;
r.m[13] = 0;
r.m[14] = -(2.f * zFar * zNear) / (zFar - zNear);
r.m[15] = 0;
return r;
}
Matrix3f QuatToMatrix(const Vector4f& q)
{
Matrix3f m;
float wx, wy, wz, xx, yy, yz, xy, xz, zz, s, x2, y2, z2;
s = 2.0f / (q.v[0] * q.v[0] + q.v[1] * q.v[1] + q.v[2] * q.v[2] + q.v[3] * q.v[3]);
x2 = q.v[0] * s;
y2 = q.v[1] * s;
z2 = q.v[2] * s;
wx = q.v[3] * x2; wy = q.v[3] * y2; wz = q.v[3] * z2;
xx = q.v[0] * x2; xy = q.v[1] * x2; xz = q.v[2] * x2;
yy = q.v[1] * y2; yz = q.v[2] * y2;
zz = q.v[2] * z2;
m.m[0] = 1.0f - (yy + zz);
m.m[1] = xy + wz;
m.m[2] = xz - wy;
m.m[3] = xy - wz;
m.m[4] = 1.0f - (xx + zz);
m.m[5] = yz + wx;
m.m[6] = xz + wy;
m.m[7] = yz - wx;
m.m[8] = 1.0f - (xx + yy);
return m;
}
Vector4f MatrixToQuat(const Matrix3f& m)
{
Vector4f r;
float trace = m.m[0] + m.m[4] + m.m[8];
if (trace > 0)
{
float s = 0.5f / sqrtf(trace + 1.0f);
r.v[3] = 0.25f / s;
r.v[0] = (m.m[5] - m.m[7]) * s;
r.v[1] = (m.m[6] - m.m[2]) * s;
r.v[2] = (m.m[1] - m.m[3]) * s;
}
else if (m.m[0] > m.m[4] && m.m[0] > m.m[8])
{
float s = 2.0f * sqrtf(1.0f + m.m[0] - m.m[4] - m.m[8]);
r.v[3] = (m.m[5] - m.m[7]) / s;
r.v[0] = 0.25f * s;
r.v[1] = (m.m[1] + m.m[3]) / s;
r.v[2] = (m.m[6] + m.m[2]) / s;
}
else if (m.m[4] > m.m[8])
{
float s = 2.0f * sqrtf(1.0f + m.m[4] - m.m[0] - m.m[8]);
r.v[3] = (m.m[6] - m.m[2]) / s;
r.v[0] = (m.m[1] + m.m[3]) / s;
r.v[1] = 0.25f * s;
r.v[2] = (m.m[5] + m.m[7]) / s;
}
else
{
float s = 2.0f * sqrtf(1.0f + m.m[8] - m.m[0] - m.m[4]);
r.v[3] = (m.m[1] - m.m[3]) / s;
r.v[0] = (m.m[6] + m.m[2]) / s;
r.v[1] = (m.m[5] + m.m[7]) / s;
r.v[2] = 0.25f * s;
}
return r.normalized();
}
Vector4f QuatFromRotateAroundX(float angle)
{
Vector4f result;
result.v[0] = sinf(angle * 0.5f);
result.v[1] = 0.f;
result.v[2] = 0.f;
result.v[3] = cosf(angle * 0.5f);
return result;
}
Vector4f QuatFromRotateAroundY(float angle)
{
Vector4f result;
result.v[0] = 0.f;
result.v[1] = sinf(angle * 0.5f);
result.v[2] = 0.f;
result.v[3] = cosf(angle * 0.5f);
return result;
}
Vector4f QuatFromRotateAroundZ(float angle)
{
Vector4f result;
result.v[0] = 0.f;
result.v[1] = 0.f;
result.v[2] = sinf(angle * 0.5f);
result.v[3] = cosf(angle * 0.5f);
return result;
}
Matrix3f TransposeMatrix(const Matrix3f& m)
{
Matrix3f r;
r.m[0] = m.m[0];
r.m[1] = m.m[3];
r.m[2] = m.m[6];
r.m[3] = m.m[1];
r.m[4] = m.m[4];
r.m[5] = m.m[7];
r.m[6] = m.m[2];
r.m[7] = m.m[5];
r.m[8] = m.m[8];
return r;
}
Matrix3f InverseMatrix(const Matrix3f& m)
{
float d;
Matrix3f r;
d = m.m[0] * (m.m[4] * m.m[8] - m.m[5] * m.m[7]);
d -= m.m[1] * (m.m[3] * m.m[8] - m.m[6] * m.m[5]);
d += m.m[2] * (m.m[3] * m.m[7] - m.m[6] * m.m[4]);
if (fabs(d) < 0.01f)
{
throw std::runtime_error("Error: matrix cannot be inversed!");
}
else
{
r.m[0] = (m.m[4] * m.m[8] - m.m[5] * m.m[7]) / d;
r.m[1] = -(m.m[1] * m.m[8] - m.m[2] * m.m[7]) / d;
r.m[2] = (m.m[1] * m.m[5] - m.m[2] * m.m[4]) / d;
r.m[3] = -(m.m[3] * m.m[8] - m.m[5] * m.m[6]) / d;
r.m[4] = (m.m[0] * m.m[8] - m.m[2] * m.m[6]) / d;
r.m[5] = -(m.m[0] * m.m[5] - m.m[2] * m.m[3]) / d;
r.m[6] = (m.m[3] * m.m[7] - m.m[6] * m.m[4]) / d;
r.m[7] = -(m.m[0] * m.m[7] - m.m[6] * m.m[1]) / d;
r.m[8] = (m.m[0] * m.m[4] - m.m[1] * m.m[3]) / d;
};
return r;
}
Matrix4f InverseMatrix(const Matrix4f& mat)
{
Matrix4f inv;
float det;
inv.m[0] = mat.m[5] * mat.m[10] * mat.m[15] -
mat.m[5] * mat.m[11] * mat.m[14] -
mat.m[9] * mat.m[6] * mat.m[15] +
mat.m[9] * mat.m[7] * mat.m[14] +
mat.m[13] * mat.m[6] * mat.m[11] -
mat.m[13] * mat.m[7] * mat.m[10];
inv.m[4] = -mat.m[4] * mat.m[10] * mat.m[15] +
mat.m[4] * mat.m[11] * mat.m[14] +
mat.m[8] * mat.m[6] * mat.m[15] -
mat.m[8] * mat.m[7] * mat.m[14] -
mat.m[12] * mat.m[6] * mat.m[11] +
mat.m[12] * mat.m[7] * mat.m[10];
inv.m[8] = mat.m[4] * mat.m[9] * mat.m[15] -
mat.m[4] * mat.m[11] * mat.m[13] -
mat.m[8] * mat.m[5] * mat.m[15] +
mat.m[8] * mat.m[7] * mat.m[13] +
mat.m[12] * mat.m[5] * mat.m[11] -
mat.m[12] * mat.m[7] * mat.m[9];
inv.m[12] = -mat.m[4] * mat.m[9] * mat.m[14] +
mat.m[4] * mat.m[10] * mat.m[13] +
mat.m[8] * mat.m[5] * mat.m[14] -
mat.m[8] * mat.m[6] * mat.m[13] -
mat.m[12] * mat.m[5] * mat.m[10] +
mat.m[12] * mat.m[6] * mat.m[9];
inv.m[1] = -mat.m[1] * mat.m[10] * mat.m[15] +
mat.m[1] * mat.m[11] * mat.m[14] +
mat.m[9] * mat.m[2] * mat.m[15] -
mat.m[9] * mat.m[3] * mat.m[14] -
mat.m[13] * mat.m[2] * mat.m[11] +
mat.m[13] * mat.m[3] * mat.m[10];
inv.m[5] = mat.m[0] * mat.m[10] * mat.m[15] -
mat.m[0] * mat.m[11] * mat.m[14] -
mat.m[8] * mat.m[2] * mat.m[15] +
mat.m[8] * mat.m[3] * mat.m[14] +
mat.m[12] * mat.m[2] * mat.m[11] -
mat.m[12] * mat.m[3] * mat.m[10];
inv.m[9] = -mat.m[0] * mat.m[9] * mat.m[15] +
mat.m[0] * mat.m[11] * mat.m[13] +
mat.m[8] * mat.m[1] * mat.m[15] -
mat.m[8] * mat.m[3] * mat.m[13] -
mat.m[12] * mat.m[1] * mat.m[11] +
mat.m[12] * mat.m[3] * mat.m[9];
inv.m[13] = mat.m[0] * mat.m[9] * mat.m[14] -
mat.m[0] * mat.m[10] * mat.m[13] -
mat.m[8] * mat.m[1] * mat.m[14] +
mat.m[8] * mat.m[2] * mat.m[13] +
mat.m[12] * mat.m[1] * mat.m[10] -
mat.m[12] * mat.m[2] * mat.m[9];
inv.m[2] = mat.m[1] * mat.m[6] * mat.m[15] -
mat.m[1] * mat.m[7] * mat.m[14] -
mat.m[5] * mat.m[2] * mat.m[15] +
mat.m[5] * mat.m[3] * mat.m[14] +
mat.m[13] * mat.m[2] * mat.m[7] -
mat.m[13] * mat.m[3] * mat.m[6];
inv.m[6] = -mat.m[0] * mat.m[6] * mat.m[15] +
mat.m[0] * mat.m[7] * mat.m[14] +
mat.m[4] * mat.m[2] * mat.m[15] -
mat.m[4] * mat.m[3] * mat.m[14] -
mat.m[12] * mat.m[2] * mat.m[7] +
mat.m[12] * mat.m[3] * mat.m[6];
inv.m[10] = mat.m[0] * mat.m[5] * mat.m[15] -
mat.m[0] * mat.m[7] * mat.m[13] -
mat.m[4] * mat.m[1] * mat.m[15] +
mat.m[4] * mat.m[3] * mat.m[13] +
mat.m[12] * mat.m[1] * mat.m[7] -
mat.m[12] * mat.m[3] * mat.m[5];
inv.m[14] = -mat.m[0] * mat.m[5] * mat.m[14] +
mat.m[0] * mat.m[6] * mat.m[13] +
mat.m[4] * mat.m[1] * mat.m[14] -
mat.m[4] * mat.m[2] * mat.m[13] -
mat.m[12] * mat.m[1] * mat.m[6] +
mat.m[12] * mat.m[2] * mat.m[5];
inv.m[3] = -mat.m[1] * mat.m[6] * mat.m[11] +
mat.m[1] * mat.m[7] * mat.m[10] +
mat.m[5] * mat.m[2] * mat.m[11] -
mat.m[5] * mat.m[3] * mat.m[10] -
mat.m[9] * mat.m[2] * mat.m[7] +
mat.m[9] * mat.m[3] * mat.m[6];
inv.m[7] = mat.m[0] * mat.m[6] * mat.m[11] -
mat.m[0] * mat.m[7] * mat.m[10] -
mat.m[4] * mat.m[2] * mat.m[11] +
mat.m[4] * mat.m[3] * mat.m[10] +
mat.m[8] * mat.m[2] * mat.m[7] -
mat.m[8] * mat.m[3] * mat.m[6];
inv.m[11] = -mat.m[0] * mat.m[5] * mat.m[11] +
mat.m[0] * mat.m[7] * mat.m[9] +
mat.m[4] * mat.m[1] * mat.m[11] -
mat.m[4] * mat.m[3] * mat.m[9] -
mat.m[8] * mat.m[1] * mat.m[7] +
mat.m[8] * mat.m[3] * mat.m[5];
inv.m[15] = mat.m[0] * mat.m[5] * mat.m[10] -
mat.m[0] * mat.m[6] * mat.m[9] -
mat.m[4] * mat.m[1] * mat.m[10] +
mat.m[4] * mat.m[2] * mat.m[9] +
mat.m[8] * mat.m[1] * mat.m[6] -
mat.m[8] * mat.m[2] * mat.m[5];
// ��������� �����������
det = mat.m[0] * inv.m[0] + mat.m[1] * inv.m[4] + mat.m[2] * inv.m[8] + mat.m[3] * inv.m[12];
if (std::fabs(det) < 0.01f)
{
throw std::runtime_error("Error: matrix cannot be inversed!");
}
// ����� ��� �������� �� �����������
det = 1.0f / det;
for (int i = 0; i < 16; i++)
{
inv.m[i] *= det;
}
return inv;
}
Matrix3f CreateZRotationMatrix(float angle)
{
Matrix3f result = Matrix3f::Identity();
result.m[0] = cosf(angle);
result.m[1] = -sinf(angle);
result.m[3] = sinf(angle);
result.m[4] = cosf(angle);
return result;
}
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2)
{
Matrix4f rx;
rx.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
rx.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
rx.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
rx.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
rx.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
rx.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
rx.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
rx.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
rx.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
rx.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
rx.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
rx.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
rx.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
rx.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
rx.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
rx.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
return rx;
}
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2)
{
Matrix3f r;
r.m[0] = m1.m[0] * m2.m[0] + m1.m[3] * m2.m[1] + m1.m[6] * m2.m[2];
r.m[1] = m1.m[1] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[7] * m2.m[2];
r.m[2] = m1.m[2] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[8] * m2.m[2];
r.m[3] = m1.m[0] * m2.m[3] + m1.m[3] * m2.m[4] + m1.m[6] * m2.m[5];
r.m[4] = m1.m[1] * m2.m[3] + m1.m[4] * m2.m[4] + m1.m[7] * m2.m[5];
r.m[5] = m1.m[2] * m2.m[3] + m1.m[5] * m2.m[4] + m1.m[8] * m2.m[5];
r.m[6] = m1.m[0] * m2.m[6] + m1.m[3] * m2.m[7] + m1.m[6] * m2.m[8] ;
r.m[7] = m1.m[1] * m2.m[6] + m1.m[4] * m2.m[7] + m1.m[7] * m2.m[8];
r.m[8] = m1.m[2] * m2.m[6] + m1.m[5] * m2.m[7] + m1.m[8] * m2.m[8];
return r;
}
Matrix3f MakeTranslationMatrix(const Vector3f& p)
{
Matrix3f r = Matrix3f::Identity();
r.m[12] = p.v[0];
r.m[13] = p.v[1];
r.m[14] = p.v[2];
return r;
}
Matrix3f MakeScaleMatrix(float scale)
{
Matrix3f r = Matrix3f::Identity();
r.m[0] = scale;
r.m[5] = scale;
r.m[10] = scale;
return r;
}
Matrix3f MakeRotationMatrix(const Vector3f& p)
{
Matrix3f r = Matrix3f::Identity();
r.m[12] = p.v[0];
r.m[13] = p.v[1];
r.m[14] = p.v[2];
return r;
}
Vector3f operator*(Vector3f v, float scale)
{
Vector3f r = v;
r.v[0] = v.v[0] * scale;
r.v[1] = v.v[1] * scale;
r.v[2] = v.v[2] * scale;
return r;
}
Vector4f operator*(Vector4f v, float scale)
{
Vector4f r = v;
r.v[0] = v.v[0] * scale;
r.v[1] = v.v[1] * scale;
r.v[2] = v.v[2] * scale;
r.v[3] = v.v[3] * scale;
return r;
}
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt)
{
Vector3f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2];
r.v[1] = v.v[0] * mt.m[3] + v.v[1] * mt.m[4] + v.v[2] * mt.m[5];
r.v[2] = v.v[0] * mt.m[6] + v.v[1] * mt.m[7] + v.v[2] * mt.m[8];
return r;
}
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt)
{
Vector4f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2] + v.v[3] * mt.m[3];
r.v[1] = v.v[0] * mt.m[4] + v.v[1] * mt.m[5] + v.v[2] * mt.m[6] + v.v[3] * mt.m[7];
r.v[2] = v.v[0] * mt.m[8] + v.v[1] * mt.m[9] + v.v[2] * mt.m[10] + v.v[3] * mt.m[11];
r.v[3] = v.v[0] * mt.m[12] + v.v[1] * mt.m[13] + v.v[2] * mt.m[14] + v.v[3] * mt.m[15];
return r;
}
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v)
{
Vector4f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[4] + v.v[2] * mt.m[8] + v.v[3] * mt.m[12];
r.v[1] = v.v[0] * mt.m[1] + v.v[1] * mt.m[5] + v.v[2] * mt.m[9] + v.v[3] * mt.m[13];
r.v[2] = v.v[0] * mt.m[2] + v.v[1] * mt.m[6] + v.v[2] * mt.m[10] + v.v[3] * mt.m[14];
r.v[3] = v.v[0] * mt.m[3] + v.v[1] * mt.m[7] + v.v[2] * mt.m[11] + v.v[3] * mt.m[15];
return r;
}
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t)
{
const float epsilon = 1e-6f;
// Нормализация входных кватернионов
Vector4f q1_norm = q1.normalized();
Vector4f q2_norm = q2.normalized();
float cosTheta = q1_norm.dot(q2_norm);
// Если q1 и q2 близки к противоположным направлениям, корректируем q2
Vector4f q2_adjusted = q2_norm;
if (cosTheta < 0.0f) {
q2_adjusted.v[0] = -q2_adjusted.v[0];
q2_adjusted.v[1] = -q2_adjusted.v[1];
q2_adjusted.v[2] = -q2_adjusted.v[2];
q2_adjusted.v[3] = -q2_adjusted.v[3];
cosTheta = -cosTheta;
}
// Если кватернионы близки, используем линейную интерполяцию
if (cosTheta > 1.0f - epsilon) {
Vector4f result;
result.v[0] = q1_norm.v[0] + t * (q2_adjusted.v[0] - q1_norm.v[0]);
result.v[1] = q1_norm.v[1] + t * (q2_adjusted.v[1] - q1_norm.v[1]);
result.v[2] = q1_norm.v[2] + t * (q2_adjusted.v[2] - q1_norm.v[2]);
result.v[3] = q1_norm.v[3] + t * (q2_adjusted.v[3] - q1_norm.v[3]);
return result.normalized();
}
// Иначе используем сферическую интерполяцию
float theta = std::acos(cosTheta);
float sinTheta = std::sin(theta);
float coeff1 = std::sin((1.0f - t) * theta) / sinTheta;
float coeff2 = std::sin(t * theta) / sinTheta;
Vector4f result;
result.v[0] = coeff1 * q1_norm.v[0] + coeff2 * q2_adjusted.v[0];
result.v[1] = coeff1 * q1_norm.v[1] + coeff2 * q2_adjusted.v[1];
result.v[2] = coeff1 * q1_norm.v[2] + coeff2 * q2_adjusted.v[2];
result.v[3] = coeff1 * q1_norm.v[3] + coeff2 * q2_adjusted.v[3];
return result.normalized();
}
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos)
{
Matrix4f r;
r.m[0] = m.m[0];
r.m[1] = m.m[1];
r.m[2] = m.m[2];
r.m[3] = 0;
r.m[4] = m.m[3];
r.m[5] = m.m[4];
r.m[6] = m.m[5];
r.m[7] = 0;
r.m[8] = m.m[6];
r.m[9] = m.m[7];
r.m[10] = m.m[8];
r.m[11] = 0;
r.m[12] = pos.v[0];
r.m[13] = pos.v[1];
r.m[14] = pos.v[2];
r.m[15] = 1.0;
return r;
}
};
-110
View File
@@ -1,110 +0,0 @@
#pragma once
#include <array>
#include <exception>
#include <stdexcept>
#include <cmath>
namespace ZL {
struct Vector4f
{
std::array<float, 4> v = { 0.f, 0.f, 0.f, 0.f };
Vector4f normalized() const {
double norm = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]);
Vector4f r;
r.v[0] = v[0] / norm;
r.v[1] = v[1] / norm;
r.v[2] = v[2] / norm;
r.v[3] = v[3] / norm;
return r;
}
double dot(const Vector4f& other) const {
return v[0] * other.v[0] + v[1] * other.v[1] + v[2] * other.v[2] + v[3] * other.v[3];
}
};
struct Vector3f
{
std::array<float, 3> v = { 0.f, 0.f, 0.f };
};
struct Vector2f
{
std::array<float, 2> v = {0.f, 0.f};
};
Vector2f operator+(const Vector2f& x, const Vector2f& y);
Vector2f operator-(const Vector2f& x, const Vector2f& y);
Vector3f operator+(const Vector3f& x, const Vector3f& y);
Vector3f operator-(const Vector3f& x, const Vector3f& y);
Vector4f operator+(const Vector4f& x, const Vector4f& y);
Vector4f operator-(const Vector4f& x, const Vector4f& y);
struct Matrix3f
{
std::array<float, 9> m = { 0.f, 0.f, 0.f,
0.f, 0.f, 0.f,
0.f, 0.f, 0.f, };
static Matrix3f Identity();
};
struct Matrix4f
{
std::array<float, 16> m = { 0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f };
static Matrix4f Identity();
float& operator()(int row, int col) {
//return m[row * 4 + col]; //OpenGL specific
return m[col * 4 + row];
}
const float& operator()(int row, int col) const {
//return m[row * 4 + col];
return m[col * 4 + row];
}
};
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2);
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar);
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar);
Matrix3f QuatToMatrix(const Vector4f& q);
Vector4f MatrixToQuat(const Matrix3f& m);
Vector4f QuatFromRotateAroundX(float angle);
Vector4f QuatFromRotateAroundY(float angle);
Vector4f QuatFromRotateAroundZ(float angle);
Vector3f operator*(Vector3f v, float scale);
Vector4f operator*(Vector4f v, float scale);
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt);
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt);
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v);
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t);
Matrix3f InverseMatrix(const Matrix3f& m);
Matrix4f InverseMatrix(const Matrix4f& m);
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2);
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2);
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos);
};
-397
View File
@@ -1,397 +0,0 @@
#include "TextModel.h"
#include <regex>
#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace ZL {
VertexDataStruct LoadFromTextFile(const std::string &fileName, const std::string &ZIPFileName) {
VertexDataStruct result;
std::ifstream filestream;
std::istringstream zipStream;
if (!ZIPFileName.empty()) {
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
} else {
filestream.open(fileName);
}
// Создаем ссылку f на нужный поток – после этого код ниже остается без изменений
std::istream &f = (!ZIPFileName.empty()) ? static_cast<std::istream &>(zipStream)
: static_cast<std::istream &>(filestream);
//Skip first 5 lines
std::string tempLine;
std::getline(f, tempLine);
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
static const std::regex pattern_int(R"([-]?\d+)");
std::smatch match;
int numberVertices;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberVertices = std::stoi(number_str);
} else {
throw std::runtime_error("No number found in the input string.");
}
std::vector <Vector3f> vertices;
vertices.resize(numberVertices);
for (int i = 0; i < numberVertices; i++) {
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
vertices[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
}
std::cout << "UV Coordinates" << std::endl;
std::getline(f, tempLine); //===UV Coordinates:
std::getline(f, tempLine); //triangle count
int numberTriangles;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
} else {
throw std::runtime_error("No number found in the input string.");
}
// Now process UVs
std::vector <std::array<Vector2f, 3>> uvCoords;
uvCoords.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++) {
std::getline(f, tempLine); //Face 0
int uvCount;
std::getline(f, tempLine);
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
uvCount = std::stoi(number_str);
} else {
throw std::runtime_error("No number found in the input string.");
}
if (uvCount != 3) {
throw std::runtime_error("more than 3 uvs");
}
std::vector<float> floatValues;
for (int j = 0; j < 3; j++) {
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
auto b = tempLine.cbegin();
auto e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
if (floatValues.size() != 2) {
throw std::runtime_error("more than 2 uvs---");
}
uvCoords[i][j] = Vector2f{floatValues[0], floatValues[1]};
}
}
std::cout << "Normals go" << std::endl;
std::getline(f, tempLine); //===Normals:
std::vector <Vector3f> normals;
normals.resize(numberVertices);
for (int i = 0; i < numberVertices; i++) {
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
normals[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
}
std::cout << "Triangles go:" << std::endl;
std::getline(f, tempLine); //===Triangles: 3974
std::vector <std::array<int, 3>> triangles;
triangles.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++) {
std::getline(f, tempLine);
std::vector<int> intValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
intValues.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
triangles[i] = {intValues[0], intValues[1], intValues[2]};
}
std::cout << "Process vertices" << std::endl;
// Now let's process vertices
for (int i = 0; i < numberTriangles; i++) {
result.PositionData.push_back(vertices[triangles[i][0]]);
result.PositionData.push_back(vertices[triangles[i][1]]);
result.PositionData.push_back(vertices[triangles[i][2]]);
/*
result.NormalData.push_back(normals[triangles[i][0]]);
result.NormalData.push_back(normals[triangles[i][1]]);
result.NormalData.push_back(normals[triangles[i][2]]);
*/
result.TexCoordData.push_back(uvCoords[i][0]);
result.TexCoordData.push_back(uvCoords[i][1]);
result.TexCoordData.push_back(uvCoords[i][2]);
}
//Swap from Blender format to OpenGL format
for (int i = 0; i < result.PositionData.size(); i++) {
Vector3f tempVec = result.PositionData[i];
result.PositionData[i].v[0] = tempVec.v[1];
result.PositionData[i].v[1] = tempVec.v[2];
result.PositionData[i].v[2] = tempVec.v[0];
/*
tempVec = result.NormalData[i];
result.NormalData[i].v[0] = tempVec.v[1];
result.NormalData[i].v[1] = tempVec.v[2];
result.NormalData[i].v[2] = tempVec.v[0];*/
}
return result;
}
VertexDataStruct
LoadFromTextFile02(const std::string &fileName, const std::string &ZIPFileName) {
VertexDataStruct result;
#ifdef __ANDROID__
// Для Android используем SDL_RWops для чтения файлов
__android_log_print(ANDROID_LOG_INFO, "TextModel",
"LoadFromTextFile02 called for Android: %s", fileName.c_str());
// Читаем файл с помощью readTextFile
std::string fileContent = readTextFile(fileName);
if (fileContent.empty()) {
__android_log_print(ANDROID_LOG_ERROR, "TextModel",
"Failed to read file: %s", fileName.c_str());
throw std::runtime_error("Failed to read file: " + fileName);
}
__android_log_print(ANDROID_LOG_INFO, "TextModel",
"File read successfully, size: %zu", fileContent.size());
std::istringstream f(fileContent);
#else
// Оригинальный код для десктопа
std::ifstream filestream;
std::istringstream zipStream;
if (!ZIPFileName.empty()) {
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
f = static_cast<std::istream &>(zipStream);
} else {
filestream.open(fileName);
if (!filestream.is_open()) {
throw std::runtime_error("Failed to open file: " + fileName);
}
f = static_cast<std::istream &>(filestream);
}
#endif
std::string tempLine;
std::smatch match;
// Обновленные регулярки
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+(\.\d+)?)");
static const std::regex pattern_int(R"([-]?\d+)");
// --- 2. Парсинг Вершин (Pos + Norm + UV) ---
// Ищем заголовок ===Vertices
while (std::getline(f, tempLine)) {
if (tempLine.find("===Vertices") != std::string::npos) break;
}
int numberVertices = 0;
if (std::regex_search(tempLine, match, pattern_count)) {
numberVertices = std::stoi(match.str());
} else {
throw std::runtime_error("Vertices header not found or invalid.");
}
// Временные буферы для хранения "уникальных" вершин перед разверткой по индексам
std::vector <Vector3f> tempPositions(numberVertices);
std::vector <Vector3f> tempNormals(numberVertices);
std::vector <Vector2f> tempUVs(numberVertices);
for (int i = 0; i < numberVertices; i++) {
std::getline(f, tempLine);
// Строка вида: V 0: Pos(x, y, z) Norm(x, y, z) UV(u, v)
std::vector<float> floatValues;
floatValues.reserve(9); // ID + 3 pos + 3 norm + 2 uv
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
// Проверка, что у нас достаточно данных
if (floatValues.size() < 9) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_WARN, "TextModel",
"Warning: vertex line has only %zu values, expected 9",
floatValues.size());
#endif
// Попробуем с меньшим количеством, возможно данные упрощенные
if (floatValues.size() >= 8) {
// Без ID
tempPositions[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
tempNormals[i] = Vector3f{floatValues[3], floatValues[4], floatValues[5]};
tempUVs[i] = Vector2f{floatValues[6], floatValues[7]};
continue;
}
throw std::runtime_error("Malformed vertex line at index " + std::to_string(i));
}
// ID - floatValues[0]
// Pos - [1], [2], [3]
// Norm - [4], [5], [6]
// UV - [7], [8]
tempPositions[i] = Vector3f{floatValues[1], floatValues[2], floatValues[3]};
tempNormals[i] = Vector3f{floatValues[4], floatValues[5], floatValues[6]};
tempUVs[i] = Vector2f{floatValues[7], floatValues[8]};
}
// --- 3. Парсинг Треугольников (Индексов) ---
// Пропускаем пустые строки до заголовка треугольников
while (std::getline(f, tempLine)) {
if (tempLine.find("===Triangles") != std::string::npos) break;
}
int numberTriangles = 0;
if (std::regex_search(tempLine, match, pattern_count)) {
numberTriangles = std::stoi(match.str());
} else {
throw std::runtime_error("Triangles header not found.");
}
// Резервируем память в result, чтобы избежать лишних аллокаций
result.PositionData.reserve(numberTriangles * 3);
result.NormalData.reserve(numberTriangles * 3);
result.TexCoordData.reserve(numberTriangles * 3);
for (int i = 0; i < numberTriangles; i++) {
std::getline(f, tempLine);
// Строка вида: Tri: 0 1 2
std::vector<int> indices;
indices.reserve(3);
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
indices.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
if (indices.size() != 3) {
throw std::runtime_error("Malformed triangle line at index " + std::to_string(i));
}
// --- 4. Заполнение VertexDataStruct (Flattening) ---
for (int k = 0; k < 3; k++) {
int idx = indices[k];
result.PositionData.push_back(tempPositions[idx]);
result.NormalData.push_back(tempNormals[idx]);
result.TexCoordData.push_back(tempUVs[idx]);
}
}
// --- 5. Конвертация координат (Blender -> OpenGL/Engine) ---
for (size_t i = 0; i < result.PositionData.size(); i++) {
Vector3f originalPos = result.PositionData[i];
result.PositionData[i].v[0] = originalPos.v[1]; // New X = Old Y
result.PositionData[i].v[1] = originalPos.v[2]; // New Y = Old Z
result.PositionData[i].v[2] = originalPos.v[0]; // New Z = Old X
Vector3f originalNorm = result.NormalData[i];
result.NormalData[i].v[0] = originalNorm.v[1];
result.NormalData[i].v[1] = originalNorm.v[2];
result.NormalData[i].v[2] = originalNorm.v[0];
}
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "TextModel",
"Model loaded successfully: %d verts, %d tris",
numberVertices, numberTriangles);
#else
std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris."
<< std::endl;
#endif
return result;
}
}
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "Math.h"
#include "Renderer.h"
#include <unordered_map>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace ZL {
VertexDataStruct
LoadFromTextFile(const std::string &fileName, const std::string &ZIPFileName = "");
VertexDataStruct
LoadFromTextFile02(const std::string &fileName, const std::string &ZIPFileName = "");
}
-285
View File
@@ -1,285 +0,0 @@
#include "TextureManager.h"
#ifdef PNG_ENABLED
#include "png.h"
#endif
namespace ZL
{
Texture::Texture(const TextureDataStruct& texData)
{
width = texData.width;
height = texData.height;
glGenTextures(1, &texID);
if (texID == 0)
{
throw std::runtime_error("glGenTextures did not work");
}
glBindTexture(GL_TEXTURE_2D, texID);
//CheckGlError();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//CheckGlError();
//This should be only for Windows
//glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
//CheckGlError();
if (texData.bitSize == TextureDataStruct::BS_24BIT)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGB, GL_UNSIGNED_BYTE, &texData.data[0]);
}
else
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGBA, GL_UNSIGNED_BYTE, &texData.data[0]);
}
//CheckGlError();
}
Texture::~Texture()
{
glDeleteTextures(1, &texID);
texID = 0;
}
GLuint Texture::getTexID()
{
return texID;
}
size_t Texture::getWidth()
{
return width;
}
size_t Texture::getHeight()
{
return height;
}
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.bitSize = TextureDataStruct::BS_24BIT;
size_t dataSize = texData.width * texData.height * 3;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 3 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
}
return texData;
}
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.bitSize = TextureDataStruct::BS_32BIT;
size_t dataSize = texData.width * texData.height * 4;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 4 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
texData.data[x + 3] = fileArr[pos++];
}
return texData;
}
#ifdef PNG_ENABLED
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName)
{
TextureDataStruct texData;
FILE* file = fopen(fullFileName.c_str(), "rb");
if (!file) {
fclose(file);
throw std::runtime_error("Could not open file " + fullFileName);
}
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) {
fclose(file);
throw std::runtime_error("Could not create PNG read structure");
}
png_infop info = png_create_info_struct(png);
if (!info) {
fclose(file);
png_destroy_read_struct(&png, nullptr, nullptr);
throw std::runtime_error("Could not create PNG info structure");
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, nullptr);
fclose(file);
throw std::runtime_error("Error during PNG read");
}
png_init_io(png, file);
png_read_info(png, info);
texData.width = png_get_image_width(png, info);
texData.height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
if (bit_depth == 16)
png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
if (color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);
png_read_update_info(png, info);
png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * texData.height);
for (int y = 0; y < texData.height; y++) {
row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png, info));
}
png_read_image(png, row_pointers);
fclose(file);
bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) || (png_get_valid(png, info, PNG_INFO_tRNS));
size_t dataSize;
if (has_alpha)
{
texData.bitSize = TextureDataStruct::BS_32BIT;
}
else
{
texData.bitSize = TextureDataStruct::BS_24BIT;
}
int channels = has_alpha ? 4 : 3;
dataSize = texData.width * texData.height * channels;
texData.data.resize(dataSize);
//for (int y = 0; y < texData.height; y++) { //Go in reverse because of UV coord start point
for (int y = texData.height-1; y >= 0; y--) {
//png_bytep row = row_pointers[y];//Go in reverse because of UV coord start point
png_bytep row = row_pointers[texData.height - 1 - y];
for (int x = 0; x < texData.width; x++) {
png_bytep px = &(row[x * 4]);
texData.data[(y * texData.width + x) * channels + 0] = px[0]; // R
texData.data[(y * texData.width + x) * channels + 1] = px[1]; // G
texData.data[(y * texData.width + x) * channels + 2] = px[2]; // B
if (has_alpha) {
texData.data[(y * texData.width + x) * channels + 3] = px[3]; // A
}
}
//free(row_pointers[y]);//Go in reverse because of UV coord start point
free(row_pointers[texData.height - 1 - y]);//Go in reverse because of UV coord start point
}
free(row_pointers);
png_destroy_read_struct(&png, &info, nullptr);
return texData;
}
#endif
}
-49
View File
@@ -1,49 +0,0 @@
#pragma once
//#include "OpenGlExtensions.h"
#include "Utils.h"
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
namespace ZL
{
struct TextureDataStruct
{
size_t width;
size_t height;
std::vector<char> data;
enum BitSize {
BS_24BIT,
BS_32BIT
};
BitSize bitSize;
};
class Texture
{
size_t width = 0;
size_t height = 0;
GLuint texID = 0;
public:
Texture(const TextureDataStruct& texData);
~Texture();
GLuint getTexID();
size_t getWidth();
size_t getHeight();
};
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName="");
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName="");
#ifdef PNG_ENABLED
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName);
#endif
}
-144
View File
@@ -1,144 +0,0 @@
#include "Utils.h"
#include <cstring>
#include <iterator>
#include <vector>
#include <iostream>
#include <algorithm>
#include <fstream>
//#include <zip.h>
#include <SDL.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace ZL {
std::string readTextFile(const std::string &filename) {
#ifdef __ANDROID__
SDL_RWops* file = SDL_RWFromFile(filename.c_str(), "rb");
if (!file) {
#ifdef DEBUG
__android_log_print(ANDROID_LOG_ERROR, "Utils",
"Failed to open: %s", filename.c_str());
#endif
return "";
}
Sint64 size = SDL_RWsize(file);
if (size <= 0) {
SDL_RWclose(file);
return "";
}
std::string content(size, '\0');
if (SDL_RWread(file, &content[0], size, 1) != 1) {
SDL_RWclose(file);
return "";
}
SDL_RWclose(file);
return content;
#else
std::ifstream f(filename);
if (!f.is_open()) {
return "";
}
return std::string((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
#endif
}
std::vector<char> readFile(const std::string &filename) {
#ifdef __ANDROID__
SDL_RWops* file = SDL_RWFromFile(filename.c_str(), "rb");
if (!file) {
return {};
}
Sint64 size = SDL_RWsize(file);
if (size <= 0) {
SDL_RWclose(file);
return {};
}
std::vector<char> data(size);
if (SDL_RWread(file, data.data(), size, 1) != 1) {
SDL_RWclose(file);
return {};
}
SDL_RWclose(file);
return data;
#else
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
std::streamsize size = file.tellg();
if (size <= 0) {
return {};
}
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
if (!file.read(buffer.data(), size)) {
return {};
}
return buffer;
#endif
}
std::vector<char> readFileFromZIP(const std::string &filename, const std::string &zipfilename) {
/*const std::string zipPath = zipfilename;
int zipErr;
zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &zipErr);
if (!archive) {
throw std::runtime_error("Ошибка открытия ZIP: " + zipPath);
}
std::string cleanFilename = filename;
if (cleanFilename.rfind("./", 0) == 0) {
cleanFilename = cleanFilename.substr(2);
}
std::cout << "Ищем в ZIP: " << cleanFilename << std::endl;
zip_file_t* zipFile = zip_fopen(archive, cleanFilename.c_str(), 0);
if (!zipFile) {
zip_close(archive);
throw std::runtime_error("Файл не найден в ZIP: " + cleanFilename);
}
zip_stat_t fileStat;
if (zip_stat(archive, cleanFilename.c_str(), 0, &fileStat) != 0) {
zip_fclose(zipFile);
zip_close(archive);
throw std::runtime_error("Ошибка чтения ZIP-статистики.");
}
std::vector<char> fileData;
fileData.resize(fileStat.size);
zip_fread(zipFile, fileData.data(), fileData.size());
zip_fclose(zipFile);
zip_close(archive);
return fileData;*/
return {};
}
bool findString(const char *in, char *list) {
size_t thisLength = strlen(in);
while (*list != 0) {
size_t length = strcspn(list, " ");
if (thisLength == length && !strncmp(in, list, length))
return true;
list += length + 1;
}
return false;
}
};
-199
View File
@@ -1,199 +0,0 @@
#include "Game.h"
#include "Environment.h"
#ifdef __ANDROID__
#include <android/log.h>
#endif
ZL::Game game;
#ifdef __ANDROID__
extern "C" int SDL_main(int argc, char* argv[]) {
#else
int main(int argc, char *argv[]) {
#endif
#ifdef __ANDROID__
// Инициализация SDL перед получением информации о дисплее
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL init failed: %s", SDL_GetError());
return 1;
}
SDL_DisplayMode displayMode;
if (SDL_GetCurrentDisplayMode(0, &displayMode) != 0) {
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GetCurrentDisplayMode failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
ZL::Environment::width = displayMode.w;
ZL::Environment::height = displayMode.h;
__android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d",
ZL::Environment::width, ZL::Environment::height);
#else
constexpr int CONST_WIDTH = 1280;
constexpr int CONST_HEIGHT = 720;
ZL::Environment::width = CONST_WIDTH;
ZL::Environment::height = CONST_HEIGHT;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
SDL_Log("SDL init failed: %s", SDL_GetError());
return 1;
}
#endif
#ifdef EMSCRIPTEN
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_Window* win = SDL_CreateWindow("Space Ship Game",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
ZL::Environment::width, ZL::Environment::height,
SDL_WINDOW_OPENGL);
if (!win) {
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_GLContext glContext = SDL_GL_CreateContext(win);
if (!glContext) {
std::cerr << "SDL_GL_CreateContext failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_GL_MakeCurrent(win, glContext);
ZL::Environment::window = win;
#else
#ifdef __ANDROID__
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
ZL::Environment::window = SDL_CreateWindow(
"Space Ship Game",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
ZL::Environment::width, ZL::Environment::height,
SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
);
#else
// Для десктопа
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
ZL::Environment::window = SDL_CreateWindow(
"Space Ship Game",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
ZL::Environment::width, ZL::Environment::height,
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
);
#endif
if (!ZL::Environment::window) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError());
#else
SDL_Log("Failed to create window: %s", SDL_GetError());
#endif
SDL_Quit();
return 1;
}
#ifdef __ANDROID__
#endif
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
if (!ctx) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError());
#else
SDL_Log("SDL_GL_CreateContext failed: %s", SDL_GetError());
#endif
SDL_DestroyWindow(ZL::Environment::window);
SDL_Quit();
return 1;
}
if (SDL_GL_MakeCurrent(ZL::Environment::window, ctx) != 0) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_MakeCurrent failed: %s", SDL_GetError());
#else
SDL_Log("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
#endif
SDL_GL_DeleteContext(ctx);
SDL_DestroyWindow(ZL::Environment::window);
SDL_Quit();
return 1;
}
// Настройка VSync
if (SDL_GL_SetSwapInterval(1) < 0) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_WARN, "Game", "Unable to set VSync: %s", SDL_GetError());
#endif
}
#endif
// Настройка игры
try {
game.setup();
} catch (const std::exception &e) {
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "Game", "Game setup failed: %s", e.what());
#else
std::cerr << "Game setup failed: " << e.what() << std::endl;
#endif
return 1;
}
#ifdef EMSCRIPTEN
emscripten_set_main_loop(MainLoop, 0, 1);
#else
bool running = true;
while (running && !game.shouldExit()) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
#ifdef __ANDROID__
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
running = false;
}
#endif
}
game.update();
#ifdef __ANDROID__
SDL_Delay(16);
#else
SDL_Delay(2);
#endif
}
#endif
return 0;
}
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

@@ -1,7 +0,0 @@
precision mediump float;
varying vec3 color;
void main()
{
gl_FragColor = vec4(color, 1.0);
}
@@ -1,9 +0,0 @@
//precision mediump float;
varying vec3 color;
void main()
{
//gl_FragColor = vec4(color, 1.0);
gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0);
}
@@ -1,9 +0,0 @@
precision mediump float;
varying vec3 color;
void main()
{
//gl_FragColor = vec4(color, 1.0);
gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0);
}
@@ -1,12 +0,0 @@
//precision mediump float;
uniform sampler2D Texture;
varying vec2 texCoord;
void main()
{
vec4 color = texture2D(Texture,texCoord).rgba;
gl_FragColor = vec4(color.rgb*0.9 + vec3(0.1, 0.1, 0.1), 1.0);
//gl_FragColor = color;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
import bpy
def append_layered_action_5_0(source_obj_name, target_obj_name):
source_obj = bpy.data.objects.get(source_obj_name)
target_obj = bpy.data.objects.get(target_obj_name)
if not (source_obj and target_obj):
print("Ошибка: Объекты не найдены")
return
src_action = source_obj.animation_data.action
tgt_action = target_obj.animation_data.action
# 1. Получаем слои (обычно первый)
src_layer = src_action.layers[0]
tgt_layer = tgt_action.layers[0]
# 2. Получаем стрипы
src_strip = src_layer.strips[0]
tgt_strip = tgt_layer.strips[0]
# Смещение (опираемся на конец диапазона целевого экшена)
offset = tgt_action.frame_range[1]
# 3. Итерируемся по channelbags в исходном стрипе
for src_bag in src_strip.channelbags:
# Ищем или создаем соответствующий bag в целевом стрипе
# Обычно они сопоставляются по имени или типу (например, 'Keyframe Channel Bag')
# В простейшем случае берем первый или сопоставляем по индексу
dst_bag = None
if len(tgt_strip.channelbags) > 0:
# Пытаемся найти по названию (если оно есть) или берем тот же индекс
dst_bag = tgt_strip.channelbags[0]
if not dst_bag:
# Если в целевом стрипе нет сумок, это странно, но можно создать
# (Метод создания может зависеть от конкретного подтипа стрипа в 5.0)
continue
#print(f"Обработка channelbag: {src_bag.name}, кривых: {len(src_bag.fcurves)}")
# 4. Итерируемся по fcurves внутри сумки
for src_fcurve in src_bag.fcurves:
dst_fcurve = dst_bag.fcurves.find(src_fcurve.data_path, index=src_fcurve.array_index)
if not dst_fcurve:
dst_fcurve = dst_bag.fcurves.new(data_path=src_fcurve.data_path, index=src_fcurve.array_index)
# 5. Копируем ключи с офсетом
for keyframe in src_fcurve.keyframe_points:
new_frame = keyframe.co[0] + offset
new_value = keyframe.co[1]
new_kp = dst_fcurve.keyframe_points.insert(new_frame, new_value, options={'FAST'})
new_kp.interpolation = keyframe.interpolation
# Обновляем интерполяцию
for fc in dst_bag.fcurves:
fc.update()
print(f"Анимация успешно дозаписана. Новый конец: {tgt_action.frame_range[1]}")
append_layered_action_5_0('Armature.001', 'Armature')
+192
View File
@@ -0,0 +1,192 @@
import bpy
import bmesh
# Имена mesh и арматуры
mesh_name = "arm"
armature_name = "Reference"
# Находим объект mesh по имени
mesh_obj = bpy.data.objects.get(mesh_name)
# Находим объект арматуры по имени
armature_obj = bpy.data.objects.get(armature_name)
# Устанавливаем текущий кадр на 0
bpy.context.scene.frame_set(0)
# Принудительно обновляем сцену, чтобы применить анимацию
bpy.context.view_layer.update()
# Открываем файл для записи
with open("C:\\Work\\Projects\\witcher001\\resources\\w\\zombie002.txt", "w") as file:
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
file.write("=== Armature Matrix ===\n")
for row in armature_obj.matrix_world:
file.write(f"{row}\n")
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
for bone in armature_obj.data.bones:
# Записываем имя кости, длину и связи
file.write(f"Bone: {bone.name}\n")
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
for row in bone.matrix:
file.write(f" {row}\n")
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
file.write(f" Children: {[child.name for child in bone.children]}\n")
# Обработка mesh
if mesh_obj and mesh_obj.type == 'MESH':
# Создаем копию mesh, чтобы не изменять оригинал
mesh_copy = mesh_obj.copy()
mesh_copy.data = mesh_obj.data.copy()
bpy.context.collection.objects.link(mesh_copy)
# Убедимся, что объект активен
bpy.context.view_layer.objects.active = mesh_copy
mesh_copy.select_set(True)
# Применяем модификатор Armature (если он есть)
for modifier in mesh_copy.modifiers:
if modifier.type == 'ARMATURE':
# Включаем модификатор, если он отключен
if not modifier.show_viewport:
modifier.show_viewport = True
if not modifier.show_render:
modifier.show_render = True
# Проверяем, что модификатор связан с арматурой
if modifier.object is None:
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
continue
# Временно применяем модификатор, чтобы получить правильные координаты вершин
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except RuntimeError as e:
print(f"Ошибка при применении модификатора Armature: {e}")
continue
# Переходим в режим редактирования
bpy.ops.object.mode_set(mode='EDIT')
# Получаем BMesh представление mesh
bm = bmesh.from_edit_mesh(mesh_copy.data)
# Записываем список вершин
file.write(f"===Vertices: {len(bm.verts)}\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
# Убедимся, что у меша есть UV слой
uv_layer = bm.loops.layers.uv.active
if not uv_layer:
file.write("UV слой не найден.\n")
if uv_layer:
file.write(f"===UV Coordinates:\n")
file.write(f"Face count: {len(bm.faces)}\n")
for face in bm.faces:
file.write(f"Face {face.index}\n")
file.write(f"UV Count: {len(face.loops)}\n")
for loop in face.loops:
uv_coords = loop[uv_layer].uv
file.write(f" UV {uv_coords}\n")
# Записываем нормали
file.write(f"===Normals:\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
# Записываем треугольники (индексы вершин)
file.write(f"===Triangles: {len(bm.faces)}\n")
for face in bm.faces:
if len(face.verts) == 3: # Проверяем, что это треугольник
verts_indices = [vert.index for vert in face.verts]
file.write(f"Triangle: {verts_indices}\n")
# Возвращаемся в объектный режим
bpy.ops.object.mode_set(mode='OBJECT')
# Записываем веса вершин
file.write("=== Vertex Weights ===\n")
for vertex in mesh_copy.data.vertices:
file.write(f"Vertex {vertex.index}:\n")
file.write(f"Vertex groups: {len(vertex.groups)}\n")
for group in vertex.groups:
group_name = mesh_copy.vertex_groups[group.group].name
file.write(f" Group: '{group_name}', Weight: {group.weight}\n")
# Удаляем временную копию mesh
bpy.data.objects.remove(mesh_copy)
else:
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
# Получаем все ключевые кадры для арматуры
file.write("=== Animation Keyframes ===\n")
if armature_obj.animation_data and armature_obj.animation_data.action:
action = armature_obj.animation_data.action
# Собираем все уникальные ключевые кадры
keyframes = set()
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
if hasattr(action, "layers"):
for layer in action.layers:
if hasattr(layer, "strips"):
for strip in layer.strips:
# Проверяем наличие channelbags (согласно вашему dir(strip))
if hasattr(strip, "channelbags"):
for bag in strip.channelbags:
for fcurve in bag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# На случай, если в этой версии используется единственное число
elif hasattr(strip, "channelbag") and strip.channelbag:
for fcurve in strip.channelbag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# Фоллбек для Legacy экшенов
if not keyframes and hasattr(action, "fcurves"):
for fcurve in action.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
keyframes = sorted(keyframes)
# Сортируем ключевые кадры
keyframes = sorted(keyframes)
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
file.write("=== Bone Transforms per Keyframe ===\n")
file.write(f"Keyframes: {len(keyframes)}\n")
for frame in keyframes:
# Устанавливаем текущий кадр
bpy.context.scene.frame_set(frame)
bpy.context.view_layer.update() # Обновляем сцену
file.write(f"Frame: {frame}\n")
for bone in armature_obj.pose.bones:
# Получаем координаты и матрицу поворота кости в мировом пространстве
matrix = bone.matrix
location = matrix.translation
rotation = matrix.to_euler()
# Записываем данные
file.write(f" Bone: {bone.name}\n")
file.write(f" Location: {location}\n")
file.write(f" Rotation: {rotation}\n")
file.write(f" Matrix:\n")
for row in matrix:
file.write(f" {row}\n")
else:
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")
+228
View File
@@ -0,0 +1,228 @@
import bpy
import bmesh
#!
# Имена mesh и арматуры
mesh_name = "Joined"
armature_name = "Armature"
# Находим объект mesh по имени
mesh_obj = bpy.data.objects.get(mesh_name)
# Находим объект арматуры по имени
armature_obj = bpy.data.objects.get(armature_name)
# Устанавливаем текущий кадр на 0
bpy.context.scene.frame_set(0)
# Принудительно обновляем сцену, чтобы применить анимацию
bpy.context.view_layer.update()
# Открываем файл для записи
with open("C:\\Work\\Media\\witcher\\2026-04-13\\output\\gg_stand_idle001.txt", "w") as file:
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
file.write("=== Armature Matrix ===\n")
for row in armature_obj.matrix_world:
file.write(f"{row}\n")
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
for bone in armature_obj.data.bones:
# Записываем имя кости, длину и связи
file.write(f"Bone: {bone.name}\n")
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
for row in bone.matrix:
file.write(f" {row}\n")
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
file.write(f" Children: {[child.name for child in bone.children]}\n")
# Обработка mesh
if mesh_obj and mesh_obj.type == 'MESH':
# Создаем копию mesh, чтобы не изменять оригинал
mesh_copy = mesh_obj.copy()
mesh_copy.data = mesh_obj.data.copy()
bpy.context.collection.objects.link(mesh_copy)
# Убедимся, что объект активен
bpy.context.view_layer.objects.active = mesh_copy
mesh_copy.select_set(True)
# Применяем модификатор Armature (если он есть)
for modifier in mesh_copy.modifiers:
if modifier.type == 'ARMATURE':
# Включаем модификатор, если он отключен
if not modifier.show_viewport:
modifier.show_viewport = True
if not modifier.show_render:
modifier.show_render = True
# Проверяем, что модификатор связан с арматурой
if modifier.object is None:
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
continue
# Временно применяем модификатор, чтобы получить правильные координаты вершин
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except RuntimeError as e:
print(f"Ошибка при применении модификатора Armature: {e}")
continue
# Переходим в режим редактирования
bpy.ops.object.mode_set(mode='EDIT')
# Получаем BMesh представление mesh
bm = bmesh.from_edit_mesh(mesh_copy.data)
# Записываем список вершин
file.write(f"===Vertices: {len(bm.verts)}\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
# Убедимся, что у меша есть UV слой
uv_layer = bm.loops.layers.uv.active
if not uv_layer:
file.write("UV слой не найден.\n")
if uv_layer:
file.write(f"===UV Coordinates:\n")
file.write(f"Face count: {len(bm.faces)}\n")
for face in bm.faces:
file.write(f"Face {face.index}\n")
file.write(f"UV Count: {len(face.loops)}\n")
for loop in face.loops:
uv_coords = loop[uv_layer].uv
file.write(f" UV {uv_coords}\n")
# Записываем нормали
file.write(f"===Normals:\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
# Записываем треугольники (индексы вершин)
file.write(f"===Triangles: {len(bm.faces)}\n")
for face in bm.faces:
if len(face.verts) == 3: # Проверяем, что это треугольник
verts_indices = [vert.index for vert in face.verts]
file.write(f"Triangle: {verts_indices}\n")
# Возвращаемся в объектный режим
bpy.ops.object.mode_set(mode='OBJECT')
# Записываем веса вершин
file.write("=== Vertex Weights (Max 5 bones per vertex) ===\n")
MAX_BONES = 5
for vertex in mesh_copy.data.vertices:
# Извлекаем все группы и веса для текущей вершины
all_weights = []
for group_element in vertex.groups:
all_weights.append({
'index': group_element.group,
'weight': group_element.weight
})
# Если костей больше лимита, фильтруем и перераспределяем
if len(all_weights) > MAX_BONES:
# Сортируем по весу (от большего к меньшему)
all_weights.sort(key=lambda x: x['weight'], reverse=True)
# Берем только топ-5
kept_weights = all_weights[:MAX_BONES]
# Считаем сумму весов оставшихся костей для нормализации
total_weight = sum(gw['weight'] for gw in kept_weights)
if total_weight > 0:
for gw in kept_weights:
gw['weight'] /= total_weight
else:
# На случай, если у всех веса были по 0.0 (редкий баг меша)
kept_weights[0]['weight'] = 1.0
final_weights = kept_weights
else:
final_weights = all_weights
file.write(f"Vertex {vertex.index}:\n")
file.write(f"Vertex groups: {len(final_weights)}\n")
for gw in final_weights:
group_name = mesh_copy.vertex_groups[gw['index']].name
file.write(f" Group: '{group_name}', Weight: {gw['weight']:.6f}\n")
# Удаляем временную копию mesh
bpy.data.objects.remove(mesh_copy)
else:
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
# Получаем все ключевые кадры для арматуры
file.write("=== Animation Keyframes ===\n")
if armature_obj.animation_data and armature_obj.animation_data.action:
action = armature_obj.animation_data.action
# Собираем все уникальные ключевые кадры
keyframes = set()
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
if hasattr(action, "layers"):
for layer in action.layers:
if hasattr(layer, "strips"):
for strip in layer.strips:
# Проверяем наличие channelbags (согласно вашему dir(strip))
if hasattr(strip, "channelbags"):
for bag in strip.channelbags:
for fcurve in bag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# На случай, если в этой версии используется единственное число
elif hasattr(strip, "channelbag") and strip.channelbag:
for fcurve in strip.channelbag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# Фоллбек для Legacy экшенов
if not keyframes and hasattr(action, "fcurves"):
for fcurve in action.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
keyframes = sorted(keyframes)
# Сортируем ключевые кадры
keyframes = sorted(keyframes)
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
file.write("=== Bone Transforms per Keyframe ===\n")
file.write(f"Keyframes: {len(keyframes)}\n")
for frame in keyframes:
# Устанавливаем текущий кадр
bpy.context.scene.frame_set(frame)
bpy.context.view_layer.update() # Обновляем сцену
file.write(f"Frame: {frame}\n")
for bone in armature_obj.pose.bones:
# Получаем координаты и матрицу поворота кости в мировом пространстве
matrix = bone.matrix
location = matrix.translation
rotation = matrix.to_euler()
# Записываем данные
file.write(f" Bone: {bone.name}\n")
file.write(f" Location: {location}\n")
file.write(f" Rotation: {rotation}\n")
file.write(f" Matrix:\n")
for row in matrix:
file.write(f" {row}\n")
else:
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")
+111
View File
@@ -0,0 +1,111 @@
import bpy
import bmesh
import mathutils
import random
import math
class SolidTreeGenerator:
def __init__(self, levels=5, length=3.0, radius=0.3):
self.levels = levels
self.base_length = length
self.base_radius = radius
# Хранилище для данных: (start_pos, end_pos, radius_start, radius_end)
self.branches_data = []
def calculate_tree(self, start_pos, direction, length, radius, level):
if level <= 0 or length < 0.1:
return
end_pos = start_pos + direction * length
# Сохраняем данные сегмента
self.branches_data.append({
'start': start_pos.copy(),
'end': end_pos.copy(),
'r_start': radius,
'r_end': radius * 0.7
})
# 1. Основной ствол (продолжение)
trunk_dir = (direction + self.get_random_vector(0.1)).normalized()
self.calculate_tree(end_pos, trunk_dir, length * 0.8, radius * 0.7, level - 1)
# 2. Боковые ветки (ветвление)
if level > 1:
num_sides = random.randint(2, 3) # Минимум 2 ветки для видимости
for _ in range(num_sides):
# Создаем вектор, сильно отклоненный от ствола (30-60 градусов)
axis = self.get_random_vector(1.0).normalized()
angle = math.radians(random.uniform(30, 60))
side_dir = direction.copy()
side_dir.rotate(mathutils.Quaternion(axis, angle))
# Боковые ветки короче
self.calculate_tree(end_pos, side_dir, length * 0.6, radius * 0.5, level - 1)
def get_random_vector(self, intensity):
return mathutils.Vector((
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity)
))
def build_mesh(self):
mesh = bpy.data.meshes.new("TreeMesh")
obj = bpy.data.objects.new("Tree", mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
skin_layer = bm.verts.layers.skin.verify()
# Словарь для предотвращения дублирования вершин в одной точке
# Ключ - кортеж координат, Значение - объект вершины BMesh
vert_map = {}
for b in self.branches_data:
# Превращаем координаты в кортежи для словаря
s_key = tuple(round(v, 4) for v in b['start'])
e_key = tuple(round(v, 4) for v in b['end'])
# Получаем или создаем начальную вершину
if s_key not in vert_map:
v_start = bm.verts.new(b['start'])
v_start[skin_layer].radius = (b['r_start'], b['r_start'])
vert_map[s_key] = v_start
else:
v_start = vert_map[s_key]
# Получаем или создаем конечную вершину
if e_key not in vert_map:
v_end = bm.verts.new(b['end'])
v_end[skin_layer].radius = (b['r_end'], b['r_end'])
vert_map[e_key] = v_end
else:
v_end = vert_map[e_key]
# Создаем ребро, если его еще нет
if not bm.edges.get((v_start, v_end)):
bm.edges.new((v_start, v_end))
# Находим корень (самую нижнюю точку) и помечаем его
root_v = min(bm.verts, key=lambda v: v.co.z)
root_v[skin_layer].use_root = True
bm.to_mesh(mesh)
bm.free()
# Модификаторы
obj.modifiers.new(name="Skin", type='SKIN')
sub = obj.modifiers.new(name="Subdiv", type='SUBSURF')
sub.levels = 1 # Для начала 1, чтобы не тормозило
# Очистка сцены
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Запуск
generator = SolidTreeGenerator(levels=5, length=3.0, radius=0.4)
generator.calculate_tree(mathutils.Vector((0,0,0)), mathutils.Vector((0,0,1)), 3.0, 0.4, 5)
generator.build_mesh()
+86
View File
@@ -0,0 +1,86 @@
import bpy
import bmesh
mesh_name = "chassis_low"
output_path = "C:\\Work\\Projects\\space-game001\\blender scripts\\output\\spaceship005.txt"
mesh_obj = bpy.data.objects.get(mesh_name)
if mesh_obj and mesh_obj.type == 'MESH':
# Обязательно применяем трансформации, чтобы координаты были актуальными
# Но для чистоты эксперимента берем mesh data как есть
# Работаем с копией меша, чтобы применить триангуляцию (если нужно)
# и не испортить сцену, но пока используем bmesh напрямую
bm = bmesh.new()
bm.from_mesh(mesh_obj.data)
# Получаем слой UV
uv_layer = bm.loops.layers.uv.active
# Словарь для хранения уникальных вершин:
# Ключ: (x, y, z, nx, ny, nz, u, v) -> Значение: новый_индекс
unique_verts_map = {}
final_vertices = [] # Список для записи в файл
final_indices = [] # Список индексов треугольников
# Проходим по всем фейсам
for face in bm.faces:
face_indices = []
# Проходим по углам (loops) фейса
for loop in face.loops:
vert = loop.vert
# 1. Координаты (округляем для надежности сравнения float)
co = (round(vert.co.x, 6), round(vert.co.y, 6), round(vert.co.z, 6))
# 2. Нормаль (если используете Smooth shading, берите vert.normal, если Flat - face.normal)
# Для простоты берем нормаль вершины
no = (round(vert.normal.x, 6), round(vert.normal.y, 6), round(vert.normal.z, 6))
# 3. UV координаты
if uv_layer:
raw_uv = loop[uv_layer].uv
uv = (round(raw_uv.x, 6), round(raw_uv.y, 6))
else:
uv = (0.0, 0.0)
# Собираем уникальный ключ данных вершины
vert_data_key = (co, no, uv)
# Проверяем, есть ли такая комбинация уже
if vert_data_key in unique_verts_map:
index = unique_verts_map[vert_data_key]
else:
index = len(final_vertices)
unique_verts_map[vert_data_key] = index
final_vertices.append(vert_data_key)
face_indices.append(index)
# Триангуляция "на лету" (если фейс - квадрат, делим на два треугольника)
# Простейший метод fan (0, 1, 2), (0, 2, 3)...
for i in range(1, len(face_indices) - 1):
final_indices.append(face_indices[0])
final_indices.append(face_indices[i])
final_indices.append(face_indices[i+1])
# --- ЗАПИСЬ В ФАЙЛ ---
with open(output_path, "w") as file:
file.write(f"===Vertices (Split by UV/Normal): {len(final_vertices)}\n")
# Формат строки: ID: X Y Z | NX NY NZ | U V
for idx, v_data in enumerate(final_vertices):
co, no, uv = v_data
file.write(f"V {idx}: Pos({co[0]}, {co[1]}, {co[2]}) Norm({no[0]}, {no[1]}, {no[2]}) UV({uv[0]}, {uv[1]})\n")
file.write(f"\n===Triangles (Indices): {len(final_indices) // 3}\n")
# Записываем тройками
for i in range(0, len(final_indices), 3):
file.write(f"Tri: {final_indices[i]} {final_indices[i+1]} {final_indices[i+2]}\n")
bm.free()
print(f"Export done. Original verts: {len(mesh_obj.data.vertices)}, Split verts: {len(final_vertices)}")
else:
print("Mesh not found")
+58
View File
@@ -0,0 +1,58 @@
# cmake/FetchDependencies.cmake
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
if(NOT EXISTS "${THIRDPARTY_DIR}")
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
endif()
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
if(NOT EXISTS "${ARCHIVE_PATH}")
message(STATUS "Downloading ${ARCHIVE_NAME}...")
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
endif()
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
message(STATUS "Extracting ${ARCHIVE_NAME}...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
)
endif()
endmacro()
# 1) ZLIB (Нужна только для инклудов, если не используете emscripten порты)
check_and_download("https://www.zlib.net/zlib132.zip" "zlib132.zip" "zlib-1.3.2" "CMakeLists.txt")
# 2) SDL2
check_and_download("https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.10.zip" "release-2.32.10.zip" "SDL-release-2.32.10" "CMakeLists.txt")
# 3) LibPNG
check_and_download("https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.51.zip" "v1.6.51.zip" "libpng-1.6.51" "CMakeLists.txt")
# 4) LibZip
check_and_download("https://github.com/nih-at/libzip/archive/refs/tags/v1.11.4.zip" "v1.11.4.zip" "libzip-1.11.4" "CMakeLists.txt")
# 5) Eigen
check_and_download("https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip" "eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
# 6) Boost
check_and_download("https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip" "boost_1_90_0.zip" "boost_1_90_0" "boost")
# 7) FreeType
check_and_download("https://download.savannah.gnu.org/releases/freetype/freetype-2.14.1.tar.gz" "freetype-2.14.1.tar.gz" "freetype-2.14.1" "CMakeLists.txt")
# 8) SDL_ttf
check_and_download("https://github.com/libsdl-org/SDL_ttf/archive/refs/tags/release-2.24.0.zip" "release-2.24.0.zip" "SDL_ttf-release-2.24.0" "CMakeLists.txt")
# 9) Lua
check_and_download("https://github.com/lua/lua/archive/refs/tags/v5.4.8.zip" "lua-v5.4.8.zip" "lua-5.4.8" "lapi.c")
# 10) sol2 (header-only C++ bindings for Lua)
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
# 11) SDL2_mixer
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
+28
View File
@@ -0,0 +1,28 @@
# cmake/FetchDependenciesLinux.cmake
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
if(NOT EXISTS "${THIRDPARTY_DIR}")
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
endif()
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
if(NOT EXISTS "${ARCHIVE_PATH}")
message(STATUS "Downloading ${ARCHIVE_NAME}...")
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
endif()
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
message(STATUS "Extracting ${ARCHIVE_NAME}...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
)
endif()
endmacro()
# 1) sol2 (header-only C++ bindings for Lua)
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
+728
View File
@@ -0,0 +1,728 @@
# cmake/ThirdParty.cmake
include("${CMAKE_CURRENT_LIST_DIR}/FetchDependencies.cmake")
macro(log msg)
message(STATUS "[ThirdParty] ${msg}")
endmacro()
set(BUILD_CONFIGS Debug Release)
# Map MinSizeRel and RelWithDebInfo to Release libs for all imported targets.
# Without this CMake warns that IMPORTED_LOCATION is missing for those configs.
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL Release)
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release)
# ===========================================
# 1) ZLIB (zlib131.zip → zlib-1.3.2) - без изменений
# ===========================================
set(ZLIB_SRC_DIR "${THIRDPARTY_DIR}/zlib-1.3.2")
set(ZLIB_BUILD_DIR "${ZLIB_SRC_DIR}/build")
set(ZLIB_INSTALL_DIR "${ZLIB_SRC_DIR}/install")
file(MAKE_DIRECTORY "${ZLIB_BUILD_DIR}")
# проверяем, собран ли уже zlib
set(_have_zlib FALSE)
foreach(candidate
"${ZLIB_INSTALL_DIR}/lib/zlibstatic.lib"
)
if(EXISTS "${candidate}")
set(_have_zlib TRUE)
break()
endif()
endforeach()
if(NOT _have_zlib)
log("Configuring zlib ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${ZLIB_SRC_DIR}"
-B "${ZLIB_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _zlib_cfg_res
)
if(NOT _zlib_cfg_res EQUAL 0)
message(FATAL_ERROR "zlib configure failed")
endif()
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_build_res
)
if(NOT _zlib_build_res EQUAL 0)
message(FATAL_ERROR "ZLIB build failed for configuration ${cfg}")
endif()
log("Installing ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_inst_res
)
if(NOT _zlib_inst_res EQUAL 0)
message(FATAL_ERROR "ZLIB install failed for configuration ${cfg}")
endif()
endforeach()
endif()
# ИСПРАВЛЕНИЕ: Используем свойства для конкретных конфигураций
add_library(zlib_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(zlib_external_lib PROPERTIES
# Динамическая линковка (если zlib.lib - это импорт-библиотека для zlibd.dll)
#IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zlibd.lib"
#IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zlib.lib"
# Можно также указать статические библиотеки, если вы хотите их использовать
IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zsd.lib"
IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zs.lib"
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INSTALL_DIR}/include"
)
# ===========================================
# 2) SDL2 (release-2.32.10.zip → SDL-release-2.32.10) - без изменений
# ===========================================
set(SDL2_SRC_DIR "${THIRDPARTY_DIR}/SDL-release-2.32.10")
set(SDL2_BUILD_DIR "${SDL2_SRC_DIR}/build")
set(SDL2_INSTALL_DIR "${SDL2_SRC_DIR}/install")
file(MAKE_DIRECTORY "${SDL2_BUILD_DIR}")
set(_have_sdl2 FALSE)
foreach(candidate
"${SDL2_INSTALL_DIR}/lib/SDL2.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2-static.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
)
if(EXISTS "${candidate}")
set(_have_sdl2 TRUE)
break()
endif()
endforeach()
if(NOT _have_sdl2)
log("Configuring SDL2 ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2_SRC_DIR}"
-B "${SDL2_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${SDL2_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR} # путь к zlib для SDL2
RESULT_VARIABLE _sdl_cfg_res
)
if(NOT _sdl_cfg_res EQUAL 0)
message(FATAL_ERROR "SDL2 configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_build_res
)
if(NOT _sdl_build_res EQUAL 0)
message(FATAL_ERROR "SDL2 build failed for configuration ${cfg}")
endif()
log("Installing SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_inst_res
)
if(NOT _sdl_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2 install failed for configuration ${cfg}")
endif()
endforeach()
# ------------------------------------------------------
endif()
# ИСПРАВЛЕНИЕ: SDL2: Используем свойства для конкретных конфигураций
add_library(SDL2_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_external_lib PROPERTIES
# Динамическая линковка SDL2
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2.lib"
# Оба include-пути: и include, и include/SDL2
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include;${SDL2_INSTALL_DIR}/include/SDL2"
)
# SDL2main (обычно статическая)
add_library(SDL2main_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2main_external_lib PROPERTIES
# ИСПРАВЛЕНО: Указываем пути для Debug и Release, используя
# соглашение, что Debug имеет суффикс 'd', а Release — нет.
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2maind.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2main.lib"
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include"
)
log("-----${SDL2_INSTALL_DIR}/lib/SDL2maind.lib")
# ===========================================
# 3) libpng (v1.6.51.zip → libpng-1.6.51) - без изменений
# ===========================================
set(LIBPNG_SRC_DIR "${THIRDPARTY_DIR}/libpng-1.6.51")
set(LIBPNG_BUILD_DIR "${LIBPNG_SRC_DIR}/build")
set(LIBPNG_INSTALL_DIR "${LIBPNG_SRC_DIR}/install") # на будущее
file(MAKE_DIRECTORY "${LIBPNG_BUILD_DIR}")
# Проверяем, есть ли уже .lib (build/Debug или install/lib)
set(_libpng_candidates
"${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
"${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
)
set(_have_png FALSE)
foreach(candidate IN LISTS _libpng_candidates)
if(EXISTS "${candidate}")
set(_have_png TRUE)
break()
endif()
endforeach()
if(NOT _have_png)
log("Configuring libpng ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBPNG_SRC_DIR}"
-B "${LIBPNG_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${LIBPNG_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _png_cfg_res
)
if(NOT _png_cfg_res EQUAL 0)
message(FATAL_ERROR "libpng configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building libpng (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${LIBPNG_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _png_build_res
)
if(NOT _png_build_res EQUAL 0)
message(FATAL_ERROR "libpng build failed for configuration ${cfg}")
endif()
# Поскольку вы не используете "cmake --install" для libpng,
# здесь нет необходимости в дополнительном шаге установки.
# Файлы .lib будут сгенерированы в подкаталоге ${LIBPNG_BUILD_DIR}/${cfg} (например, build/Debug или build/Release).
endforeach()
# ------------------------------------------------------
endif()
add_library(libpng_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libpng_external_lib PROPERTIES
# Предполагая, что libpng использует статический вариант
IMPORTED_LOCATION_DEBUG "${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
IMPORTED_LOCATION_RELEASE "${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
# png.h, pngconf.h – в SRC, pnglibconf.h – в BUILD
INTERFACE_INCLUDE_DIRECTORIES "${LIBPNG_SRC_DIR};${LIBPNG_BUILD_DIR}"
)
# ===========================================
# 4) libzip (v1.11.4.zip → libzip-1.11.4) - НОВАЯ ЗАВИСИМОСТЬ
# ===========================================
set(LIBZIP_SRC_DIR "${THIRDPARTY_DIR}/libzip-1.11.4")
set(LIBZIP_BUILD_DIR "${LIBZIP_SRC_DIR}/build")
#set(LIBZIP_INSTALL_DIR "${LIBZIP_SRC_DIR}/install")
set(LIBZIP_BASE_DIR "${LIBZIP_SRC_DIR}/install")
file(MAKE_DIRECTORY "${LIBZIP_BUILD_DIR}")
# Проверяем, собран ли уже libzip
set(_have_zip FALSE)
foreach(candidate
"${LIBZIP_BASE_DIR}-Debug/lib/zip.lib"
"${LIBZIP_BASE_DIR}-Release/lib/zip.lib"
)
if(EXISTS "${candidate}")
set(_have_zip TRUE)
break()
endif()
endforeach()
if(NOT _have_zip)
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Configuring libzip (${cfg})...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBZIP_SRC_DIR}"
-B "${LIBZIP_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${LIBZIP_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
-DENABLE_COMMONCRYPTO=OFF
-DENABLE_GNUTLS=OFF
-DENABLE_MBEDTLS=OFF
-DENABLE_OPENSSL=OFF
-DENABLE_WINDOWS_CRYPTO=OFF
-DENABLE_FUZZ=OFF
RESULT_VARIABLE _zip_cfg_res
)
if(NOT _zip_cfg_res EQUAL 0)
message(FATAL_ERROR "libzip configure failed")
endif()
log("Building libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --build "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_build_res
)
if(NOT _zip_build_res EQUAL 0)
message(FATAL_ERROR "libzip build failed for configuration ${cfg}")
endif()
log("Installing libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --install "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_inst_res
)
if(NOT _zip_inst_res EQUAL 0)
message(FATAL_ERROR "libzip install failed for configuration ${cfg}")
endif()
endforeach()
endif()
add_library(libzip_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libzip_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${LIBZIP_BASE_DIR}-Debug/lib/zip.lib" # ИСПРАВЛЕНО
IMPORTED_LOCATION_RELEASE "${LIBZIP_BASE_DIR}-Release/lib/zip.lib" # ИСПРАВЛЕНО
INTERFACE_INCLUDE_DIRECTORIES "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/include,${LIBZIP_BASE_DIR}-Release/include>"
# libzip требует zlib для линковки
INTERFACE_LINK_LIBRARIES zlib_external_lib
)
# ===========================================
# 5) FreeType (2.14.1) - dependency for SDL_ttf
# ===========================================
set(FREETYPE_SRC_DIR "${THIRDPARTY_DIR}/freetype-2.14.1")
set(FREETYPE_BASE_DIR "${FREETYPE_SRC_DIR}/install")
set(_have_freetype TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetype.lib" AND
NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetyped.lib")
set(_have_freetype FALSE)
endif()
endforeach()
if(NOT _have_freetype)
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Configuring FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${FREETYPE_SRC_DIR}"
-B "${FREETYPE_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${FREETYPE_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH="${ZLIB_INSTALL_DIR};${LIBPNG_INSTALL_DIR}"
-DCMAKE_DISABLE_FIND_PACKAGE_HarfBuzz=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_BZip2=TRUE
-DFT_DISABLE_BROTLI=ON
-DBUILD_SHARED_LIBS=OFF
RESULT_VARIABLE _ft_cfg_res
)
if(NOT _ft_cfg_res EQUAL 0)
message(FATAL_ERROR "FreeType configure failed for ${cfg}")
endif()
log("Building FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ft_build_res
)
if(NOT _ft_build_res EQUAL 0)
message(FATAL_ERROR "FreeType build failed for ${cfg}")
endif()
log("Installing FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ft_inst_res
)
if(NOT _ft_inst_res EQUAL 0)
message(FATAL_ERROR "FreeType install failed for ${cfg}")
endif()
endforeach()
endif()
set(_ft_debug_lib "")
foreach(cand
"${FREETYPE_BASE_DIR}-Debug/lib/freetyped.lib"
"${FREETYPE_BASE_DIR}-Debug/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_ft_debug_lib "${cand}")
break()
endif()
endforeach()
set(_ft_release_lib "")
foreach(cand
"${FREETYPE_BASE_DIR}-Release/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_ft_release_lib "${cand}")
break()
endif()
endforeach()
if(_ft_debug_lib STREQUAL "" OR _ft_release_lib STREQUAL "")
message(FATAL_ERROR "FreeType libs not found in ${FREETYPE_BASE_DIR}-Debug/Release")
endif()
add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(freetype_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
)
target_include_directories(freetype_external_lib INTERFACE
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>"
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
)
# ===========================================
# 6) SDL_ttf (2.24.0)
# ===========================================
set(SDL2TTF_SRC_DIR "${THIRDPARTY_DIR}/SDL_ttf-release-2.24.0")
set(SDL2TTF_BASE_DIR "${SDL2TTF_SRC_DIR}/install")
set(_have_sdl2ttf TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttf.lib" AND
NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttfd.lib")
set(_have_sdl2ttf FALSE)
endif()
endforeach()
if(NOT _have_sdl2ttf)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(cfg STREQUAL "Debug")
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
else()
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
endif()
set(_FT_PREFIX "${FREETYPE_BASE_DIR}-${cfg}")
set(_FT_LIB "")
foreach(cand
"${_FT_PREFIX}/lib/freetyped.lib"
"${_FT_PREFIX}/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_FT_LIB "${cand}")
break()
endif()
endforeach()
if(_FT_LIB STREQUAL "")
message(FATAL_ERROR "FreeType library not found for ${cfg}")
endif()
log("Configuring SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2TTF_SRC_DIR}"
-B "${SDL2TTF_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${SDL2TTF_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${_FT_PREFIX};${SDL2_INSTALL_DIR}
-DSDL2_LIBRARY=${_SDL2_LIB}
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
-DFREETYPE_LIBRARY=${_FT_LIB}
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2
-DSDL2TTF_VENDORED=OFF
-DSDL2TTF_SAMPLES=OFF
RESULT_VARIABLE _ttf_cfg_res
)
if(NOT _ttf_cfg_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf configure failed for ${cfg}")
endif()
log("Building SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ttf_build_res
)
if(NOT _ttf_build_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf build failed for ${cfg}")
endif()
log("Installing SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ttf_inst_res
)
if(NOT _ttf_inst_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf install failed for ${cfg}")
endif()
endforeach()
endif()
set(_ttf_debug_lib "")
foreach(cand
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttfd.lib"
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttf.lib"
)
if(EXISTS "${cand}")
set(_ttf_debug_lib "${cand}")
break()
endif()
endforeach()
set(_ttf_release_lib "")
foreach(cand
"${SDL2TTF_BASE_DIR}-Release/lib/SDL2_ttf.lib"
)
if(EXISTS "${cand}")
set(_ttf_release_lib "${cand}")
break()
endif()
endforeach()
if(_ttf_debug_lib STREQUAL "" OR _ttf_release_lib STREQUAL "")
message(FATAL_ERROR "SDL_ttf libs not found in install-Debug / install-Release")
endif()
add_library(SDL2_ttf_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_ttf_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_ttf_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_ttf_release_lib}"
INTERFACE_INCLUDE_DIRECTORIES
"$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include,${SDL2TTF_BASE_DIR}-Release/include>;$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include/SDL2,${SDL2TTF_BASE_DIR}-Release/include/SDL2>"
INTERFACE_LINK_LIBRARIES
"SDL2_external_lib;freetype_external_lib"
)
# ===========================================
# 7) Eigen (5.0.0.zip → eigen-5.0.0) - HEADER-ONLY
# ===========================================
set(EIGEN_SRC_DIR "${THIRDPARTY_DIR}/eigen-5.0.0")
if(NOT TARGET eigen_external_lib)
add_library(eigen_external_lib INTERFACE)
target_include_directories(eigen_external_lib INTERFACE "${EIGEN_SRC_DIR}")
endif()
# ===========================================
# 8) Boost (1.90.0) - HEADER-ONLY
# ===========================================
set(BOOST_VERSION "1.90.0")
set(BOOST_ARCHIVE_NAME "boost_1_90_0.zip")
set(BOOST_ARCHIVE "${THIRDPARTY_DIR}/${BOOST_ARCHIVE_NAME}")
# Внутри архива папка называется boost_1_90_0
set(BOOST_SRC_DIR "${THIRDPARTY_DIR}/boost_1_90_0")
if(NOT TARGET boost_external_lib)
add_library(boost_external_lib INTERFACE)
# Boost заголовки находятся непосредственно в корне распакованной папки
target_include_directories(boost_external_lib INTERFACE "${BOOST_SRC_DIR}")
endif()
# ===========================================
# 9) Lua (5.5.0) - embedded scripting language
# ===========================================
set(LUA_SRC_DIR "${THIRDPARTY_DIR}/lua-5.4.8")
if(NOT TARGET lua_static)
file(GLOB LUA_SOURCES "${LUA_SRC_DIR}/*.c")
# Exclude the standalone interpreter, compiler, and unity-build wrapper.
# onelua.c #includes all other .c files — compiling it alongside them
# causes every symbol to be defined twice.
list(REMOVE_ITEM LUA_SOURCES
"${LUA_SRC_DIR}/lua.c"
"${LUA_SRC_DIR}/luac.c"
"${LUA_SRC_DIR}/onelua.c"
)
add_library(lua_static STATIC ${LUA_SOURCES})
target_include_directories(lua_static PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(lua_static PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()
# ===========================================
# 10) sol2 (3.3.0) - header-only C++ bindings for Lua
# ===========================================
set(SOL2_SRC_DIR "${THIRDPARTY_DIR}/sol2-3.3.0")
# Apply patch for Clang/Emscripten compatibility in optional<T&>::emplace().
# The sentinel file prevents re-applying on subsequent cmake runs.
set(_sol2_sentinel "${SOL2_SRC_DIR}/.patched")
if(NOT EXISTS "${_sol2_sentinel}")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} apply --ignore-whitespace
"${CMAKE_CURRENT_LIST_DIR}/patches/sol2-3.3.0-clang-optional.patch"
WORKING_DIRECTORY "${SOL2_SRC_DIR}"
RESULT_VARIABLE _sol2_patch_res
)
if(_sol2_patch_res EQUAL 0)
file(WRITE "${_sol2_sentinel}" "patched\n")
message(STATUS "Applied sol2 Clang optional patch")
else()
message(WARNING "sol2 patch failed (exit ${_sol2_patch_res}) — Clang/Emscripten builds may not compile")
endif()
else()
message(WARNING "Git not found — cannot apply sol2 patch automatically. "
"Apply cmake/patches/sol2-3.3.0-clang-optional.patch manually.")
endif()
endif()
if(NOT TARGET sol2_external_lib)
add_library(sol2_external_lib INTERFACE)
target_include_directories(sol2_external_lib INTERFACE "${SOL2_SRC_DIR}/include")
target_link_libraries(sol2_external_lib INTERFACE lua_static)
endif()
# ===========================================
# 11) SDL2_mixer (2.8.0) – сборка из исходников
# ===========================================
set(SDL2MIXER_SRC_DIR "${THIRDPARTY_DIR}/SDL_mixer-release-2.8.0")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_SRC_DIR}/install")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_BASE_DIR}" CACHE PATH "SDL2_mixer install base directory" FORCE)
set(_have_sdl2mixer TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixer.lib" AND
NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixerd.lib")
set(_have_sdl2mixer FALSE)
endif()
endforeach()
if(NOT _have_sdl2mixer)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(cfg STREQUAL "Debug")
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
else()
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
endif()
log("Configuring SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2MIXER_SRC_DIR}"
-B "${SDL2MIXER_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${SDL2MIXER_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${SDL2_INSTALL_DIR}
-DSDL2_LIBRARY=${_SDL2_LIB}
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
-DSDL2MIXER_DEPS_SHARED=OFF
-DSDL2MIXER_VENDORED=ON
-DSDL2MIXER_SAMPLES=OFF
-DSDL2MIXER_MUSIC_CMD=OFF
-DSDL2MIXER_MOD=OFF
-DSDL2MIXER_MIDI=OFF
-DSDL2MIXER_OPUS=OFF
-DSDL2MIXER_WAVPACK=OFF
-DSDL2MIXER_MP3_MPG123=OFF
-DSDL2MIXER_MP3_DRMP3=ON
-DSDL2MIXER_FLAC_DRFLAC=ON
-DSDL2MIXER_OGG_STB=ON
-DCMAKE_DISABLE_FIND_PACKAGE_OGG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_Vorbis=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FLAC=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_MPG123=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_LibModPlug=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FluidLite=TRUE
RESULT_VARIABLE _mixer_cfg_res
OUTPUT_VARIABLE _mixer_cfg_out
ERROR_VARIABLE _mixer_cfg_err
)
if(NOT _mixer_cfg_res EQUAL 0)
message(STATUS "SDL2_mixer configure stdout: ${_mixer_cfg_out}")
message(STATUS "SDL2_mixer configure stderr: ${_mixer_cfg_err}")
message(FATAL_ERROR "SDL2_mixer configure failed for ${cfg}")
endif()
log("Building SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_build_res
)
if(NOT _mixer_build_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer build failed for ${cfg}")
endif()
log("Installing SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_inst_res
)
if(NOT _mixer_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer install failed for ${cfg}")
endif()
endforeach()
endif()
set(_mixer_debug_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixerd.lib"
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_debug_lib "${cand}")
break()
endif()
endforeach()
set(_mixer_release_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Release/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_release_lib "${cand}")
break()
endif()
endforeach()
if(_mixer_debug_lib STREQUAL "" OR _mixer_release_lib STREQUAL "")
message(FATAL_ERROR "SDL2_mixer libs not found in ${SDL2MIXER_BASE_DIR}-Debug/Release")
endif()
add_library(SDL2_mixer_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_mixer_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_mixer_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_mixer_release_lib}"
INTERFACE_INCLUDE_DIRECTORIES
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
INTERFACE_LINK_LIBRARIES
"SDL2_external_lib"
)
@@ -0,0 +1,14 @@
--- a/include/sol/optional_implementation.hpp
+++ b/include/sol/optional_implementation.hpp
@@ -2189,7 +2189,10 @@
template <class... Args>
T& emplace(Args&&... args) noexcept {
static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args");
*this = nullopt;
- this->construct(std::forward<Args>(args)...);
+ // Reference specialization stores a pointer; set it directly.
+ // construct() only exists in the non-reference specialization.
+ m_value = std::addressof(std::forward<Args>(args)...);
+ return *m_value;
}
+323
View File
@@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""
Convert a text-based bone animation file to the BSAF binary format.
Usage:
python convert_anim_to_binary.py <input.txt> <output.bin>
Binary format (BSAF v2) -- all values little-endian:
HEADER
4 bytes magic "BSAF"
uint32 version (2)
BONES
uint32 numBones
per bone:
3 x float boneStartWorld (from HEAD_LOCAL)
float boneLength
9 x float 3x3 rotation matrix (row-major)
int32 parentIndex (-1 if none)
uint32 numChildren
numChildren x int32 childIndices
BONE NAMES (v2+)
per bone:
uint32 nameLen
nameLen bytes UTF-8 name (no terminator)
VERTICES
uint32 numVertices
numVertices x 3 x float positions
UV COORDINATES
uint32 numFaces
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
NORMALS
numVertices x 3 x float normals
TRIANGLES
uint32 numTriangles
numTriangles x 3 x int32 vertex indices
VERTEX WEIGHTS
per vertex (numVertices):
uint32 numGroups
numGroups x (int32 boneIndex, float weight)
ANIMATION KEYFRAMES
uint32 numKeyframes
per keyframe:
int32 frameNumber
per bone (numBones, in index order 0..N-1):
3 x float location
16 x float 4x4 matrix (row-major)
"""
import struct
import re
import sys
def parse_floats(line):
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
def parse_first_int(line):
m = re.search(r'\d+', line)
if m:
return int(m.group())
raise ValueError(f"No integer found in: {line}")
def parse_children(line):
return re.findall(r"'([^']+)'", line)
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
idx = 0
def next_line():
nonlocal idx
line = lines[idx].rstrip()
idx += 1
return line
# --- Skip armature matrix (5 lines) ---
for _ in range(5):
next_line()
# --- Bone count ---
line = next_line() # "=== Armature Bones: 65"
num_bones = parse_first_int(line)
bone_names = []
bones = []
bone_parent_names = []
bone_children_names = []
for _ in range(num_bones):
bone = {}
# "Bone: mixamorig:Hips"
line = next_line()
bone_name = line[6:]
bone_names.append(bone_name)
# " HEAD_LOCAL: <Vector (x, y, z)>"
line = next_line()
bone['head'] = parse_floats(line)[:3]
# " TAIL_LOCAL: ..." -- skip
next_line()
# " Length: 0.123"
line = next_line()
bone['length'] = parse_floats(line)[0]
# 3x3 matrix (3 rows)
mat = []
for _ in range(3):
mat.extend(parse_floats(next_line()))
bone['matrix_3x3'] = mat
# " Parent: None" or " Parent: boneName"
line = next_line()
if line == " Parent: None":
bone_parent_names.append(None)
else:
bone_parent_names.append(line[10:])
# " Children: ['a', 'b'] or []"
line = next_line()
bone_children_names.append(parse_children(line))
bones.append(bone)
# Build name -> index map
name_to_idx = {name: i for i, name in enumerate(bone_names)}
# Resolve parent / child indices
for i in range(num_bones):
if bone_parent_names[i] is None:
bones[i]['parent'] = -1
else:
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
# --- Vertices ---
line = next_line() # "===Vertices: 5140"
num_vertices = parse_first_int(line)
vertices = []
for _ in range(num_vertices):
vertices.append(parse_floats(next_line())[:3])
# --- UV Coordinates ---
next_line() # "===UV Coordinates:"
line = next_line() # "Face count: 8602"
num_faces = parse_first_int(line)
uvs = []
for _ in range(num_faces):
next_line() # "Face N"
next_line() # "UV Count: 3"
face_uvs = []
for _ in range(3):
face_uvs.extend(parse_floats(next_line())[:2])
uvs.append(face_uvs) # 6 floats
# --- Normals ---
next_line() # "===Normals:"
normals = []
for _ in range(num_vertices):
normals.append(parse_floats(next_line())[:3])
# --- Triangles ---
line = next_line() # "===Triangles: 8602"
num_triangles = parse_first_int(line)
triangles = []
for _ in range(num_triangles):
line = next_line()
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
triangles.append(ints[:3])
# --- Vertex Weights ---
next_line() # "=== Vertex Weights ..."
vertex_weights = []
for _ in range(num_vertices):
next_line() # "Vertex N:"
line = next_line() # "Vertex groups: 2"
num_groups = parse_first_int(line)
groups = []
for _ in range(num_groups):
line = next_line()
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
bone_name = m.group(1)
weight = float(m.group(2))
groups.append((name_to_idx[bone_name], weight))
vertex_weights.append(groups)
# --- Animation Keyframes ---
next_line() # "=== Animation Keyframes ==="
next_line() # "=== Bone Transforms per Keyframe ==="
line = next_line() # "Keyframes: 32"
num_keyframes = parse_first_int(line)
keyframes = []
for _ in range(num_keyframes):
line = next_line() # "Frame: 0"
frame_number = parse_first_int(line)
bone_data = {}
for _ in range(num_bones):
line = next_line() # " Bone: mixamorig:Hips"
bone_name = line.strip()
if bone_name.startswith("Bone: "):
bone_name = bone_name[6:]
bone_idx = name_to_idx[bone_name]
# Location
location = parse_floats(next_line())[:3]
# Rotation (skip)
next_line()
# " Matrix:" (skip header)
next_line()
# 4 rows of 4 floats
matrix = []
for _ in range(4):
matrix.extend(parse_floats(next_line()))
bone_data[bone_idx] = {
'location': location,
'matrix': matrix,
}
keyframes.append((frame_number, bone_data))
# ================================================================
# Write binary file
# ================================================================
with open(output_path, 'wb') as out:
# Header
out.write(b'BSAF')
out.write(struct.pack('<I', 2))
# Bones
out.write(struct.pack('<I', num_bones))
for i in range(num_bones):
b = bones[i]
out.write(struct.pack('<3f', *b['head']))
out.write(struct.pack('<f', b['length']))
out.write(struct.pack('<9f', *b['matrix_3x3']))
out.write(struct.pack('<i', b['parent']))
out.write(struct.pack('<I', len(b['children'])))
for c in b['children']:
out.write(struct.pack('<i', c))
# Bone names (v2+)
for name in bone_names:
name_bytes = name.encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Vertices
out.write(struct.pack('<I', num_vertices))
for v in vertices:
out.write(struct.pack('<3f', *v))
# UV Coordinates
out.write(struct.pack('<I', num_faces))
for uv in uvs:
out.write(struct.pack('<6f', *uv))
# Normals
for n in normals:
out.write(struct.pack('<3f', *n))
# Triangles
out.write(struct.pack('<I', num_triangles))
for t in triangles:
out.write(struct.pack('<3i', *t))
# Vertex Weights
for vw in vertex_weights:
out.write(struct.pack('<I', len(vw)))
for bone_idx, weight in vw:
out.write(struct.pack('<if', bone_idx, weight))
# Animation Keyframes
out.write(struct.pack('<I', num_keyframes))
for frame_num, bone_data in keyframes:
out.write(struct.pack('<i', frame_num))
for i in range(num_bones):
bd = bone_data[i]
out.write(struct.pack('<3f', *bd['location']))
out.write(struct.pack('<16f', *bd['matrix']))
input_size = sum(len(l) for l in lines)
import os
output_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
print(f" Bones: {num_bones}, Vertices: {num_vertices}, Faces: {num_faces}, "
f"Triangles: {num_triangles}, Keyframes: {num_keyframes}")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
sys.exit(1)
convert(sys.argv[1], sys.argv[2])
+370
View File
@@ -0,0 +1,370 @@
#!/usr/bin/env python3
"""
Convert a text-based multi-mesh bone animation file to the BSMF binary format.
Usage:
python convert_anim_to_binary_new.py <input.txt> <output.bin>
Binary format (BSMF v2) -- all values little-endian:
HEADER
4 bytes magic "BSMF"
uint32 version (2)
ARMATURE MATRIX
16 x float 4x4 matrix (row-major)
BONES
uint32 numBones
per bone:
3 x float boneStartWorld (from HEAD_LOCAL)
float boneLength
9 x float 3x3 rotation matrix (row-major)
int32 parentIndex (-1 if none)
uint32 numChildren
numChildren x int32 childIndices
BONE NAMES
per bone:
uint32 nameLen
nameLen bytes UTF-8 name (no terminator)
MESHES
uint32 numMeshes
per mesh:
uint32 nameLength
nameLength x char meshName (UTF-8, no null terminator)
VERTICES
uint32 numVertices
numVertices x 3 x float positions
UV COORDINATES
uint32 numFaces
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
NORMALS
numVertices x 3 x float normals
TRIANGLES
uint32 numTriangles
numTriangles x 3 x int32 vertex indices
VERTEX WEIGHTS
per vertex (numVertices):
uint32 numGroups
numGroups x (int32 boneIndex, float weight)
ANIMATION KEYFRAMES
uint32 numKeyframes
per keyframe:
int32 frameNumber
per bone (numBones, in index order 0..N-1):
3 x float location
16 x float 4x4 matrix (row-major)
"""
import struct
import re
import sys
def parse_floats(line):
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
def parse_first_int(line):
m = re.search(r'\d+', line)
if m:
return int(m.group())
raise ValueError(f"No integer found in: {line}")
def parse_children(line):
return re.findall(r"'([^']+)'", line)
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
idx = 0
def next_line():
nonlocal idx
line = lines[idx].rstrip()
idx += 1
return line
# --- Armature matrix header + 4 rows ---
next_line() # "=== Armature Matrix ==="
armature_matrix = []
for _ in range(4):
armature_matrix.extend(parse_floats(next_line())[:4])
# --- Bone count ---
line = next_line() # "=== Armature Bones: 65"
num_bones = parse_first_int(line)
bone_names = []
bones = []
bone_parent_names = []
bone_children_names = []
for _ in range(num_bones):
bone = {}
# "Bone: mixamorig:Hips"
line = next_line()
bone_name = line[6:]
bone_names.append(bone_name)
# " HEAD_LOCAL: <Vector (x, y, z)>"
line = next_line()
bone['head'] = parse_floats(line)[:3]
# " TAIL_LOCAL: ..." -- skip
next_line()
# " Length: 0.123"
line = next_line()
bone['length'] = parse_floats(line)[0]
# 3x3 matrix (3 rows)
mat = []
for _ in range(3):
mat.extend(parse_floats(next_line()))
bone['matrix_3x3'] = mat
# " Parent: None" or " Parent: boneName"
line = next_line()
if line == " Parent: None":
bone_parent_names.append(None)
else:
bone_parent_names.append(line[10:])
# " Children: ['a', 'b'] or []"
line = next_line()
bone_children_names.append(parse_children(line))
bones.append(bone)
# Build name -> index map
name_to_idx = {name: i for i, name in enumerate(bone_names)}
# Resolve parent / child indices
for i in range(num_bones):
if bone_parent_names[i] is None:
bones[i]['parent'] = -1
else:
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
# --- Multi-mesh header ---
line = next_line() # "=== TOTAL MESHES TO EXPORT: 7 ==="
num_meshes = parse_first_int(line)
meshes = []
for _ in range(num_meshes):
# "=== Mesh Object: Name ==="
line = next_line()
m = re.match(r"===\s*Mesh Object:\s*(.+?)\s*===$", line)
if not m:
raise ValueError(f"Invalid mesh header: {line}")
mesh_name = m.group(1)
# --- Vertices ---
line = next_line() # "===Vertices: N"
num_vertices = parse_first_int(line)
vertices = []
for _ in range(num_vertices):
vertices.append(parse_floats(next_line())[:3])
# --- UV Coordinates ---
next_line() # "===UV Coordinates:"
line = next_line() # "Face count: M"
num_faces = parse_first_int(line)
uvs = []
for _ in range(num_faces):
next_line() # "Face N"
next_line() # "UV Count: 3"
face_uvs = []
for _ in range(3):
face_uvs.extend(parse_floats(next_line())[:2])
uvs.append(face_uvs)
# --- Normals ---
next_line() # "===Normals:"
normals = []
for _ in range(num_vertices):
normals.append(parse_floats(next_line())[:3])
# --- Triangles ---
line = next_line() # "===Triangles: M"
num_triangles = parse_first_int(line)
triangles = []
for _ in range(num_triangles):
line = next_line()
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
triangles.append(ints[:3])
# --- Vertex Weights ---
next_line() # "=== Vertex Weights (Max 5 bones per vertex) ==="
vertex_weights = []
for _ in range(num_vertices):
next_line() # "Vertex N:"
line = next_line() # "Vertex groups: K"
num_groups = parse_first_int(line)
groups = []
for _ in range(num_groups):
line = next_line()
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
bone_name = m.group(1)
weight = float(m.group(2))
groups.append((name_to_idx[bone_name], weight))
vertex_weights.append(groups)
meshes.append({
'name': mesh_name,
'num_vertices': num_vertices,
'vertices': vertices,
'num_faces': num_faces,
'uvs': uvs,
'normals': normals,
'num_triangles': num_triangles,
'triangles': triangles,
'vertex_weights': vertex_weights,
})
# --- Animation Keyframes ---
next_line() # "=== Animation Keyframes ==="
next_line() # "=== Bone Transforms per Keyframe ==="
line = next_line() # "Keyframes: N"
num_keyframes = parse_first_int(line)
keyframes = []
for _ in range(num_keyframes):
line = next_line() # "Frame: N"
frame_number = parse_first_int(line)
bone_data = {}
for _ in range(num_bones):
line = next_line() # " Bone: mixamorig:Hips"
bone_name = line.strip()
if bone_name.startswith("Bone: "):
bone_name = bone_name[6:]
bone_idx = name_to_idx[bone_name]
# Location
location = parse_floats(next_line())[:3]
# Rotation (skip)
next_line()
# " Matrix:" (skip header)
next_line()
# 4 rows of 4 floats
matrix = []
for _ in range(4):
matrix.extend(parse_floats(next_line()))
bone_data[bone_idx] = {
'location': location,
'matrix': matrix,
}
keyframes.append((frame_number, bone_data))
# ================================================================
# Write binary file
# ================================================================
with open(output_path, 'wb') as out:
# Header
out.write(b'BSMF')
out.write(struct.pack('<I', 2))
# Armature matrix (16 floats, row-major)
out.write(struct.pack('<16f', *armature_matrix))
# Bones
out.write(struct.pack('<I', num_bones))
for i in range(num_bones):
b = bones[i]
out.write(struct.pack('<3f', *b['head']))
out.write(struct.pack('<f', b['length']))
out.write(struct.pack('<9f', *b['matrix_3x3']))
out.write(struct.pack('<i', b['parent']))
out.write(struct.pack('<I', len(b['children'])))
for c in b['children']:
out.write(struct.pack('<i', c))
# Bone names
for name in bone_names:
name_bytes = name.encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Meshes
out.write(struct.pack('<I', num_meshes))
for md in meshes:
name_bytes = md['name'].encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Vertices
out.write(struct.pack('<I', md['num_vertices']))
for v in md['vertices']:
out.write(struct.pack('<3f', *v))
# UV Coordinates
out.write(struct.pack('<I', md['num_faces']))
for uv in md['uvs']:
out.write(struct.pack('<6f', *uv))
# Normals
for n in md['normals']:
out.write(struct.pack('<3f', *n))
# Triangles
out.write(struct.pack('<I', md['num_triangles']))
for t in md['triangles']:
out.write(struct.pack('<3i', *t))
# Vertex weights
for vw in md['vertex_weights']:
out.write(struct.pack('<I', len(vw)))
for bone_idx, weight in vw:
out.write(struct.pack('<if', bone_idx, weight))
# Animation Keyframes
out.write(struct.pack('<I', num_keyframes))
for frame_num, bone_data in keyframes:
out.write(struct.pack('<i', frame_num))
for i in range(num_bones):
bd = bone_data[i]
out.write(struct.pack('<3f', *bd['location']))
out.write(struct.pack('<16f', *bd['matrix']))
input_size = sum(len(l) for l in lines)
import os
output_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
print(f" Bones: {num_bones}, Meshes: {num_meshes}, Keyframes: {num_keyframes}")
for md in meshes:
print(f" - {md['name']}: {md['num_vertices']} verts, "
f"{md['num_faces']} faces, {md['num_triangles']} tris")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
sys.exit(1)
convert(sys.argv[1], sys.argv[2])
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
convert_config_meshes.py - Bulk-convert 3D mesh files referenced in a game object JSON config.
Reads <src_config>, converts every .txt mesh to binary (.txt.bin via BSMF format),
and writes the updated config to <dst_config> with meshPath values pointing to the
.bin files. Works for both regular game object configs and interactive object configs.
Mesh paths in the JSON are relative to the current working directory — run this
script from the project root.
Usage:
python convert_config_meshes.py <src_config.json> <dst_config.json>
"""
import json
import os
import sys
from convert_model_to_binary import convert
def convert_config(src_path: str, dst_path: str) -> None:
with open(src_path, 'r', encoding='utf-8') as f:
config = json.load(f)
objects = config.get("objects", [])
# Track already-converted paths so shared meshes are only processed once.
cache: dict[str, str | None] = {}
for obj in objects:
mesh_path = obj.get("meshPath")
if not mesh_path or not mesh_path.lower().endswith(".txt"):
continue
if mesh_path not in cache:
if not os.path.isfile(mesh_path):
print(f" WARNING: mesh not found, skipping: {mesh_path}")
cache[mesh_path] = None
else:
bin_path = mesh_path + ".bin"
convert(mesh_path, bin_path)
cache[mesh_path] = bin_path
if cache[mesh_path] is not None:
obj["meshPath"] = cache[mesh_path]
os.makedirs(os.path.dirname(os.path.abspath(dst_path)), exist_ok=True)
with open(dst_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
converted_count = sum(1 for v in cache.values() if v is not None)
print(f"Saved: {dst_path} ({converted_count} mesh(es) converted, "
f"{len(cache) - converted_count} skipped)")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <src_config.json> <dst_config.json>")
sys.exit(1)
convert_config(sys.argv[1], sys.argv[2])
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Convert a text-based static mesh file (.txt) to binary format (.txt.bin).
Usage:
python convert_model_to_binary.py <input.txt> [<output.bin>]
If the output path is not given it is derived by appending ".bin" to the input path,
e.g. resources/w/firebox.txt -> resources/w/firebox.txt.bin
Binary format (BSMF v1) -- all values little-endian:
HEADER
4 bytes magic "BSMF"
uint32 version (1)
uint32 numVertices
uint32 numTriangles
VERTICES (numVertices entries):
3 x float position x, y, z -- engine coordinate space
3 x float normal x, y, z -- engine coordinate space
2 x float UV u, v
TRIANGLES (numTriangles entries):
3 x uint32 vertex indices i0, i1, i2
The same Blender->engine axis swap applied by LoadFromTextFile02 is baked in here,
so the C++ binary loader can read coordinates directly without any post-processing:
engine_x = blender_y
engine_y = blender_z
engine_z = blender_x
"""
import struct
import re
import sys
import os
def _parse_floats(text):
return [float(x) for x in re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', text)]
def _parse_ints(text):
return [int(x) for x in re.findall(r'[-]?\d+', text)]
def _swap_axes(x, y, z):
"""Blender Y-up -> engine coordinate system (mirrors LoadFromTextFile02)."""
return y, z, x
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
idx = 0
def next_line():
nonlocal idx
while idx < len(lines):
line = lines[idx]
idx += 1
return line
raise EOFError("Unexpected end of file while parsing: " + input_path)
# --- Vertices ---
while True:
line = next_line()
if '===Vertices' in line:
break
m = re.search(r'\d+', line)
if not m:
raise ValueError("Could not parse vertex count from: " + line)
num_vertices = int(m.group())
positions = []
normals = []
uvs = []
for i in range(num_vertices):
line = next_line()
# V N: Pos(x, y, z) Norm(nx, ny, nz) UV(u, v)
nums = _parse_floats(line)
# nums[0] = vertex index (float-parsed), then 3 pos, 3 norm, 2 uv
if len(nums) < 9:
raise ValueError(f"Malformed vertex line {i}: {line}")
px, py, pz = _swap_axes(nums[1], nums[2], nums[3])
nx, ny, nz = _swap_axes(nums[4], nums[5], nums[6])
positions.append((px, py, pz))
normals.append((nx, ny, nz))
uvs.append((nums[7], nums[8]))
# --- Triangles ---
while True:
line = next_line()
if '===Triangles' in line:
break
m = re.search(r'\d+', line)
if not m:
raise ValueError("Could not parse triangle count from: " + line)
num_triangles = int(m.group())
triangles = []
for i in range(num_triangles):
line = next_line()
ints = _parse_ints(line)
if len(ints) != 3:
raise ValueError(f"Malformed triangle line {i}: {line}")
triangles.append(tuple(ints))
# --- Write binary ---
with open(output_path, 'wb') as out:
out.write(b'BSMF')
out.write(struct.pack('<I', 1))
out.write(struct.pack('<I', num_vertices))
out.write(struct.pack('<I', num_triangles))
for i in range(num_vertices):
out.write(struct.pack('<3f', *positions[i]))
out.write(struct.pack('<3f', *normals[i]))
out.write(struct.pack('<2f', *uvs[i]))
for tri in triangles:
out.write(struct.pack('<3I', *tri))
in_size = os.path.getsize(input_path)
out_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({in_size:,} bytes text) -> {output_path} ({out_size:,} bytes binary)")
print(f" Vertices: {num_vertices}, Triangles: {num_triangles}")
if __name__ == '__main__':
if len(sys.argv) < 2 or len(sys.argv) > 3:
print(f"Usage: {sys.argv[0]} <input.txt> [<output.bin>]")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2] if len(sys.argv) == 3 else input_path + '.bin'
convert(input_path, output_path)
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Convert an old single-mesh text animation file to the new multi-mesh text format.
The only structural difference is that the new format wraps the mesh block
between these two extra headers before the "===Vertices:" line:
=== TOTAL MESHES TO EXPORT: 1 ===
=== Mesh Object: Body ===
Usage:
python convert_old_anim_to_new.py <input.txt> <output.txt> [mesh_name]
If mesh_name is omitted, "Body" is used.
"""
import sys
def convert(input_path, output_path, mesh_name="Body"):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
# Find the first "===Vertices:" line -- that's where the bone block ends
# and the mesh block begins in the old format.
insert_at = None
for i, line in enumerate(lines):
if line.lstrip().startswith("===Vertices:"):
insert_at = i
break
if insert_at is None:
raise RuntimeError("Could not find '===Vertices:' line in input file")
header_lines = [
"=== TOTAL MESHES TO EXPORT: 1 ===\n",
f"=== Mesh Object: {mesh_name} ===\n",
]
out_lines = lines[:insert_at] + header_lines + lines[insert_at:]
with open(output_path, 'w', encoding='utf-8') as out:
out.writelines(out_lines)
print(f"Converted: {input_path} -> {output_path} (mesh name: {mesh_name})")
if __name__ == '__main__':
if len(sys.argv) not in (3, 4):
print(f"Usage: {sys.argv[0]} <input.txt> <output.txt> [mesh_name]")
sys.exit(1)
mesh_name = sys.argv[3] if len(sys.argv) == 4 else "Body"
convert(sys.argv[1], sys.argv[2], mesh_name)
+46
View File
@@ -0,0 +1,46 @@
.DS_STORE
node_modules
scripts/flow/*/.flowconfig
.flowconfig
*~
*.pyc
.grunt
_SpecRunner.html
__benchmarks__
build/
remote-repo/
coverage/
.module-cache
fixtures/dom/public/react-dom.js
fixtures/dom/public/react.js
test/the-files-to-test.generated.js
*.log*
chrome-user-data
*.sublime-project
*.sublime-workspace
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/chrome/*.crx
packages/react-devtools-extensions/chrome/*.pem
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/firefox/*.xpi
packages/react-devtools-extensions/firefox/*.pem
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/.tempUserDataDir
packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
resources
+351
View File
@@ -0,0 +1,351 @@
# Cutscene System
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
The file can contain multiple cutscenes:
```json
{
"cutscenes": [
{ "id": "intro", ... },
{ "id": "ending", ... }
]
}
```
Cutscenes and dialogues are loaded from **separate files**:
```cpp
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
```
---
## Cutscene object
```json
{
"id": "intro_cutscene",
"skippable": true,
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"onFadeInCallback": "",
"imageSegments": [ ... ],
"lines": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
### Timing model
The total cutscene duration is:
```
contentDuration = max(durationMs, max(segment.endMs for all segments))
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
```
The full timeline looks like this:
```
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
world→black black→images images→black black→world
```
---
## Image segments
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
```json
{
"path": "resources/cutscenes/bg_layer.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 300,
"fadeOutMs": 300,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `path` | string | — | Path to the PNG image (**required**) |
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
---
## Image pose
A pose defines how an image is framed on screen at a given moment.
```json
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
### Pose intuition
| Goal | Config |
|---|---|
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }` → `to: { "centerX": 0.7, "scale": 1.2 }` |
| Zoom out from close-up | `from: { "scale": 1.8 }` → `to: { "scale": 1.0 }` |
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
---
## Easing types
Controls the interpolation curve applied to pose animation between `from` and `to`.
| Value | Description |
|---|---|
| `"Linear"` | Constant speed (default) |
| `"EaseInSine"` | Slow start, fast end |
| `"EaseOutSine"` | Fast start, slow end |
| `"EaseInOutSine"` | Slow start and end, fast middle |
| `"EaseInQuad"` | Quadratic slow start |
| `"EaseOutQuad"` | Quadratic slow end |
| `"EaseInOutQuad"` | Quadratic slow start and end |
| `"EaseInCubic"` | Cubic slow start |
| `"EaseOutCubic"` | Cubic slow end |
| `"EaseInOutCubic"` | Cubic slow start and end |
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
---
## Subtitle lines
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
```json
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000,
"waitForConfirm": false,
"luaCallback": ""
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
---
## C++ API
### Starting a cutscene
```cpp
// Standalone cutscene (not part of a dialogue):
dialogueSystem.startCutscene("intro_cutscene");
// Skip the currently playing cutscene:
dialogueSystem.skipCutscene();
```
### Callbacks
```cpp
// Called when a cutscene begins:
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
// Called when a cutscene ends (receives the cutscene id):
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
// id == "intro_cutscene"
});
// Called when a subtitle line begins (receives luaCallback value):
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
// Called when the opening fade-in completes (receives onFadeInCallback value):
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
```
### Triggering from dialogue
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
```json
{
"id": "node_cutscene",
"type": "CutsceneStart",
"cutsceneId": "intro_cutscene",
"next": "node_after_cutscene"
}
```
---
## Full examples
### Minimal — static image, timed
```json
{
"id": "simple",
"durationMs": 4000,
"fadeOutMs": 300,
"fadeInMs": 300,
"endFadeOutMs": 300,
"endFadeInMs": 300,
"imageSegments": [
{
"path": "resources/cutscenes/city.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 4000
}
]
}
```
### Two-layer parallax pan
Background moves slowly left-to-right; foreground character moves faster, creating depth.
```json
{
"id": "classroom_intro",
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/cutscenes/classroom_bg.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 400,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
},
{
"path": "resources/cutscenes/classroom_teacher.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"easing": "EaseInOutSine",
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "Рассаживайтесь.",
"durationMs": 2500
}
]
}
```
### Zoom-in reveal with a second image appearing mid-way
```json
{
"id": "letter_reveal",
"durationMs": 7000,
"fadeOutMs": 400,
"fadeInMs": 600,
"endFadeOutMs": 600,
"endFadeInMs": 400,
"imageSegments": [
{
"path": "resources/cutscenes/desk_bg.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 7000
},
{
"path": "resources/cutscenes/letter_closeup.png",
"width": 1280,
"height": 720,
"startMs": 2000,
"endMs": 7000,
"fadeInMs": 800,
"easing": "EaseOutCubic",
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
}
],
"lines": [
{
"text": "Среди бумаг на столе лежит конверт.",
"durationMs": 2500
},
{
"speaker": "Главный герой",
"text": "«Явитесь в деканат немедленно».",
"durationMs": 3000
}
]
}
```
+65
View File
@@ -0,0 +1,65 @@
{
"cutscenes": [
{
"id": "test_cutscene_01",
"background": "resources/black.png",
"durationMs": 5000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/w/cutscenes/cutscene1/cutscene1_wall_x.png",
"startMs": 0,
"endMs": 8000,
"fadeInMs": 0,
"width": 1280,
"height": 720,
"from": {
"centerX": 0.3, "scale": 1.2
},
"to": {
"centerX": 0.7, "scale": 1.2
},
"easing": "Linear"
},
{
"path": "resources/w/cutscenes/cutscene1/cutscene1_aida1_x.png",
"startMs": 0,
"endMs": 8000,
"width": 1280,
"height": 720,
"from": {
"centerX": 0.3,
"centerY": 0.5,
"scale": 1.0
},
"to": {
"centerX": 0.7,
"centerY": 0.5,
"scale": 1.0
}
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
"durationMs": 2000
}
]
}
]
}
+1
View File
@@ -0,0 +1 @@
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;background:#1a1a1a;color:#e0e0e0}button{cursor:pointer;font-family:inherit;font-size:12px}input,select,textarea{font-family:inherit;font-size:12px;background:#2a2a2a;color:#e0e0e0;border:1px solid #444;border-radius:3px;padding:3px 6px}input:focus,select:focus,textarea:focus{outline:none;border-color:#5b9bd5}label{color:#aaa;font-size:11px}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:#1a1a1a}::-webkit-scrollbar-thumb{background:#444;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#666}._app_1ibpl_1{display:flex;width:100%;height:100%;overflow:hidden}._panel_e520h_1{width:200px;min-width:180px;background:#252525;border-right:1px solid #333;display:flex;flex-direction:column;overflow:hidden}._header_e520h_11{padding:10px 12px;font-size:12px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:#888;border-bottom:1px solid #333}._actions_e520h_21{display:flex;flex-direction:column;gap:6px;padding:10px;border-bottom:1px solid #333}._btn_e520h_29{background:#333;color:#ddd;border:1px solid #444;border-radius:4px;padding:5px 8px;text-align:center;transition:background .15s}._btn_e520h_29:hover:not(:disabled){background:#3d3d3d}._btn_e520h_29:disabled{opacity:.4;cursor:default}._btnPrimary_e520h_42{background:#2a4a6e;border-color:#3a6090;color:#a8d0f0}._btnPrimary_e520h_42:hover{background:#2e5480}._list_e520h_50{flex:1;overflow-y:auto;padding:4px 0}._empty_e520h_56{padding:16px 12px;color:#555;font-size:11px;line-height:1.5}._item_e520h_63{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;transition:background .1s}._item_e520h_63:hover{background:#2e2e2e}._selected_e520h_76{background:#1e3a55;border-left-color:#5b9bd5}._itemId_e520h_81{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}._deleteBtn_e520h_89{background:none;border:none;color:#666;padding:2px 4px;border-radius:3px;font-size:10px;opacity:0;transition:opacity .1s,color .1s}._item_e520h_63:hover ._deleteBtn_e520h_89{opacity:1}._deleteBtn_e520h_89:hover{color:#e06c6c}._panel_1kkcd_1{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}._previewArea_1kkcd_9{flex:0 0 auto;display:flex;justify-content:center;align-items:center;background:#111;padding:8px;max-height:60%}._wrapper_1nucj_1{width:100%;display:flex;justify-content:center;align-items:center;background:#111;padding:8px;flex-shrink:0}._viewport_1nucj_11{position:relative;aspect-ratio:16 / 9;width:100%;max-height:100%;background:#000;overflow:hidden}._empty_1nucj_20{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;color:#444;font-size:13px}._subtitleBar_1nucj_30{position:absolute;bottom:0;left:0;right:0;padding:10px 20px 14px;background:linear-gradient(transparent,#000000bf);text-align:center}._speaker_1nucj_40{font-size:11px;font-weight:600;color:#f0c060;margin-bottom:4px;text-shadow:0 1px 3px rgba(0,0,0,.8)}._text_1nucj_48{font-size:14px;color:#fff;line-height:1.5;text-shadow:0 1px 4px rgba(0,0,0,.9)}._controls_1qoyb_1{display:flex;align-items:center;gap:4px;padding:6px 10px;background:#1e1e1e;border-top:1px solid #333;border-bottom:1px solid #333;flex-shrink:0}._btn_1qoyb_12{background:#2d2d2d;border:1px solid #404040;color:#ccc;border-radius:4px;padding:4px 10px;font-size:13px;transition:background .1s;min-width:32px}._btn_1qoyb_12:hover:not(:disabled){background:#3a3a3a;color:#fff}._btn_1qoyb_12:disabled{opacity:.35;cursor:default}._active_1qoyb_26{color:#5ba3e0;border-color:#4a7aaa}._time_1qoyb_31{margin-left:10px;font-size:11px;color:#888;font-variant-numeric:tabular-nums}._container_uetgi_1{display:flex;flex-direction:column;flex:1;min-height:0;background:#1a1a1a;border-top:1px solid #333;overflow:hidden}._toolbar_uetgi_11{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#222;border-bottom:1px solid #333;flex-shrink:0}._tbBtn_uetgi_21{background:#2d2d2d;border:1px solid #404040;color:#bbb;border-radius:3px;padding:3px 8px;font-size:11px;transition:background .1s}._tbBtn_uetgi_21:hover:not(:disabled){background:#3a3a3a;color:#fff}._tbBtn_uetgi_21:disabled{opacity:.3;cursor:default}._spacer_uetgi_33{flex:1}._zoomLabel_uetgi_35{font-size:11px;color:#666;margin-right:2px}._scroll_uetgi_37{flex:1;overflow:auto;position:relative;cursor:default}._ruler_uetgi_45{display:flex;height:22px;background:#212121;border-bottom:1px solid #333;position:sticky;top:0;z-index:10;-webkit-user-select:none;user-select:none;cursor:crosshair}._tick_uetgi_57{position:absolute;top:0;bottom:0;width:1px;background:#444}._tickLabel_uetgi_65{position:absolute;top:3px;left:3px;font-size:9px;color:#666;white-space:nowrap}._layersArea_Line truncated
+40
View File
@@ -0,0 +1,40 @@
(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const p of document.querySelectorAll('link[rel="modulepreload"]'))f(p);new MutationObserver(p=>{for(const g of p)if(g.type==="childList")for(const S of g.addedNodes)S.tagName==="LINK"&&S.rel==="modulepreload"&&f(S)}).observe(document,{childList:!0,subtree:!0});function s(p){const g={};return p.integrity&&(g.integrity=p.integrity),p.referrerPolicy&&(g.referrerPolicy=p.referrerPolicy),p.crossOrigin==="use-credentials"?g.credentials="include":p.crossOrigin==="anonymous"?g.credentials="omit":g.credentials="same-origin",g}function f(p){if(p.ep)return;p.ep=!0;const g=s(p);fetch(p.href,g)}})();function Bd(u){return u&&u.__esModule&&Object.prototype.hasOwnProperty.call(u,"default")?u.default:u}var bu={exports:{}},Or={},eo={exports:{}},G={};/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var pc;function $d(){if(pc)return G;pc=1;var u=Symbol.for("react.element"),a=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),g=Symbol.for("react.provider"),S=Symbol.for("react.context"),L=Symbol.for("react.forward_ref"),z=Symbol.for("react.suspense"),U=Symbol.for("react.memo"),ee=Symbol.for("react.lazy"),D=Symbol.iterator;function H(h){return h===null||typeof h!="object"?null:(h=D&&h[D]||h["@@iterator"],typeof h=="function"?h:null)}var fe={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},me=Object.assign,re={};function q(h,k,X){this.props=h,this.context=k,this.refs=re,this.updater=X||fe}q.prototype.isReactComponent={},q.prototype.setState=function(h,k){if(typeof h!="object"&&typeof h!="function"&&h!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,h,k,"setState")},q.prototype.forceUpdate=function(h){this.updater.enqueueForceUpdate(this,h,"forceUpdate")};function ge(){}ge.prototype=q.prototype;function Ge(h,k,X){this.props=h,this.context=k,this.refs=re,this.updater=X||fe}var Be=Ge.prototype=new ge;Be.constructor=Ge,me(Be,q.prototype),Be.isPureReactComponent=!0;var ke=Array.isArray,Ze=Object.prototype.hasOwnProperty,Ee={current:null},Ne={key:!0,ref:!0,__self:!0,__source:!0};function $e(h,k,X){var Y,Z={},b=null,oe=null;if(k!=null)for(Y in k.ref!==void 0&&(oe=k.ref),k.key!==void 0&&(b=""+k.key),k)Ze.call(k,Y)&&!Ne.hasOwnProperty(Y)&&(Z[Y]=k[Y]);var J=arguments.length-2;if(J===1)Z.children=X;else if(1<J){for(var de=Array(J),Je=0;Je<J;Je++)de[Je]=arguments[Je+2];Z.children=de}if(h&&h.defaultProps)for(Y in J=h.defaultProps,J)Z[Y]===void 0&&(Z[Y]=J[Y]);return{$$typeof:u,type:h,key:b,ref:oe,props:Z,_owner:Ee.current}}function rt(h,k){return{$$typeof:u,type:h.type,key:k,ref:h.ref,props:h.props,_owner:h._owner}}function pt(h){return typeof h=="object"&&h!==null&&h.$$typeof===u}function kt(h){var k={"=":"=0",":":"=2"};return"$"+h.replace(/[=:]/g,function(X){return k[X]})}var He=/\/+/g;function De(h,k){return typeof h=="object"&&h!==null&&h.key!=null?kt(""+h.key):k.toString(36)}function I(h,k,X,Y,Z){var b=typeof h;(b==="undefined"||b==="boolean")&&(h=null);var oe=!1;if(h===null)oe=!0;else switch(b){case"string":case"number":oe=!0;break;case"object":switch(h.$$typeof){case u:case a:oe=!0}}if(oe)return oe=h,Z=Z(oe),h=Y===""?"."+De(oe,0):Y,ke(Z)?(X="",h!=null&&(X=h.replace(He,"$&/")+"/"),I(Z,k,X,"",function(Je){return Je})):Z!=null&&(pt(Z)&&(Z=rt(Z,X+(!Z.key||oe&&oe.key===Z.key?"":(""+Z.key).replace(He,"$&/")+"/")+h)),k.push(Z)),1;if(oe=0,Y=Y===""?".":Y+":",ke(h))for(var J=0;J<h.length;J++){b=h[J];var de=Y+De(b,J);oe+=I(b,k,X,de,Z)}else if(de=H(h),typeof de=="function")for(h=de.call(h),J=0;!(b=h.next()).done;)b=b.value,de=Y+De(b,J++),oe+=I(b,k,X,de,Z);else if(b==="object")throw k=String(h),Error("Objects are not valid as a React child (found: "+(k==="[object Object]"?"object with keys {"+Object.keys(h).join(", ")+"}":k)+"). If you meant to render a collection of children, use an array instead.");return oe}function Q(h,k,X){if(h==null)return h;var Y=[],Z=0;return I(h,Y,"","",function(b){return k.call(X,b,Z++)}),Y}function le(h){if(h._status===-1){var k=h._result;k=k(),k.then(function(X){(h._status===0||h._status===-1)&&(h._status=1,h._result=X)},function(X){(h._status===0||h._status===-1)&&(h._status=2,h._result=X)}),h._status===-1&&(h._status=0,h._result=k)}if(h._status===1)return h._result.default;throw h._result}var K={current:null},M={transition:null},A={ReactCurrentDispatcher:K,ReactCurrentBatchConfig:M,ReactCurrentOwner:Ee};function P(){throw Error("act(...) is not supported in production builds of React.")}return G.Children={map:Q,forEach:function(h,k,X){Q(h,function(){k.apply(this,arguments)},X)},count:function(h){var k=0;return Q(h,function(){k++}),k},toArray:function(h){return Q(h,function(k){return k})||[]},only:function(h){if(!pt(h))throw Error("React.Children.only expected to receive a single React element child.");return h}},G.Component=q,G.Fragment=s,G.Profiler=p,G.PureComponent=Ge,G.StrictMode=f,G.Suspense=z,G.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=A,G.act=P,G.cloneElement=function(h,k,X){if(h==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+h+".");var Y=me({},h.props),Z=h.key,b=h.ref,oe=h._owner;if(k!=null){if(k.ref!==void 0&&(b=k.ref,oe=Ee.current),k.key!==void 0&&(Z=""+k.key),h.type&&h.type.defaultProps)var J=h.type.defaultProps;for(de in k)Ze.call(k,de)&&!Ne.hasOwnProperty(de)&&(Y[de]=k[de]===void 0&&J!==void 0?J[de]:k[de])}var de=arguments.length-2;if(de===1)Y.children=X;else if(1<de){J=Array(de);for(var Je=0;Je<de;Je++)J[Je]=arguments[Je+2];Y.children=J}return{$$typeof:u,type:h.type,key:Z,ref:b,props:Y,_owner:oe}},G.createContext=function(h){return h={$$typeof:S,_currLine truncated
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var hc;function Hd(){if(hc)return Or;hc=1;var u=vo(),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),f=Object.prototype.hasOwnProperty,p=u.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,g={key:!0,ref:!0,__self:!0,__source:!0};function S(L,z,U){var ee,D={},H=null,fe=null;U!==void 0&&(H=""+U),z.key!==void 0&&(H=""+z.key),z.ref!==void 0&&(fe=z.ref);for(ee in z)f.call(z,ee)&&!g.hasOwnProperty(ee)&&(D[ee]=z[ee]);if(L&&L.defaultProps)for(ee in z=L.defaultProps,z)D[ee]===void 0&&(D[ee]=z[ee]);return{$$typeof:a,type:L,key:H,ref:fe,props:D,_owner:p.current}}return Or.Fragment=s,Or.jsx=S,Or.jsxs=S,Or}var vc;function Wd(){return vc||(vc=1,bu.exports=Hd()),bu.exports}var y=Wd(),ze=vo();const Zl=Bd(ze);var Jl={},to={exports:{}},Ke={},no={exports:{}},ro={};/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var yc;function Vd(){return yc||(yc=1,(function(u){function a(M,A){var P=M.length;M.push(A);e:for(;0<P;){var h=P-1>>>1,k=M[h];if(0<p(k,A))M[h]=A,M[P]=k,P=h;else break e}}function s(M){return M.length===0?null:M[0]}function f(M){if(M.length===0)return null;var A=M[0],P=M.pop();if(P!==A){M[0]=P;e:for(var h=0,k=M.length,X=k>>>1;h<X;){var Y=2*(h+1)-1,Z=M[Y],b=Y+1,oe=M[b];if(0>p(Z,P))b<k&&0>p(oe,Z)?(M[h]=oe,M[b]=P,h=b):(M[h]=Z,M[Y]=P,h=Y);else if(b<k&&0>p(oe,P))M[h]=oe,M[b]=P,h=b;else break e}}return A}function p(M,A){var P=M.sortIndex-A.sortIndex;return P!==0?P:M.id-A.id}if(typeof performance=="object"&&typeof performance.now=="function"){var g=performance;u.unstable_now=function(){return g.now()}}else{var S=Date,L=S.now();u.unstable_now=function(){return S.now()-L}}var z=[],U=[],ee=1,D=null,H=3,fe=!1,me=!1,re=!1,q=typeof setTimeout=="function"?setTimeout:null,ge=typeof clearTimeout=="function"?clearTimeout:null,Ge=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Be(M){for(var A=s(U);A!==null;){if(A.callback===null)f(U);else if(A.startTime<=M)f(U),A.sortIndex=A.expirationTime,a(z,A);else break;A=s(U)}}function ke(M){if(re=!1,Be(M),!me)if(s(z)!==null)me=!0,le(Ze);else{var A=s(U);A!==null&&K(ke,A.startTime-M)}}function Ze(M,A){me=!1,re&&(re=!1,ge($e),$e=-1),fe=!0;var P=H;try{for(Be(A),D=s(z);D!==null&&(!(D.expirationTime>A)||M&&!kt());){var h=D.callback;if(typeof h=="function"){D.callback=null,H=D.priorityLevel;var k=h(D.expirationTime<=A);A=u.unstable_now(),typeof k=="function"?D.callback=k:D===s(z)&&f(z),Be(A)}else f(z);D=s(z)}if(D!==null)var X=!0;else{var Y=s(U);Y!==null&&K(ke,Y.startTime-A),X=!1}return X}finally{D=null,H=P,fe=!1}}var Ee=!1,Ne=null,$e=-1,rt=5,pt=-1;function kt(){return!(u.unstable_now()-pt<rt)}function He(){if(Ne!==null){var M=u.unstable_now();pt=M;var A=!0;try{A=Ne(!0,M)}finally{A?De():(Ee=!1,Ne=null)}}else Ee=!1}var De;if(typeof Ge=="function")De=function(){Ge(He)};else if(typeof MessageChannel<"u"){var I=new MessageChannel,Q=I.port2;I.port1.onmessage=He,De=function(){Q.postMessage(null)}}else De=function(){q(He,0)};function le(M){Ne=M,Ee||(Ee=!0,De())}function K(M,A){$e=q(function(){M(u.unstable_now())},A)}u.unstable_IdlePriority=5,u.unstable_ImmediatePriority=1,u.unstable_LowPriority=4,u.unstable_NormalPriority=3,u.unstable_Profiling=null,u.unstable_UserBlockingPriority=2,u.unstable_cancelCallback=function(M){M.callback=null},u.unstable_continueExecution=function(){me||fe||(me=!0,le(Ze))},u.unstable_forceFrameRate=function(M){0>M||125<M?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):rt=0<M?Math.floor(1e3/M):5},u.unstable_getCurrentPriorityLevel=function(){return H},u.unstable_getFirstCallbackNode=function(){return s(z)},u.unstable_next=function(M){switch(H){case 1:case 2:case 3:var A=3;break;default:A=H}var P=H;H=A;try{return M()}finally{H=P}},u.unstable_pauseExecution=function(){},u.unstable_requestPaint=function(){},u.unstable_runWithPriority=function(M,A){switch(M){case 1:case 2:case 3:case 4:case 5:break;default:M=3}var P=H;H=M;try{return A()}finally{H=P}},u.unstable_scheduleCallback=function(M,A,P){var h=u.unstable_now();switch(typeof P=="object"&&P!==null?(P=P.delay,P=typeof P=="number"&&0<P?h+P:h):P=h,M){case 1:var k=-1;break;case 2:k=250;break;case 5:k=1073741823;break;case 4:k=1e4;break;default:k=5e3}return k=P+k,M={id:ee++,callback:A,priorityLevel:M,startTime:P,expirationTime:k,sortIndex:-1},P>h?(M.sortIndex=P,a(U,M),s(z)===null&&M===s(U)&&(re?(ge($e),$e=-1):re=!0,K(ke,P-h))):(M.sortIndex=k,a(z,M),me||fe||(me=!0,le(Ze))),M},u.unstable_shouldYield=kt,u.unstable_wrapCallback=function(M){var A=H;return function(){var P=H;H=A;try{return M.apply(this,arguments)}finally{H=P}}}})(ro)),ro}var gc;function Qd(){return gc||(gc=1,no.exports=Vd()),no.exports}/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var _c;function Xd(){if(_c)return Ke;_c=1;var u=vo(),a=Qd();function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var f=new Set,p={};function g(e,t){S(e,t),S(e+"Capture",t)}function S(e,t){for(p[e]=t,e=0;e<t.length;e++)f.add(t[e])}var L=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),z=Object.prototype.hasOwnProperty,U=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ee={},D={};function H(e){return z.call(D,e)?!0:z.call(ee,e)?!1:U.test(e)?D[e]=!0:(ee[e]=!0,!1)}function fe(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function me(e,t,n,r){if(t===null||typeof t>"u"||fe(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function re(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var q={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){q[e]=new re(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];q[t]=new re(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){q[e]=new re(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){q[e]=new re(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){q[e]=new re(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){q[e]=new re(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){q[e]=new re(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){q[e]=new re(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){q[e]=new re(e,5,!1,e.toLowerCase(),null,!1,!1)});var ge=/[\-:]([a-z])/g;function Ge(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ge,Ge);q[t]=new re(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){q[e]=new re(e,1,!1,e.toLowerCase(),null,!1,!1)}),q.xlinkHref=new re("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){q[e]=new re(e,1,!1,e.toLowerCase(),null,!0,!0)});function Be(e,t,n,r){var l=q.hasOwnProperty(t)?q[t]:null;(l!==null?l.type!==0:r||!(2<t.length)||t[0]!=="o"& Line truncated
`+h+e}var X=!1;function Y(e,t){if(!e||X)return"";X=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(w){var r=w}Reflect.construct(e,[],t)}else{try{t.call()}catch(w){r=w}e.call(t.prototype)}else{try{throw Error()}catch(w){r=w}e()}}catch(w){if(w&&r&&typeof w.stack=="string"){for(var l=w.stack.split(`
`),i=r.stack.split(`
`),o=l.length-1,c=i.length-1;1<=o&&0<=c&&l[o]!==i[c];)c--;for(;1<=o&&0<=c;o--,c--)if(l[o]!==i[c]){if(o!==1||c!==1)do if(o--,c--,0>c||l[o]!==i[c]){var d=`
`+l[o].replace(" at new "," at ");return e.displayName&&d.includes("<anonymous>")&&(d=d.replace("<anonymous>",e.displayName)),d}while(1<=o&&0<=c);break}}}finally{X=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?k(e):""}function Z(e){switch(e.tag){case 5:return k(e.type);case 16:return k("Lazy");case 13:return k("Suspense");case 19:return k("SuspenseList");case 0:case 2:case 15:return e=Y(e.type,!1),e;case 11:return e=Y(e.type.render,!1),e;case 1:return e=Y(e.type,!0),e;default:return""}}function b(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ne:return"Fragment";case Ee:return"Portal";case rt:return"Profiler";case $e:return"StrictMode";case De:return"Suspense";case I:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case kt:return(e.displayName||"Context")+".Consumer";case pt:return(e._context.displayName||"Context")+".Provider";case He:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Q:return t=e.displayName||null,t!==null?t:b(e.type)||"Memo";case le:t=e._payload,e=e._init;try{return b(e(t))}catch{}}return null}function oe(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return b(t);case 8:return t===$e?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function J(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function de(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Je(e){var t=de(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Br(e){e._valueTracker||(e._valueTracker=Je(e))}function _o(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=de(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return P({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function wo(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=J(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function So(e,t){t=t.checked,t!=null&&Be(e,"checked",t,!1)}function oi(e,t){So(e,t);var n=J(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?si(e,t.type,n):t.hasOwnProperty("defaultValue")&&si(e,t.type,J(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ko(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function si(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Gn=Array.isArray;function Sn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l<n.length;l++)t["$"+n[l]]=!0;for(n=0;n<e.length;n++)l=t.hasOwnProperty("$"+e[n].value),e[n].selected!==l&&(e[n].selected=l),l&&r&&(e[n].defaultSelected=!0)}else{for(n=""+J(n),t=null,l=0;l<e.length;l++){if(e[l].value===n){e[l].selected=!0,r&&(e[l].defaultSelected=!0);return}t!==null||e[l].disabled||(t=e[l])}t!==null&&(t.selected=!0)}}function ai(e,t){if(t.dangerouslySetInnerHTML!= Line truncated
`).replace(bf,"")}function al(e,t,n){if(t=zs(t),zs(e)!==t&&n)throw Error(s(425))}function cl(){}var Vi=null,Qi=null;function Xi(e,t){return e==="textarea"||e==="noscript"||typeof t.children=="string"||typeof t.children=="number"||typeof t.dangerouslySetInnerHTML=="object"&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var Yi=typeof setTimeout=="function"?setTimeout:void 0,ed=typeof clearTimeout=="function"?clearTimeout:void 0,Ts=typeof Promise=="function"?Promise:void 0,td=typeof queueMicrotask=="function"?queueMicrotask:typeof Ts<"u"?function(e){return Ts.resolve(null).then(e).catch(nd)}:Yi;function nd(e){setTimeout(function(){throw e})}function Ki(e,t){var n=t,r=0;do{var l=n.nextSibling;if(e.removeChild(n),l&&l.nodeType===8)if(n=l.data,n==="/$"){if(r===0){e.removeChild(l),or(t);return}r--}else n!=="$"&&n!=="$?"&&n!=="$!"||r++;n=l}while(n);or(t)}function Wt(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t==="$"||t==="$!"||t==="$?")break;if(t==="/$")return null}}return e}function Os(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="$"||n==="$!"||n==="$?"){if(t===0)return e;t--}else n==="/$"&&t++}e=e.previousSibling}return null}var jn=Math.random().toString(36).slice(2),Ct="__reactFiber$"+jn,gr="__reactProps$"+jn,Pt="__reactContainer$"+jn,Gi="__reactEvents$"+jn,rd="__reactListeners$"+jn,ld="__reactHandles$"+jn;function ln(e){var t=e[Ct];if(t)return t;for(var n=e.parentNode;n;){if(t=n[Pt]||n[Ct]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=Os(e);e!==null;){if(n=e[Ct])return n;e=Os(e)}return t}e=n,n=e.parentNode}return null}function _r(e){return e=e[Ct]||e[Pt],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function In(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(s(33))}function fl(e){return e[gr]||null}var Zi=[],zn=-1;function Vt(e){return{current:e}}function ce(e){0>zn||(e.current=Zi[zn],Zi[zn]=null,zn--)}function se(e,t){zn++,Zi[zn]=e.current,e.current=t}var Qt={},Te=Vt(Qt),We=Vt(!1),un=Qt;function Tn(e,t){var n=e.type.contextTypes;if(!n)return Qt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function Ve(e){return e=e.childContextTypes,e!=null}function dl(){ce(We),ce(Te)}function Rs(e,t,n){if(Te.current!==Qt)throw Error(s(168));se(Te,t),se(We,n)}function Fs(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(s(108,oe(e)||"Unknown",l));return P({},n,r)}function pl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Qt,un=Te.current,se(Te,e),se(We,We.current),!0}function Ds(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=Fs(e,t,un),r.__reactInternalMemoizedMergedChildContext=e,ce(We),ce(Te),se(Te,e)):ce(We),se(We,n)}var jt=null,ml=!1,Ji=!1;function As(e){jt===null?jt=[e]:jt.push(e)}function id(e){ml=!0,As(e)}function Xt(){if(!Ji&&jt!==null){Ji=!0;var e=0,t=ue;try{var n=jt;for(ue=1;e<n.length;e++){var r=n[e];do r=r(!0);while(r!==null)}jt=null,ml=!1}catch(l){throw jt!==null&&(jt=jt.slice(e+1)),Bo(_i,Xt),l}finally{ue=t,Ji=!1}}return null}var On=[],Rn=0,hl=null,vl=0,lt=[],it=0,on=null,It=1,zt="";function sn(e,t){On[Rn++]=vl,On[Rn++]=hl,hl=e,vl=t}function Us(e,t,n){lt[it++]=It,lt[it++]=zt,lt[it++]=on,on=e;var r=It;e=zt;var l=32-mt(r)-1;r&=~(1<<l),n+=1;var i=32-mt(t)+l;if(30<i){var o=l-l%5;i=(r&(1<<o)-1).toString(32),r>>=o,l-=o,It=1<<32-mt(t)+l|n<<l|r,zt=i+e}else It=1<<i|n<<l|r,zt=e}function qi(e){e.return!==null&&(sn(e,1),Us(e,1,0))}function bi(e){for(;e===hl;)hl=On[--Rn],On[Rn]=null,vl=On[--Rn],On[Rn]=null;for(;e===on;)on=lt[--it],lt[it]=null,zt=lt[--it],lt[it]=null,It=lt[--it],lt[it]=null}var be=null,et=null,pe=!1,vt=null;function Bs(e,t){var n=at(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,t=e.deletions,t===null?(e.deletions=[n],e.flags|=16):t.push(n)}function $s(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,be=e,et=Wt(t.firstChild),!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,be=e,et=null,!0):!1;case 13:return t=t.nodeType!==8?null:t,t!==null?(n=on!==null?{id:It,overflow:zt}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},n=at(18,null,null,0),n.stateNode=t,n.return=e,e.child=n,be=e,et=null,!0):!1;default:return!1}}function eu(e){return(e.mode&1)!==0&&(e.flags&128)===0}function tu(e){if(pe){var t=et;if(t){var n=t;if(!$s(e,t)){if(eu(e))throw Error(s(418));t=Wt(n.nextSibling);var r=be;t&&$s(e,t)?Bs(r,n):(e.flags=e.flags&-4097|2,pe=!1,be=e)}}else{if(eu(e))throw Error(s(418));e.flags=e.flags&-4097|2,pe=!1,be=e}}}function Hs(e){for(e=e. Line truncated
Error generating stack: `+i.message+`
`+i.stack}return{value:e,source:t,stack:l,digest:null}}function xu(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Cu(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var md=typeof WeakMap=="function"?WeakMap:Map;function ka(e,t,n){n=Ot(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Fl||(Fl=!0,Uu=r),Cu(e,t)},n}function xa(e,t,n){n=Ot(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Cu(e,t)}}var i=e.stateNode;return i!==null&&typeof i.componentDidCatch=="function"&&(n.callback=function(){Cu(e,t),typeof r!="function"&&(Zt===null?Zt=new Set([this]):Zt.add(this));var o=t.stack;this.componentDidCatch(t.value,{componentStack:o!==null?o:""})}),n}function Ca(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new md;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Md.bind(null,e,t,n),t.then(e,e))}function Ea(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Na(e,t,n,r,l){return(e.mode&1)===0?(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Ot(-1,1),t.tag=2,Kt(n,t,1))),n.lanes|=1),e):(e.flags|=65536,e.lanes=l,e)}var hd=ke.ReactCurrentOwner,Qe=!1;function Ae(e,t,n,r){t.child=e===null?Xs(t,null,n,r):Dn(t,e.child,n,r)}function La(e,t,n,r,l){n=n.render;var i=t.ref;return Un(t,l),r=hu(e,t,n,r,i,l),n=vu(),e!==null&&!Qe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Rt(e,t,l)):(pe&&n&&qi(t),t.flags|=1,Ae(e,t,r,l),t.child)}function Ma(e,t,n,r,l){if(e===null){var i=n.type;return typeof i=="function"&&!Xu(i)&&i.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=i,Pa(e,t,i,r,l)):(e=Hl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(i=e.child,(e.lanes&l)===0){var o=i.memoizedProps;if(n=n.compare,n=n!==null?n:pr,n(o,r)&&e.ref===t.ref)return Rt(e,t,l)}return t.flags|=1,e=en(i,r),e.ref=t.ref,e.return=t,t.child=e}function Pa(e,t,n,r,l){if(e!==null){var i=e.memoizedProps;if(pr(i,r)&&e.ref===t.ref)if(Qe=!1,t.pendingProps=r=i,(e.lanes&l)!==0)(e.flags&131072)!==0&&(Qe=!0);else return t.lanes=e.lanes,Rt(e,t,l)}return Eu(e,t,n,r,l)}function ja(e,t,n){var r=t.pendingProps,l=r.children,i=e!==null?e.memoizedState:null;if(r.mode==="hidden")if((t.mode&1)===0)t.memoizedState={baseLanes:0,cachePool:null,transitions:null},se(Wn,tt),tt|=n;else{if((n&1073741824)===0)return e=i!==null?i.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,se(Wn,tt),tt|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=i!==null?i.baseLanes:n,se(Wn,tt),tt|=r}else i!==null?(r=i.baseLanes|n,t.memoizedState=null):r=n,se(Wn,tt),tt|=r;return Ae(e,t,l,n),t.child}function Ia(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Eu(e,t,n,r,l){var i=Ve(n)?un:Te.current;return i=Tn(t,i),Un(t,l),n=hu(e,t,n,r,i,l),r=vu(),e!==null&&!Qe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Rt(e,t,l)):(pe&&r&&qi(t),t.flags|=1,Ae(e,t,n,l),t.child)}function za(e,t,n,r,l){if(Ve(n)){var i=!0;pl(t)}else i=!1;if(Un(t,l),t.stateNode===null)Il(e,t),wa(t,n,r),ku(t,n,r,l),r=!0;else if(e===null){var o=t.stateNode,c=t.memoizedProps;o.props=c;var d=o.context,w=n.contextType;typeof w=="object"&&w!==null?w=ut(w):(w=Ve(n)?un:Te.current,w=Tn(t,w));var C=n.getDerivedStateFromProps,E=typeof C=="function"||typeof o.getSnapshotBeforeUpdate=="function";E||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(c!==r||d!==w)&&Sa(t,o,r,w),Yt=!1;var x=t.memoizedState;o.state=x,kl(t,r,o,l),d=t.memoizedState,c!==r||x!==d||We.current||Yt?(typeof C=="function"&&(Su(t,n,C,r),d=t.memoizedState),(c=Yt||_a(t,n,c,r,x,d,w))?(E||typeof o.UNSAFE_componentWillMount!="function"&&typeof o.componentWillMount!="function"||(typeof o.componentWillMount=="function"&&o.componentWillMount(),typeof o.UNSAFE_componentWillMount=="function"&&o.UNSAFE_componentWillMount()),typeof o.componentDidMount=="function"&&(t.flags|=4194308)):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=d),o.props=r,o.state=d,o.context=w,r=c):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,Ks(e,t),c=t.memoizedProps,w=t.type===t.elementType?c:yt(t.type,c),o.props=w,E=t.pendingProps,x=o.context,d=n.contextType,typeof d=="object"&&d!==null?d=ut(d):(d=Ve(n)?un:Te.current,d=Tn(t,d));var j=n.getDerivedStateFromProps;(C=typeof j=="function"||typeof o.getSnapshotBeforeUpdate=="function")||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(c!==E||x!==d)&&Sa(t,o,r,d),Yt=!1,x=t.memoizedState,o.state=x,kl(t,r,o, Line truncated
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cutscene Editor</title>
<script type="module" crossorigin src="/assets/index-D_5Tak8P.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B96C0g1n.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cutscene Editor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1890
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "cutscene-editor",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"immer": "^10.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.5"
}
}
+6
View File
@@ -0,0 +1,6 @@
.app {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
}
+14
View File
@@ -0,0 +1,14 @@
import styles from './App.module.css';
import LeftPanel from './components/LeftPanel/LeftPanel';
import CenterPanel from './components/CenterPanel/CenterPanel';
import RightPanel from './components/RightPanel/RightPanel';
export default function App() {
return (
<div className={styles.app}>
<LeftPanel />
<CenterPanel />
<RightPanel />
</div>
);
}
@@ -0,0 +1,18 @@
.panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.previewArea {
flex: 0 1 420px;
min-height: 120px;
display: flex;
justify-content: center;
align-items: center;
background: #111;
padding: 8px;
overflow: hidden;
}
@@ -0,0 +1,16 @@
import styles from './CenterPanel.module.css';
import Preview from '../Preview/Preview';
import Controls from '../Controls/Controls';
import Timeline from '../Timeline/Timeline';
export default function CenterPanel() {
return (
<div className={styles.panel}>
<div className={styles.previewArea}>
<Preview />
</div>
<Controls />
<Timeline />
</div>
);
}
@@ -0,0 +1,94 @@
.controls {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #1e1e1e;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.btn {
background: #2d2d2d;
border: 1px solid #404040;
color: #ccc;
border-radius: 4px;
padding: 4px 10px;
font-size: 13px;
transition: background 0.1s;
min-width: 32px;
flex-shrink: 0;
}
.btn:hover:not(:disabled) { background: #3a3a3a; color: #fff; }
.btn:disabled { opacity: 0.35; cursor: default; }
.active {
color: #5ba3e0;
border-color: #4a7aaa;
}
/* ── Scrubber ───────────────────────────────────────────────── */
.scrubber {
flex: 1;
min-width: 0;
height: 4px;
-webkit-appearance: none;
appearance: none;
border-radius: 2px;
outline: none;
cursor: pointer;
border: none;
padding: 0;
/* filled portion via CSS variable set inline */
background: linear-gradient(
to right,
#5b9bd5 0%,
#5b9bd5 var(--progress, 0%),
#3a3a3a var(--progress, 0%),
#3a3a3a 100%
);
}
.scrubber:disabled {
opacity: 0.3;
cursor: default;
}
.scrubber::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #5b9bd5;
cursor: pointer;
border: 2px solid #1e1e1e;
transition: transform 0.1s;
}
.scrubber:not(:disabled)::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.scrubber::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #5b9bd5;
cursor: pointer;
border: 2px solid #1e1e1e;
}
.scrubber::-moz-range-track {
height: 4px;
border-radius: 2px;
background: #3a3a3a;
}
.time {
flex-shrink: 0;
font-size: 11px;
color: #888;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
@@ -0,0 +1,67 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { usePlayback } from '../../hooks/usePlayback';
import styles from './Controls.module.css';
function formatMs(ms: number) {
const s = Math.floor(ms / 1000);
const frac = Math.floor((ms % 1000) / 10).toString().padStart(2, '0');
return `${s}.${frac}s`;
}
export default function Controls() {
usePlayback();
const { playState, currentTimeMs, setPlayState, setCurrentTime } = useCutsceneStore();
const cutscene = useSelectedCutscene();
const totalMs = cutscene
? Math.max(
cutscene.durationMs,
cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0)
) + cutscene.endFadeOutMs + cutscene.endFadeInMs
: 0;
function play() {
if (playState === 'stopped' || currentTimeMs >= totalMs) setCurrentTime(0);
setPlayState('playing');
}
function pause() { setPlayState('paused'); }
function stop() { setPlayState('stopped'); setCurrentTime(0); }
function handleScrub(e: React.ChangeEvent<HTMLInputElement>) {
const ms = Number(e.target.value);
setCurrentTime(ms);
if (playState === 'playing') setPlayState('paused');
}
const progress = totalMs > 0 ? currentTimeMs / totalMs : 0;
return (
<div className={styles.controls}>
<button className={styles.btn} onClick={stop} title="Stop" disabled={!cutscene}>⏹</button>
<button className={styles.btn} onClick={() => setCurrentTime(0)} title="Rewind" disabled={!cutscene}>⏮</button>
{playState === 'playing' ? (
<button className={`${styles.btn} ${styles.active}`} onClick={pause} title="Pause" disabled={!cutscene}>⏸</button>
) : (
<button className={`${styles.btn} ${styles.active}`} onClick={play} title="Play" disabled={!cutscene}>▶</button>
)}
<input
className={styles.scrubber}
type="range"
min={0}
max={totalMs || 1}
step={16}
value={currentTimeMs}
onChange={handleScrub}
disabled={!cutscene}
style={{ '--progress': `${progress * 100}%` } as React.CSSProperties}
/>
<div className={styles.time}>
{formatMs(currentTimeMs)} / {formatMs(totalMs)}
</div>
</div>
);
}
@@ -0,0 +1,100 @@
.panel {
width: 200px;
min-width: 180px;
background: #252525;
border-right: 1px solid #333;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
padding: 10px 12px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #888;
border-bottom: 1px solid #333;
}
.actions {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
border-bottom: 1px solid #333;
}
.btn {
background: #333;
color: #ddd;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
text-align: center;
transition: background 0.15s;
}
.btn:hover:not(:disabled) { background: #3d3d3d; }
.btn:disabled { opacity: 0.4; cursor: default; }
.btnPrimary {
composes: btn;
background: #2a4a6e;
border-color: #3a6090;
color: #a8d0f0;
}
.btnPrimary:hover { background: #2e5480; }
.list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.empty {
padding: 16px 12px;
color: #555;
font-size: 11px;
line-height: 1.5;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
cursor: pointer;
user-select: none;
border-left: 3px solid transparent;
transition: background 0.1s;
}
.item:hover { background: #2e2e2e; }
.selected {
background: #1e3a55;
border-left-color: #5b9bd5;
}
.itemId {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.deleteBtn {
background: none;
border: none;
color: #666;
padding: 2px 4px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
transition: opacity 0.1s, color 0.1s;
}
.item:hover .deleteBtn { opacity: 1; }
.deleteBtn:hover { color: #e06c6c; }
@@ -0,0 +1,76 @@
import { useRef } from 'react';
import { useCutsceneStore } from '../../store/cutsceneStore';
import { parseFile, triggerDownload } from '../../utils/fileIO';
import styles from './LeftPanel.module.css';
export default function LeftPanel() {
const fileInputRef = useRef<HTMLInputElement>(null);
const { file, selectedCutsceneId, loadFile, addCutscene, deleteCutscene, selectCutscene, getExportData } = useCutsceneStore();
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!f) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const json = JSON.parse(ev.target!.result as string);
loadFile(parseFile(json));
} catch {
alert('Invalid JSON file');
}
};
reader.readAsText(f);
e.target.value = '';
}
function handleSave() {
const data = getExportData();
if (data) triggerDownload(data);
}
function handleDelete(id: string) {
if (confirm(`Delete cutscene "${id}"?`)) deleteCutscene(id);
}
return (
<div className={styles.panel}>
<div className={styles.header}>Cutscenes</div>
<div className={styles.actions}>
<button className={styles.btnPrimary} onClick={() => fileInputRef.current?.click()}>
Load JSON
</button>
<input ref={fileInputRef} type="file" accept=".json" style={{ display: 'none' }} onChange={handleFileChange} />
<button className={styles.btn} onClick={handleSave} disabled={!file}>
Save JSON
</button>
<button className={styles.btn} onClick={addCutscene}>
+ New
</button>
</div>
<div className={styles.list}>
{!file || file.cutscenes.length === 0 ? (
<div className={styles.empty}>No cutscenes. Load a JSON or create new.</div>
) : (
file.cutscenes.map(c => (
<div
key={c.id}
className={`${styles.item} ${c.id === selectedCutsceneId ? styles.selected : ''}`}
onClick={() => selectCutscene(c.id)}
>
<span className={styles.itemId}>{c.id}</span>
<button
className={styles.deleteBtn}
onClick={(e) => { e.stopPropagation(); handleDelete(c.id); }}
title="Delete"
>
✕
</button>
</div>
))
)}
</div>
</div>
);
}
@@ -0,0 +1,51 @@
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.viewport {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
max-height: 100%;
background: #000;
overflow: hidden;
}
.empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #444;
font-size: 13px;
}
.subtitleBar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px 20px 14px;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
text-align: center;
}
.speaker {
font-size: 11px;
font-weight: 600;
color: #f0c060;
margin-bottom: 4px;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
.text {
font-size: 14px;
color: #fff;
line-height: 1.5;
text-shadow: 0 1px 4px rgba(0,0,0,0.9);
}
@@ -0,0 +1,85 @@
import { useRef, useEffect, useState } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { computeSegmentState, poseToStyle } from '../../utils/rendering';
import styles from './Preview.module.css';
const LOGICAL_W = 1280;
const LOGICAL_H = 720;
function computeSubtitle(cutscene: ReturnType<typeof useSelectedCutscene>, currentMs: number) {
if (!cutscene) return null;
let elapsed = 0;
for (const line of cutscene.lines) {
const dur = line.durationMs > 0
? line.durationMs
: Math.max(1500, Math.round((line.text.length / 17) * 1000));
if (currentMs >= elapsed && currentMs < elapsed + dur) return line;
elapsed += dur;
}
return null;
}
export default function Preview() {
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize, setContainerSize] = useState({ w: LOGICAL_W, h: LOGICAL_H });
const currentTimeMs = useCutsceneStore(s => s.currentTimeMs);
const cutscene = useSelectedCutscene();
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver(entries => {
const e = entries[0];
if (e) setContainerSize({ w: e.contentRect.width, h: e.contentRect.height });
});
ro.observe(el);
return () => ro.disconnect();
}, []);
const subtitle = computeSubtitle(cutscene, currentTimeMs);
return (
<div className={styles.wrapper}>
<div className={styles.viewport} ref={containerRef}>
{!cutscene ? (
<div className={styles.empty}>Select or create a cutscene</div>
) : (
<>
{cutscene.imageSegments.map((seg, i) => {
const state = computeSegmentState(seg, currentTimeMs);
if (!state) return null;
const imgStyle = poseToStyle(
state.pose,
seg.width || LOGICAL_W,
seg.height || LOGICAL_H,
containerSize.w,
containerSize.h,
);
return (
<img
key={i}
src={`/${seg.path}`}
alt=""
style={{ ...imgStyle, opacity: state.alpha }}
draggable={false}
/>
);
})}
{subtitle && (
<div className={styles.subtitleBar}>
{subtitle.speaker && (
<div className={styles.speaker}>{subtitle.speaker}</div>
)}
<div className={styles.text}>{subtitle.text}</div>
</div>
)}
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,72 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import type { Cutscene } from '../../types/cutscene';
import styles from './RightPanel.module.css';
type CutscenePatch = Partial<Omit<Cutscene, 'imageSegments' | 'lines'>>;
function NumField({ label, value, onChange, min }: {
label: string; value: number; onChange: (v: number) => void; min?: number;
}) {
return (
<div className={styles.field}>
<label>{label}</label>
<input
type="number"
value={value}
min={min}
onChange={e => onChange(Number(e.target.value))}
/>
</div>
);
}
export default function CutsceneProperties() {
const cutscene = useSelectedCutscene();
const updateCutscene = useCutsceneStore(s => s.updateCutscene);
if (!cutscene) return <div className={styles.empty}>No cutscene selected</div>;
function upd(patch: CutscenePatch) {
updateCutscene(cutscene!.id, patch);
}
return (
<div className={styles.section}>
<div className={styles.sectionTitle}>Cutscene</div>
<div className={styles.field}>
<label>ID</label>
<input
type="text"
value={cutscene.id}
onChange={e => upd({ id: e.target.value })}
/>
</div>
<div className={styles.field}>
<label>Skippable</label>
<input
type="checkbox"
checked={cutscene.skippable}
onChange={e => upd({ skippable: e.target.checked })}
/>
</div>
<NumField label="Duration (ms)" value={cutscene.durationMs} onChange={v => upd({ durationMs: v })} min={0} />
<NumField label="Fade out (ms)" value={cutscene.fadeOutMs} onChange={v => upd({ fadeOutMs: v })} min={0} />
<NumField label="Fade in (ms)" value={cutscene.fadeInMs} onChange={v => upd({ fadeInMs: v })} min={0} />
<NumField label="End fade out (ms)" value={cutscene.endFadeOutMs} onChange={v => upd({ endFadeOutMs: v })} min={0} />
<NumField label="End fade in (ms)" value={cutscene.endFadeInMs} onChange={v => upd({ endFadeInMs: v })} min={0} />
<div className={styles.field}>
<label>onFadeIn callback</label>
<input
type="text"
value={cutscene.onFadeInCallback}
onChange={e => upd({ onFadeInCallback: e.target.value })}
placeholder="lua function name"
/>
</div>
</div>
);
}
@@ -0,0 +1,132 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import { AVAILABLE_IMAGES } from '../../constants/images';
import { EASING_OPTIONS } from '../../constants/easings';
import type { ImagePose } from '../../types/cutscene';
import styles from './RightPanel.module.css';
function NumField({ label, value, onChange, min, step }: {
label: string; value: number; onChange: (v: number) => void; min?: number; step?: number;
}) {
return (
<div className={styles.field}>
<label>{label}</label>
<input
type="number"
value={value}
min={min}
step={step ?? 1}
onChange={e => onChange(Number(e.target.value))}
/>
</div>
);
}
function PoseFields({ label, pose, onChange }: {
label: string;
pose: ImagePose;
onChange: (patch: Partial<ImagePose>) => void;
}) {
return (
<div className={styles.poseGroup}>
<div className={styles.poseTitle}>{label}</div>
<div className={styles.poseRow}>
<div className={styles.field}>
<label>centerX</label>
<input type="number" value={pose.centerX} step={0.01} min={0} max={1}
onChange={e => onChange({ centerX: Number(e.target.value) })} />
</div>
<div className={styles.field}>
<label>centerY</label>
<input type="number" value={pose.centerY} step={0.01} min={0} max={1}
onChange={e => onChange({ centerY: Number(e.target.value) })} />
</div>
<div className={styles.field}>
<label>scale</label>
<input type="number" value={pose.scale} step={0.05} min={0.1}
onChange={e => onChange({ scale: Number(e.target.value) })} />
</div>
</div>
</div>
);
}
export default function LayerProperties() {
const cutscene = useSelectedCutscene();
const { selectedLayerIndex, updateLayer, updateLayerFrom, updateLayerTo } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
updateLayer: s.updateLayer,
updateLayerFrom: s.updateLayerFrom,
updateLayerTo: s.updateLayerTo,
})));
if (!cutscene || selectedLayerIndex === null) return null;
const seg = cutscene.imageSegments[selectedLayerIndex];
if (!seg) return null;
return (
<div className={styles.section}>
<div className={styles.sectionTitle}>Layer {selectedLayerIndex + 1}</div>
<div className={styles.field}>
<label>Image</label>
<select
value={AVAILABLE_IMAGES.includes(seg.path) ? seg.path : '__custom__'}
onChange={e => {
if (e.target.value !== '__custom__') updateLayer(selectedLayerIndex, { path: e.target.value });
}}
>
{AVAILABLE_IMAGES.map(img => (
<option key={img} value={img}>{img.split('/').pop()}</option>
))}
{!AVAILABLE_IMAGES.includes(seg.path) && (
<option value="__custom__">(custom)</option>
)}
</select>
</div>
<div className={styles.field}>
<label>Path (manual)</label>
<input
type="text"
value={seg.path}
onChange={e => updateLayer(selectedLayerIndex, { path: e.target.value })}
placeholder="resources/..."
/>
</div>
<div className={styles.row2}>
<NumField label="Width" value={seg.width} onChange={v => updateLayer(selectedLayerIndex, { width: v })} min={1} />
<NumField label="Height" value={seg.height} onChange={v => updateLayer(selectedLayerIndex, { height: v })} min={1} />
</div>
<div className={styles.row2}>
<NumField label="Start (ms)" value={seg.startMs} onChange={v => updateLayer(selectedLayerIndex, { startMs: v })} min={0} />
<NumField label="End (ms)" value={seg.endMs} onChange={v => updateLayer(selectedLayerIndex, { endMs: v })} min={0} />
</div>
<div className={styles.row2}>
<NumField label="Fade in (ms)" value={seg.fadeInMs} onChange={v => updateLayer(selectedLayerIndex, { fadeInMs: v })} min={0} />
<NumField label="Fade out (ms)" value={seg.fadeOutMs} onChange={v => updateLayer(selectedLayerIndex, { fadeOutMs: v })} min={0} />
</div>
<div className={styles.field}>
<label>Easing</label>
<select value={seg.easing} onChange={e => updateLayer(selectedLayerIndex, { easing: e.target.value as typeof seg.easing })}>
{EASING_OPTIONS.map(e => <option key={e} value={e}>{e}</option>)}
</select>
</div>
<PoseFields
label="From"
pose={seg.from}
onChange={patch => updateLayerFrom(selectedLayerIndex, patch)}
/>
<PoseFields
label="To"
pose={seg.to}
onChange={patch => updateLayerTo(selectedLayerIndex, patch)}
/>
</div>
);
}
@@ -0,0 +1,208 @@
.panel {
width: 260px;
min-width: 240px;
background: #252525;
border-left: 1px solid #333;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.tab {
flex: 1;
padding: 8px 6px;
background: none;
border: none;
color: #777;
font-size: 11px;
border-bottom: 2px solid transparent;
transition: color 0.1s;
}
.tab:hover { color: #bbb; }
.tabActive {
color: #5b9bd5;
border-bottom-color: #5b9bd5;
}
.scroll {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
/* ── Sections ──────────────────────────────────────────────────── */
.section {
padding: 10px;
border-bottom: 1px solid #2e2e2e;
}
.sectionTitle {
font-size: 11px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.sectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.addBtn {
background: #2a4a6e;
border: 1px solid #3a6090;
color: #a8d0f0;
border-radius: 3px;
padding: 2px 8px;
font-size: 11px;
}
.addBtn:hover { background: #2e5480; }
/* ── Fields ────────────────────────────────────────────────────── */
.field {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 5px;
gap: 6px;
}
.field label {
flex-shrink: 0;
width: 100px;
text-align: right;
}
.field input[type="text"],
.field input[type="number"],
.field select,
.field textarea {
flex: 1;
min-width: 0;
width: 100%;
}
.field textarea {
resize: vertical;
min-height: 40px;
}
.field input[type="checkbox"] {
width: auto;
margin: 0;
}
.row2 {
display: flex;
gap: 6px;
}
.row2 .field {
flex: 1;
flex-direction: column;
align-items: flex-start;
}
.row2 .field label {
width: auto;
text-align: left;
margin-bottom: 2px;
}
/* ── Pose groups ───────────────────────────────────────────────── */
.poseGroup {
margin-top: 8px;
background: #1e1e1e;
border-radius: 4px;
padding: 6px 8px;
}
.poseTitle {
font-size: 10px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 5px;
}
.poseRow {
display: flex;
gap: 6px;
}
.poseRow .field {
flex: 1;
flex-direction: column;
align-items: flex-start;
margin-bottom: 0;
}
.poseRow .field label {
width: auto;
text-align: left;
margin-bottom: 2px;
}
/* ── Subtitle line cards ───────────────────────────────────────── */
.lineCard {
background: #1e1e1e;
border: 1px solid #2e2e2e;
border-radius: 4px;
padding: 8px;
margin-bottom: 6px;
}
.lineHeader {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 6px;
}
.lineIndex {
font-size: 10px;
color: #666;
flex: 1;
}
.iconBtn {
background: none;
border: 1px solid #3a3a3a;
color: #888;
border-radius: 3px;
padding: 1px 5px;
font-size: 11px;
}
.iconBtn:hover:not(:disabled) { background: #333; color: #ccc; }
.iconBtn:disabled { opacity: 0.3; cursor: default; }
.iconBtnDanger {
composes: iconBtn;
color: #a05050;
border-color: #5a2a2a;
}
.iconBtnDanger:hover { background: #3a2020; color: #e06c6c; }
.emptyLines {
color: #555;
font-size: 11px;
padding: 4px 0;
}
.empty {
padding: 20px;
color: #555;
font-size: 11px;
text-align: center;
}
@@ -0,0 +1,41 @@
import { useCutsceneStore } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import CutsceneProperties from './CutsceneProperties';
import LayerProperties from './LayerProperties';
import SubtitleLines from './SubtitleLines';
import styles from './RightPanel.module.css';
export default function RightPanel() {
const { selectedLayerIndex, selectLayer } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
selectLayer: s.selectLayer,
})));
return (
<div className={styles.panel}>
{/* Tab strip */}
<div className={styles.tabs}>
<button
className={`${styles.tab} ${selectedLayerIndex === null ? styles.tabActive : ''}`}
onClick={() => selectLayer(null)}
>
Cutscene
</button>
{selectedLayerIndex !== null && (
<button className={`${styles.tab} ${styles.tabActive}`}>
Layer {selectedLayerIndex + 1}
</button>
)}
</div>
<div className={styles.scroll}>
{selectedLayerIndex !== null ? (
<LayerProperties />
) : (
<CutsceneProperties />
)}
<SubtitleLines />
</div>
</div>
);
}
@@ -0,0 +1,89 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import styles from './RightPanel.module.css';
export default function SubtitleLines() {
const cutscene = useSelectedCutscene();
const { addLine, removeLine, moveLineUp, moveLineDown, updateLine } = useCutsceneStore(useShallow(s => ({
addLine: s.addLine,
removeLine: s.removeLine,
moveLineUp: s.moveLineUp,
moveLineDown: s.moveLineDown,
updateLine: s.updateLine,
})));
if (!cutscene) return null;
return (
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionTitle}>Subtitle Lines</div>
<button className={styles.addBtn} onClick={addLine}>+ Add</button>
</div>
{cutscene.lines.length === 0 && (
<div className={styles.emptyLines}>No lines. Click + Add.</div>
)}
{cutscene.lines.map((line, i) => (
<div key={i} className={styles.lineCard}>
<div className={styles.lineHeader}>
<span className={styles.lineIndex}>#{i + 1}</span>
<button className={styles.iconBtn} onClick={() => moveLineUp(i)} disabled={i === 0} title="Move up">↑</button>
<button className={styles.iconBtn} onClick={() => moveLineDown(i)} disabled={i === cutscene.lines.length - 1} title="Move down">↓</button>
<button className={styles.iconBtnDanger} onClick={() => removeLine(i)} title="Remove">✕</button>
</div>
<div className={styles.field}>
<label>Speaker</label>
<input
type="text"
value={line.speaker}
onChange={e => updateLine(i, { speaker: e.target.value })}
placeholder="(none)"
/>
</div>
<div className={styles.field}>
<label>Text</label>
<textarea
value={line.text}
rows={2}
onChange={e => updateLine(i, { text: e.target.value })}
/>
</div>
<div className={styles.row2}>
<div className={styles.field}>
<label>Duration (ms)</label>
<input
type="number"
value={line.durationMs}
min={0}
onChange={e => updateLine(i, { durationMs: Number(e.target.value) })}
/>
</div>
<div className={styles.field}>
<label>Wait confirm</label>
<input
type="checkbox"
checked={line.waitForConfirm}
onChange={e => updateLine(i, { waitForConfirm: e.target.checked })}
/>
</div>
</div>
<div className={styles.field}>
<label>Lua callback</label>
<input
type="text"
value={line.luaCallback}
onChange={e => updateLine(i, { luaCallback: e.target.value })}
placeholder="function name"
/>
</div>
</div>
))}
</div>
);
}
@@ -0,0 +1,192 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background: #1a1a1a;
border-top: 1px solid #333;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #222;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.tbBtn {
background: #2d2d2d;
border: 1px solid #404040;
color: #bbb;
border-radius: 3px;
padding: 3px 8px;
font-size: 11px;
transition: background 0.1s;
}
.tbBtn:hover:not(:disabled) { background: #3a3a3a; color: #fff; }
.tbBtn:disabled { opacity: 0.3; cursor: default; }
.spacer { flex: 1; }
.zoomLabel { font-size: 11px; color: #666; margin-right: 2px; }
.scroll {
flex: 1;
overflow: auto;
position: relative;
cursor: default;
}
/* ── Ruler ────────────────────────────────────────────────────── */
.ruler {
display: flex;
height: 22px;
background: #212121;
border-bottom: 1px solid #333;
position: sticky;
top: 0;
z-index: 10;
user-select: none;
cursor: crosshair;
}
.tick {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: #444;
}
.tickLabel {
position: absolute;
top: 3px;
left: 3px;
font-size: 9px;
color: #666;
white-space: nowrap;
}
/* ── Layers area ──────────────────────────────────────────────── */
.layersArea {
position: relative;
}
.playhead {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #e05050;
z-index: 20;
pointer-events: none;
}
.gridLine {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: rgba(255,255,255,0.04);
pointer-events: none;
}
/* ── Layer row ────────────────────────────────────────────────── */
.row {
position: absolute;
left: 0;
right: 0;
height: 32px;
display: flex;
align-items: center;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.1s;
}
.row:hover { background: rgba(255,255,255,0.03); }
.rowSelected { background: rgba(91,155,213,0.08); }
.rowLabel {
position: absolute;
left: 0;
width: 140px;
height: 100%;
display: flex;
align-items: center;
padding: 0 8px;
background: #1e1e1e;
border-right: 1px solid #2a2a2a;
z-index: 5;
overflow: hidden;
flex-shrink: 0;
}
.layerName {
font-size: 11px;
color: #bbb;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Segment bar ──────────────────────────────────────────────── */
.bar {
position: absolute;
height: 22px;
border-radius: 3px;
cursor: grab;
top: 5px;
border: 1px solid rgba(255,255,255,0.15);
box-sizing: border-box;
min-width: 4px;
}
.bar:active { cursor: grabbing; }
.handle {
position: absolute;
top: 0;
bottom: 0;
width: 6px;
cursor: ew-resize;
z-index: 2;
}
.handleLeft { left: 0; border-radius: 3px 0 0 3px; background: rgba(255,255,255,0.15); }
.handleRight { right: 0; border-radius: 0 3px 3px 0; background: rgba(255,255,255,0.15); }
/* ── Subtitle row ─────────────────────────────────────────────── */
.subtitleRow {
position: absolute;
left: 0;
right: 0;
height: 14px;
border-bottom: 1px solid #2a2a2a;
}
.subtitleLabel {
position: absolute;
left: 0;
width: 140px;
height: 100%;
background: #1e1e1e;
border-right: 1px solid #2a2a2a;
display: flex;
align-items: center;
padding: 0 8px;
font-size: 9px;
color: #555;
z-index: 5;
}
.subtitleBlock {
position: absolute;
top: 2px;
height: 10px;
background: #8a6040;
border-radius: 2px;
border: 1px solid #aa7050;
opacity: 0.8;
}
@@ -0,0 +1,285 @@
import { useRef, useCallback, useState, useEffect } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import styles from './Timeline.module.css';
const LABEL_WIDTH = 140;
const ROW_HEIGHT = 32;
const SUBTITLE_ROW_HEIGHT = 14;
const MIN_ZOOM = 20; // px per second
const MAX_ZOOM = 400;
const LAYER_COLORS = ['#4a7fc1', '#c17a4a', '#4ac17a', '#c14a7a', '#7a4ac1', '#c1b44a', '#4ac1c1'];
function layerColor(i: number) { return LAYER_COLORS[i % LAYER_COLORS.length]; }
function basename(path: string) {
return path.split('/').pop() ?? path;
}
export default function Timeline() {
const cutscene = useSelectedCutscene();
const { selectedLayerIndex, currentTimeMs, playState } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
currentTimeMs: s.currentTimeMs,
playState: s.playState,
})));
const { selectLayer, addLayer, removeLayer, moveLayerUp, moveLayerDown, updateLayer, setCurrentTime, setPlayState } = useCutsceneStore(useShallow(s => ({
selectLayer: s.selectLayer,
addLayer: s.addLayer,
removeLayer: s.removeLayer,
moveLayerUp: s.moveLayerUp,
moveLayerDown: s.moveLayerDown,
updateLayer: s.updateLayer,
setCurrentTime: s.setCurrentTime,
setPlayState: s.setPlayState,
})));
const [pxPerSec, setPxPerSec] = useState(60);
const scrollRef = useRef<HTMLDivElement>(null);
const isDraggingPlayhead = useRef(false);
const dragState = useRef<{
type: 'move' | 'left' | 'right';
layerIndex: number;
startX: number;
startMs: number;
endMs: number;
durationMs: number;
} | null>(null);
const msToX = useCallback((ms: number) => (ms / 1000) * pxPerSec, [pxPerSec]);
const xToMs = useCallback((x: number) => Math.max(0, Math.round((x / pxPerSec) * 1000)), [pxPerSec]);
const totalMs = cutscene
? Math.max(
cutscene.durationMs,
cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0),
10000,
) + 2000
: 12000;
const rulerWidth = Math.ceil(msToX(totalMs));
// Ruler ticks
const tickStepMs = pxPerSec >= 100 ? 500 : pxPerSec >= 50 ? 1000 : 2000;
const labelStepMs = pxPerSec >= 100 ? 1000 : pxPerSec >= 50 ? 2000 : 4000;
const ticks: number[] = [];
for (let ms = 0; ms <= totalMs; ms += tickStepMs) ticks.push(ms);
// Scroll wheel zoom
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
function onWheel(e: WheelEvent) {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
setPxPerSec(prev => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, prev * (e.deltaY < 0 ? 1.15 : 0.87))));
}
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
// Auto-scroll playhead into view while playing
useEffect(() => {
if (playState !== 'playing') return;
const el = scrollRef.current;
if (!el) return;
const x = msToX(currentTimeMs) + LABEL_WIDTH;
const { scrollLeft, clientWidth } = el;
if (x > scrollLeft + clientWidth - 40) {
el.scrollLeft = x - clientWidth + 80;
}
}, [currentTimeMs, playState, msToX]);
// ── Playhead drag ──────────────────────────────────────────────────────────
function onRulerMouseDown(e: React.MouseEvent) {
if (e.button !== 0) return;
isDraggingPlayhead.current = true;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const x = e.clientX - rect.left - LABEL_WIDTH + (scrollRef.current?.scrollLeft ?? 0);
setCurrentTime(xToMs(x));
if (playState === 'playing') setPlayState('paused');
function onMove(ev: MouseEvent) {
const x2 = ev.clientX - rect.left - LABEL_WIDTH + (scrollRef.current?.scrollLeft ?? 0);
setCurrentTime(xToMs(x2));
}
function onUp() {
isDraggingPlayhead.current = false;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
// ── Segment bar drag ───────────────────────────────────────────────────────
function onBarMouseDown(e: React.MouseEvent, layerIndex: number, type: 'move' | 'left' | 'right') {
e.preventDefault();
e.stopPropagation();
selectLayer(layerIndex);
const seg = cutscene!.imageSegments[layerIndex];
dragState.current = {
type,
layerIndex,
startX: e.clientX,
startMs: seg.startMs,
endMs: seg.endMs,
durationMs: seg.endMs - seg.startMs,
};
function onMove(ev: MouseEvent) {
if (!dragState.current) return;
const dx = ev.clientX - dragState.current.startX;
const dMs = Math.round((dx / pxPerSec) * 1000);
const { type, layerIndex: li, startMs, endMs, durationMs } = dragState.current;
if (type === 'move') {
const newStart = Math.max(0, startMs + dMs);
updateLayer(li, { startMs: newStart, endMs: newStart + durationMs });
} else if (type === 'left') {
const newStart = Math.max(0, Math.min(endMs - 100, startMs + dMs));
updateLayer(li, { startMs: newStart });
} else {
const newEnd = Math.max(startMs + 100, endMs + dMs);
updateLayer(li, { endMs: newEnd });
}
}
function onUp() {
dragState.current = null;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
const playheadX = msToX(currentTimeMs) + LABEL_WIDTH;
// Subtitle timing for display
let subtitleBlocks: { x: number; w: number; text: string }[] = [];
if (cutscene) {
let elapsed = 0;
for (const line of cutscene.lines) {
const dur = line.durationMs > 0
? line.durationMs
: Math.max(1500, Math.round((line.text.length / 17) * 1000));
subtitleBlocks.push({ x: msToX(elapsed), w: msToX(dur), text: line.text || '…' });
elapsed += dur;
}
}
const layerCount = cutscene?.imageSegments.length ?? 0;
const totalHeight = layerCount * ROW_HEIGHT + SUBTITLE_ROW_HEIGHT + 20;
return (
<div className={styles.container}>
{/* Toolbar */}
<div className={styles.toolbar}>
<button className={styles.tbBtn} onClick={addLayer} disabled={!cutscene} title="Add layer">+ Layer</button>
{selectedLayerIndex !== null && cutscene && (
<>
<button className={styles.tbBtn} onClick={() => moveLayerUp(selectedLayerIndex!)} disabled={selectedLayerIndex! <= 0} title="Move up">↑</button>
<button className={styles.tbBtn} onClick={() => moveLayerDown(selectedLayerIndex!)} disabled={selectedLayerIndex! >= layerCount - 1} title="Move down">↓</button>
<button className={styles.tbBtn} onClick={() => removeLayer(selectedLayerIndex!)} title="Remove layer" style={{ color: '#e06c6c' }}>✕ Layer</button>
</>
)}
<div className={styles.spacer} />
<span className={styles.zoomLabel}>Zoom:</span>
<button className={styles.tbBtn} onClick={() => setPxPerSec(p => Math.max(MIN_ZOOM, p * 0.75))}>−</button>
<button className={styles.tbBtn} onClick={() => setPxPerSec(p => Math.min(MAX_ZOOM, p * 1.33))}>+</button>
<button className={styles.tbBtn} onClick={() => setPxPerSec(60)}>Reset</button>
</div>
{/* Scrollable area */}
<div className={styles.scroll} ref={scrollRef}>
{/* Ruler */}
<div
className={styles.ruler}
style={{ width: LABEL_WIDTH + rulerWidth }}
onMouseDown={onRulerMouseDown}
>
<div style={{ width: LABEL_WIDTH, flexShrink: 0 }} />
<div style={{ position: 'relative', flex: 1 }}>
{ticks.map(ms => (
<div
key={ms}
className={styles.tick}
style={{ left: msToX(ms) }}
>
{ms % labelStepMs === 0 && (
<span className={styles.tickLabel}>{ms / 1000}s</span>
)}
</div>
))}
</div>
</div>
{/* Layers + playhead overlay */}
<div
className={styles.layersArea}
style={{ width: LABEL_WIDTH + rulerWidth, minHeight: totalHeight }}
>
{/* Playhead */}
<div className={styles.playhead} style={{ left: playheadX }} />
{/* Grid lines */}
{ticks.filter(ms => ms % labelStepMs === 0).map(ms => (
<div key={ms} className={styles.gridLine} style={{ left: LABEL_WIDTH + msToX(ms) }} />
))}
{/* Layer rows */}
{cutscene?.imageSegments.map((seg, i) => {
const isSelected = selectedLayerIndex === i;
const color = layerColor(i);
const barX = LABEL_WIDTH + msToX(seg.startMs);
const barW = Math.max(4, msToX(seg.endMs) - msToX(seg.startMs));
return (
<div
key={i}
className={`${styles.row} ${isSelected ? styles.rowSelected : ''}`}
style={{ top: i * ROW_HEIGHT }}
onClick={() => selectLayer(i)}
>
{/* Label */}
<div className={styles.rowLabel} style={{ borderLeft: `3px solid ${color}` }}>
<span className={styles.layerName}>{basename(seg.path) || `Layer ${i + 1}`}</span>
</div>
{/* Bar */}
<div
className={styles.bar}
style={{ left: barX, width: barW, background: color + (isSelected ? 'cc' : '88') }}
onMouseDown={e => onBarMouseDown(e, i, 'move')}
>
<div className={`${styles.handle} ${styles.handleLeft}`}
onMouseDown={e => onBarMouseDown(e, i, 'left')} />
<div className={`${styles.handle} ${styles.handleRight}`}
onMouseDown={e => onBarMouseDown(e, i, 'right')} />
</div>
</div>
);
})}
{/* Subtitle blocks row */}
{cutscene && (
<div
className={styles.subtitleRow}
style={{ top: layerCount * ROW_HEIGHT }}
>
<div className={styles.subtitleLabel}>Subtitles</div>
{subtitleBlocks.map((b, i) => (
<div
key={i}
className={styles.subtitleBlock}
style={{ left: LABEL_WIDTH + b.x, width: Math.max(2, b.w) }}
title={b.text}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import type { EasingType } from '../types/cutscene';
export const EASING_OPTIONS: EasingType[] = [
'Linear',
'EaseInSine', 'EaseOutSine', 'EaseInOutSine',
'EaseInQuad', 'EaseOutQuad', 'EaseInOutQuad',
'EaseInCubic', 'EaseOutCubic', 'EaseInOutCubic',
];
export function applyEasing(t: number, easing: EasingType): number {
const pi = Math.PI;
switch (easing) {
case 'Linear': return t;
case 'EaseInSine': return 1 - Math.cos((t * pi) / 2);
case 'EaseOutSine': return Math.sin((t * pi) / 2);
case 'EaseInOutSine': return -(Math.cos(pi * t) - 1) / 2;
case 'EaseInQuad': return t * t;
case 'EaseOutQuad': return 1 - (1 - t) * (1 - t);
case 'EaseInOutQuad': return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
case 'EaseInCubic': return t * t * t;
case 'EaseOutCubic': return 1 - Math.pow(1 - t, 3);
case 'EaseInOutCubic':return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
default: return t;
}
}
+15
View File
@@ -0,0 +1,15 @@
export const AVAILABLE_IMAGES: string[] = [
'resources/w/cutscenes/cutscene2/cs2_background.png',
'resources/w/cutscenes/cutscene2/cs2_books.png',
'resources/w/cutscenes/cutscene2/cs2_chair.png',
'resources/w/cutscenes/cutscene2/cs2_gg001.png',
'resources/w/cutscenes/cutscene2/cs2_gg002.png',
'resources/w/cutscenes/cutscene2/cs2_gg003.png',
'resources/w/cutscenes/cutscene2/cs2_gg004.png',
'resources/w/cutscenes/cutscene3/cs2_foreground.png',
'resources/black.png',
'resources/w/cutscenes/cutscene_exit_darklands/img.png',
'resources/w/cutscenes/cutscene_exit_darklands/img2.png',
'resources/w/cutscenes/cutscene3/img.png',
'resources/w/white.png',
];
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useRef } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../store/cutsceneStore';
export function usePlayback() {
const { playState, currentTimeMs, setPlayState, setCurrentTime } = useCutsceneStore();
const cutscene = useSelectedCutscene();
const rafRef = useRef<number | null>(null);
const lastTsRef = useRef<number | null>(null);
useEffect(() => {
if (playState !== 'playing') {
lastTsRef.current = null;
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
return;
}
function totalDuration() {
if (!cutscene) return 5000;
const segMax = cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0);
const content = Math.max(cutscene.durationMs, segMax);
return content + cutscene.endFadeOutMs + cutscene.endFadeInMs;
}
function tick(ts: number) {
if (lastTsRef.current === null) lastTsRef.current = ts;
const delta = ts - lastTsRef.current;
lastTsRef.current = ts;
const newTime = useCutsceneStore.getState().currentTimeMs + delta;
const total = totalDuration();
if (newTime >= total) {
setCurrentTime(total);
setPlayState('stopped');
return;
}
setCurrentTime(newTime);
rafRef.current = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
}, [playState, cutscene, setPlayState, setCurrentTime]);
return null;
}
+52
View File
@@ -0,0 +1,52 @@
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 13px;
background: #1a1a1a;
color: #e0e0e0;
}
button {
cursor: pointer;
font-family: inherit;
font-size: 12px;
}
input, select, textarea {
font-family: inherit;
font-size: 12px;
background: #2a2a2a;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 3px;
padding: 3px 6px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #5b9bd5;
}
label {
color: #aaa;
font-size: 11px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track { background: #1a1a1a; }
::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #666; }
Loaded 100 of 1066 files, more files were not shown because too many files have changed in this diff. Show more