Compare commits

...
52 Commits
Author SHA1 Message Date
Vladislav Khorev c6d98bd72a Working about pathfinding 2026-11-08 17:24:15 +06:00
Vladislav Khorev 6a5adb1c79 Added nav 2026-11-08 16:51:31 +06:00
Vladislav Khorev 40527f10ac Added battles in networking 2026-11-07 14:27:22 +06:00
Vladislav Khorev be8ee31f03 Remove player health, use playermap 2026-11-07 13:48:54 +06:00
Vladislav Khorev b932a07cba Fixing player HP display 2026-11-07 01:57:29 +06:00
Vladislav Khorev f4bd3b6bed Working on hostile NPCs, fixing darklands mode for battle zone 2026-11-07 00:03:03 +06:00
Vladislav Khorev 2e6b93f3ac player connect-disconnect 2026-11-06 22:07:47 +06:00
Vladislav Khorev 3f68a8ba07 Remove broad command 2026-11-06 21:30:17 +06:00
Vladislav Khorev 1ab9f300b6 Also interpolate taxi and interactive objects 2026-11-06 15:33:15 +06:00
Vladislav Khorev 28846b9f25 Working on interpolation 2026-11-06 15:14:48 +06:00
Vladislav Khorev 23fef6f349 Set up basic deterministic lockstep 2026-11-05 23:02:53 +06:00
Vladislav Khorev 28c6e83eda Working on the server 2026-11-05 20:12:30 +06:00
Vladislav Khorev 075750442f Implemented the real server 2026-10-30 16:37:30 +06:00
Vladislav Khorev 931236bec0 fixed anim bug 2026-10-30 15:09:13 +06:00
Vladislav Khorev 297bd7377f Separating state out of the location 2026-10-30 14:56:24 +06:00
Vladislav Khorev 5e9a663812 More work on states, added bot 2026-10-29 22:37:30 +06:00
Vladislav Khorev 66e93d46f4 Major refactoring - internal server state machine implemented 2026-10-29 00:09:16 +06:00
Vladislav Khorev 71c654e5b6 Merge branch 'tutorial-refactor' of gitea.fishrungames.com:salmon-engine-projects/shadow-over-bishkek001 into tutorial-refactor 2026-10-27 20:25:15 +06:00
Vladislav Khorev 570f051281 Fixing bugs 2026-10-27 20:24:33 +06:00
Vladislav Khorev 0240721d46 Working on path visualization 2026-10-27 20:04:18 +06:00
Vladislav Khorev e350ec26ac Working on tutorial 2026-10-25 02:04:22 +06:00
Vladislav Khorev 96e748715b Added korean location 2026-10-25 01:29:38 +06:00
Vladislav Khorev d5ef126264 working on refactoring 2026-10-24 22:37:20 +06:00
Vladislav Khorev 9b71ee447a Working on loading 2026-10-23 23:49:54 +06:00
Vladislav Khorev d38a75ea72 working on campaign 2026-10-23 22:47:55 +06:00
Vladislav Khorev 9fa120d4ad Working on tutorial refactoring 2026-10-23 22:03:55 +06:00
Vladislav Khorev ea8f64c940 Restore back old navigation system 2026-10-22 23:02:25 +06:00
Vladislav Khorev c9c6b7e7fb Working on minor fixing, working on pathfinding 2026-08-19 03:40:02 +05:00
Vladislav Khorev a11c4614ea Resize Android even more, fixing crash when changing languages on Android 2026-08-01 20:18:18 +03:00
Vladislav Khorev 5cbe7a345e Android version bug fix 2026-08-01 19:42:21 +03:00
Vladislav Khorev 2add42c1cb Minor changes for the Windows non-steam version 2026-07-25 17:27:21 +03:00
Vladislav Khorev 4039c24372 change permissions 2026-07-25 16:58:18 +03:00
Vladislav Khorev 7df68febca Fixing minor stuff, working on Web version 2026-07-25 16:56:02 +03:00
Vladislav Khorev 5ecf5fb9d7 Adapting music to web, fixing scripts, fixing bug in initial state 2026-07-24 23:58:29 +03:00
Vladislav Khorev c3f449b999 Prepare a version for Steam and fixing bugs 2026-07-22 10:59:20 +03:00
Vladislav Khorev 8a0a8390b2 Windows, make portable version + NSIS installer 2026-07-20 21:54:43 +03:00
Vladislav Khorev d39f5ca68b Working on adapting for Windows 2026-07-20 20:48:41 +03:00
Vladislav Khorev 1c20333f30 Fixing UI bugs, some clean up 2026-07-20 18:09:16 +03:00
Vladislav Khorev 1d8e103690 Android build is successfully uploaded to Google Play 2026-07-19 23:21:23 +03:00
Vladislav Khorev 4c9ecb31d8 Fixing shadows going apeshit in dorm on Android 2026-07-19 22:34:03 +03:00
Vladislav Khorev 2e749ed26e Fixing Android stuff 2026-07-19 21:38:22 +03:00
Vladislav Khorev fc93eb6680 Fixing android bugs and more 2026-07-19 19:49:50 +03:00
Vladislav Khorev 5b21ba615e Added localization to Android 2026-07-19 13:58:26 +03:00
Vladislav Khorev 02ba86331b Working on shadow bug 2026-07-19 13:29:35 +03:00
Vladislav Khorev 6a79e8e41e Fixing screen resize bugs 2026-07-19 13:07:39 +03:00
Vladislav Khorev 6406c4f258 Fixing bug with save game, sounds, and some hints are out of screen on Android 2026-07-19 12:19:28 +03:00
Vladislav Khorev 9f812f0318 Android fixing logs 2026-07-18 20:44:54 +03:00
Vladislav Khorev adf9871861 Remove accidentally added files 2026-07-18 20:22:49 +03:00
Vladislav Khorev 825a56a4a4 Added music and perfom bug fixes 2026-07-18 20:21:31 +03:00
Vladislav Khorev e5e3d7bb28 Working on Android build 2026-07-18 17:46:05 +03:00
Vladislav Khorev e773b51985 Working on Android version 2026-07-18 13:19:08 +03:00
Vladislav Khorev 0975f3b9cd Some clean up, web version prepare 2026-07-17 21:27:47 +03:00
585 changed files with 63881 additions and 13307 deletions

No files matched your search

+3
View File
@@ -5,3 +5,6 @@
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text
+5
View File
@@ -409,3 +409,8 @@ web_resources/
pc_resources/
resources_hd/
web_resources_x2/
android_resources/
.artifacts/
*.zip
+76
View File
@@ -0,0 +1,76 @@
# Character Presentation Interpolation
The authoritative game simulation still advances in exact 100 ms ticks. Character
interpolation is a rendering-only feature and does not modify `CharacterState`,
network commands, tick IDs, state hashes, or deterministic lockstep behavior.
## How it works
Each runtime `Character` keeps two presentation samples:
- `previous`: the visual values received at the preceding completed simulation tick;
- `current`: the visual values received at the latest completed simulation tick.
Rendering calculates an alpha from the real elapsed time since the latest completed
tick, clamped to the 100 ms simulation interval. It renders the delayed
`previous -> current` transition. This introduces at most one simulation tick of
presentation latency, but it avoids predicting a state that has not arrived from
the authoritative simulation.
The following values are interpolated:
- Position uses linear interpolation.
- Facing angle uses the shortest angular path, including wraparound at ±π.
- Animation frame remains fractional and is interpolated when both samples use
the same animation state. Looping clips treat the last frame to first frame
transition as a cyclic interval instead of interpolating backward through the
entire clip.
The interpolated values are used by mesh, weapon, shadow, night-lighting, name,
and health-bar rendering. The local camera and shadow-map center follow the same
interpolated player position; otherwise the camera would jump to the newest
authoritative position while the player model was still in its presentation
transition. Gameplay queries, commands, callbacks, collision, and presentation
events continue to use the authoritative snapshot.
## Other presentation animations
Teleport-zone star particles are advanced by `Location::updatePresentation()`
once per render frame using the real frame delta. They are no longer advanced
only from the 100 ms simulation-tick callback, so their visual motion remains
continuous without affecting teleport activation or other authoritative state.
Dialogue typewriter text and cutscene presentation timers use the same
per-render-frame path. This keeps character reveals, subtitle timing, fades, and
cutscene image poses smooth instead of stepping in 100 ms increments.
Interactive objects retain authoritative move, rotate, scale, and fade tasks on
the simulation tick. Their previous and current visual transforms are sampled
at tick boundaries and interpolated only while drawing, so doors and similar
objects remain logically deterministic while their presentation is smooth.
The same render-only sampling is used for `NpcCar` taxi visuals. Its position,
body rotation, wheel rotation, and steering angle are interpolated for drawing;
the authoritative taxi movement and waypoint logic remain fixed-step.
## Animation skinning
GPU and CPU bone interpolation receive the fractional render frame instead of an
integer-truncated frame. Bone positions are linearly interpolated and bone
rotations use spherical interpolation as before. Frames at the end of a clip are
clamped to the final keyframe.
## Discontinuities
The presentation buffer is reset instead of blended when a character is first
observed, changes animation state or enabled state, or has not received a tick
for a sufficiently long gap. This prevents stale positions, animation clips, or
disabled characters from producing visible interpolation artifacts.
## Local and networked games
Local games continue accumulating wall-clock remainder time and executing exact
100 ms simulation updates. Networked games continue consuming only contiguous
server tick messages. Both paths publish completed snapshots to the same runtime
presentation objects; interpolation only changes how those snapshots are drawn
between updates.
+537
View File
@@ -0,0 +1,537 @@
# Game State Architecture
## Purpose
The game separates authoritative gameplay state from rendering and runtime
resources. The same snapshot-and-command boundary supports the in-process
single-player server and a WebSocket deterministic-lockstep client/server.
## Architecture Overview
The architecture has six layers:
1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
3. `ClientLocationState` owns per-client `LocationLocalState` records, including camera orientation. Graphical hosts own this state; it is never replicated.
4. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
5. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
6. Protocol-v2 WebSocket hosts assign preconfigured player slots and exchange
one initial state followed by ordered 100 ms tick bundles.
```text
Input / UI
| typed GameCommand
v
IGameServer
|
v
InternalGameServer ---- owns ----> GameState
| |
| update | stable state objects
v v
Location / Character / InteractiveObject runtime
|
v
Renderer
IGameServer ---- immutable GameSnapshot ----> UI and other clients
```
## Authoritative State Model
### `GameState`
`GameState` is the top-level state object. It contains:
- Campaign ID and current location ID.
- Day, night, dawn, and darklands flags.
- A map of campaign-level players keyed by stable player ID.
- A map of typed `LocationGameState` records keyed by location ID.
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values.
Live `Location` pointers and rendering objects are no longer stored in `GameState`.
Embedded state models such as `Inventory` and `QuestJournal` contain only value
data and deterministic operations; they do not retain runtime callbacks.
### Campaign-level players
Player `CharacterState` objects are stored in `GameState::players`. The current single-player game uses the ID `"player"`.
The player state is no longer owned by a `Character` or duplicated as a separately authoritative state for each loaded location. This permits additional player IDs to be introduced later without changing the aggregate ownership model.
Every player, including the one controlled by the current client, remains in this
same authoritative collection. `IGameServer::localPlayerId()` identifies which
entry is local to a particular client connection; it is not a separate state
object.
`LocationGameState` does not retain a player pointer or back-reference to `GameState`.
Its update boundary receives `GameState::players` by reference, allowing character
lookup and simulation to treat every campaign player by stable ID.
### `LocationGameState`
Each location record owns:
- One `LocationNetworkState`.
- NPC `CharacterState` objects keyed by configured ID.
- `InteractiveObjectState` objects keyed by configured ID.
- Trigger-zone enabled/inside flags.
- Teleport-zone active flags.
- Serialized dialogue runtime state.
- Serialized Lua runtime globals.
World-mode flags (`darklands`, `night`, and `dawn`) exist only at the aggregate
`GameState` level. Location rendering and scripts read those values from the
top-level snapshot rather than maintaining per-location mirrors.
State objects use `shared_ptr` internally to keep addresses stable for the
remaining local runtime bindings. These pointers never appear in public snapshots.
### Stable identity
- Locations use campaign configuration IDs.
- Players use keys from `GameState::players`.
- NPCs use required location-local configuration IDs.
- Interactive objects use required location-local configuration IDs.
- Trigger and teleport zones use their configured IDs.
Missing, empty, duplicate, and reserved IDs are content errors. Runtime references,
Lua APIs, snapshots, and persistence all use these IDs rather than configuration
array positions.
## Server Interface
`IGameServer` is the public state boundary. It exposes:
- `snapshot()` for an immutable, revisioned `GameSnapshot`.
- `submit()` for typed `GameCommand` values.
- `update()` for advancing a local fixed tick or polling a remote implementation.
- `save()` and `load()` for server-side persistence.
No public method returns a mutable state pointer or reference.
### Snapshots
`GameSnapshot` and its nested snapshot types are value-only read models. They contain no renderer objects, runtime `Location` pointers, mutable references, or active callbacks.
Snapshots include:
- Campaign and world-mode state.
- All players.
- Locations, NPCs, interactive objects, triggers, and teleports.
- Inventory item IDs.
- Quest views.
- Tutorial, chat, taxi, money, and health data.
- Script-global values.
Snapshots have a monotonically increasing revision. `InternalGameServer` caches the current immutable snapshot and rebuilds it only after the authoritative state changes.
### Commands
`GameCommand` is a C++17 `std::variant` of semantic command types. Current commands cover:
- World mode and current location.
- Player health and money.
- Tutorial progression and flags.
- Chat history and unread state.
- Taxi state.
- Inventory additions and removals.
- Quest status, objective completion/visibility, and active-objective changes.
- Script globals.
- Character transforms, attack targets, navigation destinations, stopping,
facing, replanning, health, damage, death reset, attack combos, and animation
metadata. Character position, facing angle, home position, face target, and
enabled state use separate semantic commands; no whole-character commit
command is exposed.
- Interactive-object activation.
- Trigger-zone state.
- Quest-definition reloads.
The remote implementation serializes destination intent from clients, while
the server emits connection-lifecycle presence commands. The server derives the
character and location from the assigned connection slot; arbitrary state
mutation commands are not accepted from the network.
`SetCharacterEnabledCommand` is consumed by the server and replicated to every
client in the ordered tick bundle when a slot connects or disconnects.
### Presentation events
`IGameServer::drainEvents()` returns value-only events produced by successful
authoritative mutations. Inventory add/remove and quest unlock/complete/fail/
objective-complete events contain stable IDs only. `Game` drains these events and
performs local tutorial, toast, and audio presentation through `MenuManager`.
Loading and persistence restoration clear pending events and remain silent.
## Internal Server
`InternalGameServer` is the current `IGameServer` implementation. It:
- Exclusively owns the aggregate `GameState`.
- Executes typed commands against authoritative state.
- Advances the active location simulation in exact 100 ms ticks.
- Builds and caches immutable snapshots.
- Delegates save/load serialization to the aggregate state model.
`LocalGameServerHost` is the graphical client's integration adapter. It owns loaded
runtime `Location` objects, forwards simulation events to them, and synchronizes
dialogue, Lua, trigger, and teleport auxiliary state. `Game` uses the explicit
`IGameServerHost` lifecycle interface and is no longer a friend of
`InternalGameServer`. The standalone server does not compile this adapter.
Location loading has two phases. `LocationContentLoader` parses renderer-free
`LocationDefinition` data, and `LocationGameState::setup()` creates/reconciles
authoritative NPC, interactive-object, navigation, taxi, trigger, teleport, and
animation state. The graphical `Location::setup()` runs afterward and only builds
presentation objects from that definition and the prepared state. Lua and dialogue
runtime behavior remains client-side.
### Animation and combat
Logical animation frame clocks live in `CharacterState`. The renderer-free content
loader reads animation frame metadata before authoritative entities are created.
Runtime `Character` construction only loads models for presentation and does not
submit metadata mutations back to the server.
`CharacterState::update()` returns value-only events containing arrival Lua function
names, attack impacts, navigation requests, and death-animation completion.
`LocationGameState` applies queued damage after every character has advanced.
Navigation replans are consumed by `InternalGameServer` as deterministic
simulation consequences rather than being resubmitted by presentation code.
Runtime `Character` objects only pose loaded models and produce audio, particles,
and presentation callbacks.
### Character state operations
Gameplay operations mutate the authoritative `CharacterState`, not the runtime
`Character`:
- `setTarget()` normalizes the destination, avoids redundant requests, stores the
optional Lua arrival function name, and asks the authoritative `LocationNetworkState` to
call `planPathExceptMyself()`.
- `forceReplan()` recalculates the current requested destination using the same
location navigation state.
- `stopInPlace()` clears navigation and any pending arrival function.
- `stopAndRotateToTarget()`, `resetPlayerAfterDeath()`, `setHp()`, and
`tryAttackCombo()` update their respective state directly.
`InternalGameServer::submit()` resolves the character and location, then invokes
these operations for the corresponding semantic command. The runtime `Location`
provides convenience methods for submitting those commands from input, scripts,
collision handling, and presentation-event processing.
## Runtime and Visual Object Bindings
Runtime visual objects do not own their authoritative state objects.
- `Location` keeps its `IGameServer` reference, stable location ID, and a shared
`LocationLocalState` supplied by the graphical host. It resolves its immutable
`LocationNetworkState` view from `IGameServer::snapshot()`, while rendering and
camera input access the local state directly.
- `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
`Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
`shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
while shared ownership keeps that whole immutable snapshot alive. Update, input,
editor, and draw operations capture one such pointer and use it consistently
throughout the operation. A missing or unloaded entity produces an empty pointer
and presentation or input work is skipped.
The authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover pending
interaction targets, navigation switching and area changes, editor obstacles, and
taxi reset. Camera orientation is deliberately not a server command. No
whole-location commit command is exposed.
Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`.
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
callback. Character construction is presentation-only; frame ranges are already
present in `CharacterState` after content setup. It focuses on model posing,
rendering, audio, particles, and runtime callbacks.
Death-animation and health-change callbacks remain runtime-only on `Character`.
Arrival behavior is represented solely by the serialized Lua function name in
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
when it consumes the arrival event.
`Location` owns presentation-only runtime characters for all non-local entries in
`GameSnapshot::players`. It reconciles them by stable ID as snapshots change,
reuses the local-player assets and animation set, and includes them in all render,
shadow, lighting, animation, damage, and name-tag passes. The local entry is
excluded using `IGameServer::localPlayerId()` because its visual is already owned
by `Game`.
`GameObjectLoader` now:
1. Receives immutable object/NPC definitions parsed by `LocationContentLoader`.
2. Resolves already-prepared authoritative entities by stable ID.
3. Constructs meshes, textures, VBOs, `InteractiveObject`, and `Character`
presentation wrappers without creating or overwriting authoritative state.
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
## State Update Flow
Render frames and simulation ticks are separate:
1. SDL input and UI presentation use the real elapsed render-frame time.
2. A local host accumulates `steady_clock` elapsed time and runs zero or more
exact 100 ms simulation ticks. A remote host polls transport every frame but
advances only from contiguous server tick messages.
3. Commands assigned to tick `N` are applied in server sequence order before
simulation tick `N`.
4. The server freezes location character observations and updates every player,
NPC, taxi, and interactive object with exactly 100 ms. An empty player
collection remains valid.
5. Attack impacts are queued and applied only after all characters advance.
Navigation recovery is applied inside the deterministic state path.
6. The server invalidates its cached snapshot and advances its revision.
7. Runtime locations consume the completed tick's value-only events and perform
rendering, audio, particles, and supported presentation callbacks.
8. Rendering can occur between ticks and never changes authoritative tick time.
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
## Campaign and Location Lifecycle
Only one location is normally loaded as a runtime object.
### Campaign loading
When a campaign starts:
- Existing campaign state and runtime locations are cleared.
- Quest definitions are loaded.
- The graphical single-player host requests a campaign-level `"player"` state and
creates its visual wrapper.
- A standalone host may deliberately create no players; locations continue to tick.
- The battle-zone test bot is an explicit host option rather than an unconditional
campaign side effect.
### Location loading
When a location is loaded:
- `LocationContentLoader` parses a renderer-free `LocationDefinition`.
- `LocationGameState::setup()` creates or reconciles navigation, taxi, NPC,
interactive-object, trigger, teleport, and animation state.
- Saved mutable fields are overlaid on configuration defaults by stable ID.
- `Location::setup()` builds static and entity visuals from the prepared definition
without mutating authoritative state.
- The shared campaign player is attached.
- Runtime visuals are created for other player IDs currently present in the
snapshot and are subsequently spawned or removed as snapshots change.
- Dialogue, Lua, trigger/teleport visuals, and presentation callbacks are initialized.
- The runtime `Location` is owned and registered by `LocalGameServerHost`;
`InternalGameServer` remains headless.
### Location unloading
When leaving a location:
- Auxiliary runtime state is captured.
- The visual/runtime `Location` is destroyed.
- Its typed `LocationGameState` remains in `GameState`.
The player state remains campaign-level and survives location changes.
## Save and Load Schema
Authoritative persistence currently uses strict schema version `5`.
`schemaVersion` is required and unsupported versions are rejected rather than
migrated. A
single-player save is a separate versioned envelope containing `networkState`
(the authoritative `GameState` JSON) and `localState` (the client-local location
map). Lockstep sends that authoritative document only in `initial_state`; later
messages contain numbered command bundles.
- Campaign players are stored once in an ID-keyed `players` object.
- Locations are keyed by location ID.
- Each location stores NPC and interactive-object state in ID-keyed objects.
- Pending interaction targets and character references use stable IDs.
- Character navigation persistence includes waypoints, actual previous-tick
movement, blocked/retry state, the blocker ID, and the NPC cooldown sequence
and cursor.
- Immutable content such as meshes, initial taxi configuration, and object
definitions is reconstructed from campaign configuration.
- Configuration is loaded first and mutable saved state is overlaid in place.
Snapshot-backed runtime objects then resolve the resulting state by stable ID.
Old user save slots are intentionally unsupported. Bundled start-state snapshots
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
## UI, Scripts, Dialogue, and Quests
### UI
`MenuManager` no longer holds `GameState&` or a mutable `Inventory*`.
It now:
- Reads inventory, quests, tutorial state, chat, money, health, location, and world mode from `GameSnapshot`.
- Submits typed commands for mutations.
- Continues to own presentation-only UI trees, screen state, textures, and callbacks.
### Scripts
Lua remains an in-process single-player runtime. Character, inventory, quest, and
campaign-global bindings submit semantic commands through `Location`; the server
invokes operations on centrally owned state. Snapshot queries provide global
values, inventory counts, item presence, and synchronous quest validation for the
existing Lua API. NPC,
interactive-object, trigger-zone, and teleport-zone bindings address entities by
configured ID. Other object and global-state bindings remain trusted local
integrations.
Each location script registers initialization, enter, and exit callbacks.
Initialization runs only when the location has no retained state; loading the Lua
file again for rendering does not repeat state-resetting setup. Enter and exit
callbacks continue to run for every location transition.
The scalar Lua runtime state for each location is stored under `LocationGameState::scriptGlobals`. Multiplayer location scripts are expected to be empty, so remote script execution is intentionally outside the current scope.
### Dialogue and cutscenes
Dialogue runtime state is captured in each `LocationGameState`. Dialogue global
flags use snapshot getter and command setter callbacks rather than retaining a
mutable map pointer. Dialogue and cutscene rendering remain local presentation
systems.
Dialogues and cutscenes are outside the planned multiplayer gameplay mode, but their existing single-player save and callback behavior is retained.
### Quests, inventory, and tutorial
These callback-free state systems remain part of `GameState` for single-player
compatibility. UI access, mutations, and presentation notifications cross the
server interface. They can remain empty or disabled in multiplayer mode as planned.
## Multiplayer Lockstep
Protocol v2 uses server-sequenced deterministic lockstep:
- The server assigns a connection to a preconfigured campaign player slot.
- Player slots are disabled in the authoritative initial state. A successful
connection schedules `SetCharacterEnabledCommand(enabled=true)` for the next
lockstep tick; disconnect schedules the corresponding `enabled=false` command.
- Connection receives one authoritative state at a completed tick plus its
assigned player ID, tick duration, input delay, and compatibility fingerprint.
- Destination input is normalized and scheduled two ticks in the future.
- The server broadcasts every numbered tick, including empty ticks. Clients
buffer out-of-order messages and advance only contiguous tick IDs.
- All replicas apply the same globally sequenced commands before one exact
100 ms update.
- Post-tick canonical hashes exclude persistence timestamps and local
presentation state. A mismatch is diagnosed but not automatically repaired.
No wall-clock timestamp participates in simulation. `steady_clock` paces the
server and local render accumulator only, so different machine clocks do not
affect command placement or state. The current missing-packet policy is to stall;
there is no prediction or rollback.
Current boundaries remain:
- client intent is limited to `SetCharacterDestinationCommand`,
`SetCharacterTargetCommand`, `ClearCharacterAttackTargetCommand`, and
`TryCharacterAttackComboCommand`; server presence changes use
`SetCharacterEnabledCommand` in tick bundles;
- player characters must already exist in the initial state and campaign slot
list; dynamic spawning and reconnect history are not implemented;
- authentication and automatic checkpoint resynchronization are absent;
- multiplayer Lua/dialogue shared behavior is unsupported;
- deterministic compatibility targets identical executable and content builds.
Floating-point simulation is not guaranteed bit-identical across platforms.
## Platform Build Integration
The new state and server files are registered in:
- `proj-windows/CMakeLists.txt`
- `proj-linux/CMakeLists.txt`
- `proj-web/CMakeLists.txt`
- `proj-android/app/jni/src/CMakeLists.txt`
The refactor uses portable C++17 and introduces no Windows-only gameplay code.
The standalone target lives under `server/`. It links the authoritative state,
campaign, content-definition, and portable resource sources only; SDL, OpenGL,
audio, Lua, dialogue presentation, and rendering sources are absent. It can prepare
a selected campaign/location with zero players. The Windows server adds
Boost.Beast transport and runs a fixed 100 ms lockstep loop; other platforms
continue to compile the portable state core without that transport.
The Windows Debug target was verified with:
```powershell
cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -- /m
```
## Important Constraints
- Runtime C++ callbacks are not part of `CharacterState` or public snapshots;
serializable Lua arrival function names are state data.
- `InteractiveObjectState` animation callbacks are also removed from public snapshots.
- Character target and attacker references use stable IDs (`"player"` or a unique
location-local NPC ID).
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
- `IGameServerLifecycle` is the graphics-free campaign/location administration
boundary. `LocalGameServerHost` adds client-only runtime construction and
auxiliary-state synchronization without making `Game` a server friend.
- Empty player maps are valid. Player-specific interactions remain inactive until
a player entity is added.
- Pathfinding uses integer cardinal/diagonal costs and compares priority,
accumulated cost, then cell index.
- Gameplay randomness must be seeded and serialized; the `random_device` battle
bot is disabled for multiplayer sessions.
- A lockstep compatibility fingerprint must match before the client accepts the
initial state.
### Dynamic Navigation Occupancy
`LocationGameState` is the authoritative boundary for character-aware planning.
It collects enabled, living, stationary players and NPCs in stable ID order and
passes their positions and collision radii to `LocationNetworkState` and
`PathFinder`. The requester is excluded; moving characters are transparent to
planning. “Moving” here means actual previous-tick displacement
(`movedLastTick`), not merely owning an active path, so visually stalled
characters remain planning obstacles. Route planning uses full avoidance radii;
the movement guard uses smaller contact radii.
Every character movement step is guarded against stable-ID-ordered reservations
initialized from tick-start geometry. Accepted steps update their reservation
before later characters advance, preventing two movers from ending a tick
overlapped. Blocked/retry counters, cooldown sequence/cursor, and blocker ID are
serialized character state. Head-on players and bots cancel their commands;
NPCs retain their targets and consume the next deterministic, ID-shuffled
cooldown before replanning. Static navigation topology changes route through
the same centralized replanning path.
## Main Files
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
- `src/LocationState.h` / `src/LocationState.cpp` — separate local camera and authoritative network location records, plus single-player persistence composition.
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — authoritative typed location state, simulation, and ID-keyed persistence.
- `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
parsing, and animation metadata loading.
- `src/game_server/IGameServer.h` — public snapshot and command contract.
- `src/game_server/IGameServerLifecycle.h` — graphics-free campaign/location lifecycle.
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
- `src/game_server/LocalGameServerHost.h` / `.cpp` — graphical-client runtime adapter.
- `src/game_server/NetworkProtocol.h` / `.cpp` — protocol-v2 messages,
lockstep timeline, validation, and canonical hashes.
- `src/game_server/WebsocketGameServer.h` / `.cpp` and
`WebsocketGameClient.h` / `.cpp` — slot assignment, input scheduling, tick
broadcasting, buffering, and replica advancement.
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
interactions, scripts, and presentation-event consumption.
- `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
immutable `CharacterState` snapshots by stable ID.
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
simulation, navigation operations, combat state, and logical animation clock.
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
- `src/items/GameObjectLoader.h` / `.cpp` — presentation builders for prepared definitions.
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.
+98 -62
View File
@@ -13,7 +13,7 @@ This document describes the grid-based pathfinding used for the player and all N
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)
8. [Character Movement Guard](#8-character-movement-guard)
9. [Dynamic Replanning](#9-dynamic-replanning)
10. [Key Constants Reference](#10-key-constants-reference)
@@ -103,9 +103,11 @@ Navigation areas can be toggled at runtime via `PathFinder::setAreaAvailable()`,
**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.
**Step costs:** integer `10` for cardinal and `14` for diagonal.
**Heuristic:** Euclidean distance in cell units to the end cell.
**Heuristic:** octile distance using the same `10`/`14` scale. Queue entries
compare priority, accumulated cost, then linear cell index; stale entries are
rejected. This avoids floating-point route-order differences.
**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.
@@ -130,7 +132,9 @@ while anchor is not the last cell:
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.
Line-of-sight uses deterministic integer grid traversal and applies the same
no-corner-cutting rule as A*. The result is a minimal set of waypoints connected
by straight, obstacle-free segments.
---
@@ -138,22 +142,20 @@ Line-of-sight is checked by stepping along the segment in increments of `cellSiz
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:
**`findPathToNearest`** runs one A\* search against the requested static and
dynamic occupancy. If the destination is unreachable, the same search returns
the reachable cell geometrically closest to it. It never retries without
dynamic obstacles.
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.
- While processing cells, it tracks `bestIndex` — the visited cell with the
smallest squared grid distance to the end cell. Equal distances use the
lower linear cell index.
- 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.
`LocationGameState` applies this behavior to players and NPCs.
---
@@ -163,7 +165,10 @@ When a path is planned, other characters can temporarily mark cells as blocked t
**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.
`LocationGameState::collectPathObstacles` creates a stable-ID-sorted snapshot
from authoritative players and NPCs. `PathFinder` stamps circles into a
generation-tagged overlay and reuses A\* buffers; it does not copy or mutate the
static walkability grid per query.
**Which characters become obstacles:**
@@ -171,74 +176,101 @@ 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).
- It did not physically move during the previous simulation tick, unless it
just blocked this requester and is promoted for a forced replan. An active
path alone does not make a stalled character transparent.
**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.
**Obstacle radius:** the character's full `collisionRadius`, inflated by the
navigation map's `agentRadius` and one cell half-diagonal for conservative
rasterization. There is no obstacle-dropping fallback; an occupied destination
resolves to a nearby reachable cell.
---
## 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`.
`CharacterState::setTarget(destination, onArrived)` sets a new walk target. It
calls the centralized location planner to generate a waypoint list. The result
is stored in `pathWaypoints`; the reachable endpoint is stored in `walkTarget`
and the original command destination in `requestedWalkTarget`.
Each frame in `Character::update`:
Each fixed simulation tick in `CharacterState::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.
`CharacterState::isMoving()` reports whether navigation remains active.
`movedLastTick` separately records actual displacement and controls dynamic
obstacle filtering.
**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.
**Stopping in place:** `CharacterState::stopInPlace()` sets `walkTarget` and
`requestedWalkTarget` to the current position, clears the path and callback, and
resets blockage state. It is used to cancel player/bot movement after a
persistent contact and by other explicit stop actions.
---
## 8. Character Collision Resolution
## 8. Character Movement Guard
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`.
Pathfinding steers around stationary characters. A second authoritative guard
checks each proposed movement segment against stable-ID-ordered reservations
initialized from tick-start circles. An accepted step updates that character's
reservation before later characters advance. A step that approaches or sweeps
through another circle is rejected rather than pushing either character.
**Algorithm** (3 iterations per frame):
Characters that moved during the previous tick are transparent during planning
but remain solid to the movement guard. Planning uses the full
`collisionRadius` (default 0.45 m); contact checks use the smaller
`movementContactRadius` (default 0.32 m). NPC JSON can override these as
`navigationAvoidanceRadius` and `movementContactRadius`. This tolerance avoids
false bumps from grid discretization while still preventing stacking.
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.
When moving characters remain blocked head-on or while crossing for three
ticks, both stop. Player and bot destinations are cancelled; the user or bot
may issue another command. NPCs retain `requestedWalkTarget` and retry later.
Characters following in the same direction simply wait for spacing and do not
accumulate stuck ticks. A rejected movement step uses the standing animation.
---
## 9. Dynamic Replanning
When characters move they can displace each other or enter each other's planned paths. `Location::updateDynamicReplans` handles this:
Rejected non-following movement increments serialized `movementBlockedTicks`.
After three blocked ticks an NPC stops and schedules a retry. When its cooldown
expires, `InternalGameServer` promotes the blocker to a hard obstacle and
replans to `requestedWalkTarget`.
**Every frame:**
Each NPC has `navigationRetryCooldownTicks`, normally ten distinct values.
`LocationDefinition` deterministically shuffles the sequence from the stable NPC
ID, and every retry consumes the next value. The sequence and cursor are
serialized, preventing synchronized retry loops while preserving lockstep.
NPC JSON can override it with `navigationRetryCooldownTicks` or
`navigationRetryCooldownMs`.
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.
Example NPC tuning:
**`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.
```json
{
"navigationAvoidanceRadius": 0.45,
"movementContactRadius": 0.32,
"navigationRetryCooldownMs": [
200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100
]
}
```
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.
The configured values are clamped to positive tick counts and shuffled
deterministically per NPC ID. Supplying different ten-value sets gives explicit
control for unusually dense locations.
Navigation map switches, area availability changes, and added static obstacle
polygons also force centralized replanning.
Opposing traffic in a one-character-wide corridor can wait until one route or
blocker changes; this policy intentionally does not implement space-time
cooperative reservations.
---
@@ -249,12 +281,16 @@ The relatively generous trigger distance (1.8 m vs the old 1.1 m) and cooldown (
| `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 |
| `movementContactRadius` | `CharacterState` / NPC config | 0.32 m | Smaller movement-guard radius |
| `kWalkThreshold` | `CharacterState.h` | 0.05 m | Distance below which a waypoint is considered reached |
| `TARGET_REPLAN_THRESHOLD` | `CharacterState.cpp` | 0.25 m | Deduplication threshold in `setTarget` |
| `BLOCKED_REPLAN_TICKS` | `CharacterState.cpp` | 3 ticks | Blocked steps before stopping |
| default retry sequence | `CharacterState.cpp` | 2–11 ticks | Ten ID-shuffled NPC retry cooldowns |
| `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 |
### Reference benchmark
`GameServerPathFinderTests --benchmark` is non-gating. On the development
Windows Release build with an 80×80 grid, the current implementation completed
100 obstacle-aware queries in 15 ms and 1000 queries in 176 ms. Treat these as
comparative measurements rather than cross-machine budgets.
+41
View File
@@ -185,3 +185,44 @@ make -j$(nproc) -C build #Компилируем
Для постройки без звука
rm -rf build #Очищаем build папку
cmake -B build -DAUDIO=1 #Пересоздаём конфигурацию CMake
# Cmake Build NSIS and Portable for Windows:
```
cmake --build . --config Release
cpack -C Release
```
Если есть такая ошибка:
CPack Error: Cannot find NSIS compiler makensis: likely it is not installed, or not in your PATH
CPack Error: Could not read NSIS registry value. This is usually caused by NSIS not being installed. Please install NSIS from http://nsis.sourceforge.net
CPack Error: Cannot initialize the generator NSIS
То нужно установить nsis отсюда: https://nsis.sourceforge.io/Download
# Steam windows
```
cmake -DSTEAMSDK=ON ..
cmake --build . --config Release
```
# Steam Linux
```
docker run -it --rm -v "${PWD}:/work2" -w /work2 registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest bash
apt-get update
apt-get install libboost-dev libeigen3-dev liblua5.4-dev libzip-dev libglu1-mesa-dev
cmake -DSTEAMSDK=ON -DCMAKE_BUILD_TYPE=Release ..
cmake --build . -j 4
```
+201
View File
@@ -0,0 +1,201 @@
# WebSocket Deterministic Lockstep Networking
## Scope
The Windows WebSocket transport uses Boost.Asio and Boost.Beast to run a
server-sequenced deterministic lockstep simulation. A client receives one
authoritative state at a completed tick boundary. After that, the server sends
only numbered tick messages containing ordered destination, combat-intent,
and presence commands (or an empty command list).
Client intent is intentionally limited to
`SetCharacterDestinationCommand`, `SetCharacterTargetCommand`,
`ClearCharacterAttackTargetCommand`, and
`TryCharacterAttackComboCommand`.
The server also sends `SetCharacterEnabledCommand` in tick bundles when a
configured player slot connects or disconnects.
## Runtime Architecture
### Server
`WebsocketGameServer` wraps the headless `InternalGameServer`.
- The simulation tick is fixed at 100 ms.
- Each accepted connection is assigned the first free character ID from the
campaign's `networkPlayerSlots`; the connection is rejected when all slots
are occupied.
- Player slots start disabled. The server consumes and broadcasts an enabled
command on the next tick for each connection and disconnection, including to
the newly connected client.
- The initial message contains the state, completed snapshot tick, assigned
player ID, tick duration, two-tick input delay, and compatibility fingerprint.
- Destination and character-action inputs cross from the Asio thread through a
queue. The simulation thread validates client sequence numbers, replaces
client identity with the assigned slot, and schedules commands two ticks in
the future.
- Every tick is broadcast, including ticks with no commands. No periodic state
snapshots are sent.
### Client
`WebsocketGameClient` implements `IGameServer`.
- It connects asynchronously to `ws://127.0.0.1:8080/`.
- Received messages are queued by the network thread and applied on the game
thread from `update()`.
- It maintains an internal `InternalGameServer` replica so existing snapshot
consumers and transitional runtime-state bindings continue to work.
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
authoritative snapshots cannot overwrite camera movement.
- `submit()` serializes destination and tagged character-action intent. Other
command variants remain local no-ops for a remote connection.
- Tick bundles are buffered by ID. The replica advances only while the next
contiguous tick is available, applying its commands before one exact 100 ms
simulation step.
- Completed simulation events are passed to the runtime `Location` for
presentation, and ordinary server events are drainable through `IGameServer`.
- Client-side save and load calls are ignored because persistence remains
server-side.
`RemoteGameServerHost` prepares local presentation resources and runtime
locations against the replica and presents completed-tick events. It never
advances simulation from a render-frame delta.
## Campaign Routing
The graphical game uses stable router objects for `IGameServer` and
`IGameServerHost`, allowing existing UI and runtime references to remain valid.
When `NETWORK` is defined:
- `battle_zone_multiplayer` uses `WebsocketGameClient` and `RemoteGameServerHost`.
- `main`, `battle_zone`, and every other campaign use `InternalGameServer` and
`LocalGameServerHost`.
The WebSocket client is created only when `battle_zone_multiplayer` is selected
and is destroyed when that campaign is unloaded. The single-player and
multiplayer Battle Zone entries share location content but use separate
start-state files. At startup, the standalone server loads the multiplayer
start state; each remote client restores the received authoritative state and
its own local presentation state. The multiplayer campaign currently declares the
pre-existing `player` and `player2` state entries as connection
slots; the random test-bot controller is disabled.
The campaign's `networkCompatibility` token is the fingerprint (currently
`battle-zone-lockstep-v5`); release builds must bump it whenever deterministic
content changes.
If DNS resolution, TCP connection, the WebSocket handshake, or the active socket
fails, the client unloads the remote campaign and returns to the campaign
selection screen.
## JSON Protocol v2
Every message uses a versioned JSON envelope:
```json
{
"protocolVersion": 2,
"type": "tick",
"payload": {
"tickId": 81,
"commands": [],
"stateHash": "0123456789abcdef"
}
}
```
Message types are:
- `initial_state` (server to client): one state snapshot and lockstep metadata;
- `destination_input` (client to server): monotonic client sequence and a
centimetre-normalized finite target;
- `character_action` (client to server): monotonic client sequence, an action
tag (`set_target`, `clear_attack_target`, or `try_attack_combo`), and a
target ID for `set_target`;
- `tick` (server to client): tick ID, globally sequenced destination,
character-action, and character-presence commands, and post-tick diagnostic
state hash;
- `error` (server to client): invalid input, incompatibility, or full server.
Protocol v1 is not compatible. Malformed payloads, non-finite or out-of-range
coordinates, duplicate client sequences, empty action IDs, and unsupported
commands cannot enter the authoritative command stream. The server derives the
location and player character from the assigned connection slot; clients cannot
use these actions to control another player or an NPC.
WebSocket ordering is not used as simulation time. Integer tick IDs are the
only authoritative clock. The server uses `steady_clock` only to pace its loop,
and clients may have different wall clocks without affecting the result. A
client stalls on a missing tick instead of predicting or rolling back.
Single-player persistence remains a separate envelope combining authoritative
and client-local state. `savedAt`, camera state, and presentation objects are
excluded from the diagnostic deterministic hash.
## Build Configuration
Networking is enabled only for Windows:
- `proj-windows/CMakeLists.txt` defines `NETWORK`, compiles the WebSocket client
and remote host, and links `ws2_32` and `mswsock`.
- `server/CMakeLists.txt` defines `NETWORK` for the Windows `GameServer`, compiles
the WebSocket server, and links the Windows socket libraries.
- Linux, Android, and Web builds do not define `NETWORK` and do not compile the
network transport.
The previous Web-build `NETWORK` definition was removed.
## Running
Build the server:
```powershell
cmake -S server -B server/build
cmake --build server/build --config Debug --target GameServer
```
Run it from the repository root:
```powershell
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
```
The listener defaults to `0.0.0.0:8080`. It can be changed with:
```text
--address IP
--port N
```
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
## Verification
CTest covers protocol-v2 round trips and validation, input-delay ordering,
fixed-tick replica behavior, campaign slot metadata, initial connection and
slot assignment, connection/disconnection presence propagation, destination
submission, and contiguous tick consumption.
## Current Limitations
- Paths are calculated independently on every lockstep participant from the
same ordered commands and tick-start occupancy. Integer A* costs, stable
character IDs, serialized previous-tick movement and blocked/retry state, and deterministic
nearest-reachable ties keep route selection aligned.
- Moving characters are omitted from A* but remain solid to the authoritative
movement guard. After three blocked ticks, players/bots cancel movement while
NPCs stop and consume the next serialized cooldown from their deterministic
retry sequence before replanning around the blocker.
- Opposing traffic can wait in a one-character-wide corridor because the
current policy does not reserve future space-time cells.
- Only preconfigured player slots can connect; dynamic join/spawn commands are
not implemented.
- There is no authentication or reconnect/replay history.
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
- The endpoint is hardcoded in the graphical client.
- Multiplayer Lua and dialogue behavior remains unsupported.
- Determinism is supported for identical executable/content builds. The
floating-point simulation is not guaranteed bit-identical across platforms
or compilers.
+1 -1
View File
@@ -25,7 +25,7 @@ macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
endmacro()
# 1) ZLIB (Нужна только для инклудов, если не используете emscripten порты)
check_and_download("https://www.zlib.net/zlib132.zip" "zlib132.zip" "zlib-1.3.2" "CMakeLists.txt")
check_and_download("https://github.com/madler/zlib/releases/download/v1.3.2/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")
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
optimize_web_resources.py - Resize PNG images for the web build.
Copies <src> to <dst>/resources/, then downscales all PNG files:
- resources/w/ui/img/** -> 1/8 original size
- all other PNGs -> 1/4 original size
Files listed in EXCEPTIONS are copied unchanged.
Usage (manual):
python optimize_web_resources.py [--src resources] [--dst web_resources]
Usage (CMake):
${Python3_EXECUTABLE} optimize_web_resources.py --src <abs_src> --dst <abs_dst>
"""
import argparse
import os
import shutil
try:
from PIL import Image
except ImportError:
raise SystemExit("Pillow is required: pip install Pillow")
# Paths relative to the resources directory — these files are never resized.
EXCEPTIONS = {
"loading.png",
"loading_land003_en.png",
"loading_land003_ru.png",
"black.png",
"transparent.png",
"w/blue.png",
"w/red.png",
"w/spark.png",
"w/star.png",
"w/star_red.png",
"w/white.png",
"w/ui/img/main/aboutPage003android.png",
"w/ui_en/img/main/aboutPage003android.png",
"w/exterior/texAtlas002.png",
}
# Path prefixes (relative to resources root, forward-slash) that get 8x reduction.
HIGH_REDUCTION_PREFIXES = (
"w/ui/img",
)
LOW_REDUCTION_POSTFIXES = (
"texAtlas002.png",
"Building_work014.png",
"Building_Floors_tex004.png",
"Building_Walls_tex006.png",
"asphalt003small.png",
"Ext_Building004.png",
"Plane3_tex002.png",
"darklands_building002.png",
"Staircase001.png",
"Staircase_Obj002.png"
)
def _resize_factor(rel: str) -> int:
norm = rel.replace("\\", "/")
for prefix in HIGH_REDUCTION_PREFIXES:
if norm.startswith(prefix):
return 8
for prefix in LOW_REDUCTION_POSTFIXES:
if norm.endswith(prefix):
return 2
return 4
def _next_pot(n: int) -> int:
if n < 1:
return 1
p = 1
while p < n:
p <<= 1
return p
def optimize(src_dir: str, dst_parent: str) -> None:
src_dir = os.path.abspath(src_dir)
dst_resources = os.path.join(os.path.abspath(dst_parent), "resources")
print(f"Copying {src_dir} -> {dst_resources}")
if os.path.exists(dst_resources):
shutil.rmtree(dst_resources)
shutil.copytree(src_dir, dst_resources)
total = 0
for root, _dirs, files in os.walk(dst_resources):
for filename in files:
if not filename.lower().endswith(".png"):
continue
full_path = os.path.join(root, filename)
rel = os.path.relpath(full_path, dst_resources).replace("\\", "/")
if rel in EXCEPTIONS:
print(f" skip (exception): {rel}")
continue
factor = _resize_factor(rel)
with Image.open(full_path) as img:
orig_w, orig_h = img.size
new_w = _next_pot(max(1, orig_w // factor))
new_h = _next_pot(max(1, orig_h // factor))
resized = img.resize((new_w, new_h), Image.LANCZOS)
resized.save(full_path)
print(f" {rel}: {orig_w}x{orig_h} -> {new_w}x{new_h} (/{factor} + POT)")
total += 1
print(f"Done. Resized {total} PNG file(s).")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Optimize PNG images for web build")
parser.add_argument("--src", default="resources",
help="Source resources directory (default: resources)")
parser.add_argument("--dst", default="android_resources",
help="Destination parent directory; will contain a 'resources' subdir "
"(default: android_resources)")
args = parser.parse_args()
optimize(args.src, args.dst)
+27 -1
View File
@@ -27,7 +27,18 @@ except ImportError:
# Paths relative to the resources directory — these files are never resized.
EXCEPTIONS = {
"loading.png",
"loading_land003_en.png",
"loading_land003_ru.png",
"black.png",
"transparent.png",
"w/blue.png",
"w/red.png",
"w/spark.png",
"w/star.png",
"w/star_red.png",
"w/white.png",
"w/ui/img/main/aboutPage003android.png",
"w/ui_en/img/main/aboutPage003android.png"
}
# Path prefixes (relative to resources root, forward-slash) that get 8x reduction.
@@ -35,13 +46,28 @@ HIGH_REDUCTION_PREFIXES = (
"w/ui/img",
)
LOW_REDUCTION_POSTFIXES = (
"texAtlas002.png",
"Building_work014.png",
"Building_Floors_tex004.png",
"Building_Walls_tex006.png",
"asphalt003small.png",
"Ext_Building004.png",
"Plane3_tex002.png",
"darklands_building002.png",
"Staircase001.png",
"Staircase_Obj002.png"
)
def _resize_factor(rel: str) -> int:
norm = rel.replace("\\", "/")
for prefix in HIGH_REDUCTION_PREFIXES:
if norm.startswith(prefix):
return 8
return 4
for prefix in LOW_REDUCTION_POSTFIXES:
if norm.endswith(prefix):
return 2
return 8
def _next_pot(n: int) -> int:
+1 -1
View File
@@ -61,5 +61,5 @@ app/jni/libpng
app/jni/SDL
app/jni/zlib
app/src/main/assets/resources
app/src/main/assets
+1 -1
View File
@@ -1 +1 @@
### Перед запуском в папке ```app/jni/```(рядом с src) нужно создать три папки с исходниками библиотек ```libpng```, ```SDL```, ```zlib```
### Android version
+17 -13
View File
@@ -1,4 +1,4 @@
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY');
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY')
def buildAsApplication = !buildAsLibrary
if (buildAsApplication) {
apply plugin: 'com.android.application'
@@ -9,30 +9,33 @@ else {
android {
if (buildAsApplication) {
namespace "org.libsdl.app"
namespace "fishrungames.shadowoverbishkek"
}
compileSdkVersion 34
compileSdkVersion 37
defaultConfig {
minSdkVersion 19
targetSdkVersion 34
versionCode 1
versionName "1.0"
minSdkVersion 21
targetSdkVersion 37
versionCode 4
versionName "1.0.7"
externalNativeBuild {
/*ndkBuild {
arguments "APP_PLATFORM=android-19"
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}*/
cmake {
arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static"
cppFlags "-std=c++11 -frtti -fexceptions"
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
arguments /*"-DANDROID_APP_PLATFORM=android-19",*/ "-DANDROID_STL=c++_static"
cppFlags "-std=c++17 -frtti -fexceptions"
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// Enables code optimizations.
minifyEnabled = true
// Enables resource shrinking.
shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
applicationVariants.all { variant ->
@@ -62,7 +65,7 @@ android {
libraryVariants.all { variant ->
variant.outputs.all { output ->
if (output.outputFileName != null && output.outputFileName.endsWith(".aar")) {
output.outputFileName = "org.libsdl.app.aar"
output.outputFileName = "fishrungames.shadowoverbishkek.aar"
}
}
}
@@ -70,5 +73,6 @@ android {
}
dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation fileTree(include: ['*.jar'], dir: 'libs')
}
+5 -1
View File
@@ -21,7 +21,7 @@ set(TP_ROOT "${THIRDPARTY_DIR}")
# ==============================================================================
# --- ZLIB ---
add_subdirectory("${TP_ROOT}/zlib-1.3.1" zlib-build)
#add_subdirectory("${TP_ROOT}/zlib-1.3.2" zlib-build)
# --- LIBPNG ---
set(PNG_STATIC ON CACHE BOOL "Build static library" FORCE)
@@ -39,6 +39,10 @@ add_subdirectory("${TP_ROOT}/libpng-1.6.51" libpng-build)
# --- SDL2 ---
# Android-версия SDL требует специфичных настроек, но add_subdirectory обычно подхватывает их сама
add_subdirectory("${TP_ROOT}/SDL-release-2.32.10" sdl-build)
add_subdirectory("${TP_ROOT}/SDL_mixer-release-2.8.0" sdl-mixer-build)
add_subdirectory("${TP_ROOT}/SDL_ttf-release-2.24.0" sdl-ttf-build)
# --- LIBZIP ---
# Отключаем поиск системных крипто-библиотек, так как в NDK их может не быть в стандартных путях
+132 -37
View File
@@ -33,62 +33,157 @@ endforeach()
# Создаем кастомную цель, которая будет запускать процесс копирования
add_custom_target(sync_resources ALL DEPENDS ${RES_OUTPUTS})
set(LUA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/lua-5.4.8")
file(GLOB LUA_SOURCES "${LUA_SRC_DIR}/*.c")
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}")
add_library(main SHARED
SDL_android_main.c
../../../../src/BoneAnimatedModel.cpp
../../../../src/Environment.cpp
../../../../src/Game.cpp
../../../../src/main.cpp
../../../../src/Projectile.cpp
../../../../src/SparkEmitter.cpp
../../../../src/TextModel.cpp
../../../../src/UiManager.cpp
../../../../src/utils/Perlin.cpp
../../../../src/utils/TaskManager.cpp
../../../../src/utils/Utils.cpp
../../../../src/navigation/PathFinder.cpp
../../../../src/planet/PlanetData.cpp
../../../../src/planet/PlanetObject.cpp
../../../../src/planet/StoneObject.cpp
../../../../src/render/FrameBuffer.cpp
../../../../src/render/ShadowMap.cpp
../../../../src/Game.cpp
../../../../src/Game.h
../../../../src/Character.cpp
../../../../src/Character.h
../../../../src/CharacterState.cpp
../../../../src/CharacterState.h
../../../../src/Environment.cpp
../../../../src/Environment.h
../../../../src/Localization.cpp
../../../../src/Localization.h
../../../../src/render/Renderer.cpp
../../../../src/render/Renderer.h
../../../../src/render/PathVisualization.cpp
../../../../src/render/PathVisualization.h
../../../../src/render/ShaderManager.cpp
../../../../src/render/ShaderManager.h
../../../../src/render/TextureManager.cpp
../../../../src/render/TextureManager.h
../../../../src/TextModel.cpp
../../../../src/TextModel.h
../../../../src/AudioPlayerAsync.cpp
../../../../src/AudioPlayerAsync.h
../../../../src/BoneAnimatedModelNew.cpp
../../../../src/BoneAnimatedModelNew.h
../../../../src/render/OpenGlExtensions.cpp
../../../../src/render/OpenGlExtensions.h
../../../../src/utils/Utils.cpp
../../../../src/utils/Utils.h
../../../../src/SparkEmitter.cpp
../../../../src/SparkEmitter.h
../../../../src/TeleportZone.h
../../../../src/TeleportZone.cpp
../../../../src/utils/TaskManager.cpp
../../../../src/utils/TaskManager.h
../../../../src/render/FrameBuffer.cpp
../../../../src/render/FrameBuffer.h
../../../../src/render/ShadowMap.cpp
../../../../src/render/ShadowMap.h
../../../../src/UiManager.cpp
../../../../src/UiManager.h
../../../../src/render/TextRenderer.h
../../../../src/render/TextRenderer.cpp
../../../../src/MenuManager.h
../../../../src/MenuManager.cpp
../../../../src/Location.h
../../../../src/Location.cpp
../../../../src/LocationState.h
../../../../src/LocationState.cpp
../../../../src/LocationGameState.h
../../../../src/LocationGameState.cpp
../../../../src/LocationEditor.h
../../../../src/LocationEditor.cpp
../../../../src/NpcCar.h
../../../../src/NpcCar.cpp
../../../../src/GameConstants.h
../../../../src/GameConstants.cpp
../../../../src/GameState.h
../../../../src/GameState.cpp
../../../../src/content/LocationDefinition.cpp
../../../../src/game_server/IGameServer.h
../../../../src/game_server/InternalGameServer.h
../../../../src/game_server/InternalGameServer.cpp
../../../../src/game_server/LocalGameServerHost.cpp
../../../../src/ScriptEngine.h
../../../../src/ScriptEngine.cpp
../../../../src/navigation/PathFinder.h
../../../../src/navigation/PathFinder.cpp
../../../../src/items/GameObjectLoader.h
../../../../src/items/GameObjectLoader.cpp
../../../../src/items/Item.h
../../../../src/items/Item.cpp
../../../../src/items/Inventory.cpp
../../../../src/items/ItemRegistry.h
../../../../src/items/ItemRegistry.cpp
../../../../src/Campaign.cpp
../../../../src/items/InteractiveObject.h
../../../../src/items/InteractiveObject.cpp
../../../../src/items/InteractiveObjectState.cpp
../../../../src/items/InteractiveObjectState.h
../../../../src/dialogue/DialogueTypes.h
../../../../src/dialogue/DialogueDatabase.h
../../../../src/dialogue/DialogueDatabase.cpp
../../../../src/dialogue/DialogueRuntime.h
../../../../src/dialogue/DialogueRuntime.cpp
../../../../src/dialogue/DialogueOverlay.h
../../../../src/dialogue/DialogueOverlay.cpp
../../../../src/dialogue/DialogueSystem.h
../../../../src/dialogue/DialogueSystem.cpp
../../../../src/dialogue/TranslationDatabase.h
../../../../src/dialogue/TranslationDatabase.cpp
../../../../src/quest/QuestTypes.h
../../../../src/quest/QuestJournal.h
../../../../src/quest/QuestJournal.cpp
../../../../src/cutscene/CutsceneTypes.h
../../../../src/cutscene/CutsceneDatabase.h
../../../../src/cutscene/CutsceneDatabase.cpp
../../../../src/cutscene/CutsceneRuntime.h
../../../../src/cutscene/CutsceneRuntime.cpp
../../../../src/cutscene/CutsceneOverlay.h
../../../../src/cutscene/CutsceneOverlay.cpp
../../../../src/render/UiQuad.h
)
# Подключаем заголовки
target_include_directories(main PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../SDL/include
${CMAKE_CURRENT_SOURCE_DIR}/../zlib
${CMAKE_CURRENT_SOURCE_DIR}/../libpng
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/eigen-5.0.0
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/boost_1_90_0
${CMAKE_CURRENT_SOURCE_DIR}/../../../../src
${CMAKE_CURRENT_SOURCE_DIR}/../libzip
)
# ВАЖНО: Линкуемся с png_static (статика) или png_shared (динамика)
# Так как мы установили PNG_STATIC=ON и PNG_SHARED=OFF,
# должна создаться цель png_static
find_library(OPENGLES3_LIB GLESv3)
target_link_libraries(main
png_static # ← ЭТО ПРАВИЛЬНОЕ ИМЯ ЦЕЛИ!
${OPENGLES3_LIB}
png_static
z
SDL2
)
find_library(OPENGLES2_LIB GLESv2)
target_link_libraries(main
${OPENGLES2_LIB} # OpenGL ES 2.0/3.0
SDL2_ttf
SDL2_mixer
log
android
OpenSLES
dl
zip
lua_static
#freetype
)
# Подключаем заголовки
target_include_directories(main PRIVATE
#${CMAKE_CURRENT_SOURCE_DIR}/../SDL/include
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/SDL_ttf-release-2.24.0/external/freetype/include
#${CMAKE_CURRENT_SOURCE_DIR}/../zlib
#${CMAKE_CURRENT_SOURCE_DIR}/../libpng
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/eigen-5.0.0
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/boost_1_90_0
${CMAKE_CURRENT_SOURCE_DIR}/../../../../src
${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/sol2-3.3.0/include
#${CMAKE_CURRENT_SOURCE_DIR}/../libzip
)
add_dependencies(main sync_resources)
+6
View File
@@ -16,6 +16,9 @@
# public *;
#}
# SDL2 specific rules
-keep class org.libsdl.app.** { *; }
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLInputConnection {
void nativeCommitText(java.lang.String, int);
void nativeGenerateScancodeForUnichar(char);
@@ -96,3 +99,6 @@
void hapticRun(int, float, int);
void hapticStop(int);
}
# Keep our main activity
-keep class fishrungames.shadowoverbishkek.ShadowOverBishkekActivity { *; }
@@ -8,7 +8,7 @@
android:installLocation="auto">
<!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" />
<uses-feature android:glEsVersion="0x00030000" />
<!-- Touchscreen support -->
<uses-feature
@@ -52,7 +52,7 @@
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
<!-- Create a Java class extending SDLActivity and place it in a
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
directory under shadowoverbishkek/main/java matching the package, e.g. shadowoverbishkek/main/java/com/gamemaker/game/MyGame.java
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
in the XML below.
@@ -62,14 +62,14 @@
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:theme="@style/Theme.App.Starting"
android:hardwareAccelerated="true" >
<!-- Example of setting SDL hints from AndroidManifest.xml:
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
-->
<activity android:name="SDLActivity"
<activity android:name=".ShadowOverBishkekActivity"
android:label="@string/app_name"
android:alwaysRetainTaskState="true"
android:launchMode="singleInstance"
@@ -0,0 +1,13 @@
package fishrungames.shadowoverbishkek;
import android.os.Bundle;
import androidx.core.splashscreen.SplashScreen;
import org.libsdl.app.SDLActivity;
public class ShadowOverBishkekActivity extends SDLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Тень над Бишкеком</string>
</resources>
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">Game</string>
<string name="app_name">Shadow Over Bishkek</string>
</resources>
@@ -4,4 +4,13 @@
<style name="AppTheme" parent="android:Theme.NoTitleBar.Fullscreen">
<!-- Customize your theme here. -->
</style>
<!-- Тема для Splash Screen -->
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<!-- Черный фон -->
<item name="windowSplashScreenBackground">#000000</item>
<!-- Ваша картинка 512x512 -->
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<!-- Ссылка на основную тему, которая применится после загрузки -->
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'com.android.tools.build:gradle:9.3.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+12 -1
View File
@@ -9,7 +9,18 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.builtInKotlin=false
android.defaults.buildfeatures.resvalues=true
android.dependency.useConstraints=true
android.enableAppCompileTimeRClass=false
android.newDsl=false
android.r8.optimizedResourceShrinking=false
android.r8.strictFullModeForKeepRules=false
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.uniquePackageNames=false
android.usesSdkInManifest.disallowed=false
org.gradle.jvmargs=-Xmx4096m
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -0,0 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
toolchainVersion=21
+1 -1
View File
@@ -1,6 +1,6 @@
#Sat Jan 10 10:31:05 MSK 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+17 -1
View File
@@ -85,6 +85,8 @@ add_executable(ShadowOverBishkekDemo
../src/Localization.h
../src/render/Renderer.cpp
../src/render/Renderer.h
../src/render/PathVisualization.cpp
../src/render/PathVisualization.h
../src/render/ShaderManager.cpp
../src/render/ShaderManager.h
../src/render/TextureManager.cpp
@@ -119,6 +121,8 @@ add_executable(ShadowOverBishkekDemo
../src/Location.cpp
../src/LocationState.h
../src/LocationState.cpp
../src/LocationGameState.h
../src/LocationGameState.cpp
../src/LocationEditor.h
../src/LocationEditor.cpp
../src/NpcCar.h
@@ -127,6 +131,15 @@ add_executable(ShadowOverBishkekDemo
../src/GameConstants.cpp
../src/GameState.h
../src/GameState.cpp
../src/content/LocationDefinition.h
../src/content/LocationDefinition.cpp
../src/game_server/IGameServer.h
../src/game_server/IGameServerLifecycle.h
../src/game_server/IGameServerHost.h
../src/game_server/InternalGameServer.h
../src/game_server/InternalGameServer.cpp
../src/game_server/LocalGameServerHost.h
../src/game_server/LocalGameServerHost.cpp
../src/ScriptEngine.h
../src/ScriptEngine.cpp
../src/navigation/PathFinder.h
@@ -135,11 +148,14 @@ add_executable(ShadowOverBishkekDemo
../src/items/GameObjectLoader.cpp
../src/items/Item.h
../src/items/Item.cpp
../src/items/Inventory.cpp
../src/items/ItemRegistry.h
../src/items/ItemRegistry.cpp
../src/Campaign.cpp
../src/items/InteractiveObject.h
../src/items/InteractiveObject.cpp
../src/items/InteractiveObjectState.h
../src/items/InteractiveObjectState.cpp
../src/dialogue/DialogueTypes.h
../src/dialogue/DialogueDatabase.h
../src/dialogue/DialogueDatabase.cpp
@@ -290,7 +306,7 @@ endif()
# ===========================================
set(RUNTIME_RESOURCE_DIRS
"resources"
"audio"
"music"
)
foreach(resdir IN LISTS RUNTIME_RESOURCE_DIRS)
+56 -15
View File
@@ -8,13 +8,19 @@ if(NOT CMAKE_MAKE_PROGRAM AND WIN32)
endif()
endif()
project(bishkek-witcher LANGUAGES C CXX)
project(ShadowOverBishkekDemo LANGUAGES C CXX)
option(OPTIMIZE_WEB_RESOURCES "Resize PNG images for web build (requires Python3 + Pillow)" ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (CMAKE_BUILD_TYPE STREQUAL "Release")
# Максимальная оптимизация кода и LTO (Link-Time Optimization)
add_compile_options(-O3 -flto)
add_link_options(-O3 -flto)
endif()
# --- АВТО-ЗАГРУЗКА ЗАВИСИМОСТЕЙ ---
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/FetchDependencies.cmake")
# Теперь гарантированно есть папка ../thirdparty со всеми исходниками
@@ -73,6 +79,8 @@ set(SOURCES
../src/Localization.h
../src/render/Renderer.cpp
../src/render/Renderer.h
../src/render/PathVisualization.cpp
../src/render/PathVisualization.h
../src/render/ShaderManager.cpp
../src/render/ShaderManager.h
../src/render/TextureManager.cpp
@@ -107,12 +115,21 @@ set(SOURCES
../src/Location.cpp
../src/LocationState.h
../src/LocationState.cpp
../src/LocationGameState.h
../src/LocationGameState.cpp
../src/LocationEditor.h
../src/LocationEditor.cpp
../src/NpcCar.h
../src/NpcCar.cpp
../src/GameConstants.h
../src/GameConstants.cpp
../src/GameState.h
../src/GameState.cpp
../src/content/LocationDefinition.cpp
../src/game_server/IGameServer.h
../src/game_server/InternalGameServer.h
../src/game_server/InternalGameServer.cpp
../src/game_server/LocalGameServerHost.cpp
../src/ScriptEngine.h
../src/ScriptEngine.cpp
../src/navigation/PathFinder.h
@@ -121,10 +138,13 @@ set(SOURCES
../src/items/GameObjectLoader.cpp
../src/items/Item.h
../src/items/Item.cpp
../src/items/Inventory.cpp
../src/items/ItemRegistry.h
../src/items/ItemRegistry.cpp
../src/Campaign.cpp
../src/items/InteractiveObject.h
../src/items/InteractiveObject.cpp
../src/items/InteractiveObjectState.cpp
../src/items/InteractiveObjectState.h
../src/dialogue/DialogueTypes.h
../src/dialogue/DialogueDatabase.h
@@ -150,10 +170,10 @@ set(SOURCES
../src/render/UiQuad.h
)
add_executable(bishkek-witcher ${SOURCES})
add_executable(ShadowOverBishkekDemo ${SOURCES})
# Настройка путей к инклудам (используем скачанные исходники)
target_include_directories(bishkek-witcher PRIVATE
target_include_directories(ShadowOverBishkekDemo PRIVATE
../src
../thirdparty/eigen-5.0.0
../thirdparty/boost_1_90_0
@@ -168,7 +188,7 @@ set(ENABLE_COMMONCRYPTO OFF CACHE BOOL "" FORCE)
add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build)
target_link_libraries(bishkek-witcher PRIVATE zip z lua_static websocket.js)
target_link_libraries(ShadowOverBishkekDemo PRIVATE zip z lua_static websocket.js)
# Эмскриптен-флаги
set(EMSCRIPTEN_FLAGS
@@ -182,10 +202,9 @@ set(EMSCRIPTEN_FLAGS
#"-pthread"
#"-sUSE_PTHREADS=1"
"-fexceptions"
"-DNETWORK"
)
target_compile_options(bishkek-witcher PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
target_compile_options(ShadowOverBishkekDemo PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
# Only loading.png and the shaders used before resources.zip is ready are preloaded.
# resources.zip is downloaded asynchronously at runtime and served as a separate file.
@@ -195,6 +214,7 @@ set(EMSCRIPTEN_LINK_FLAGS
#"-sPTHREAD_POOL_SIZE=4"
"-sALLOW_MEMORY_GROWTH=1"
"-sFULL_ES3=1"
"-lidbfs.js"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loadingProgressBar.png@resources/loadingProgressBar.png"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loadingProgressBarFrame.png@resources/loadingProgressBarFrame.png"
@@ -202,15 +222,14 @@ set(EMSCRIPTEN_LINK_FLAGS
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_interior.lua@resources/start_uni_interior.lua"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_exterior.lua@resources/start_uni_exterior.lua"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_dorm.lua"
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../audio@audio"
)
# Применяем настройки линковки
target_link_options(bishkek-witcher PRIVATE ${EMSCRIPTEN_LINK_FLAGS})
target_link_options(ShadowOverBishkekDemo PRIVATE ${EMSCRIPTEN_LINK_FLAGS})
# Для совместимости со старыми версиями CMake, если target_link_options недостаточно
string(REPLACE ";" " " EMSCRIPTEN_LINK_FLAGS_STR "${EMSCRIPTEN_LINK_FLAGS}")
set_target_properties(bishkek-witcher PROPERTIES
set_target_properties(ShadowOverBishkekDemo PROPERTIES
LINK_FLAGS "${EMSCRIPTEN_LINK_FLAGS_STR}"
SUFFIX ".html"
)
@@ -254,23 +273,40 @@ else()
)
endif()
# music resources
set(MUSIC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../music")
set(MUSIC_ZIP "${CMAKE_CURRENT_BINARY_DIR}/music.zip")
get_filename_component(MUSIC_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
add_custom_command(
OUTPUT "${MUSIC_ZIP}"
COMMAND ${CMAKE_COMMAND} -E tar "cf" "${MUSIC_ZIP}" --format=zip "music"
WORKING_DIRECTORY "${MUSIC_PARENT_DIR}"
DEPENDS "${MUSIC_PARENT_DIR}/music"
)
add_custom_target(pack_resources DEPENDS "${RESOURCES_ZIP}")
add_dependencies(bishkek-witcher pack_resources)
add_dependencies(ShadowOverBishkekDemo pack_resources)
add_custom_target(pack_music DEPENDS "${MUSIC_ZIP}")
add_dependencies(ShadowOverBishkekDemo pack_music)
# Определяем путь к директории установки (относительно папки билда)
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/public")
# 1. Устанавливаем основной HTML файл
install(TARGETS bishkek-witcher
install(TARGETS ShadowOverBishkekDemo
RUNTIME DESTINATION .
)
# 2. Устанавливаем сопутствующие файлы (JS, WASM и сгенерированный архив данных)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.js"
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.wasm"
"${CMAKE_CURRENT_BINARY_DIR}/bishkek-witcher.data"
"${CMAKE_CURRENT_BINARY_DIR}/ShadowOverBishkekDemo.js"
"${CMAKE_CURRENT_BINARY_DIR}/ShadowOverBishkekDemo.wasm"
"${CMAKE_CURRENT_BINARY_DIR}/ShadowOverBishkekDemo.data"
DESTINATION .
)
@@ -278,10 +314,15 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/index.html"
DESTINATION .
)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/favicon.ico"
DESTINATION .
)
# resources.zip is served separately and downloaded asynchronously at runtime
install(FILES "${RESOURCES_ZIP}" DESTINATION .)
install(FILES "${MUSIC_ZIP}" DESTINATION .)
add_custom_command(TARGET bishkek-witcher POST_BUILD
add_custom_command(TARGET ShadowOverBishkekDemo POST_BUILD
COMMAND ${CMAKE_COMMAND} --install .
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Automatically deploying to public directory..."
+11
View File
@@ -37,3 +37,14 @@ Run:
```
emrun --no_browser --port 8080 public
```
# Release build
```
mkdir build_release
cd build_release
emcmake cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --config Release
cmake --install . --prefix ../dist
```
-5
View File
@@ -1,5 +0,0 @@
<!doctypehtml><html lang=en-us><head><meta charset=utf-8><meta content="text/html; charset=utf-8"http-equiv=Content-Type><title>Emscripten-Generated Code</title><style>body{font-family:arial;margin:0;padding:none}.emscripten{padding-right:0;margin-left:auto;margin-right:auto;display:block}div.emscripten{text-align:center}div.emscripten_border{border:1px solid #000}canvas.emscripten{border:0 none;background-color:#000}#emscripten_logo{display:inline-block;margin:0;padding:6px;width:265px}.spinner{height:30px;width:30px;margin:0;margin-top:20px;margin-left:20px;display:inline-block;vertical-align:top;-webkit-animation:rotation .8s linear infinite;-moz-animation:rotation .8s linear infinite;-o-animation:rotation .8s linear infinite;animation:rotation .8s linear infinite;border-left:5px solid #ebebeb;border-right:5px solid #ebebeb;border-bottom:5px solid #ebebeb;border-top:5px solid #787878;border-radius:100%;background-color:#bdd72e}@-webkit-keyframes rotation{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(360deg)}}@-moz-keyframes rotation{from{-moz-transform:rotate(0)}to{-moz-transform:rotate(360deg)}}@-o-keyframes rotation{from{-o-transform:rotate(0)}to{-o-transform:rotate(360deg)}}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}#status{display:inline-block;vertical-align:top;margin-top:30px;margin-left:20px;font-weight:700;color:#787878}#progress{height:20px;width:300px}#controls{display:inline-block;float:right;vertical-align:top;margin-top:30px;margin-right:20px}#output{width:100%;height:200px;margin:0 auto;margin-top:10px;border-left:0;border-right:0px;padding-left:0;padding-right:0;display:block;background-color:#000;color:#fff;font-family:'Lucida Console',Monaco,monospace;outline:0}</style></head><body>
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<a href=http://emscripten.org><img id=emscripten_logo src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZ4AAABiCAMAAABXhUXrAAAC+lBMVEUAAAAwMDDh4eE5OTc4Nzn29vY5OTk3Nzf19fW72TP29vb29vbj4+M6Ozbu7u5ITDfj4+Pr6+vr6+tcZTb19fXi4uJTWjamwjTt7e3r6+vv7+9QVja21zStzDRteTZsdzdlcDW11DPv7+/19fXr6+tkbTqy0jSvzzW41zN8jDXD4DD29vaNozV/kDXx8fHi4uL29vaqxzTv7+9uejfI5TDi4uLh4eHs7OyGlje10zT29vb6+vr29vb19fW10TLy8vLz8/OnwTOMojWuxzTl5eWUqzXj4+Pw8PCguTSBlDX19fXl5eWftTXk5OTj4+P29vadtDR3hjXi4uL19fXj4+N6izZpdjWivDN7izWMoTXF4jCZszWiuzSpxDOQozSxzDGszDrp6eni4uL19fUjIyMbGxsQEBAfHx8nJyf///8XFxf29vcTExP19fQMDAy00zT9/f0CAgK31DO51TL5+fn6+/qy0jX8/Pv7/fnj4+MICAi81zG+1jD6+/bq7tvl6tbx8u3j6NTp6en09fHs8N5paWnr79z4+vPz8/Pl5eUxMTHn7Nm/2C/o7d3c5NEsLCzf5dPi4uJ8fHzi5tOvzzTy9exiYmI1NTXa4M7v7+/y9ubm69jp7OPh59bx8fH2+u/w+eg/Pz/u8ek5OTnn6t9FRUW+3TPj59uz1Ta22DXu7u7s7ufj6ddMTExcXFyeuCvx9ene3t7Q1MPCwsKis0DU2szn7cbr6+uXl5d2dnZRUVHv9ebX19e1tbWjo6OdnZ1xcXGpvyZVVVW6urqmqZzOzs7Gxsa9z4OcvCTs8eTu8t+QkJC52zXD3jDb29u+vr6GhobS0tLk77OwsLDI2I2Kiop/f3+30TCnp6eqqqrCx7iivSza3sy6va7Kysq/2Dm61CuCgoKtra3X54+yzyrU1NSZtTC10yvC21KQpifv9cuEhISYsyfp8sHL4Gypxi3Kz77X4bfd66Olt1fO2qmrrqDAzJO4x32zuaLe6rG0ym3M1ZV+lyimqppoJhxDAAAAY3RSTlMABHoRDPYIGHP+unWwIgQuuxUwVsSFRc8PHAc69t9MO2XjCvIlJPDq7mDxrKpuV+bi10Uu/fLNUETWh3jq0cBr3LyagDqymzzGmJKNZdqpnZN7ZmG2g3WykIPewJ6ncZP49X4lcShzAAApYUlEQVR42uyaS2gTURRA3zDjjE8dB01N1CSTjJqvjRgTMPETTesnFdEEqa1aP5CViIzMQBRGrUWJghGEuPG7EHlmVNCFuHHl0pLZiJCFCwVdGPGDH3ThxjsxiaKtGj/UQg8tk5eZdwv3cO+7DEXfMmfh+DVzJ7TE3EXLFkxBY/xzZs2buzy7O9sau+Fn3NL1C9AY/5TpG5Zmf59xbfMmozH+GQvbsn/I3KlojH/D5EXjsn/Mkg1ojH/BlLnZv8KimWiMv2+nLfuXWDN2AP11Zk3IfmHXruzuloAdX9cPGnEia00YVIOJAS6aQqOWyet3f5Fz+OK5Syda49zF3V8JWoZGmk4DkGgWmQhRWERjaPSy4VhTzrFz508WiwMtUSwWr139Imj2ajTCxOKFgpHiGGTiSewvFIIuFo1WZizJNrhaPDnQf/b4wZY4frZ/oHj5UNNP20iPB5QYLBgr63pYzgalJDBotLK+aefcyWL/cZCTP/Lr5POmof5i8WrzNcJ4NMKYejq4esFw8YIR5ketnqnN4rl0cuDscXAzePszZ35G7am7oAgqaKB4tVE/86ejkSUQ3W+EGtOAa/N+I8WP2ua2qHHunDvZb9oZvDF4qxWenbkxaPrpL15s+JmGfoTHBXCIFQMihyhkEgl0bt++zQWLWCcQgw+MSxThxOBqawSw4rbOzgCHEBVxASxyuQMRytzs7uyEvVQtdgjieEKGVrA09IhBjazkPGJoWwSCxCwWC+xFHjcg/v/z3JTldTuHoHYOHhw8c+vD25st8fZ9/jYU0NmBa8fqodqmoB+QCALWWCIYjSYiph9u+2ajUNBIcBOLeqJagfSxFBKjweBmDwxeBc1IwzqWiELSowkBeTbD/kSkA/Z3UohOw2bNCG6DZ9DazYamGfGwpqnWhh5wpaVDcRjgNosoEo0aUQiL1kaBPva/97MgW+cynDsHj9z+WK0+f3CnBa5XqzdfnzkC9VM8t6s+/y1Ew8MkNFXz9ziIpqrGVkiiJ05UgjGBZYAK2OBuD0dRW+GKRYpP+zQSotHaoKYRrKkkTbk2wy1b0oDnMzCYGbXvtSBU0loDHjAMAkvJztRTvwkedGrmX9PinCvpgLBWsJ10aH4r87/rmbWoWTw1O++r119VHj3a/6s8qlSe3KlWX5/JnzgO5VP3swoNDxVJYlXBRJJUVfXbEQu68Eq3O4VVkuIoC1Gxjac8friLAwwlSNhpZ2JBFTszG21YS/CMK6yqihZWVOKmt2IVt7s7VJUEqIhfBW+9vSkNQjRmNQqeUDEOOzW4iAzdB38mxLAogEk7/9/rmdHWmAuKZw/mz7yuXn/yqPDr7N8Pv5VX1Zv9g0egfK7W9Syf/gM9TB9WVEeX1eJTFWJBAUPBHQLHiQ5VlVyU6AcRPLsWFKokw6AQIb0cncCq18LxSYyTPEPbQF2vRSJBi0gUyDIdwqbKNFbwFivPWSUV9zSGATqhKljKWC02uLuJZt1EwX0cBRH9Fu6/HxhWzK/rOT9w/OBg/u3zJ5DwoXj4Fd86qtypfrgB7a3Z3a5M/VF3S0LVZHhItqmHTkLmrRzD0GGs+kXKE1dVr522YVlRSC/LJbBkZcWggldaLT2wIUOznE/FKUEIdGeEHtjstnZLCg4LnBPiujmaFeIqac7VvKQofjfPMV3gpYNGdigxqM4AwaAV/e/Mq4/Vh+Hkyd++VX1VGbqHPX3W5MXT73rfk+dvBwfzxweO1F2fnoeGh3Yq2GbnWHqrIhOLHdJnE2iKYmy64rewTBIrDlF06Fu8CuliRbMHURkiK2GneUCthEc9DigZjmZomncqitfmJzqW3FzMoWAzEhIkhWyk63oEhwwiQFY3kUEPRSdVxStAAY6G4kHLZmdrHCr2H8zffgfFM7SexzubvPn+ZKpcv/nitqmn/vLuwo9Ga86rlFfyDLQdRXFYrQ5ZScIKMWFVgVMGtRNFD3QRvDEMEug+3WHhqLQuy2VC/LZuAXIawzLpolmKZSN+RVYIwb52C88EsFyuvStwYQW7mcZcDYuaq17Qswk+QBUpYq14wORQTJ+zYuG8BVNnoP+A8U09Z82x7XplGD3Pdu75zM5nD7+/XXnw4B50t/6GnmM/mg1cei27iPPJstMOevTaQRHxyzgFnjaV5VzahsPWFJwkLp+e4ml2K+jZ0tVtEXga0h4qy+VMLbcuhyz72ns3woFD175uNytimy77LQ09a3XZay4oiKGHYFegLMvplD5c8SxY1DYuazJ//bxZaKSZOK6uZwD0nLl/vaIN3duO7qzbOfDy0fB68g0969DwBHS53M1QcM1BFdkluZQEPZDdXLmLY83rKaej3MX3lfRUOlfeyLFUn57TMxz0M4alkCnQ4W7o0W1C/XszQDsUj8cpl5yNmZlKl0APBfYlOSdboYl6fHLOB08OWTwr5kI+mrSN+P+3fKUn/1nPUEBv27PXZM/Ox4+076npyX+lZxIaFshuTl9ppyPxklzeSHNJPeezIwTLks/KUSjmyOVykH9ukw4fdJsd0pgp5+SwRRDE2guE9nLJZ2fM3NPOklzqFQR7qJM2dZfCohCIl2AX35ire2DV4aE8STlXgmmbotgeiAvKOHb4XtJk8WTUEiOi55H2Yue+GtDahtKjPLgO1fOLejogPSUpIUHuU3CSh8qQt+7uOCy7eUifxwtWzEwGTD3l3tpp4i/lct64pJMOio7ZSrkdIdEDodi0GSse9+t+gYqYXr2SXvLBdvETsebzm0QQxXEI7K6i69ouArqgSAUOYgWNv6g/KrGpVeNvGw0aTTwIIUaCLuiBSxMl4oGYEBs9SIzR7JpwMB70oMZo4kE5mWg9WZNq4EIP/Qt8M7OWtizbEOz67YEZ5s3s9H14M28GlKyMBuO0HDq3A70Mo7GMZ1BxQC14+i7O1eVw+3z0x1MrJTGdTK4GdDrEE5WLxbSMfOSCjzBtCgCfSkWSnUOciUaJHWoaYoy2Crjejy5njPQGpyxJklxxRuhDTyVUfHrUAA3c3gqqVWS/jTYPVBBYKRJFLDnifeaClN7vr0jpIqykHEYiSMW0Uy149sDsm/gcNLQr/fFMxLNI15OwtHWKxxySJPvwfr/PNcAzyPdsxOWUnaEBNwu7DLSfdYGACrfb5VobYcgqxgfsoVB0yM2aDtmxQhvMiBt3KhoK2QOnBMZMswGf5LMPC3bov4PD7oc10+U6xu93AsCAgMc3cH4SPC2v7hOZZFJMKOWHx+f/tnnVstmCNxYKz1U11TMYT7K+uAq1n03tY23g8a5NS1GO43kbi+iAixkBbo+hRrxHe20gWObMHLxyxI20meFsAseg046XE0AcWEBns4kVbAL0RbsKw7t5gWUF6Cco0WHiYAyG5YfdPMdAD5xPpH1uleBRfqx0Tbz94e3bDyMZBdCWdQZtOSzWJoX1xFPLkeApTV4dR1rcCZ7VzqIUwYdKE3YX8IEyqeGq0Qxl8DYpgBsVI1yDd2ncFSwa76MGVCZFGlk2RsMygcAGX1q0CB4yd3Hk65NRUPmNSMLn/sF5L5WblLBu6/rHeO7c+tQCT3Xieg4p++BLHWlqvBM87grggV0GPG1QZMSatvhbU39XsZ7du1FsbiXCjzu0wbsGUrm02s4TJrHz8mYhj/DkH3/MkOv3jfMcUPtU8Ly8260bnp/1TI7wyYDipclqJ3hLine truncated
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+1 -1
View File
@@ -39,7 +39,7 @@
function loadGameScript() {
var s = document.createElement('script');
s.src = 'bishkek-witcher.js';
s.src = 'ShadowOverBishkekDemo.js';
s.async = true;
document.body.appendChild(s);
}
-2
View File
@@ -1,2 +0,0 @@
<!doctypehtml><html lang=en-us><head><meta charset=utf-8><meta content="text/html; charset=utf-8"http-equiv=Content-Type><title>Emscripten-Generated Code</title><style>body{font-family:arial;margin:0;padding:none}.emscripten{padding-right:0;margin-left:auto;margin-right:auto;display:block}div.emscripten{text-align:center}div.emscripten_border{border:1px solid #000}canvas.emscripten{border:0 none;background-color:#000}#emscripten_logo{display:inline-block;margin:0;padding:6px;width:265px}.spinner{height:30px;width:30px;margin:0;margin-top:20px;margin-left:20px;display:inline-block;vertical-align:top;-webkit-animation:rotation .8s linear infinite;-moz-animation:rotation .8s linear infinite;-o-animation:rotation .8s linear infinite;animation:rotation .8s linear infinite;border-left:5px solid #ebebeb;border-right:5px solid #ebebeb;border-bottom:5px solid #ebebeb;border-top:5px solid #787878;border-radius:100%;background-color:#bdd72e}@-webkit-keyframes rotation{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(360deg)}}@-moz-keyframes rotation{from{-moz-transform:rotate(0)}to{-moz-transform:rotate(360deg)}}@-o-keyframes rotation{from{-o-transform:rotate(0)}to{-o-transform:rotate(360deg)}}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}#status{display:inline-block;vertical-align:top;margin-top:30px;margin-left:20px;font-weight:700;color:#787878}#progress{height:20px;width:300px}#controls{display:inline-block;float:right;vertical-align:top;margin-top:30px;margin-right:20px}#output{width:100%;height:200px;margin:0 auto;margin-top:10px;border-left:0;border-right:0px;padding-left:0;padding-right:0;display:block;background-color:#000;color:#fff;font-family:'Lucida Console',Monaco,monospace;outline:0}</style></head><body><script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script><a href=http://emscripten.org><img id=emscripten_logo src=""></a><div class=spinner id=spinner></div><div class=emscripten id=status>Downloading...</div><span id=controls><span><input type=checkbox id=resize>Resize canvas</span> <span><input type=checkbox id=pointerLock checked>Lock/hide mouse pointer    </span><span><input type=button onclick='Module.requestFullscreen(document.getElementById("pointerLock").checked,document.getElementById("resize").checked)'value=Fullscreen></span></span><div class=emscripten><progress hidden id=progress max=100 value=0></progress></div><div class=emscripten_border><canvas class=emscripten id=canvas oncontextmenu=event.preventDefault() tabindex=-1></canvas></div><textarea id=output rows=8></textarea><script>var statusElement=document.getElementById("status"),progressElement=document.getElementById("progress"),spinnerElement=document.getElementById("spinner"),canvasElement=document.getElementById("canvas"),outputElement=document.getElementById("output");outputElement&&(outputElement.value=""),canvasElement.addEventListener("webglcontextlost",(e=>{alert("WebGL context lost. You will need to reload the page."),e.preventDefault()}),!1);var Module={print(...e){if(console.log(...e),outputElement){var t=e.join(" ");outputElement.value+=t+"\n",outputElement.scrollTop=outputElement.scrollHeight}},canvas:canvasElement,setStatus(e){if(Module.setStatus.last||(Module.setStatus.last={time:Date.now(),text:""}),e!==Module.setStatus.last.text){var t=e.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/),n=Date.now();t&&n-Module.setStatus.last.time<30||(Module.setStatus.last.time=n,Module.setStatus.last.text=e,t?(e=t[1],progressElement.value=100*parseInt(t[2]),progressElement.max=100*parseInt(t[4]),progressElement.hidden=!1,spinnerElement.hidden=!1):(progressElement.value=null,progressElement.max=null,progressElement.hidden=!0,e||(spinnerElement.style.display="none")),statusElement.innerHTML=e)}},totalDependencies:0,monitorRunDependencies(e){this.totalDependencies=Math.max(this.totalDependencies,e),Module.setStatus(e?"Preparing... ("+(this.totalDependencies-e)+"/"+this.totalDependencies+")":"All downloads complete.")}};Module.setStatus("Downloading..."),window.onerror=e=>{Module.setStatus("Exception thrown, see JavaScript console"),spinnerElement.style.display="none",Module.setStatus=e=>{e&&console.error("[post-exception status] "+e)}}</script><script async src="space-game001.js" crossorigin="anonymous"></script></body></html>
-200
View File
@@ -1,200 +0,0 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Sky Trek Tales</title>
<style>
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000;
position: fixed;
}
#canvas {
display: block;
position: absolute;
top: 0; left: 0;
width: 100vw; height: 100vh;
border: none;
}
#fs-button {
position: absolute;
top: 10px; right: 10px;
padding: 10px;
z-index: 10;
background: rgba(255,255,255,0.3);
color: white; border: 1px solid white;
cursor: pointer;
font-family: sans-serif;
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* Nick modal */
#nickOverlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.85);
z-index: 9999;
}
#nickBox {
background: #111;
border: 1px solid #444;
padding: 24px;
width: 320px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
text-align: center;
}
#nickBox h2 { margin: 0 0 12px 0; font-size: 18px; color: #eee; }
#nickBox input[type="text"] {
width: 100%;
padding: 10px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 12px;
border: 1px solid #333;
background: #000;
color: #fff;
}
#nickBox button {
padding: 10px 16px;
font-size: 16px;
background: #2a9fd6;
border: none;
color: #fff;
cursor: pointer;
}
</style>
</head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<div id="nickOverlay" style="display:none;">
<div id="nickBox">
<h2>Enter your nickname</h2>
<input id="nickInput" type="text" maxlength="32" placeholder="Player" />
<div>
<button id="nickSubmit">Start</button>
</div>
</div>
</div>
<script>
// Utility: подготовить глобальный Module до загрузки Emscripten-скрипта
function prepareModuleEnvironment() {
window.Module = window.Module || {};
var canvasEl = document.getElementById('canvas');
// Устанавливаем canvas для Emscripten, чтобы createContext не падал
window.Module.canvas = canvasEl;
// Подготовим заглушку setStatus, если ещё нет
window.Module.setStatus = window.Module.setStatus || function (text) {
var statusElement = document.getElementById("status");
statusElement.innerHTML = text;
statusElement.style.display = text ? 'block' : 'none';
};
}
// Show overlay only if no nickname saved.
function loadGameScript() {
var s = document.createElement('script');
s.src = 'space-game001.js';
s.async = true;
document.body.appendChild(s);
}
function showNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'flex';
var input = document.getElementById('nickInput');
input.focus();
}
function hideNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'none';
}
function saveNickAndStart(nick) {
try {
if (!nick || nick.trim() === '') nick = 'Player';
localStorage.setItem('spacegame_nick', nick);
} catch (e) {
console.warn('localStorage not available', e);
}
hideNickOverlay();
// перед загрузкой скрипта гарантируем, что Module.canvas задан
prepareModuleEnvironment();
loadGameScript();
}
document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(function(e) {
console.error('Fullscreen error: ' + e.message);
});
} else {
document.exitFullscreen();
}
});
document.addEventListener('DOMContentLoaded', function() {
// Готовим Module сразу — даже если откроется модалка, поле canvas будет доступно для скрипта (если он загружается позже)
prepareModuleEnvironment();
var stored = null;
try {
stored = localStorage.getItem('spacegame_nick');
} catch (e) {
console.warn('localStorage not available', e);
}
if (stored && stored.trim() !== '') {
// Nick is present — start immediately
loadGameScript();
} else {
// Show modal to request nickname before loading WASM
showNickOverlay();
var submit = document.getElementById('nickSubmit');
var input = document.getElementById('nickInput');
submit.addEventListener('click', function() {
saveNickAndStart(input.value);
});
input.addEventListener('input', function() {
// Strip any character that is not a-z, A-Z, 0-9 or space
var pos = this.selectionStart;
var cleaned = this.value.replace(/[^a-zA-Z0-9 ]/g, '');
if (cleaned !== this.value) {
this.value = cleaned;
this.setSelectionRange(pos - 1, pos - 1);
}
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
saveNickAndStart(input.value);
}
});
}
});
window.addEventListener("orientationchange", function() {
// Chrome на Android обновляет innerWidth/Height не мгновенно.
// Ждем завершения анимации поворота.
setTimeout(() => {
// В Emscripten это вызовет ваш onWindowResized в C++
window.dispatchEvent(new Event('resize'));
}, 200);
});
</script>
</body>
</html>
+122 -54
View File
@@ -27,6 +27,8 @@ add_executable(ShadowOverBishkekDemo WIN32
../src/Localization.h
../src/render/Renderer.cpp
../src/render/Renderer.h
../src/render/PathVisualization.cpp
../src/render/PathVisualization.h
../src/render/ShaderManager.cpp
../src/render/ShaderManager.h
../src/render/TextureManager.cpp
@@ -61,6 +63,8 @@ add_executable(ShadowOverBishkekDemo WIN32
../src/Location.cpp
../src/LocationState.h
../src/LocationState.cpp
../src/LocationGameState.h
../src/LocationGameState.cpp
../src/LocationEditor.h
../src/LocationEditor.cpp
../src/NpcCar.h
@@ -69,6 +73,17 @@ add_executable(ShadowOverBishkekDemo WIN32
../src/GameConstants.cpp
../src/GameState.h
../src/GameState.cpp
../src/content/LocationDefinition.h
../src/content/LocationDefinition.cpp
../src/game_server/IGameServer.h
../src/game_server/IGameServerLifecycle.h
../src/game_server/IGameServerHost.h
../src/game_server/GameServerRouter.h
../src/game_server/GameServerHostRouter.h
../src/game_server/InternalGameServer.h
../src/game_server/InternalGameServer.cpp
../src/game_server/LocalGameServerHost.h
../src/game_server/LocalGameServerHost.cpp
../src/ScriptEngine.h
../src/ScriptEngine.cpp
../src/navigation/PathFinder.h
@@ -77,11 +92,14 @@ add_executable(ShadowOverBishkekDemo WIN32
../src/items/GameObjectLoader.cpp
../src/items/Item.h
../src/items/Item.cpp
../src/items/Inventory.cpp
../src/items/ItemRegistry.h
../src/items/ItemRegistry.cpp
../src/Campaign.cpp
../src/items/InteractiveObject.h
../src/items/InteractiveObject.cpp
../src/items/InteractiveObjectState.h
../src/items/InteractiveObjectState.cpp
../src/dialogue/DialogueTypes.h
../src/dialogue/DialogueDatabase.h
../src/dialogue/DialogueDatabase.cpp
@@ -104,6 +122,7 @@ add_executable(ShadowOverBishkekDemo WIN32
../src/cutscene/CutsceneOverlay.h
../src/cutscene/CutsceneOverlay.cpp
../src/render/UiQuad.h
app.rc
)
# Установка проекта по умолчанию для Visual Studio
@@ -118,15 +137,12 @@ set_target_properties(ShadowOverBishkekDemo PROPERTIES
OUTPUT_NAME "ShadowOverBishkekDemo"
)
# Определения препроцессора:
# PNG_ENABLED – включает код PNG в TextureManager
# SDL_MAIN_HANDLED – отключает переопределение main -> SDL_main
# Определения препроцессора
target_compile_definitions(ShadowOverBishkekDemo PRIVATE
WIN32_LEAN_AND_MEAN
_WIN32_WINNT=0x0A00
PNG_ENABLED
# SDL_MAIN_HANDLED
# DEBUG_LIGHT
# SHOW_PATH
NETWORK
)
# Линкуем с SDL2main, если он вообще установлен
@@ -148,7 +164,16 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
# Линкуем OpenGL (Windows)
if(WIN32)
target_link_libraries(ShadowOverBishkekDemo PRIVATE opengl32)
target_sources(ShadowOverBishkekDemo PRIVATE
../src/game_server/NetworkProtocol.h
../src/game_server/NetworkProtocol.cpp
../src/game_server/WebsocketGameClient.h
../src/game_server/WebsocketGameClient.cpp
../src/game_server/RemoteGameServerHost.h
../src/game_server/RemoteGameServerHost.cpp
)
target_link_libraries(
ShadowOverBishkekDemo PRIVATE opengl32 ws2_32 mswsock)
endif()
# ===========================================
@@ -157,18 +182,15 @@ endif()
if(STEAMSDK)
message(STATUS "Steamworks SDK integration is ENABLED.")
# Макрос STEAMSDK для условной компиляции в C++ коде (#ifdef STEAMSDK)
target_compile_definitions(ShadowOverBishkekDemo PRIVATE STEAMSDK)
set(STEAM_SDK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/steamworks_sdk_164/sdk")
# Подключение заголовочных файлов Steamworks
target_include_directories(ShadowOverBishkekDemo PRIVATE
"${STEAM_SDK_DIR}/public"
"${STEAM_SDK_DIR}/public/steam"
)
# Определение разрядности сборки (x86 vs x64) для выбора версий lib и dll
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(STEAM_LIB_DIR "${STEAM_SDK_DIR}/redistributable_bin/win64")
set(STEAM_LIB_NAME "steam_api64")
@@ -179,7 +201,6 @@ if(STEAMSDK)
set(STEAM_DLL_NAME "steam_api.dll")
endif()
# Поиск библиотеки линковки (.lib)
find_library(STEAM_LIBRARY
NAMES ${STEAM_LIB_NAME}
PATHS ${STEAM_LIB_DIR}
@@ -193,22 +214,18 @@ if(STEAMSDK)
message(FATAL_ERROR "Steam API library (${STEAM_LIB_NAME}) NOT found in ${STEAM_LIB_DIR}!")
endif()
# Генерация временного файла steam_appid.txt с ID демо-версии (4945840)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/steam_appid.txt" "4945840")
else()
message(STATUS "Steamworks SDK integration is DISABLED.")
endif()
# ===========================================
# Копирование SDL2d.dll и zlibd.dll рядом с exe
# Копирование DLL и ресурсов для локального запуска в Visual Studio
# ===========================================
if (WIN32)
# SDL2: в Debug - SDL2d.dll, в Release - SDL2.dll
set(SDL2_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2_INSTALL_DIR}/bin/SDL2d.dll,${SDL2_INSTALL_DIR}/bin/SDL2.dll>")
set(SDL2_DLL_DST "$<IF:$<CONFIG:Debug>,$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/SDL2d.dll,$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/SDL2.dll>")
set(LIBZIP_DLL_SRC "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/bin/zip.dll,${LIBZIP_BASE_DIR}-Release/bin/zip.dll>")
set(ZLIB_DLL_SRC "$<IF:$<CONFIG:Debug>,${ZLIB_INSTALL_DIR}/bin/zd.dll,${ZLIB_INSTALL_DIR}/bin/z.dll>")
@@ -216,47 +233,26 @@ if (WIN32)
set(SDL2TTF_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/bin/SDL2_ttfd.dll,${SDL2TTF_BASE_DIR}-Release/bin/SDL2_ttf.dll>")
set(SDL2MIXER_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/bin/SDL2_mixerd.dll,${SDL2MIXER_BASE_DIR}-Release/bin/SDL2_mixer.dll>")
add_custom_command(TARGET ShadowOverBishkekDemo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying DLLs to output folder..."
# Копируем SDL2
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL_SRC}"
"${SDL2_DLL_DST}"
# Копируем LIBZIP (целевое имя всегда zip.dll)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LIBZIP_DLL_SRC}"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/zip.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${ZLIB_DLL_SRC}"
"${ZLIB_DLL_DST}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2TTF_DLL_SRC}"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_DLL_SRC}" "${SDL2_DLL_DST}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${LIBZIP_DLL_SRC}" "$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/zip.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ZLIB_DLL_SRC}" "${ZLIB_DLL_DST}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2TTF_DLL_SRC}" "$<TARGET_FILE_DIR:ShadowOverBishkekDemo>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-$<CONFIG>/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
)
# Если включен Steam, добавляем шаги копирования DLL и appid в этот же POST_BUILD
if(STEAMSDK)
add_custom_command(TARGET ShadowOverBishkekDemo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying Steam SDK components..."
# Копирование dll к исполняемому файлу
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${STEAM_LIB_DIR}/${STEAM_DLL_NAME}"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/${STEAM_DLL_NAME}"
# Копирование steam_appid.txt к исполняемому файлу
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_BINARY_DIR}/steam_appid.txt"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/steam_appid.txt"
# Дублируем steam_appid.txt в корень сборки (полезно при отладке прямо из MSVC)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_BINARY_DIR}/steam_appid.txt"
"${CMAKE_BINARY_DIR}/steam_appid.txt"
@@ -264,27 +260,99 @@ if (WIN32)
endif()
endif()
# ===========================================
# Копирование ресурсов после сборки
# ===========================================
# Какие папки с ресурсами нужно копировать
set(RUNTIME_RESOURCE_DIRS
"resources"
"audio"
)
# Копируем ресурсы и шейдеры в папку exe и в корень build/
# Копирование ресурсов локально
set(RUNTIME_RESOURCE_DIRS "resources" "music")
foreach(resdir IN LISTS RUNTIME_RESOURCE_DIRS)
add_custom_command(TARGET ShadowOverBishkekDemo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying ${resdir} to runtime folders..."
# 1) туда, где лежит exe (build/Debug, build/Release и т.п.)
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/../${resdir}"
"$<TARGET_FILE_DIR:ShadowOverBishkekDemo>/${resdir}"
# 2) в корень build, если захочешь запускать из этой папки
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/../${resdir}"
"${CMAKE_BINARY_DIR}/${resdir}"
)
endforeach()
# ===========================================
# ПРАВИЛА ИНСТАЛЛЯЦИИ (Для CPack и создания пакетов)
# ===========================================
# 1. Основной исполняемый файл
install(TARGETS ShadowOverBishkekDemo DESTINATION .)
# 2. Необходимые DLL
if(WIN32)
install(FILES
"${SDL2_INSTALL_DIR}/bin/SDL2.dll"
"${LIBZIP_BASE_DIR}-Release/bin/zip.dll"
"${ZLIB_INSTALL_DIR}/bin/z.dll"
"${SDL2TTF_BASE_DIR}-Release/bin/SDL2_ttf.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-Release/bin/SDL2_mixer.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/concrt140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140_1.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140_2.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140_atomic_wait.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140_clr0400.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140_codecvt_ids.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/msvcp140d.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/OpenAL32.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcamp140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vccorlib140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcomp140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcruntime140.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcruntime140_1.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcruntime140_1_clr0400.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/vcruntime140_threads.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/dist_dlls/wrap_oal.dll"
DESTINATION .
)
if(STEAMSDK)
install(FILES
"${STEAM_LIB_DIR}/${STEAM_DLL_NAME}"
"${CMAKE_CURRENT_BINARY_DIR}/steam_appid.txt"
DESTINATION .
)
endif()
endif()
# 3. Директории с ресурсами
foreach(resdir IN LISTS RUNTIME_RESOURCE_DIRS)
install(DIRECTORY "${CMAKE_SOURCE_DIR}/../${resdir}" DESTINATION .)
endforeach()
# ===========================================
# НАСТРОЙКА CPACK (Создание ZIP Portable и NSIS Installer)
# ===========================================
set(CPACK_PACKAGE_NAME "ShadowOverBishkekDemo")
set(CPACK_PACKAGE_VENDOR "Fish Run Games")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Shadow Over Bishkek Demo Version")
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "0")
set(CPACK_PACKAGE_VERSION_PATCH "5")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Fish Run Games/Shadow Over Bishkek Demo")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt")
# Настройки NSIS инсталлятора
set(CPACK_NSIS_DISPLAY_NAME "Shadow Over Bishkek Demo")
set(CPACK_NSIS_PACKAGE_NAME "Shadow Over Bishkek Demo")
set(CPACK_NSIS_HELP_LINK "https://fishrungames.com/contact.html")
set(CPACK_NSIS_URL_INFO_ABOUT "https://shadowoverbishkek.com/")
set(CPACK_NSIS_MODIFY_PATH OFF)
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
# Иконки и ярлыки в меню Пуск и на Рабочем столе
set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icon.ico")
set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/icon.ico")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
"CreateShortCut '$DESKTOP\\\\Shadow Over Bishkek Demo.lnk' '$INSTDIR\\\\ShadowOverBishkekDemo.exe'"
)
# Форматы генераторов: ZIP (Portable) и NSIS (Установщик)
set(CPACK_GENERATOR "ZIP;NSIS")
include(CPack)
+21
View File
@@ -0,0 +1,21 @@
END USER LICENSE AGREEMENT (EULA)
Game: Shadow Over Bishkek (Demo Version)
Developer: Fish Run Games
1. GRANT OF LICENSE
Fish Run Games grants you a non-exclusive, non-transferable, limited license to download, install, and play the Shadow Over Bishkek Demo for personal, non-commercial entertainment purposes.
2. OWNERSHIP & INTELLECTUAL PROPERTY
The game, including all code, graphics, audio, characters, storyline, and assets, is the intellectual property of Fish Run Games and is protected by copyright and other intellectual property laws. You may not reverse engineer, decompile, modify, or redistribute the software without express written permission.
3. DISCLAIMER OF WARRANTY
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4. USER DATA AND SAVES
Save files and local configuration options are stored locally on your device. Fish Run Games is not responsible for any loss of save data resulting from software updates, system crashes, or uninstallation.
5. TERMINATION
This license is effective until terminated. Your rights under this license will terminate automatically without notice from Fish Run Games if you fail to comply with any term(s) of this agreement.
Copyright (c) 2026 Fish Run Games. All rights reserved.
+1
View File
@@ -0,0 +1 @@
IDI_ICON1 ICON "icon.ico"
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+47
View File
@@ -0,0 +1,47 @@
{
"campaigns": [
{
"id": "main",
"title": { "ru": "Основная игра", "en": "Main Game" },
"locations": [
"resources/config/locations/location_dorm.json",
"resources/config/locations/uni_exterior.json",
"resources/config/locations/uni_interior.json"
],
"startLocation": "location_dorm",
"startMusic": "music/obshaga.ogg",
"startState": "resources/config/start_state.json",
"quests": "resources/config/quests.json",
"tutorial": true,
"loadingScreen": "resources/w/ui/screen_loading.json"
},
{
"id": "battle_zone",
"title": { "ru": "Зона боя (одиночная)", "en": "Battle Zone (Single Player)" },
"locations": [
"resources/config/locations/battle_zone_uni_exterior.json"
],
"startLocation": "uni_exterior",
"startMusic": "music/bishkek univer day.ogg",
"startState": "resources/config/start_state_battle_zone_singleplayer.json",
"quests": "resources/config/quests_battle_zone.json",
"tutorial": false,
"loadingScreen": "resources/w/ui/screen_loading.json"
},
{
"id": "battle_zone_multiplayer",
"title": { "ru": "Зона боя (сетевая)", "en": "Battle Zone (Multiplayer)" },
"locations": [
"resources/config/locations/battle_zone_uni_exterior.json"
],
"startLocation": "uni_exterior",
"startMusic": "music/bishkek univer day.ogg",
"startState": "resources/config/start_state_battle_zone.json",
"quests": "resources/config/quests_battle_zone.json",
"tutorial": false,
"networkPlayerSlots": ["player", "player2"],
"networkCompatibility": "battle-zone-lockstep-v5",
"loadingScreen": "resources/w/ui/screen_loading.json"
}
]
}
@@ -1,6 +1,7 @@
{
"objects": [
{
"id": "WashingMachine002",
"name": "WashingMachine002",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -27,6 +28,7 @@
"activateFunction": "on_washing_machine1_click"
},
{
"id": "Dryer002",
"name": "Dryer002",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -53,6 +55,7 @@
"activateFunction": "on_dryer1_click"
},
{
"id": "Toilet001",
"name": "Toilet001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -79,6 +82,7 @@
"activateFunction": "on_toilet_click"
},
{
"id": "Sink002",
"name": "Sink002",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -105,6 +109,7 @@
"activateFunction": "on_sink1_click"
},
{
"id": "Sink003",
"name": "Sink003",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -131,6 +136,7 @@
"activateFunction": "on_sink1_click"
},
{
"id": "Sink004",
"name": "Sink004",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -157,6 +163,7 @@
"activateFunction": "on_sink1_click"
},
{
"id": "Sink005",
"name": "Sink005",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -183,6 +190,7 @@
"activateFunction": "on_sink1_click"
},
{
"id": "Shower001",
"name": "Shower001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -209,6 +217,7 @@
"activateFunction": "on_shower_click"
},
{
"id": "Freezer001",
"name": "Freezer001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -235,6 +244,7 @@
"activateFunction": "on_freezer_click"
},
{
"id": "Boombox001",
"name": "Boombox001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -261,6 +271,7 @@
"activateFunction": "on_boombox_click"
},
{
"id": "WashingMachine001",
"name": "WashingMachine001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -287,6 +298,7 @@
"activateFunction": "on_washing_machine1_click"
},
{
"id": "Shelf001",
"name": "Shelf001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -313,6 +325,7 @@
"activateFunction": "on_shelf_click"
},
{
"id": "Stove001",
"name": "Stove001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -339,6 +352,7 @@
"activateFunction": "on_stove1_click"
},
{
"id": "Dryer001",
"name": "Dryer001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -365,6 +379,7 @@
"activateFunction": "on_dryer1_click"
},
{
"id": "Sink001",
"name": "Sink001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -391,6 +406,7 @@
"activateFunction": "on_sink1_click"
},
{
"id": "Door_Room_1_1_0_Leaf_001",
"name": "Door_Room_1_1_0_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_1_1_0_Leaf_001.txt.bin",
@@ -398,7 +414,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 10.0,
"approachRadius": 0.89,
"boundsMaxX": 0.01552116870880127,
@@ -415,6 +431,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Room_1_-1_0_Leaf_001",
"name": "Door_Room_1_-1_0_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_1_-1_0_Leaf_001.txt.bin",
@@ -422,7 +439,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 10.0,
"approachRadius": 0.8,
"boundsMaxX": 0.10440409183502197,
@@ -439,6 +456,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Room_-1_1_0_Leaf_001",
"name": "Door_Room_-1_1_0_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_-1_1_0_Leaf_001.txt.bin",
@@ -446,7 +464,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -10.0,
"approachRadius": 0.8,
"boundsMaxX": 0.05766594409942627,
@@ -463,6 +481,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Room_-1_-1_0_Leaf_001",
"name": "Door_Room_-1_-1_0_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_0_Leaf_001.txt.bin",
@@ -470,7 +489,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -10.0,
"approachRadius": 0.8,
"boundsMaxX": 0.10906755924224854,
@@ -487,6 +506,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Room_1_1_1_Leaf_001",
"name": "Door_Room_1_1_1_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_1_1_1_Leaf_001.txt.bin",
@@ -494,7 +514,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 15.7,
"approachRadius": 0.8,
"boundsMaxX": 0.046350955963134766,
@@ -511,6 +531,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Room_1_-1_1_Leaf_001",
"name": "Door_Room_1_-1_1_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_1_-1_1_Leaf_001.txt.bin",
@@ -518,7 +539,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 15.7,
"approachRadius": 0.800000011920929,
"boundsMaxX": 0.12420153617858887,
@@ -535,6 +556,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Utility_1_1_2_Leaf_001",
"name": "Door_Utility_1_1_2_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Utility_1_1_2_Leaf_001.txt.bin",
@@ -542,7 +564,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 19.95,
"approachRadius": 0.8,
"boundsMaxX": -0.050987482070922852,
@@ -559,6 +581,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Utility_1_-1_2_Leaf_001",
"name": "Door_Utility_1_-1_2_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Utility_1_-1_2_Leaf_001.txt.bin",
@@ -566,7 +589,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": 19.95,
"approachRadius": 0.8,
"boundsMaxX": 0.073349356651306152,
@@ -583,6 +606,7 @@
"activateFunction": "on_locked_door_click"
},
{
"id": "Door_Utility_-1_1_2_Leaf_001",
"name": "Door_Utility_-1_1_2_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Utility_-1_1_2_Leaf_001.txt.bin",
@@ -590,7 +614,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -19.95,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -599,6 +623,7 @@
"interactionRadius": 0.0
},
{
"id": "Door_Utility_-1_-1_2_Leaf_001",
"name": "Door_Utility_-1_-1_2_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Utility_-1_-1_2_Leaf_001.txt.bin",
@@ -606,7 +631,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -19.95,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -615,13 +640,14 @@
"interactionRadius": 0.0
},
{
"id": "Phone001",
"name": "Phone001",
"texturePath": "resources/w/dorm2/phone001_tex001.png",
"meshPath": "resources/w/dorm2/phone001_001.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 6.6644,
"positionX": 6.6644004991889,
"positionY": 0.9,
"positionZ": -12.5262,
"approachRadius": 0.6,
@@ -640,6 +666,7 @@
"activateFunction": "on_phone_pickup"
},
{
"id": "Journal001",
"name": "Journal001",
"texturePath": "resources/w/dorm2/Book_Cover_tex002.png",
"meshPath": "resources/w/dorm2/journal002_002.txt.bin",
@@ -647,7 +674,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 5.84296,
"positionY": 0.9,
"positionY": 0.9284919999539852,
"positionZ": -12.4661,
"approachRadius": 0.6,
"boundsMaxX": 0.17081403732299805,
@@ -665,6 +692,7 @@
"activateFunction": "on_journal_pickup"
},
{
"id": "Bed001",
"name": "Bed001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -691,21 +719,23 @@
"activateFunction": "on_bed_sleep"
},
{
"id": "Music001",
"name": "Music001",
"texturePath": "resources/w/exterior/texAtlas002.png",
"meshPath": "resources/w/dorm2/D_Music_004.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 5.10067,
"positionY": 0.91385,
"positionZ": -11.9566,
"positionX": 5.1006695008111,
"positionY": 1.0181855017900467,
"positionZ": -11.958324999397993,
"approachRadius": 0.6,
"scale": 1.0,
"interactionRadius": 0.3,
"activateFunction": ""
},
{
"id": "Door_Bath_-1_1_1_Leaf_002",
"name": "Door_Bath_-1_1_1_Leaf_002",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Bath_-1_1_1_Leaf_002.txt.bin",
@@ -713,7 +743,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 2.825,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -14.3,
"pivotX": -0.6,
"pivotY": 0.0,
@@ -733,6 +763,7 @@
"activateFunction": "on_player_bathroom_click"
},
{
"id": "Door_Bath_-1_-1_1_Leaf_002",
"name": "Door_Bath_-1_-1_1_Leaf_002",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Bath_-1_-1_1_Leaf_002.txt.bin",
@@ -740,7 +771,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -2.825,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -14.3,
"approachRadius": 0.8,
"boundsMaxX": 0.55439233779907227,
@@ -757,6 +788,7 @@
"activateFunction": "on_alik_bathroom_click"
},
{
"id": "Door_Room_-1_1_1_Leaf_001",
"name": "Door_Room_-1_1_1_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_-1_1_1_Leaf_001.txt.bin",
@@ -764,7 +796,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -15.7,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -784,6 +816,7 @@
"activateFunction": "on_player_door_click"
},
{
"id": "Door_Room_-1_-1_1_Leaf_001",
"name": "Door_Room_-1_-1_1_Leaf_001",
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_1_Leaf_001.txt.bin",
@@ -791,7 +824,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -1.1,
"positionY": 0.0,
"positionY": 0.9695000052452087,
"positionZ": -15.7,
"approachRadius": 0.8,
"boundsMaxX": 0.13942182064056396,
@@ -811,6 +844,7 @@
"activateFunction": "on_alik_door_click"
},
{
"id": "Room_Cover_Bath_W_N_2_001",
"name": "Room_Cover_Bath_W_N_2_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_Bath_W_N_2_002.txt.bin",
@@ -818,13 +852,14 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionX": 2.8249999284744263,
"positionY": 2.8500000476837157,
"positionZ": -12.849999904632568,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_LivingRoom_W_N_2_001",
"name": "Room_Cover_LivingRoom_W_N_2_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_N_2_002.txt.bin",
@@ -832,13 +867,14 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionX": 4.949999928474426,
"positionY": 2.8500000476837157,
"positionZ": -14.25,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_LivingRoom_W_S_2_001",
"name": "Room_Cover_LivingRoom_W_S_2_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_S_2_002.txt.bin",
@@ -846,13 +882,14 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionX": -4.949999928474426,
"positionY": 2.8500000476837157,
"positionZ": -14.25,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_Main_Hall_And_Corridors_002",
"name": "Room_Cover_Main_Hall_And_Corridors_002",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_Main_Hall_And_Corridors_002.txt.bin",
@@ -861,12 +898,13 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionY": 2.8500000476837157,
"positionZ": 0.0,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_Others_001",
"name": "Room_Cover_Others_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_All_002.txt.bin",
@@ -875,12 +913,13 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionY": 2.8500000476837157,
"positionZ": 4.250000476837158,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_Utility_W_N_3_001",
"name": "Room_Cover_Utility_W_N_3_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_N_3_002.txt.bin",
@@ -888,13 +927,14 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionX": 4.949999928474426,
"positionY": 2.8500000476837157,
"positionZ": -19.950000762939453,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Room_Cover_Utility_W_S_3_001",
"name": "Room_Cover_Utility_W_S_3_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_S_3_002.txt.bin",
@@ -902,13 +942,14 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -0.1,
"positionZ": 0.0,
"positionX": -4.949999928474426,
"positionY": 2.8500000476837157,
"positionZ": -19.950000762939453,
"scale": 1.0,
"interactionRadius": 0.0
},
{
"id": "Roof",
"name": "Roof",
"texturePath": "resources/black.png",
"meshPath": "resources/w/dorm2/Shadow_Roof001_002.txt.bin",
@@ -919,7 +960,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": 0.1,
"positionY": 3.2050000190734864,
"positionZ": 0.0,
"scale": 1.0,
"interactionRadius": 0.0
@@ -1,15 +1,16 @@
{
"objects": [
{
"id": "Container001",
"name": "Container001",
"texturePath": "resources/w/exterior/derbis001_tex001.png",
"meshPath": "resources/w/exterior/Derbis_001.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -26.8049,
"positionY": 0.0,
"positionZ": 9.11524,
"positionX": -27.003390023612976,
"positionY": 0.32889999728649855,
"positionZ": 8.820399544944763,
"approachRadius": 1.5,
"boundsMaxX": 2.9518299102783203,
"boundsMaxY": 0.5,
@@ -25,15 +26,16 @@
"activateFunction": "on_container_clicked"
},
{
"id": "car001",
"name": "car001",
"texturePath": "resources/e/car4096blue_v002.png",
"meshPath": "resources/e/car004.txt.bin",
"rotationX": 0.0,
"rotationY": 90.0,
"rotationZ": 0.0,
"positionX": -10.5,
"positionY": 0.55,
"positionZ": -14.4195,
"positionX": -10.503284412622452,
"positionY": 0.9012498408555984,
"positionZ": -14.861633367061614,
"approachRadius": 0.5,
"boundsMaxX": 0.8858480453491211,
"boundsMaxY": 0.7,
@@ -49,6 +51,7 @@
"scale": 0.7
},
{
"id": "car001wheels",
"name": "car001wheels",
"texturePath": "resources/w/exterior/Wheel_base003.png",
"meshPath": "resources/w/exterior/wheels002.txt.bin",
@@ -56,8 +59,8 @@
"rotationY": 90.0,
"rotationZ": 0.0,
"positionX": -10.5,
"positionY": 0.0,
"positionZ": -14.4195,
"positionY": 0.3359999924898147,
"positionZ": -14.839499933242797,
"approachRadius": 0.5,
"boundsMaxX": 0,
"boundsMaxY": 0.0,
@@ -1,6 +1,7 @@
{
"objects": [
{
"id": "Bookshelf002",
"name": "Bookshelf002",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -27,6 +28,7 @@
"castShadow": false
},
{
"id": "Bookshelf003",
"name": "Bookshelf003",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -53,6 +55,7 @@
"castShadow": false
},
{
"id": "Bookshelf004",
"name": "Bookshelf004",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -79,6 +82,7 @@
"castShadow": false
},
{
"id": "Bookshelf005",
"name": "Bookshelf005",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -105,15 +109,16 @@
"castShadow": false
},
{
"id": "Cake001",
"name": "Cake001",
"texturePath": "resources/w/exterior/texAtlas002.png",
"meshPath": "resources/w/interior/D_Cake_005.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0,
"positionY": -5.0,
"positionZ": 0,
"positionX": -5.365539073944092,
"positionY": 0.9888315200805664,
"positionZ": 2.468366503715515,
"approachRadius": 0.6,
"boundsMaxX": 0.19605731964111328,
"boundsMaxY": 0.4,
@@ -131,6 +136,7 @@
"castShadow": false
},
{
"id": "Computer001",
"name": "Computer001",
"texturePath": "resources/w/interior/computer_texture002.png",
"textureDarkandsPath": "resources/w/interior/computer_texture002_dark.png",
@@ -138,9 +144,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 5.0916,
"positionY": 1.1534,
"positionZ": 0.751629,
"positionX": 5.01753252658844,
"positionY": 1.353411484324932,
"positionZ": 0.6829135021486282,
"approachRadius": 1.2,
"boundsMaxX": 0.6253643035888672,
"boundsMaxY": 0.5,
@@ -155,15 +161,16 @@
"activateFunction": "on_computer_clicked"
},
{
"id": "Knife001",
"name": "Knife001",
"texturePath": "resources/w/gg/Props.png",
"meshPath": "resources/w/interior/Knife_lay001_004.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": -4.8327,
"positionX": -4.959765496635623,
"positionY": 0.93551,
"positionZ": 2.2418,
"positionZ": 2.369899506953731,
"approachRadius": 1.4,
"boundsMaxX": 0.30867958068847656,
"boundsMaxY": 0.4,
@@ -177,6 +184,7 @@
"castShadowNight" : false
},
{
"id": "Book001",
"name": "Book001",
"texturePath": "resources/w/interior/Book_Cover_tex003.png",
"meshPath": "resources/w/interior/book002_002.txt.bin",
@@ -184,7 +192,7 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 1.84795,
"positionY": 0.6595,
"positionY": 0.6809705001562834,
"positionZ": 2.90552,
"approachRadius": 0.6,
"boundsMaxX": 0.2350165843963623,
@@ -202,6 +210,7 @@
"castShadowNight" : false
},
{
"id": "Bookshelf001",
"name": "Bookshelf001",
"texturePath": "resources/transparent.png",
"meshPath": "resources/w/Cube_001.txt.bin",
@@ -228,6 +237,7 @@
"castShadow": false
},
{
"id": "ReportCard001",
"name": "ReportCard001",
"texturePath": "resources/w/exterior/texAtlas002.png",
"meshPath": "resources/w/interior/D_report_cardGG_005.txt.bin",
@@ -252,6 +262,7 @@
"activateFunction": "on_report_card_pickup"
},
{
"id": "Note001",
"name": "Note001",
"texturePath": "resources/w/interior/note001_tex003.png",
"meshPath": "resources/w/interior/note001_002.txt.bin",
@@ -276,15 +287,16 @@
"activateFunction": "on_note_pickup"
},
{
"id": "car001",
"name": "car001",
"texturePath": "resources/e/car4096blue_v002.png",
"meshPath": "resources/e/car004.txt.bin",
"rotationX": 0.0,
"rotationY": 90.0,
"rotationZ": 0.0,
"positionX": -10.5,
"positionY": -5.0,
"positionZ": -14.4195,
"positionX": -10.503284412622452,
"positionY": -4.648750159144401,
"positionZ": -14.861633367061614,
"approachRadius": 0.6,
"boundsMaxX": 0.0,
"boundsMaxY": 0.0,
@@ -299,6 +311,7 @@
"scale": 0.7
},
{
"id": "car001wheels",
"name": "car001wheels",
"texturePath": "resources/w/exterior/Wheel_base003.png",
"meshPath": "resources/w/exterior/wheels002.txt.bin",
@@ -306,8 +319,8 @@
"rotationY": 90.0,
"rotationZ": 0.0,
"positionX": -10.5,
"positionY": -5.0,
"positionZ": -14.4195,
"positionY": -4.664000007510185,
"positionZ": -14.839499933242797,
"approachRadius": 0.6,
"boundsMaxX": 0.0,
"boundsMaxY": 0.0,
@@ -322,6 +335,7 @@
"scale": 0.7
},
{
"id": "Room_N_2_Leaf001",
"name": "Room_N_2_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputRoom_N_2_Leaf001.txt.bin",
@@ -330,7 +344,7 @@
"rotationZ": 0.0,
"positionX": 1.5,
"positionY": 0.975,
"positionZ": 6.965,
"positionZ": 6.400000002384186,
"approachRadius": 0.8,
"boundsMaxX": 0.06785714626312256,
"boundsMaxY": 1.0,
@@ -349,6 +363,7 @@
"activateFunction": "on_library_door_click"
},
{
"id": "Room_N_1_Leaf001",
"name": "Room_N_1_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputRoom_N_1_Leaf001.txt.bin",
@@ -357,7 +372,7 @@
"rotationZ": 0.0,
"positionX": 1.5,
"positionY": 0.975,
"positionZ": -1.035,
"positionZ": -1.5999999976158141,
"approachRadius": 0.8,
"boundsMaxX": 0.10772454738616943,
"boundsMaxY": 1.0,
@@ -376,6 +391,7 @@
"activateFunction": "on_n2_door_click"
},
{
"id": "Room_S_1_Leaf001",
"name": "Room_S_1_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputRoom_S_1_Leaf001.txt.bin",
@@ -384,7 +400,7 @@
"rotationZ": 0.0,
"positionX": -1.5,
"positionY": 0.975,
"positionZ": -1.035,
"positionZ": -1.5999999976158141,
"approachRadius": 0.8,
"boundsMaxX": 0.12115073204040527,
"boundsMaxY": 1.0,
@@ -403,6 +419,7 @@
"activateFunction": "on_s2_door_click"
},
{
"id": "Room_S_2_Leaf001",
"name": "Room_S_2_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputRoom_S_2_Leaf001.txt.bin",
@@ -411,7 +428,7 @@
"rotationZ": 0.0,
"positionX": -1.5,
"positionY": 0.975,
"positionZ": 6.965,
"positionZ": 6.400000002384186,
"approachRadius": 0.8,
"boundsMaxX": 0.09747374057769775,
"boundsMaxY": 1.0,
@@ -430,15 +447,16 @@
"activateFunction": "on_teachers_door_click"
},
{
"id": "Hall_Leaf001",
"name": "Hall_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputHall_Leaf001.txt.bin",
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.565,
"positionX": 0.0,
"positionY": 0.975,
"positionZ": 8,
"positionZ": 8.000001000240445,
"approachRadius": 0.9,
"boundsMaxX": 0.6243956089019775,
"boundsMaxY": 1.0,
@@ -457,6 +475,7 @@
"activateFunction": "on_hall_door_click"
},
{
"id": "Room_S_0_Leaf001",
"name": "Room_S_0_Leaf001",
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
"meshPath": "resources/w/interior/outputRoom_S_0_Leaf001.txt.bin",
@@ -465,7 +484,7 @@
"rotationZ": 0.0,
"positionX": -1.5,
"positionY": 0.975,
"positionZ": -9.035,
"positionZ": -9.599999020778657,
"approachRadius": 0.8,
"boundsMaxX": 0.10914039611816406,
"boundsMaxY": 1.0,
@@ -484,6 +503,7 @@
"activateFunction": "on_s1_door_click"
},
{
"id": "Room_Cover_Corridor_001",
"name": "Room_Cover_Corridor_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_Corridor_003.txt.bin",
@@ -491,9 +511,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": 2.2500000596046448,
"positionY": 2.4499998092651367,
"positionZ": -3.999999761581421,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -501,6 +521,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_Main_Hall_001",
"name": "Room_Cover_Main_Hall_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_Main_Hall_003.txt.bin",
@@ -509,8 +530,8 @@
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionY": 2.4499998092651367,
"positionZ": 12.0,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -518,6 +539,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_South_1_001",
"name": "Room_Cover_South_1_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_South_1_003.txt.bin",
@@ -525,9 +547,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": -3.7500000596046448,
"positionY": 2.4499998092651367,
"positionZ": -12.0,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -535,6 +557,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_North_2_001",
"name": "Room_Cover_North_2_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_North_2_003.txt.bin",
@@ -542,9 +565,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": 3.7500000596046448,
"positionY": 2.4499998092651367,
"positionZ": -4.000000048428774,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -552,6 +575,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_South_2_001",
"name": "Room_Cover_South_2_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_South_2_003.txt.bin",
@@ -559,9 +583,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": -3.7500000596046448,
"positionY": 2.4499998092651367,
"positionZ": -4.000000048428774,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -569,6 +593,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_North_3_001",
"name": "Room_Cover_North_3_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_North_3_003.txt.bin",
@@ -576,9 +601,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": 3.7500000596046448,
"positionY": 2.4499998092651367,
"positionZ": 4.000000048428774,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
@@ -586,6 +611,7 @@
"interactionRadius": 0.0
},
{
"id": "Room_Cover_South_3_001",
"name": "Room_Cover_South_3_001",
"texturePath": "resources/black.png",
"meshPath": "resources/w/interior/Room_Cover_South_3_003.txt.bin",
@@ -593,9 +619,9 @@
"rotationX": 0.0,
"rotationY": 0.0,
"rotationZ": 0.0,
"positionX": 0.0,
"positionY": -5.0,
"positionZ": 0.0,
"positionX": -3.7500000596046448,
"positionY": 2.4499998092651367,
"positionZ": 4.000000048428774,
"scale": 1.0,
"pivotX": 0.0,
"pivotY": 0.0,
+3 -3
View File
@@ -3,7 +3,7 @@
{
"id": "main_hall",
"positionX": -3.1,
"positionY": 6.0,
"positionY": 4.8,
"positionZ": 0.0,
"directionX": 0.0,
"directionY": -1.0,
@@ -20,7 +20,7 @@
"limitX": 3.75,
"limitY": 2.75,
"positionX": 4.95,
"positionY": 3.0,
"positionY": 3.6,
"positionZ": -14.25,
"directionX": 0.0,
"directionY": -1.0,
@@ -55,7 +55,7 @@
"limitX": 3.75,
"limitY": 2.75,
"positionX": -4.95,
"positionY": 4.0,
"positionY": 4.2,
"positionZ": -19.95,
"directionX": 0.0,
"directionY": -1.0,
@@ -0,0 +1,13 @@
{
"id": "uni_exterior",
"gameObjects": "resources/w/shelter/game_objects.json",
"interactiveObjects": "resources/w/shelter/interactive_objects.json",
"npcs": "resources/config/npcs_battle_zone.json",
"dialogues": "resources/dialogue/battle_zone_dialogues.json",
"navigation": [
"resources/navigation/battle_zone001.txt",
"resources/navigation/dorm0_large.json"
],
"script": "resources/start_battle_zone.lua",
"playerPosition": [-6, 0, 12.8]
}
@@ -0,0 +1,32 @@
{
"id": "location_dorm",
"gameObjects": "resources/config/gameobjects_dorm_new_x.json",
"interactiveObjects": "resources/config/interactive_objects_dorm_x.json",
"npcs": "resources/config/npcs_dorm.json",
"dialogues": "resources/dialogue/dorm_dialogues004.json",
"navigation": [
"resources/navigation/dorm3_bca.txt",
"resources/navigation/dorm3_ca.txt",
"resources/navigation/dorm3_ba.txt",
"resources/navigation/dorm3_a.txt",
"resources/navigation/dorm3_b.txt",
"resources/navigation/dorm3_all_open.txt"
],
"script": "resources/start_dorm.lua",
"teleports": "resources/config/teleports_dorm.json",
"triggerZones": "resources/config/trigger_zones_dorm.json",
"lights": "resources/config/lights_dorm.json",
"playerPosition": [6.76345, 0, -14.6022],
"taxi": {
"triggerPosition": [-19.0, 0, 0],
"waypoints": [
[-20.85, 0, 30.0],
[-19.35, 0, 24.0],
[-19.35, 0, 14.0]
],
"initialPosition": [-20.85, 0, 64],
"initialRotation": 0,
"waypointReachRadius": 5
},
"lockInteractiveObjectsDuringTutorial": true
}
@@ -0,0 +1,27 @@
{
"id": "uni_exterior",
"gameObjects": "resources/config/gameobjects_uni_exterior_x.json",
"interactiveObjects": "resources/config/interactive_objects_uni_exterior_x.json",
"npcs": "resources/config/npcs_uni_exterior.json",
"dialogues": "resources/dialogue/uni_exterior_dialogues003.json",
"navigation": [
"resources/navigation/uni_exterior3_trees_car.txt",
"resources/navigation/uni_exterior3_trees.txt"
],
"script": "resources/start_uni_exterior.lua",
"teleports": "resources/config/teleports_uni_exterior.json",
"triggerZones": "resources/config/trigger_zones_uni_exterior.json",
"lights": "resources/config/lights_uni_exterior.json",
"playerPosition": [5, 0, -18.4],
"taxi": {
"triggerPosition": [-17.6, 0.05, -45],
"waypoints": [
[-17.6, 0.05, -90.0],
[-17.6, 0.05, -75.0],
[-17.6, 0.05, -55.0]
],
"initialPosition": [-17.6, 0.05, -100],
"initialRotation": 180,
"waypointReachRadius": 5
}
}
@@ -0,0 +1,31 @@
{
"id": "uni_interior",
"gameObjects": "resources/config/gameobjects_uni_interior_x.json",
"interactiveObjects": "resources/config/interactive_objects_uni_interior_x.json",
"npcs": "resources/config/npcs_uni_interior.json",
"dialogues": "resources/dialogue/uni_interior_dialogues_010.json",
"navigation": [
"resources/navigation/uni_interior4_unlocked_n2.txt",
"resources/navigation/uni_interior3_n2_hall.txt",
"resources/navigation/uni_interior4_n2_lr_hall.txt",
"resources/navigation/uni_interior4_n2_lr_tr_hall.txt",
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.txt",
"resources/navigation/uni_interior3_darklands_all_open.txt",
"resources/navigation/uni_interior4_unlocked_hall.txt",
"resources/navigation/uni_interior4_unlocked_n3.txt",
"resources/navigation/uni_interior4_unlocked_s1.txt",
"resources/navigation/uni_interior4_unlocked_s2.txt",
"resources/navigation/uni_interior4_unlocked_s3.txt",
"resources/navigation/uni_interior4_unlocked_s3_s1.txt",
"resources/navigation/uni_interior4_unlocked_s3_hall.txt",
"resources/navigation/uni_interior4_unlocked_s3_n3.txt",
"resources/navigation/uni_interior4_unlocked_s3_n2.txt",
"resources/navigation/uni_interior4_unlocked_s3_s2.txt",
"resources/navigation/uni_interior4_locked.txt"
],
"script": "resources/start_uni_interior.lua",
"teleports": "resources/config/teleports_uni_interior.json",
"triggerZones": "resources/config/trigger_zones_uni_interior.json",
"lights": "resources/config/lights_uni_interior.json",
"playerPosition": [0.942694, 0, -9.63104]
}
+166
View File
@@ -0,0 +1,166 @@
{
"npcs": [
{
"id": "ghost_01x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -12.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_02x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -12.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_03x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -12.5,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_04x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -12.5,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_05x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -13.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_06x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -13.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
}
]
}
+1 -1
View File
@@ -145,7 +145,7 @@
"canAttack": true
},
{
"id": "npc_02_woman",
"id": "npc_altynai",
"name": "Алтынай",
"animationIdlePath": "resources/w/girl/girl_idle001_small.anim",
"animationWalkPath": "resources/w/girl/girl_walk010_small.anim",
+4
View File
@@ -0,0 +1,4 @@
{
"quests": [
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,574 @@
{
"localState": {
"locations": {
"uni_exterior": {
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002
}
}
},
"networkState": {
"campaignId": "battle_zone",
"chatHistory": [
[],
[],
[]
],
"chatPreviewMsg": [
"",
"",
""
],
"chatUnread": [
true,
true,
true
],
"currentLocationId": "",
"globalFloats": {
},
"globalInts": {},
"inventory": {
"items": []
},
"isDarklands": true,
"isDawn": false,
"isNight": true,
"locations": {
"uni_exterior": {
"dialogue": {
"active": false
},
"interactiveObjects": {
"intBin_Cans": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.6000000238418579,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Plastic": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.399999976158142,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_A": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 2.200000047683716,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_B": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 3.0,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intRoof": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 3.200000047683716,
"positionZ": 0.0,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.00": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": -1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.01": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.02": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
}
},
"npcs": {
"ghost_01x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-12.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
3,
10,
5,
6,
8,
7,
9,
4,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-12.0
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-12.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-12.0
]
},
"ghost_02x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-12.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
11,
3,
10,
2,
9,
5,
4,
7,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-12.0
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-12.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-12.0
]
},
"ghost_03x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-12.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
4,
8,
3,
6,
10,
9,
7,
5,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-12.5
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-12.5
]
},
"ghost_04x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-12.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
3,
4,
7,
10,
5,
11,
9,
2,
8,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-12.5
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-12.5
]
},
"ghost_05x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-13.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
7,
2,
9,
4,
10,
6,
5,
3,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-13.0
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-13.0
]
},
"ghost_06x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-13.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
7,
11,
5,
4,
3,
10,
9,
8,
6,
2
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-13.0
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-13.0
]
}
},
"scriptGlobals": {},
"state": {
"activeNavigationIndex": 0,
"targetInteractNpcId": "",
"targetInteractiveObjectId": "",
"targetTeleportZoneId": "",
"taxi": {
"arrivedCallbackCalled": false,
"car": {
"currentWaypoint": 0,
"mode": 1,
"position": [
-100.0,
-100.0,
-100.0
],
"rotation": 0.0,
"steeringAngle": 0.0,
"velocity": 0.0,
"waypoints": [],
"wheelAngle": 0.0
}
}
},
"teleportZones": {},
"triggerZones": {}
}
},
"money": 5500,
"playerHp": 200.0,
"playerMaxHp": 200.0,
"players": {
"player2": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [[
-6.4,
0.0,
13.0
]],
"position": [
-6.4,
0.0,
13.0
],
"requestedWalkTarget": [
-6.4,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.4,
0.0,
13.0
]
},
"player": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-5.8,
0.0,
12.8
]
],
"position": [
-5.8,
0.0,
12.8
],
"requestedWalkTarget": [
-5.8,
0.0,
12.8
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-5.8,
0.0,
12.8
]
}
},
"questJournal": {
"quests": []
},
"savedAt": "2026-11-05 17:43",
"schemaVersion": 5,
"taxiIsCalled": false,
"tutorialJournalScreenOpened": false,
"tutorialNeedOpenTaxiScreen": false,
"tutorialPhoneChatScreenOpened": false,
"tutorialStep": 0,
"tutorialTaxiHintDone": false
},
"persistenceVersion": 1
}
@@ -0,0 +1,574 @@
{
"localState": {
"locations": {
"uni_exterior": {
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002
}
}
},
"networkState": {
"campaignId": "battle_zone",
"chatHistory": [
[],
[],
[]
],
"chatPreviewMsg": [
"",
"",
""
],
"chatUnread": [
true,
true,
true
],
"currentLocationId": "",
"globalFloats": {
},
"globalInts": {},
"inventory": {
"items": []
},
"isDarklands": true,
"isDawn": false,
"isNight": true,
"locations": {
"uni_exterior": {
"dialogue": {
"active": false
},
"interactiveObjects": {
"intBin_Cans": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.6000000238418579,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Plastic": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.399999976158142,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_A": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 2.200000047683716,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_B": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 3.0,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intRoof": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 3.200000047683716,
"positionZ": 0.0,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.00": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": -1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.01": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.02": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
}
},
"npcs": {
"ghost_01x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-12.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
3,
10,
5,
6,
8,
7,
9,
4,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-12.0
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-12.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-12.0
]
},
"ghost_02x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-12.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
11,
3,
10,
2,
9,
5,
4,
7,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-12.0
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-12.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-12.0
]
},
"ghost_03x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-12.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
4,
8,
3,
6,
10,
9,
7,
5,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-12.5
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-12.5
]
},
"ghost_04x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-12.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
3,
4,
7,
10,
5,
11,
9,
2,
8,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-12.5
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-12.5
]
},
"ghost_05x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.599999904632568,
0.0,
-13.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
7,
2,
9,
4,
10,
6,
5,
3,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-5.599999904632568,
0.0,
-13.0
],
"requestedWalkTarget": [
-5.599999904632568,
0.0,
-13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.599999904632568,
0.0,
-13.0
]
},
"ghost_06x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.400000095367432,
0.0,
-13.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
7,
11,
5,
4,
3,
10,
9,
8,
6,
2
],
"navigationRetryTicks": 0,
"pathWaypoints": [],
"position": [
-6.400000095367432,
0.0,
-13.0
],
"requestedWalkTarget": [
-6.400000095367432,
0.0,
-13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.400000095367432,
0.0,
-13.0
]
}
},
"scriptGlobals": {},
"state": {
"activeNavigationIndex": 0,
"targetInteractNpcId": "",
"targetInteractiveObjectId": "",
"targetTeleportZoneId": "",
"taxi": {
"arrivedCallbackCalled": false,
"car": {
"currentWaypoint": 0,
"mode": 1,
"position": [
-100.0,
-100.0,
-100.0
],
"rotation": 0.0,
"steeringAngle": 0.0,
"velocity": 0.0,
"waypoints": [],
"wheelAngle": 0.0
}
}
},
"teleportZones": {},
"triggerZones": {}
}
},
"money": 5500,
"playerHp": 200.0,
"playerMaxHp": 200.0,
"players": {
"player2": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [[
-6.8,
0.0,
13.0
]],
"position": [
-6.8,
0.0,
13.0
],
"requestedWalkTarget": [
-6.8,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
13.0
]
},
"player": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-5.4,
0.0,
12.8
]
],
"position": [
-5.4,
0.0,
12.8
],
"requestedWalkTarget": [
-5.4,
0.0,
12.8
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-5.4,
0.0,
12.8
]
}
},
"questJournal": {
"quests": []
},
"savedAt": "2026-11-05 17:43",
"schemaVersion": 5,
"taxiIsCalled": false,
"tutorialJournalScreenOpened": false,
"tutorialNeedOpenTaxiScreen": false,
"tutorialPhoneChatScreenOpened": false,
"tutorialStep": 0,
"tutorialTaxiHintDone": false
},
"persistenceVersion": 1
}
@@ -0,0 +1,4 @@
{
"dialogues": [
]
}
+6 -6
View File
@@ -2276,14 +2276,14 @@
"en": "And maybe she will even give me some difficult assignment."
},
{
"key": "Я даже уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"ru": "Я даже уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"en": "I'm even sure she's looking for me. She wants to give me some kind of assignment."
"key": "Я уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"ru": "Я уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"en": "I'm sure she's looking for me. She wants to give me some kind of assignment."
},
{
"key": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу. Ошо!",
"ru": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу. Ошо!",
"en": "Until you bring me a knife from the teachers' room, I won't let you go anywhere."
"en": "Until you bring me a knife from the teachers' room, I won't let you go anywhere. Osho!"
},
{
"key": "Я слышала она не смогла сдать курсовую по манасоведению.",
@@ -2293,7 +2293,7 @@
{
"key": "Она прыгнула с окна и разбилась насмерть деп.",
"ru": "Она прыгнула с окна и разбилась насмерть деп.",
"en": "She jumped from the window and fell to her death, they said."
"en": "She jumped from the window and fell to her death, dep."
},
{
"key": "Да она надоела, все время скафнит.",
@@ -2308,7 +2308,7 @@
{
"key": "Так что жду тебя в универе! Не вздумай прогулять! Ошо!",
"ru": "Так что жду тебя в универе! Не вздумай прогулять! Ошо!",
"en": "So I'm waiting for you at the university! Don't even think about skipping!"
"en": "So I'm waiting for you at the university! Don't even think about skipping! Osho!"
}
]
}
@@ -412,6 +412,70 @@
}
]
},
{
"id": "dialog_chat_aiperi003",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Айпери ты куда убежала?",
"next": "line_2",
"chatBubble": "out"
},
{
"id": "line_2",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Ты достал уже, я не могу тебя ждать вечно! Возишься как сонная муха.",
"next": "line_3",
"chatBubble": "in"
},
{
"id": "line_3",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "У меня нет времени ждать тебя, мне уже пора на курсы ехать.",
"next": "line_4",
"chatBubble": "in"
},
{
"id": "line_4",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Так когда я тебе нож верну?",
"next": "line_5",
"chatBubble": "out"
},
{
"id": "line_5",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Приходи завтра в универ! Я каждый день в унике с раннего утра.",
"next": "line_6",
"chatBubble": "in"
},
{
"id": "line_6",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Хорошо, встретимся завтра.",
"next": "end_1",
"chatBubble": "out"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_car001",
"start": "line_1",
@@ -293,14 +293,6 @@
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Почему?",
"next": "line_30"
},
{
"id": "line_30",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Она опять будет скафнить.",
"next": "line_25"
},
{
@@ -308,7 +300,7 @@
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_aiperi.png",
"text": "Я даже уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"text": "Я уверена, она меня ищет. Она хочет мне какое-то задание дать.",
"next": "line_26"
},
{
Loaded 100 of 585 files, more files were not shown because too many files have changed in this diff. Show more