Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fb81ad45 | ||
|
|
423d8b8f52 | ||
|
|
3fe21230cc | ||
|
|
8beaa76720 | ||
|
|
2a7498695a | ||
|
|
c6d98bd72a | ||
|
|
6a5adb1c79 | ||
|
|
40527f10ac | ||
|
|
be8ee31f03 | ||
|
|
b932a07cba | ||
|
|
f4bd3b6bed | ||
|
|
2e6b93f3ac | ||
|
|
3f68a8ba07 | ||
|
|
1ab9f300b6 | ||
|
|
28846b9f25 | ||
|
|
23fef6f349 | ||
|
|
28c6e83eda | ||
|
|
075750442f | ||
|
|
931236bec0 | ||
|
|
297bd7377f | ||
|
|
5e9a663812 | ||
|
|
66e93d46f4 |
No files matched your search
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -0,0 +1,213 @@
|
||||
# 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 a `ws://` or `wss://` endpoint (`WebsocketEndpoint::parse`).
|
||||
- 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 for Windows and the Emscripten web client:
|
||||
|
||||
- `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.
|
||||
- `proj-web/CMakeLists.txt` defines `NETWORK`, compiles the shared protocol,
|
||||
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
||||
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
||||
does not use Boost.Asio or Boost.Beast there.
|
||||
- `wss://` (TLS) is supported by the Windows client only. `proj-windows` also defines
|
||||
`NETWORK_TLS` and links static OpenSSL 3.4.1, which CMake downloads and builds
|
||||
automatically at configure time (`cmake/ThirdParty.cmake`, section 12) using a
|
||||
downloaded portable Strawberry Perl and the nmake from the installed Visual Studio
|
||||
(located through `vswhere`). Builds go to `thirdparty/openssl-3.4.1/install-Debug`
|
||||
and `install-Release`; later configures skip them. `NETWORK_TLS` unset (server tests)
|
||||
means `wss://` endpoints fail with a logged error.
|
||||
- Linux and Android builds do not define `NETWORK` and do not compile the
|
||||
network transport.
|
||||
|
||||
## 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 (`kMultiplayerServerUrl` in
|
||||
`Game.cpp`, currently `wss://api.shadowoverbishkek.com/server001/`).
|
||||
- TLS certificate or hostname problems are logged but deliberately do not abort the
|
||||
connection. `GameServer` itself has no TLS; it must sit behind a TLS-terminating proxy.
|
||||
- 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.
|
||||
@@ -56,3 +56,30 @@ check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip"
|
||||
|
||||
# 11) SDL2_mixer
|
||||
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
|
||||
|
||||
# 12) OpenSSL (TLS for wss://) and a native Windows Perl needed to build it
|
||||
if(WIN32)
|
||||
check_and_download("https://github.com/openssl/openssl/releases/download/openssl-3.4.1/openssl-3.4.1.tar.gz" "openssl-3.4.1.tar.gz" "openssl-3.4.1" "Configure")
|
||||
|
||||
# Strawberry Perl portable. The zip has no top-level folder, so it is
|
||||
# extracted into its own directory. (Git's MSYS perl cannot build OpenSSL.)
|
||||
set(STRAWBERRY_PERL_DIR "${THIRDPARTY_DIR}/strawberry-perl")
|
||||
set(STRAWBERRY_PERL_ARCHIVE "${THIRDPARTY_DIR}/strawberry-perl-5.42.3.1-64bit-portable.zip")
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_ARCHIVE}")
|
||||
message(STATUS "Downloading Strawberry Perl...")
|
||||
file(DOWNLOAD
|
||||
"https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54231_64bit/strawberry-perl-5.42.3.1-64bit-portable.zip"
|
||||
"${STRAWBERRY_PERL_ARCHIVE}" SHOW_PROGRESS)
|
||||
endif()
|
||||
message(STATUS "Extracting Strawberry Perl...")
|
||||
file(MAKE_DIRECTORY "${STRAWBERRY_PERL_DIR}")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${STRAWBERRY_PERL_ARCHIVE}"
|
||||
WORKING_DIRECTORY "${STRAWBERRY_PERL_DIR}"
|
||||
)
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
||||
message(FATAL_ERROR "Strawberry Perl extraction failed")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
+103
-1
@@ -725,4 +725,106 @@ set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
||||
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"SDL2_external_lib"
|
||||
)
|
||||
)
|
||||
# ===========================================
|
||||
# 12) OpenSSL (3.4.1) - TLS for wss:// (Windows only, static libs)
|
||||
# ===========================================
|
||||
if(WIN32)
|
||||
set(OPENSSL_SRC_DIR "${THIRDPARTY_DIR}/openssl-3.4.1")
|
||||
set(OPENSSL_BASE_DIR "${OPENSSL_SRC_DIR}/install")
|
||||
|
||||
set(_have_openssl TRUE)
|
||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
||||
if(NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libssl.lib" OR
|
||||
NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libcrypto.lib")
|
||||
set(_have_openssl FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT _have_openssl)
|
||||
# Locate Visual Studio's vcvarsall.bat (provides cl, link, nmake).
|
||||
set(_vswhere "$ENV{ProgramFiles\(x86\)}/Microsoft Visual Studio/Installer/vswhere.exe")
|
||||
set(_vcvarsall "")
|
||||
if(EXISTS "${_vswhere}")
|
||||
execute_process(
|
||||
COMMAND "${_vswhere}" -latest -products * -property installationPath
|
||||
OUTPUT_VARIABLE _vs_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if(_vs_path AND EXISTS "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
||||
set(_vcvarsall "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
||||
endif()
|
||||
endif()
|
||||
if(_vcvarsall STREQUAL "")
|
||||
message(FATAL_ERROR "Visual Studio (vcvarsall.bat) not found; it is required to build OpenSSL")
|
||||
endif()
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_ossl_vc_arch x64)
|
||||
set(_ossl_target VC-WIN64A)
|
||||
else()
|
||||
set(_ossl_vc_arch x86)
|
||||
set(_ossl_target VC-WIN32)
|
||||
endif()
|
||||
|
||||
file(TO_NATIVE_PATH "${_vcvarsall}" _vcvarsall_native)
|
||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/bin" _perl_bin_native)
|
||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/site/bin" _perl_site_native)
|
||||
|
||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
||||
set(_ossl_build "${OPENSSL_SRC_DIR}/build-${cfg}")
|
||||
file(REMOVE_RECURSE "${_ossl_build}")
|
||||
file(MAKE_DIRECTORY "${_ossl_build}")
|
||||
file(TO_NATIVE_PATH "${_ossl_build}" _ossl_build_native)
|
||||
file(TO_NATIVE_PATH "${OPENSSL_SRC_DIR}/Configure" _ossl_configure_native)
|
||||
file(TO_NATIVE_PATH "${OPENSSL_BASE_DIR}-${cfg}" _ossl_prefix_native)
|
||||
|
||||
set(_ossl_cfg_flags "")
|
||||
if(cfg STREQUAL "Debug")
|
||||
# /MDd: link the dynamic debug CRT like the game (static libs default to /MTd)
|
||||
set(_ossl_cfg_flags "--debug /MDd")
|
||||
else()
|
||||
set(_ossl_cfg_flags "--release /MD")
|
||||
endif()
|
||||
|
||||
# A batch file avoids cmd.exe quoting problems.
|
||||
set(_ossl_bat "${_ossl_build}/build_openssl.bat")
|
||||
file(WRITE "${_ossl_bat}"
|
||||
"@echo off
|
||||
call \"${_vcvarsall_native}\" ${_ossl_vc_arch} || exit /b 1
|
||||
set \"PATH=${_perl_bin_native};${_perl_site_native};%PATH%\"
|
||||
set CL=/MP
|
||||
cd /d \"${_ossl_build_native}\" || exit /b 1
|
||||
perl \"${_ossl_configure_native}\" ${_ossl_target} no-shared no-asm no-tests no-docs ${_ossl_cfg_flags} --prefix=\"${_ossl_prefix_native}\" --openssldir=\"${_ossl_prefix_native}/ssl\" || exit /b 1
|
||||
nmake || exit /b 1
|
||||
nmake install_sw || exit /b 1
|
||||
")
|
||||
log("Building OpenSSL (${cfg}) - this takes several minutes ...")
|
||||
execute_process(
|
||||
COMMAND cmd /c "${_ossl_bat}"
|
||||
RESULT_VARIABLE _ossl_res
|
||||
)
|
||||
if(NOT _ossl_res EQUAL 0)
|
||||
message(FATAL_ERROR "OpenSSL build failed for ${cfg} (see output above)")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_library(openssl_crypto_external_lib UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(openssl_crypto_external_lib PROPERTIES
|
||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libcrypto.lib"
|
||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libcrypto.lib"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
||||
INTERFACE_LINK_LIBRARIES "ws2_32;crypt32;advapi32;user32;gdi32"
|
||||
)
|
||||
|
||||
add_library(openssl_ssl_external_lib UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(openssl_ssl_external_lib PROPERTIES
|
||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libssl.lib"
|
||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libssl.lib"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
||||
INTERFACE_LINK_LIBRARIES "openssl_crypto_external_lib"
|
||||
)
|
||||
endif()
|
||||
@@ -1,14 +1,15 @@
|
||||
--- a/include/sol/optional_implementation.hpp
|
||||
+++ b/include/sol/optional_implementation.hpp
|
||||
@@ -2189,7 +2189,10 @@
|
||||
template <class... Args>
|
||||
T& emplace(Args&&... args) noexcept {
|
||||
@@ -2191,8 +2191,10 @@ namespace sol {
|
||||
static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args");
|
||||
|
||||
|
||||
*this = nullopt;
|
||||
- this->construct(std::forward<Args>(args)...);
|
||||
- return value();
|
||||
+ // Reference specialization stores a pointer; set it directly.
|
||||
+ // construct() only exists in the non-reference specialization.
|
||||
+ m_value = std::addressof(std::forward<Args>(args)...);
|
||||
+ return *m_value;
|
||||
}
|
||||
|
||||
/// Swaps this optional with the other.
|
||||
@@ -96,6 +96,8 @@ add_library(main SHARED
|
||||
../../../../src/Location.cpp
|
||||
../../../../src/LocationState.h
|
||||
../../../../src/LocationState.cpp
|
||||
../../../../src/LocationGameState.h
|
||||
../../../../src/LocationGameState.cpp
|
||||
../../../../src/LocationEditor.h
|
||||
../../../../src/LocationEditor.cpp
|
||||
../../../../src/NpcCar.h
|
||||
@@ -104,6 +106,11 @@ add_library(main SHARED
|
||||
../../../../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
|
||||
@@ -112,11 +119,13 @@ add_library(main SHARED
|
||||
../../../../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
|
||||
|
||||
@@ -121,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
|
||||
@@ -129,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
|
||||
@@ -137,12 +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
|
||||
|
||||
+34
-1
@@ -115,6 +115,8 @@ 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
|
||||
@@ -123,6 +125,23 @@ set(SOURCES
|
||||
../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/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
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
@@ -131,11 +150,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
|
||||
@@ -163,6 +184,8 @@ set(SOURCES
|
||||
|
||||
add_executable(ShadowOverBishkekDemo ${SOURCES})
|
||||
|
||||
target_compile_definitions(ShadowOverBishkekDemo PRIVATE NETWORK)
|
||||
|
||||
# Настройка путей к инклудам (используем скачанные исходники)
|
||||
target_include_directories(ShadowOverBishkekDemo PRIVATE
|
||||
../src
|
||||
@@ -172,6 +195,16 @@ target_include_directories(ShadowOverBishkekDemo PRIVATE
|
||||
)
|
||||
|
||||
# Сборка libzip через add_subdirectory (Emscripten соберет её из исходников)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_DOC OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_OSSFUZZ OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_REGRESS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_BZIP2 OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_LZMA OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_MBEDTLS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_ZSTD OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_GNUTLS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_OPENSSL OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_WINDOWS_CRYPTO OFF CACHE BOOL "" FORCE)
|
||||
@@ -193,7 +226,6 @@ set(EMSCRIPTEN_FLAGS
|
||||
#"-pthread"
|
||||
#"-sUSE_PTHREADS=1"
|
||||
"-fexceptions"
|
||||
"-DNETWORK"
|
||||
)
|
||||
|
||||
target_compile_options(ShadowOverBishkekDemo PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
||||
@@ -214,6 +246,7 @@ 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}/../resources/start_dorm.lua@resources/start_battle_zone.lua"
|
||||
)
|
||||
|
||||
# Применяем настройки линковки
|
||||
|
||||
@@ -63,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
|
||||
@@ -71,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
|
||||
@@ -79,12 +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
|
||||
@@ -125,7 +140,10 @@ set_target_properties(ShadowOverBishkekDemo PROPERTIES
|
||||
# Определения препроцессора
|
||||
target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
_WIN32_WINNT=0x0A00
|
||||
PNG_ENABLED
|
||||
NETWORK
|
||||
NETWORK_TLS
|
||||
)
|
||||
|
||||
# Линкуем с SDL2main, если он вообще установлен
|
||||
@@ -142,12 +160,22 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
||||
eigen_external_lib
|
||||
boost_external_lib
|
||||
sol2_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
openssl_ssl_external_lib
|
||||
)
|
||||
|
||||
# Линкуем 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()
|
||||
|
||||
# ===========================================
|
||||
|
||||
@@ -17,7 +17,20 @@
|
||||
},
|
||||
{
|
||||
"id": "battle_zone",
|
||||
"title": { "ru": "Зона боя", "en": "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"
|
||||
],
|
||||
@@ -26,6 +39,8 @@
|
||||
"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,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,4 +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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+319
-1201
File diff suppressed because it is too large.
Load diff
@@ -1,88 +1,487 @@
|
||||
{
|
||||
"campaignId": "battle_zone",
|
||||
"chatHistory": [
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"chatPreviewMsg": [
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"chatUnread": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"currentLocationName": "uni_exterior",
|
||||
"globalFloats": {
|
||||
"player_hp": 200.0
|
||||
},
|
||||
"globalInts": {},
|
||||
"inventory": {
|
||||
"items": []
|
||||
},
|
||||
"isDarklands": false,
|
||||
"isDawn": false,
|
||||
"isNight": false,
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"dialogueState": {
|
||||
"active": false
|
||||
},
|
||||
"interactiveObjectStates": [],
|
||||
"locationState": {
|
||||
"activeNavigationIndex": 0,
|
||||
"localState": {
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002,
|
||||
"isDarklands": false,
|
||||
"isDawn": false,
|
||||
"isNight": false,
|
||||
"targetInteractNpcIndex": -1,
|
||||
"targetInteractiveObjectIndex": -1,
|
||||
"targetTeleportZoneIndex": -1,
|
||||
"taxi": {
|
||||
"taxiArrivedCallbackCalled": false,
|
||||
"taxiCar": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
"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
|
||||
},
|
||||
"taxiCarInitialState": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
"intBin_Plastic": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.399999976158142,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"taxiCarTriggerPositionX": 0.0,
|
||||
"taxiCarTriggerPositionY": 0.0,
|
||||
"taxiCarTriggerPositionZ": 0.0,
|
||||
"taxiDefaultWaypoints": []
|
||||
}
|
||||
},
|
||||
"npcStates": [],
|
||||
"playerState": {
|
||||
"attackTargetIndex": -1,
|
||||
"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": true,
|
||||
"faceTargetIndex": -1,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
@@ -91,35 +490,85 @@
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [[
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
]],
|
||||
"position": [
|
||||
-6, 0, 12.8
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6, 0, 12.8
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6, 0, 12.8
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
},
|
||||
"scriptGlobals": null,
|
||||
"teleportZoneState": {},
|
||||
"triggerZoneState": {}
|
||||
}
|
||||
"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
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
},
|
||||
"savedAt": "2026-07-24 23:40",
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
"tutorialNeedOpenTaxiScreen": false,
|
||||
"tutorialPhoneChatScreenOpened": false,
|
||||
"tutorialStep": 9
|
||||
"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,456 @@
|
||||
{
|
||||
"agentRadius": 0.30000001192092896,
|
||||
"areas": [
|
||||
{
|
||||
"available": true,
|
||||
"name": "main_corridor",
|
||||
"polygon": [
|
||||
[
|
||||
-60.0,
|
||||
110.0
|
||||
],
|
||||
[
|
||||
60.0,
|
||||
110.0
|
||||
],
|
||||
[
|
||||
60.0,
|
||||
-110.0
|
||||
],
|
||||
[
|
||||
-60.0,
|
||||
-110.0
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"boundaryPadding": 0.07500000298023224,
|
||||
"cellSize": 0.10000000149011612,
|
||||
"floorY": 0.0,
|
||||
"objectPadding": 0.07500000298023224,
|
||||
"obstacles": [
|
||||
{
|
||||
"name": "editor_obstacle_1",
|
||||
"polygon": [
|
||||
[
|
||||
-4.674264907836914,
|
||||
4.511208534240723
|
||||
],
|
||||
[
|
||||
4.500116348266602,
|
||||
4.49705696105957
|
||||
],
|
||||
[
|
||||
6.988797664642334,
|
||||
4.994457244873047
|
||||
],
|
||||
[
|
||||
8.309992790222168,
|
||||
28.58877182006836
|
||||
],
|
||||
[
|
||||
-4.647348403930664,
|
||||
30.00424575805664
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_2",
|
||||
"polygon": [
|
||||
[
|
||||
-15.515826225280762,
|
||||
30.504249572753906
|
||||
],
|
||||
[
|
||||
-7.626216888427734,
|
||||
30.143882751464844
|
||||
],
|
||||
[
|
||||
-7.626260757446289,
|
||||
-30.035873413085938
|
||||
],
|
||||
[
|
||||
-10.609762191772461,
|
||||
-30.106216430664063
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_3",
|
||||
"polygon": [
|
||||
[
|
||||
-4.697506904602051,
|
||||
-4.453675270080566
|
||||
],
|
||||
[
|
||||
4.581188201904297,
|
||||
-4.497550964355469
|
||||
],
|
||||
[
|
||||
8.761575698852539,
|
||||
-5.748187065124512
|
||||
],
|
||||
[
|
||||
6.705105781555176,
|
||||
-30.826133728027344
|
||||
],
|
||||
[
|
||||
-4.899751663208008,
|
||||
-30.273645401000977
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_4",
|
||||
"polygon": [
|
||||
[
|
||||
4.480845928192139,
|
||||
-4.497610569000244
|
||||
],
|
||||
[
|
||||
4.471078395843506,
|
||||
4.440389633178711
|
||||
],
|
||||
[
|
||||
7.488897800445557,
|
||||
6.500348091125488
|
||||
],
|
||||
[
|
||||
7.610373020172119,
|
||||
-7.108349800109863
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_5",
|
||||
"polygon": [
|
||||
[
|
||||
1.4347565174102783,
|
||||
-3.5085272789001465
|
||||
],
|
||||
[
|
||||
-1.44767427444458,
|
||||
-3.5077664852142334
|
||||
],
|
||||
[
|
||||
-1.4244537353515625,
|
||||
-4.550441741943359
|
||||
],
|
||||
[
|
||||
1.5041074752807617,
|
||||
-4.66478157043457
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_6",
|
||||
"polygon": [
|
||||
[
|
||||
-1.4455971717834473,
|
||||
1.4518743753433228
|
||||
],
|
||||
[
|
||||
-1.7430613040924072,
|
||||
1.4484080076217651
|
||||
],
|
||||
[
|
||||
-1.7421774864196777,
|
||||
1.7649677991867065
|
||||
],
|
||||
[
|
||||
-1.462749719619751,
|
||||
1.7526912689208984
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_7",
|
||||
"polygon": [
|
||||
[
|
||||
1.4451713562011719,
|
||||
1.41898775100708
|
||||
],
|
||||
[
|
||||
1.4399998188018799,
|
||||
1.7420461177825928
|
||||
],
|
||||
[
|
||||
1.7347050905227661,
|
||||
1.736159086227417
|
||||
],
|
||||
[
|
||||
1.7765860557556152,
|
||||
1.4179844856262207
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_8",
|
||||
"polygon": [
|
||||
[
|
||||
1.7767610549926758,
|
||||
-1.4403706789016724
|
||||
],
|
||||
[
|
||||
1.7636981010437012,
|
||||
-1.730869174003601
|
||||
],
|
||||
[
|
||||
1.4837110042572021,
|
||||
-1.7566866874694824
|
||||
],
|
||||
[
|
||||
1.478219747543335,
|
||||
-1.4404852390289307
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_9",
|
||||
"polygon": [
|
||||
[
|
||||
-1.4296274185180664,
|
||||
-1.4213972091674805
|
||||
],
|
||||
[
|
||||
-1.4656789302825928,
|
||||
-1.7051677703857422
|
||||
],
|
||||
[
|
||||
-1.73427152633667,
|
||||
-1.7311439514160156
|
||||
],
|
||||
[
|
||||
-1.7377328872680664,
|
||||
-1.4628331661224365
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_10",
|
||||
"polygon": [
|
||||
[
|
||||
0.35316944122314453,
|
||||
3.638571262359619
|
||||
],
|
||||
[
|
||||
0.5663708448410034,
|
||||
3.529160976409912
|
||||
],
|
||||
[
|
||||
0.9152454137802124,
|
||||
3.7827951908111572
|
||||
],
|
||||
[
|
||||
0.7800401449203491,
|
||||
4.1122283935546875
|
||||
],
|
||||
[
|
||||
0.3589729368686676,
|
||||
4.105776786804199
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_11",
|
||||
"polygon": [
|
||||
[
|
||||
1.432550072669983,
|
||||
3.4731945991516113
|
||||
],
|
||||
[
|
||||
1.0865048170089722,
|
||||
3.7390546798706055
|
||||
],
|
||||
[
|
||||
1.2317941188812256,
|
||||
4.127427577972412
|
||||
],
|
||||
[
|
||||
1.5596503019332886,
|
||||
4.186410903930664
|
||||
],
|
||||
[
|
||||
1.7367746829986572,
|
||||
3.893272638320923
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_12",
|
||||
"polygon": [
|
||||
[
|
||||
2.2451412677764893,
|
||||
3.536006450653076
|
||||
],
|
||||
[
|
||||
2.516268253326416,
|
||||
3.788959503173828
|
||||
],
|
||||
[
|
||||
2.364431142807007,
|
||||
4.175443649291992
|
||||
],
|
||||
[
|
||||
2.0266497135162354,
|
||||
4.191457271575928
|
||||
],
|
||||
[
|
||||
1.9045233726501465,
|
||||
3.723470687866211
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_13",
|
||||
"polygon": [
|
||||
[
|
||||
2.671405792236328,
|
||||
3.8145058155059814
|
||||
],
|
||||
[
|
||||
2.8594112396240234,
|
||||
3.5803170204162598
|
||||
],
|
||||
[
|
||||
3.2140555381774902,
|
||||
3.6300761699676514
|
||||
],
|
||||
[
|
||||
3.3128409385681152,
|
||||
3.914318799972534
|
||||
],
|
||||
[
|
||||
3.035449981689453,
|
||||
4.19616174697876
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_14",
|
||||
"polygon": [
|
||||
[
|
||||
-0.7818725109100342,
|
||||
-1.3299269676208496
|
||||
],
|
||||
[
|
||||
0.7862813472747803,
|
||||
-1.321362018585205
|
||||
],
|
||||
[
|
||||
0.7948278188705444,
|
||||
-0.8404130935668945
|
||||
],
|
||||
[
|
||||
-0.8415207862854004,
|
||||
-0.8753911852836609
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_15",
|
||||
"polygon": [
|
||||
[
|
||||
0.8721977472305298,
|
||||
-0.7764003276824951
|
||||
],
|
||||
[
|
||||
1.302225947380066,
|
||||
-0.7648682594299316
|
||||
],
|
||||
[
|
||||
1.3214731216430664,
|
||||
0.7846521139144897
|
||||
],
|
||||
[
|
||||
0.8680744767189026,
|
||||
0.7555575370788574
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_16",
|
||||
"polygon": [
|
||||
[
|
||||
0.7762007713317871,
|
||||
0.8522758483886719
|
||||
],
|
||||
[
|
||||
0.7863373756408691,
|
||||
1.2722206115722656
|
||||
],
|
||||
[
|
||||
-0.8070647716522217,
|
||||
1.327103614807129
|
||||
],
|
||||
[
|
||||
-0.806971549987793,
|
||||
0.8900301456451416
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_17",
|
||||
"polygon": [
|
||||
[
|
||||
-0.872557520866394,
|
||||
-0.8267583847045898
|
||||
],
|
||||
[
|
||||
-0.8642703890800476,
|
||||
0.7986812591552734
|
||||
],
|
||||
[
|
||||
-1.3173141479492188,
|
||||
0.785850465297699
|
||||
],
|
||||
[
|
||||
-1.300905704498291,
|
||||
-0.7977303266525269
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_18",
|
||||
"polygon": [
|
||||
[
|
||||
-4.552978515625,
|
||||
29.516841888427734
|
||||
],
|
||||
[
|
||||
-7.71390438079834,
|
||||
29.30852699279785
|
||||
],
|
||||
[
|
||||
-7.651302814483643,
|
||||
29.88724136352539
|
||||
],
|
||||
[
|
||||
-4.585191249847412,
|
||||
29.89825439453125
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor_obstacle_19",
|
||||
"polygon": [
|
||||
[
|
||||
-4.6034159660339355,
|
||||
-29.88680648803711
|
||||
],
|
||||
[
|
||||
-4.6567511558532715,
|
||||
-29.23015785217285
|
||||
],
|
||||
[
|
||||
-7.734859943389893,
|
||||
-29.268037796020508
|
||||
],
|
||||
[
|
||||
-7.670672416687012,
|
||||
-29.951431274414063
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,9 @@
|
||||
-- uni_exterior level with static objects and navigation only.
|
||||
-- ============================================
|
||||
|
||||
function callback_init_location()
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
end
|
||||
|
||||
@@ -10,6 +13,7 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
+27
-32
@@ -2,18 +2,8 @@
|
||||
-- NPC PATROL WAYPOINTS
|
||||
-- ============================================
|
||||
|
||||
bathroom_door_opened = false
|
||||
player_door_opened = false
|
||||
alik_door_opened = false
|
||||
|
||||
|
||||
game_api.start_dialogue("dialog_start001")
|
||||
|
||||
phone_picked_up = false
|
||||
journal_picked_up = false
|
||||
|
||||
function on_npc_interact(npc_index)
|
||||
if npc_index == 0 then
|
||||
function on_npc_interact(npc_id)
|
||||
if npc_id == "npc_alik" then
|
||||
--[[
|
||||
local player_alik_aware = game_api.getIntValue("alik_aware")
|
||||
local player_container_aware = game_api.getIntValue("player_container_aware")
|
||||
@@ -25,7 +15,7 @@ function on_npc_interact(npc_index)
|
||||
else
|
||||
game_api.start_dialogue("dialog_alik001")
|
||||
end]]
|
||||
game_api.player_stop_and_rotate_to_npc(0)
|
||||
game_api.player_stop_and_rotate_to_npc("npc_alik")
|
||||
game_api.start_dialogue("dialog_alik002")
|
||||
end
|
||||
end
|
||||
@@ -73,7 +63,7 @@ function on_bed_sleep()
|
||||
else
|
||||
game_api.start_cutscene("sleep_cutscene001")
|
||||
game_api.setIntValue("need_sleep", 0)
|
||||
game_api.set_player_hp(200)
|
||||
game_api.refill_current_player_hp()
|
||||
--game_api.set_day() - done in cutscene
|
||||
local day = game_api.getIntValue("day")
|
||||
game_api.setIntValue("day", day + 1)
|
||||
@@ -119,8 +109,8 @@ end
|
||||
|
||||
function callback_deactivate_cover_alik_room()
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.npc_rotate_to(0, 90)
|
||||
game_api.set_npc_enabled("npc_alik", true)
|
||||
game_api.npc_rotate_to("npc_alik", 90)
|
||||
end
|
||||
|
||||
function callback_deactivate_cover_bathroom()
|
||||
@@ -161,7 +151,7 @@ function closeAlikDoor()
|
||||
game_api.activate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
||||
game_api.set_object_alpha("Room_Cover_LivingRoom_W_S_2_001", 1)
|
||||
game_api.set_object_rotation("Door_Room_-1_-1_1_Leaf_001", 0)
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled("npc_alik", false)
|
||||
if (bathroom_door_opened) then
|
||||
game_api.switch_navigation(3) -- closed only alik (a)
|
||||
else
|
||||
@@ -330,11 +320,25 @@ function on_toilet_click()
|
||||
game_api.player_stop()
|
||||
end
|
||||
|
||||
function callback_init_location()
|
||||
bathroom_door_opened = false
|
||||
player_door_opened = false
|
||||
alik_door_opened = false
|
||||
phone_picked_up = false
|
||||
journal_picked_up = false
|
||||
|
||||
game_api.start_dialogue("dialog_start001")
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_N_2_001")
|
||||
game_api.rotate_object("Door_Utility_-1_1_2_Leaf_001", 90, 0.01, nil)
|
||||
game_api.rotate_object("Door_Utility_-1_-1_2_Leaf_001", -90, 0.01, nil)
|
||||
game_api.set_npc_enabled("npc_alik", false)
|
||||
game_api.switch_navigation(0)
|
||||
print("----callback_init_location----")
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
print("Enter location dorm")
|
||||
local hp = game_api.getFloatValue("player_hp")
|
||||
game_api.set_player_hp(hp)
|
||||
game_api.set_teleport_active(0, false)
|
||||
game_api.set_teleport_active("tp_dorm_to_loc2", false)
|
||||
if (game_api.is_night()) then
|
||||
closeAlikDoor()
|
||||
end
|
||||
@@ -342,30 +346,21 @@ end
|
||||
|
||||
function callback_exit_location()
|
||||
print("Exit location dorm")
|
||||
local hp = game_api.get_player_hp()
|
||||
game_api.setFloatValue("player_hp", hp)
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
|
||||
function callback_call_taxi()
|
||||
game_api.set_teleport_active(0, true)
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_teleport_active("tp_dorm_to_loc2", true)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_N_2_001")
|
||||
|
||||
game_api.rotate_object("Door_Utility_-1_1_2_Leaf_001", 90, 0.01, nil)
|
||||
game_api.rotate_object("Door_Utility_-1_-1_2_Leaf_001", -90, 0.01, nil)
|
||||
|
||||
game_api.set_call_taxi_callback(
|
||||
callback_call_taxi
|
||||
)
|
||||
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.switch_navigation(0)
|
||||
print("Lua script loaded successfully--!")
|
||||
@@ -1,16 +1,5 @@
|
||||
print("Lua script loaded successfully!")
|
||||
|
||||
function callback_enter_location()
|
||||
end
|
||||
|
||||
function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
|
||||
function darklands_exit001_enter_callback()
|
||||
if game_api.is_darklands() then
|
||||
print("Lua script start_darklands_transition")
|
||||
@@ -77,11 +66,12 @@ game_api.set_enter_night_callback(
|
||||
)
|
||||
|
||||
|
||||
function callback_init_location()
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
print("Enter location uni exterior")
|
||||
local hp = game_api.getFloatValue("player_hp")
|
||||
game_api.set_player_hp(hp)
|
||||
game_api.set_teleport_active(0, false)
|
||||
game_api.set_teleport_active("tp_loc2_to_dorm", false)
|
||||
if (game_api.is_night()) then
|
||||
game_api.deactivate_interactive_object("car001")
|
||||
game_api.deactivate_interactive_object("car001wheels")
|
||||
@@ -95,11 +85,10 @@ end
|
||||
|
||||
function callback_exit_location()
|
||||
print("Exit location uni exterior")
|
||||
local hp = game_api.get_player_hp()
|
||||
game_api.setFloatValue("player_hp", hp)
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
@@ -108,10 +97,6 @@ game_api.set_trigger_zone_callbacks("darklands_exit001",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
)
|
||||
game_api.set_trigger_zone_callbacks("darklands_exit002",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
)
|
||||
game_api.set_trigger_zone_callbacks("darklands_exit003",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
@@ -129,12 +114,12 @@ game_api.set_trigger_zone_callbacks("darklands_exit005",
|
||||
|
||||
function taxi_zone001_enter_callback()
|
||||
game_api.start_dialogue("dialog_taxi001")
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
function callback_call_taxi()
|
||||
game_api.set_teleport_active(0, true)
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_teleport_active("tp_loc2_to_dorm", true)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
game_api.set_call_taxi_callback(
|
||||
|
||||
+129
-145
@@ -1,14 +1,6 @@
|
||||
CONST_MSG_EMPTY = "(пусто)"
|
||||
CONST_MSG_WHERE = "Бекзат, ты где? Я жду..."
|
||||
|
||||
hall_door_opened = false
|
||||
teacher_door_opened = false
|
||||
|
||||
player_hold_book = false
|
||||
teacher_arrived_to_library = false
|
||||
teacher_told_about_book = false
|
||||
|
||||
|
||||
--[[
|
||||
0 - day0
|
||||
1 - darklands
|
||||
@@ -17,14 +9,6 @@ teacher_told_about_book = false
|
||||
4 - day
|
||||
|
||||
]]
|
||||
was_state_when_player_left = 0
|
||||
was_day_when_player_left = 0
|
||||
|
||||
morning_can_open_door_index = 0
|
||||
morning_did_open_door_index = 0
|
||||
|
||||
|
||||
ghost_gone = false
|
||||
|
||||
function dialog_aiperi_give_key()
|
||||
local player_hold_key = game_api.getIntValue("player_hold_key")
|
||||
@@ -65,7 +49,7 @@ function knife_dialog_zone001_enter_callback()
|
||||
if (player_hold_knife == 0) then
|
||||
local lection_is_over = game_api.getIntValue("lection_is_over")
|
||||
if lection_is_over == 1 then
|
||||
game_api.npc_stop_and_rotate_to_player(1)
|
||||
game_api.npc_stop_and_rotate_to_player("npc_02_woman")
|
||||
if (player_hold_key == 1) then
|
||||
game_api.start_dialogue("knife_dialog002")
|
||||
else
|
||||
@@ -79,7 +63,7 @@ function knife_dialog_zone001_enter_callback()
|
||||
if (aiperi_talked_after_knife == 0) then
|
||||
game_api.start_dialogue("knife_dialog_second001")
|
||||
end
|
||||
game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -110,11 +94,11 @@ game_api.set_trigger_zone_callbacks("knife_dialog_zone001",
|
||||
function on_knife_pickup()
|
||||
game_api.pickup_item("knife")
|
||||
game_api.deactivate_interactive_object("Knife001")
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.setIntValue("player_hold_knife", 1)
|
||||
|
||||
game_api.set_trigger_zone_enabled(2, true)
|
||||
game_api.npc_walk_to(0, -4.57412, 0, 6.78495, on_teacher_arrived2)
|
||||
game_api.set_trigger_zone_enabled("teacher_dialog_zone001", true)
|
||||
game_api.npc_walk_to("npc_teacher01", -4.57412, 0, 6.78495, on_teacher_arrived2)
|
||||
game_api.quest_set_objective_completed("aiperi_knife", "aiperi_knife_take")
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -133,14 +117,14 @@ function on_book_pickup()
|
||||
player_hold_book = true
|
||||
game_api.quest_set_objective_completed("study_project", "study_project_book")
|
||||
game_api.start_dialogue("book_dialog003")
|
||||
--game_api.set_trigger_zone_enabled(3, true)
|
||||
--game_api.set_trigger_zone_enabled("book_dialog_zone001", true)
|
||||
end
|
||||
else
|
||||
game_api.remove_item("book")
|
||||
game_api.activate_interactive_object("Book001")
|
||||
player_hold_book = false
|
||||
game_api.start_dialogue("book_dialog004")
|
||||
--game_api.set_trigger_zone_enabled(3, false)
|
||||
--game_api.set_trigger_zone_enabled("book_dialog_zone001", false)
|
||||
end
|
||||
else
|
||||
print("on_book_pickup step2")
|
||||
@@ -218,14 +202,14 @@ function on_cake_clicked()
|
||||
game_api.player_stop()
|
||||
end
|
||||
|
||||
function on_npc_interact(npc_index)
|
||||
function on_npc_interact(npc_id)
|
||||
local player_ghost_aware = game_api.getIntValue("ghost_aware")
|
||||
local player_hold_key = game_api.getIntValue("player_hold_key")
|
||||
local day = game_api.getIntValue("day")
|
||||
|
||||
print("[Lua] NPC interaction! Index: " .. tostring(npc_index))
|
||||
if npc_index == 1 then
|
||||
game_api.player_stop_and_rotate_to_npc(1)
|
||||
print("[Lua] NPC interaction! ID: " .. tostring(npc_id))
|
||||
if npc_id == "npc_02_woman" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||
if (game_api.is_dawn()) or (day > 0) then
|
||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||
else
|
||||
@@ -236,8 +220,8 @@ function on_npc_interact(npc_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
if npc_index == 0 then
|
||||
game_api.player_stop_and_rotate_to_npc(0)
|
||||
if npc_id == "npc_teacher01" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_teacher01")
|
||||
if (day == 0) then
|
||||
if (teacher_arrived_to_library) then
|
||||
if (teacher_told_about_book) then
|
||||
@@ -264,8 +248,8 @@ function on_npc_interact(npc_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
if npc_index == 2 then
|
||||
game_api.player_stop_and_rotate_to_npc(2)
|
||||
if npc_id == "npc_04_ghost" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_04_ghost")
|
||||
local report_card_signed = game_api.getIntValue("report_card_signed")
|
||||
if (report_card_signed == 1) then
|
||||
game_api.start_dialogue("dialog_with_ghost003")
|
||||
@@ -273,31 +257,30 @@ function on_npc_interact(npc_index)
|
||||
game_api.start_dialogue("dialog_with_ghost001")
|
||||
end
|
||||
end
|
||||
if npc_index == 6 then
|
||||
game_api.player_stop_and_rotate_to_npc(6)
|
||||
if npc_id == "npc_altynai" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_altynai")
|
||||
game_api.start_dialogue("dialog_female_student002")
|
||||
end
|
||||
end
|
||||
|
||||
function on_quest_over()
|
||||
ghost_gone = true
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.quest_set_objective_completed("ghost_release", "ghost_release_show")
|
||||
|
||||
game_api.start_cutscene("game_complete_cutscene001")
|
||||
end
|
||||
|
||||
function on_darklands_over()
|
||||
game_api.set_player_hp(10)
|
||||
game_api.resetPlayerAfterDeath()
|
||||
end
|
||||
|
||||
function teacher_zone001_enter_callback()
|
||||
game_api.start_dialogue("teacher_dialog002")
|
||||
game_api.set_trigger_zone_enabled(2, false)
|
||||
game_api.set_trigger_zone_enabled("teacher_dialog_zone001", false)
|
||||
teacher_told_about_book = true
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -474,9 +457,9 @@ function on_hall_door_click()
|
||||
game_api.switch_navigation(1)
|
||||
game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
|
||||
game_api.fade_object("Room_Cover_Main_Hall_001", 0, 0.5, callback_deactivate_cover_main_hall)
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.set_npc_enabled(6, true)
|
||||
game_api.npc_rotate_to(6, 180)
|
||||
game_api.set_npc_enabled("npc_teacher01", true)
|
||||
game_api.set_npc_enabled("npc_altynai", true)
|
||||
game_api.npc_rotate_to("npc_altynai", 180)
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -558,22 +541,22 @@ function on_n2_door_click()
|
||||
end
|
||||
|
||||
function on_teacher_arrived()
|
||||
game_api.npc_rotate_to(0, 180)
|
||||
game_api.npc_rotate_to("npc_teacher01", 180)
|
||||
teacher_arrived_to_library = true
|
||||
end
|
||||
|
||||
function on_student_arrived()
|
||||
game_api.npc_rotate_to(6, 270)
|
||||
game_api.npc_rotate_to("npc_altynai", 270)
|
||||
end
|
||||
|
||||
function on_teacher_arrived2()
|
||||
game_api.npc_rotate_to(0, 0)
|
||||
game_api.npc_rotate_to("npc_teacher01", 0)
|
||||
end
|
||||
|
||||
function book_dialog_zone001_enter_callback()
|
||||
if (game_api.is_darklands()) then
|
||||
game_api.start_dialogue("ghost_dialog002")
|
||||
game_api.set_trigger_zone_enabled(3, false)
|
||||
game_api.set_trigger_zone_enabled("book_dialog_zone001", false)
|
||||
game_api.advance_darklands_hud()
|
||||
else
|
||||
if (player_hold_book) and (game_api.is_night() == false) then
|
||||
@@ -621,9 +604,9 @@ function on_sleep_cutscene()
|
||||
print("Cutscene 2 done!")
|
||||
|
||||
game_api.set_night()
|
||||
--game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
--game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
game_api.switch_navigation(16)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Hall_Leaf001", 0)
|
||||
@@ -649,14 +632,14 @@ function on_teacher_arrived_intermediate()
|
||||
game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil)
|
||||
game_api.fade_object("Room_Cover_North_3_001", 0, 0.5, callback_deactivate_cover_n3)
|
||||
game_api.switch_navigation(2)
|
||||
game_api.npc_walk_to(0, 4.94194, 0, 6.64111, on_teacher_arrived)
|
||||
game_api.npc_walk_to("npc_teacher01", 4.94194, 0, 6.64111, on_teacher_arrived)
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
end
|
||||
|
||||
function on_lection_cutscene_fadein_callback()
|
||||
print("Lection cutscene in progress!")
|
||||
-- -0.0234149 0 10.1139
|
||||
game_api.npc_set_position(0, 0.0, 0.0, 10.1139)
|
||||
game_api.npc_set_position("npc_teacher01", 0.0, 0.0, 10.1139)
|
||||
|
||||
--4.91002 0 10.375
|
||||
--game_api.player_set_position(-2.41002, 0, 10.375)
|
||||
@@ -670,10 +653,10 @@ function callback_on_lection_cutscene_over()
|
||||
print("Cutscene done!")
|
||||
game_api.setIntValue("lection_is_over", 1)
|
||||
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_rotate_to(1, 180)
|
||||
game_api.npc_walk_to(0, 0.758123, 0, 6.50063, on_teacher_arrived_intermediate)
|
||||
game_api.npc_walk_to(6, 5.28998, 0, -5.525, on_student_arrived)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_rotate_to("npc_02_woman", 180)
|
||||
game_api.npc_walk_to("npc_teacher01", 0.758123, 0, 6.50063, on_teacher_arrived_intermediate)
|
||||
game_api.npc_walk_to("npc_altynai", 5.28998, 0, -5.525, on_student_arrived)
|
||||
|
||||
local aiperi_knife_aware = game_api.getIntValue("aiperi_knife_aware")
|
||||
|
||||
@@ -715,38 +698,36 @@ function on_bookshelf_teacher_clicked()
|
||||
on_report_card_pickup()
|
||||
end
|
||||
|
||||
first_time_darklands = true
|
||||
|
||||
function callback_enter_darklands()
|
||||
if (first_time_darklands) then
|
||||
game_api.start_dialogue("ghost_dialog001")
|
||||
first_time_darklands = false
|
||||
end
|
||||
|
||||
game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
if (ghost_gone) then
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
else
|
||||
game_api.set_npc_enabled(2, true)
|
||||
game_api.set_npc_enabled(3, true)
|
||||
game_api.set_npc_enabled(4, true)
|
||||
game_api.set_npc_enabled(5, true)
|
||||
game_api.set_npc_enabled("npc_04_ghost", true)
|
||||
game_api.set_npc_enabled("ghost_01x", true)
|
||||
game_api.set_npc_enabled("ghost_02x", true)
|
||||
game_api.set_npc_enabled("ghost_03x", true)
|
||||
|
||||
game_api.npc_set_hp(3, 35)
|
||||
game_api.npc_set_hp(4, 35)
|
||||
game_api.npc_set_hp(5, 35)
|
||||
game_api.npc_set_hp("ghost_01x", 35)
|
||||
game_api.npc_set_hp("ghost_02x", 35)
|
||||
game_api.npc_set_hp("ghost_03x", 35)
|
||||
|
||||
game_api.npc_set_position(3, 0, 0, -6.4)
|
||||
game_api.npc_set_position(4, -5.0, 0, 12)
|
||||
game_api.npc_set_position(5, 5.0, 0, 12)
|
||||
game_api.npc_set_position("ghost_01x", 0, 0, -6.4)
|
||||
game_api.npc_set_position("ghost_02x", -5.0, 0, 12)
|
||||
game_api.npc_set_position("ghost_03x", 5.0, 0, 12)
|
||||
end
|
||||
|
||||
game_api.switch_navigation(5)
|
||||
@@ -755,13 +736,13 @@ end
|
||||
|
||||
function callback_exit_darklands()
|
||||
game_api.start_cutscene("darklands_exit001")
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
morning_did_open_door_index = 0
|
||||
setDay1MorningSetup()
|
||||
@@ -873,13 +854,13 @@ function setDay0setup()
|
||||
|
||||
game_api.deactivate_interactive_object("Note001")
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.activate_interactive_object("car001")
|
||||
game_api.activate_interactive_object("car001wheels")
|
||||
@@ -903,7 +884,6 @@ function setDay1setup()
|
||||
game_api.activate_interactive_object("Room_Cover_South_2_001")
|
||||
game_api.deactivate_interactive_object("Room_Cover_South_3_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", -90)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", -90)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -911,15 +891,15 @@ function setDay1setup()
|
||||
game_api.set_object_rotation("Room_S_2_Leaf001", 90)
|
||||
game_api.set_object_rotation("Hall_Leaf001", 90)
|
||||
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, true)
|
||||
game_api.set_npc_enabled("npc_teacher01", true)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", true)
|
||||
|
||||
game_api.npc_set_position(1, 0.98998, 0, -7.525)
|
||||
game_api.npc_set_position("npc_02_woman", 0.98998, 0, -7.525)
|
||||
|
||||
game_api.deactivate_interactive_object("Cake001")
|
||||
|
||||
@@ -939,7 +919,6 @@ function setDay1MorningSetup()
|
||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -964,15 +943,15 @@ function setDay1MorningSetup()
|
||||
|
||||
-- TODO: Make check for teacher_door_opened false or true
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.npc_set_position(1, 0.98998, 0, -7.525)
|
||||
game_api.npc_set_position("npc_02_woman", 0.98998, 0, -7.525)
|
||||
|
||||
if morning_did_open_door_index == 1 then
|
||||
game_api.deactivate_interactive_object("Room_Cover_South_1_001")
|
||||
@@ -1043,7 +1022,6 @@ function setDay1NightSetup()
|
||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -1059,13 +1037,13 @@ function setDay1NightSetup()
|
||||
game_api.set_object_alpha("Room_Cover_Corridor_001", 1)
|
||||
game_api.set_object_alpha("Room_Cover_Main_Hall_001", 1)
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.deactivate_interactive_object("car001")
|
||||
game_api.deactivate_interactive_object("car001wheels")
|
||||
@@ -1222,8 +1200,8 @@ game_api.set_chat_callbacks(
|
||||
)
|
||||
|
||||
function callback_aiperi_dialog_morning()
|
||||
game_api.player_stop_and_rotate_to_npc(1)
|
||||
game_api.npc_stop_and_rotate_to_player(1)
|
||||
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||
game_api.npc_stop_and_rotate_to_player("npc_02_woman")
|
||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||
end
|
||||
|
||||
@@ -1246,9 +1224,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, 6.5)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 0.0, 0, 9.1, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, 6.5)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 0.0, 0, 9.1, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(0.0, 0.0, 10.0, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1268,9 +1246,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0.0, 0, 6.43818)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 2.34349, 0, 6.31229, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0.0, 0, 6.43818)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 2.34349, 0, 6.31229, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(3.52174, 0, 6.28993, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1290,9 +1268,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 2.11898, 0, -1.7074, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 2.11898, 0, -1.7074, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(3.36055, 0, -2.0345, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1312,9 +1290,9 @@ function on_aiperi_opens_door()
|
||||
game_api.switch_navigation(9)
|
||||
end
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, -2.454, 0, -1.61871, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", -2.454, 0, -1.61871, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(-3.63363, 0, -1.50727, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1334,9 +1312,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -9.77948)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, -2.53977, 0, -9.77852, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -9.77948)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", -2.53977, 0, -9.77852, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(-3.64494, 0, -9.66711, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1345,14 +1323,28 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
function on_aiperi_morning_dialog_finished()
|
||||
game_api.npc_walk_to(1, 0.98998, 0, -7.525, nil)
|
||||
game_api.npc_walk_to("npc_02_woman", 0.98998, 0, -7.525, nil)
|
||||
end
|
||||
|
||||
function callback_init_location()
|
||||
hall_door_opened = false
|
||||
teacher_door_opened = false
|
||||
player_hold_book = false
|
||||
teacher_arrived_to_library = false
|
||||
teacher_told_about_book = false
|
||||
was_state_when_player_left = 0
|
||||
was_day_when_player_left = 0
|
||||
morning_can_open_door_index = 0
|
||||
morning_did_open_door_index = 0
|
||||
ghost_gone = false
|
||||
first_time_darklands = true
|
||||
|
||||
setDay0setup()
|
||||
game_api.switch_navigation(0)
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
print("Enter location uni interior-")
|
||||
--player_left_darklands = false
|
||||
local hp = game_api.getFloatValue("player_hp")
|
||||
game_api.set_player_hp(hp)
|
||||
local day = game_api.getIntValue("day")
|
||||
|
||||
print("day is:")
|
||||
@@ -1406,11 +1398,6 @@ end
|
||||
|
||||
function callback_exit_location()
|
||||
print("Exit location uni interior")
|
||||
--player_left_darklands = false
|
||||
local hp = game_api.get_player_hp()
|
||||
game_api.setFloatValue("player_hp", hp)
|
||||
|
||||
|
||||
--[[
|
||||
0 - day0
|
||||
1 - darklands
|
||||
@@ -1437,6 +1424,7 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
@@ -1467,9 +1455,5 @@ end
|
||||
|
||||
game_api.set_cutscene_callback("game_complete_cutscene001", callback_game_complete_cutscene)
|
||||
|
||||
|
||||
|
||||
setDay0setup()
|
||||
game_api.switch_navigation(0)
|
||||
--debugAllOpen()
|
||||
print("Lua script loaded successfully!---!")
|
||||
+2845
-1419
File diff suppressed because it is too large.
Load diff
@@ -1,108 +1,124 @@
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"id": "intBin_Cans",
|
||||
"name": "intBin_Cans",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Cans_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/intBin_Cans_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Cans.txt.bin",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.6,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Plastic",
|
||||
"name": "intBin_Plastic",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Plastic_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/intBin_Plastic_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Plastic.txt.bin",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.4,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Waste_A",
|
||||
"name": "intBin_Waste_A",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Waste_A_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/intBin_Waste_A_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Waste_A.txt.bin",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 2.2,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Waste_B",
|
||||
"name": "intBin_Waste_B",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Waste_B_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/intBin_Waste_B_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Waste_B.txt.bin",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 3.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intRoof",
|
||||
"name": "intRoof",
|
||||
"texturePath": "resources/w/shelter/models/intRoof_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/intRoof_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intRoof.txt",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 3.2,
|
||||
"positionZ": 0.0,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.00",
|
||||
"name": "intVending.00",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.01",
|
||||
"name": "intVending.01",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.02",
|
||||
"name": "intVending.02",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
"rotationX": 0.0,
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
"interactionRadius": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,85 @@
|
||||
===Vertices (Split by UV/Normal): 4
|
||||
===Vertices (Split by UV/Normal): 42
|
||||
V 0: Pos(30.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(60.0, -15.36)
|
||||
V 1: Pos(-30.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-60.0, -9.36)
|
||||
V 2: Pos(-30.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-60.0, -15.36)
|
||||
V 3: Pos(30.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(60.0, -9.36)
|
||||
V 1: Pos(27.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(54.0, -9.36)
|
||||
V 2: Pos(27.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(54.0, -15.36)
|
||||
V 3: Pos(-27.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-54.0, -15.359999)
|
||||
V 4: Pos(-30.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-60.0, -9.36)
|
||||
V 5: Pos(-30.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-60.0, -15.36)
|
||||
V 6: Pos(-24.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-48.0, -15.36)
|
||||
V 7: Pos(-27.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-54.0, -9.360001)
|
||||
V 8: Pos(-21.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-42.0, -15.36)
|
||||
V 9: Pos(-24.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-48.0, -9.360001)
|
||||
V 10: Pos(-18.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-36.0, -15.36)
|
||||
V 11: Pos(-21.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-42.0, -9.360001)
|
||||
V 12: Pos(-15.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-30.0, -15.36)
|
||||
V 13: Pos(-18.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-36.0, -9.360001)
|
||||
V 14: Pos(-12.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-24.0, -15.36)
|
||||
V 15: Pos(-15.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-30.000002, -9.360001)
|
||||
V 16: Pos(-9.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-17.999998, -15.36)
|
||||
V 17: Pos(-12.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-24.000004, -9.360001)
|
||||
V 18: Pos(-6.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-11.999998, -15.36)
|
||||
V 19: Pos(-9.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-18.000002, -9.360001)
|
||||
V 20: Pos(-3.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-5.999998, -15.36)
|
||||
V 21: Pos(-6.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-12.000002, -9.360001)
|
||||
V 22: Pos(0.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(2e-06, -15.359999)
|
||||
V 23: Pos(-3.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-6.000002, -9.360001)
|
||||
V 24: Pos(3.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(6.000002, -15.359999)
|
||||
V 25: Pos(0.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(-2e-06, -9.360001)
|
||||
V 26: Pos(6.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(12.000002, -15.36)
|
||||
V 27: Pos(3.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(5.999998, -9.360001)
|
||||
V 28: Pos(9.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(18.000002, -15.36)
|
||||
V 29: Pos(6.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(11.999998, -9.360001)
|
||||
V 30: Pos(12.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(24.000004, -15.360001)
|
||||
V 31: Pos(9.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(17.999998, -9.360001)
|
||||
V 32: Pos(15.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(30.000002, -15.360001)
|
||||
V 33: Pos(12.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(24.0, -9.360001)
|
||||
V 34: Pos(18.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(36.0, -15.360001)
|
||||
V 35: Pos(15.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(30.0, -9.360001)
|
||||
V 36: Pos(21.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(42.0, -15.360001)
|
||||
V 37: Pos(18.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(36.0, -9.360001)
|
||||
V 38: Pos(24.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(48.0, -15.36)
|
||||
V 39: Pos(21.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(42.0, -9.360001)
|
||||
V 40: Pos(24.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(48.0, -9.360001)
|
||||
V 41: Pos(30.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(60.0, -9.36)
|
||||
|
||||
===Triangles (Indices): 2
|
||||
===Triangles (Indices): 40
|
||||
Tri: 0 1 2
|
||||
Tri: 0 3 1
|
||||
Tri: 3 4 5
|
||||
Tri: 6 7 3
|
||||
Tri: 8 9 6
|
||||
Tri: 10 11 8
|
||||
Tri: 12 13 10
|
||||
Tri: 14 15 12
|
||||
Tri: 16 17 14
|
||||
Tri: 18 19 16
|
||||
Tri: 20 21 18
|
||||
Tri: 22 23 20
|
||||
Tri: 24 25 22
|
||||
Tri: 26 27 24
|
||||
Tri: 28 29 26
|
||||
Tri: 30 31 28
|
||||
Tri: 32 33 30
|
||||
Tri: 34 35 32
|
||||
Tri: 36 37 34
|
||||
Tri: 38 39 36
|
||||
Tri: 2 40 38
|
||||
Tri: 0 41 1
|
||||
Tri: 3 7 4
|
||||
Tri: 6 9 7
|
||||
Tri: 8 11 9
|
||||
Tri: 10 13 11
|
||||
Tri: 12 15 13
|
||||
Tri: 14 17 15
|
||||
Tri: 16 19 17
|
||||
Tri: 18 21 19
|
||||
Tri: 20 23 21
|
||||
Tri: 22 25 23
|
||||
Tri: 24 27 25
|
||||
Tri: 26 29 27
|
||||
Tri: 28 31 29
|
||||
Tri: 30 33 31
|
||||
Tri: 32 35 33
|
||||
Tri: 34 37 35
|
||||
Tri: 36 39 37
|
||||
Tri: 38 40 39
|
||||
Tri: 2 1 40
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,859 @@
|
||||
===Vertices (Split by UV/Normal): 582
|
||||
V 0: Pos(0.32, 0.0, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.312667)
|
||||
V 1: Pos(0.295641, 0.122459, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.001953)
|
||||
V 2: Pos(0.32, 0.0, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.001953)
|
||||
V 3: Pos(0.226274, 0.226274, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.001953)
|
||||
V 4: Pos(0.295641, 0.122459, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.312667)
|
||||
V 5: Pos(0.295641, 0.122459, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.001953)
|
||||
V 6: Pos(0.226274, 0.226274, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.312667)
|
||||
V 7: Pos(0.122459, 0.295641, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.001953)
|
||||
V 8: Pos(0.226274, 0.226274, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.001953)
|
||||
V 9: Pos(0.122459, 0.295641, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.312667)
|
||||
V 10: Pos(0.0, 0.32, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 11: Pos(0.122459, 0.295641, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.001953)
|
||||
V 12: Pos(0.0, 0.32, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 13: Pos(-0.122459, 0.295641, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.001953)
|
||||
V 14: Pos(0.0, 0.32, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 15: Pos(-0.226274, 0.226274, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.001953)
|
||||
V 16: Pos(-0.122459, 0.295641, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.312667)
|
||||
V 17: Pos(-0.122459, 0.295641, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.001953)
|
||||
V 18: Pos(-0.226274, 0.226274, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.312667)
|
||||
V 19: Pos(-0.295641, 0.122459, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.001953)
|
||||
V 20: Pos(-0.226274, 0.226274, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.001953)
|
||||
V 21: Pos(-0.295641, 0.122459, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.312667)
|
||||
V 22: Pos(-0.32, 0.0, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 23: Pos(-0.295641, 0.122459, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.001953)
|
||||
V 24: Pos(-0.32, 0.0, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 25: Pos(-0.295641, -0.122459, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.001953)
|
||||
V 26: Pos(-0.32, 0.0, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 27: Pos(-0.226274, -0.226274, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.001953)
|
||||
V 28: Pos(-0.295641, -0.122459, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.312667)
|
||||
V 29: Pos(-0.295641, -0.122459, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.001953)
|
||||
V 30: Pos(-0.226274, -0.226274, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.312667)
|
||||
V 31: Pos(-0.122459, -0.295641, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.001953)
|
||||
V 32: Pos(-0.226274, -0.226274, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.001953)
|
||||
V 33: Pos(-0.122459, -0.295641, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.312667)
|
||||
V 34: Pos(0.0, -0.32, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 35: Pos(-0.122459, -0.295641, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.001953)
|
||||
V 36: Pos(0.0, -0.32, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 37: Pos(0.122459, -0.295641, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.001953)
|
||||
V 38: Pos(0.0, -0.32, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 39: Pos(0.226274, -0.226274, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.001953)
|
||||
V 40: Pos(0.122459, -0.295641, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.312667)
|
||||
V 41: Pos(0.122459, -0.295641, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.001953)
|
||||
V 42: Pos(0.226274, -0.226274, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.312667)
|
||||
V 43: Pos(0.295641, -0.122459, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.001953)
|
||||
V 44: Pos(0.226274, -0.226274, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.001953)
|
||||
V 45: Pos(0.295641, -0.122459, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.312667)
|
||||
V 46: Pos(0.32, 0.0, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.001953)
|
||||
V 47: Pos(0.295641, -0.122459, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.001953)
|
||||
V 48: Pos(0.324, 0.0, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.599609, 0.506836)
|
||||
V 49: Pos(0.299337, -0.123989, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.505859, 0.694336)
|
||||
V 50: Pos(0.299337, -0.123989, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.505859, 0.506836)
|
||||
V 51: Pos(0.299337, 0.123989, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.693359, 0.506836)
|
||||
V 52: Pos(0.324, 0.0, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.599609, 0.694336)
|
||||
V 53: Pos(0.324, 0.0, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.599609, 0.506836)
|
||||
V 54: Pos(0.316891, 0.13126, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.505859)
|
||||
V 55: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.547526)
|
||||
V 56: Pos(0.343, 0.0, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.505859)
|
||||
V 57: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.547526)
|
||||
V 58: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.589193)
|
||||
V 59: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.547526)
|
||||
V 60: Pos(0.309, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.630859)
|
||||
V 61: Pos(0.293332, 0.121502, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.589193)
|
||||
V 62: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.589193)
|
||||
V 63: Pos(0.285479, 0.118249, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.048828, 0.630859)
|
||||
V 64: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.672526)
|
||||
V 65: Pos(0.309, 0.0, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.630859)
|
||||
V 66: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.672526)
|
||||
V 67: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.714193)
|
||||
V 68: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.672526)
|
||||
V 69: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.714193)
|
||||
V 70: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.755859)
|
||||
V 71: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.714193)
|
||||
V 72: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.547526)
|
||||
V 73: Pos(0.242538, 0.242538, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.095703, 0.505859)
|
||||
V 74: Pos(0.316891, 0.13126, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.505859)
|
||||
V 75: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.589193)
|
||||
V 76: Pos(0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.547526)
|
||||
V 77: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.547526)
|
||||
V 78: Pos(0.285479, 0.118249, 0.9) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 79: Pos(0.224506, 0.224506, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 80: Pos(0.293332, 0.121502, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.589193)
|
||||
V 81: Pos(0.293332, 0.121502, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.672526)
|
||||
V 82: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.630859)
|
||||
V 83: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.630859)
|
||||
V 84: Pos(0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.672526)
|
||||
V 85: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.714193)
|
||||
V 86: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.672526)
|
||||
V 87: Pos(0.236527, 0.236527, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.714193)
|
||||
V 88: Pos(0.316891, 0.13126, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.755859)
|
||||
V 89: Pos(0.309038, 0.128008, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.714193)
|
||||
V 90: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.547526)
|
||||
V 91: Pos(0.13126, 0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.142578, 0.505859)
|
||||
V 92: Pos(0.242538, 0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.505859)
|
||||
V 93: Pos(0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.589193)
|
||||
V 94: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.547526)
|
||||
V 95: Pos(0.121502, 0.293332, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.589193)
|
||||
V 96: Pos(0.218496, 0.218496, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.630859)
|
||||
V 97: Pos(0.224506, 0.224506, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.589193)
|
||||
V 98: Pos(0.118249, 0.285479, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.630859)
|
||||
V 99: Pos(0.224506, 0.224506, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.672526)
|
||||
V 100: Pos(0.218496, 0.218496, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.630859)
|
||||
V 101: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.672526)
|
||||
V 102: Pos(0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.714193)
|
||||
V 103: Pos(0.128008, 0.309038, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.714193)
|
||||
V 104: Pos(0.242538, 0.242538, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.755859)
|
||||
V 105: Pos(0.236527, 0.236527, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.714193)
|
||||
V 106: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 107: Pos(0.128008, 0.309038, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.547526)
|
||||
V 108: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.505859)
|
||||
V 109: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.547526)
|
||||
V 110: Pos(0.0, 0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.589193)
|
||||
V 111: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.589193)
|
||||
V 112: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.630859)
|
||||
V 113: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.189453, 0.589193)
|
||||
V 114: Pos(0.121502, 0.293332, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.589193)
|
||||
V 115: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.189453, 0.630859)
|
||||
V 116: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.672526)
|
||||
V 117: Pos(0.118249, 0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.630859)
|
||||
V 118: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.714193)
|
||||
V 119: Pos(0.0, 0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.672526)
|
||||
V 120: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.672526)
|
||||
V 121: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.189453, 0.714193)
|
||||
V 122: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.755859)
|
||||
V 123: Pos(0.128008, 0.309038, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.714193)
|
||||
V 124: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.505859)
|
||||
V 125: Pos(0.0, 0.3345, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 126: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 127: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.547526)
|
||||
V 128: Pos(0.0, 0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.547526)
|
||||
V 129: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 130: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.589193)
|
||||
V 131: Pos(0.0, 0.3175, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 132: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.236328, 0.630859)
|
||||
V 133: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.672526)
|
||||
V 134: Pos(0.0, 0.309, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.630859)
|
||||
V 135: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.714193)
|
||||
V 136: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.672526)
|
||||
V 137: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.714193)
|
||||
V 138: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 139: Pos(0.0, 0.3345, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 140: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.547526)
|
||||
V 141: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.283203, 0.505859)
|
||||
V 142: Pos(-0.13126, 0.316891, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.505859)
|
||||
V 143: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.589193)
|
||||
V 144: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.547526)
|
||||
V 145: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.547526)
|
||||
V 146: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.283203, 0.589193)
|
||||
V 147: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.630859)
|
||||
V 148: Pos(-0.121502, 0.293332, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.589193)
|
||||
V 149: Pos(-0.121502, 0.293332, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.672526)
|
||||
V 150: Pos(-0.218496, 0.218496, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.630859)
|
||||
V 151: Pos(-0.118249, 0.285479, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.630859)
|
||||
V 152: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.672526)
|
||||
V 153: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.714193)
|
||||
V 154: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.672526)
|
||||
V 155: Pos(-0.13126, 0.316891, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.755859)
|
||||
V 156: Pos(-0.236527, 0.236527, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.714193)
|
||||
V 157: Pos(-0.128008, 0.309038, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.714193)
|
||||
V 158: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.547526)
|
||||
V 159: Pos(-0.316891, 0.13126, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.080078, 0.505859)
|
||||
V 160: Pos(-0.242538, 0.242538, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.505859)
|
||||
V 161: Pos(-0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.547526)
|
||||
V 162: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.589193)
|
||||
V 163: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.547526)
|
||||
V 164: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.630859)
|
||||
V 165: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.589193)
|
||||
V 166: Pos(-0.224506, 0.224506, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.589193)
|
||||
V 167: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.672526)
|
||||
V 168: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.630859)
|
||||
V 169: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.630859)
|
||||
V 170: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.672526)
|
||||
V 171: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.714193)
|
||||
V 172: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.672526)
|
||||
V 173: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.714193)
|
||||
V 174: Pos(-0.242538, 0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.755859)
|
||||
V 175: Pos(-0.236527, 0.236527, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.714193)
|
||||
V 176: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 177: Pos(-0.309038, 0.128008, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.547526)
|
||||
V 178: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.505859)
|
||||
V 179: Pos(-0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.589193)
|
||||
V 180: Pos(-0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.589193)
|
||||
V 181: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.630859)
|
||||
V 182: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.126953, 0.589193)
|
||||
V 183: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.589193)
|
||||
V 184: Pos(-0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 185: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 186: Pos(-0.285479, 0.118249, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.630859)
|
||||
V 187: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.714193)
|
||||
V 188: Pos(-0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.672526)
|
||||
V 189: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.672526)
|
||||
V 190: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.126953, 0.714193)
|
||||
V 191: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.755859)
|
||||
V 192: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.714193)
|
||||
V 193: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.505859)
|
||||
V 194: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 195: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 196: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.547526)
|
||||
V 197: Pos(-0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.547526)
|
||||
V 198: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 199: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.589193)
|
||||
V 200: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 201: Pos(-0.285479, -0.118249, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.173828, 0.630859)
|
||||
V 202: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.672526)
|
||||
V 203: Pos(-0.309, 0.0, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.630859)
|
||||
V 204: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.714193)
|
||||
V 205: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.672526)
|
||||
V 206: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.714193)
|
||||
V 207: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 208: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 209: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.547526)
|
||||
V 210: Pos(-0.242538, -0.242538, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.220703, 0.505859)
|
||||
V 211: Pos(-0.316891, -0.13126, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.505859)
|
||||
V 212: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.589193)
|
||||
V 213: Pos(-0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.547526)
|
||||
V 214: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.547526)
|
||||
V 215: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 216: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 217: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.589193)
|
||||
V 218: Pos(-0.293332, -0.121502, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.672526)
|
||||
V 219: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.630859)
|
||||
V 220: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.630859)
|
||||
V 221: Pos(-0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.672526)
|
||||
V 222: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.714193)
|
||||
V 223: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.672526)
|
||||
V 224: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.714193)
|
||||
V 225: Pos(-0.316891, -0.13126, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.755859)
|
||||
V 226: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.714193)
|
||||
V 227: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.547526)
|
||||
V 228: Pos(-0.13126, -0.316891, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.267578, 0.505859)
|
||||
V 229: Pos(-0.242538, -0.242538, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.505859)
|
||||
V 230: Pos(-0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.589193)
|
||||
V 231: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.547526)
|
||||
V 232: Pos(-0.121502, -0.293332, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.589193)
|
||||
V 233: Pos(-0.218496, -0.218496, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.630859)
|
||||
V 234: Pos(-0.224506, -0.224506, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.589193)
|
||||
V 235: Pos(-0.118249, -0.285479, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.630859)
|
||||
V 236: Pos(-0.224506, -0.224506, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.672526)
|
||||
V 237: Pos(-0.218496, -0.218496, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.630859)
|
||||
V 238: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.672526)
|
||||
V 239: Pos(-0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.714193)
|
||||
V 240: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.714193)
|
||||
V 241: Pos(-0.242538, -0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.755859)
|
||||
V 242: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.714193)
|
||||
V 243: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 244: Pos(-0.128008, -0.309038, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.547526)
|
||||
V 245: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.505859)
|
||||
V 246: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.547526)
|
||||
V 247: Pos(0.0, -0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.589193)
|
||||
V 248: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.589193)
|
||||
V 249: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.630859)
|
||||
V 250: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.064453, 0.589193)
|
||||
V 251: Pos(-0.121502, -0.293332, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.589193)
|
||||
V 252: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.064453, 0.630859)
|
||||
V 253: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.672526)
|
||||
V 254: Pos(-0.118249, -0.285479, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.630859)
|
||||
V 255: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.714193)
|
||||
V 256: Pos(0.0, -0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.672526)
|
||||
V 257: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.672526)
|
||||
V 258: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.064453, 0.714193)
|
||||
V 259: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.755859)
|
||||
V 260: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.714193)
|
||||
V 261: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.505859)
|
||||
V 262: Pos(0.0, -0.3345, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 263: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 264: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.547526)
|
||||
V 265: Pos(0.0, -0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.547526)
|
||||
V 266: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 267: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.589193)
|
||||
V 268: Pos(0.0, -0.3175, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 269: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.111328, 0.630859)
|
||||
V 270: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.672526)
|
||||
V 271: Pos(0.0, -0.309, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.630859)
|
||||
V 272: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.714193)
|
||||
V 273: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.672526)
|
||||
V 274: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.714193)
|
||||
V 275: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 276: Pos(0.0, -0.3345, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 277: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.547526)
|
||||
V 278: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.158203, 0.505859)
|
||||
V 279: Pos(0.13126, -0.316891, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.505859)
|
||||
V 280: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.589193)
|
||||
V 281: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.547526)
|
||||
V 282: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.547526)
|
||||
V 283: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.158203, 0.589193)
|
||||
V 284: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.630859)
|
||||
V 285: Pos(0.121502, -0.293332, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.589193)
|
||||
V 286: Pos(0.121502, -0.293332, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.672526)
|
||||
V 287: Pos(0.218496, -0.218496, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.630859)
|
||||
V 288: Pos(0.118249, -0.285479, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.630859)
|
||||
V 289: Pos(0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.672526)
|
||||
V 290: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.714193)
|
||||
V 291: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.672526)
|
||||
V 292: Pos(0.13126, -0.316891, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.755859)
|
||||
V 293: Pos(0.236527, -0.236527, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.714193)
|
||||
V 294: Pos(0.128008, -0.309038, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.714193)
|
||||
V 295: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.547526)
|
||||
V 296: Pos(0.316891, -0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.205078, 0.505859)
|
||||
V 297: Pos(0.242538, -0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.505859)
|
||||
V 298: Pos(0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.547526)
|
||||
V 299: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.589193)
|
||||
V 300: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.547526)
|
||||
V 301: Pos(0.218496, -0.218496, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.630859)
|
||||
V 302: Pos(0.293332, -0.121502, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.589193)
|
||||
V 303: Pos(0.224506, -0.224506, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.589193)
|
||||
V 304: Pos(0.224506, -0.224506, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.672526)
|
||||
V 305: Pos(0.285479, -0.118249, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.630859)
|
||||
V 306: Pos(0.218496, -0.218496, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.630859)
|
||||
V 307: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.672526)
|
||||
V 308: Pos(0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.714193)
|
||||
V 309: Pos(0.309038, -0.128008, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.714193)
|
||||
V 310: Pos(0.242538, -0.242538, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.755859)
|
||||
V 311: Pos(0.236527, -0.236527, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.714193)
|
||||
V 312: Pos(0.343, 0.0, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.505859)
|
||||
V 313: Pos(0.309038, -0.128008, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.547526)
|
||||
V 314: Pos(0.316891, -0.13126, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.505859)
|
||||
V 315: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.589193)
|
||||
V 316: Pos(0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.589193)
|
||||
V 317: Pos(0.285479, -0.118249, 0.9) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.630859)
|
||||
V 318: Pos(0.3175, 0.0, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.251953, 0.589193)
|
||||
V 319: Pos(0.293332, -0.121502, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.589193)
|
||||
V 320: Pos(0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 321: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 322: Pos(0.285479, -0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.630859)
|
||||
V 323: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.714193)
|
||||
V 324: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.672526)
|
||||
V 325: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.672526)
|
||||
V 326: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.251953, 0.714193)
|
||||
V 327: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.755859)
|
||||
V 328: Pos(0.309038, -0.128008, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.714193)
|
||||
V 329: Pos(0.308, 0.0, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 330: Pos(0.284555, 0.117866, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 331: Pos(0.284555, 0.117866, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 332: Pos(0.284555, 0.117866, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 333: Pos(0.217789, 0.217789, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 334: Pos(0.217789, 0.217789, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 335: Pos(0.217789, 0.217789, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 336: Pos(0.117866, 0.284555, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 337: Pos(0.117866, 0.284555, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 338: Pos(0.117866, 0.284555, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 339: Pos(0.0, 0.308, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 340: Pos(0.0, 0.308, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 341: Pos(0.0, 0.308, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 342: Pos(-0.117866, 0.284555, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 343: Pos(-0.117866, 0.284555, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 344: Pos(-0.117866, 0.284555, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 345: Pos(-0.217789, 0.217789, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 346: Pos(-0.217789, 0.217789, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 347: Pos(-0.217789, 0.217789, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 348: Pos(-0.284555, 0.117866, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 349: Pos(-0.284555, 0.117866, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 350: Pos(-0.284555, 0.117866, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 351: Pos(-0.308, 0.0, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 352: Pos(-0.308, 0.0, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 353: Pos(-0.308, 0.0, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 354: Pos(-0.284555, -0.117866, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 355: Pos(-0.284555, -0.117866, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 356: Pos(-0.284555, -0.117866, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 357: Pos(-0.217789, -0.217789, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 358: Pos(-0.217789, -0.217789, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 359: Pos(-0.217789, -0.217789, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 360: Pos(-0.117866, -0.284555, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 361: Pos(-0.117866, -0.284555, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 362: Pos(-0.117866, -0.284555, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 363: Pos(0.0, -0.308, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 364: Pos(0.0, -0.308, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 365: Pos(0.0, -0.308, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 366: Pos(0.117866, -0.284555, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 367: Pos(0.117866, -0.284555, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 368: Pos(0.117866, -0.284555, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 369: Pos(0.217789, -0.217789, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 370: Pos(0.217789, -0.217789, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 371: Pos(0.217789, -0.217789, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 372: Pos(0.284555, -0.117866, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 373: Pos(0.284555, -0.117866, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 374: Pos(0.284555, -0.117866, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 375: Pos(0.308, 0.0, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 376: Pos(0.308, 0.0, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 377: Pos(-0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 378: Pos(0.0, 0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 379: Pos(0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 380: Pos(0.295641, 0.122459, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.312667)
|
||||
V 381: Pos(0.226274, 0.226274, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.312667)
|
||||
V 382: Pos(0.122459, 0.295641, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.312667)
|
||||
V 383: Pos(0.0, 0.32, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 384: Pos(-0.122459, 0.295641, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.312667)
|
||||
V 385: Pos(-0.226274, 0.226274, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.312667)
|
||||
V 386: Pos(-0.295641, 0.122459, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.312667)
|
||||
V 387: Pos(-0.32, 0.0, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 388: Pos(-0.295641, -0.122459, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.312667)
|
||||
V 389: Pos(-0.226274, -0.226274, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.312667)
|
||||
V 390: Pos(-0.122459, -0.295641, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.312667)
|
||||
V 391: Pos(0.0, -0.32, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 392: Pos(0.122459, -0.295641, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.312667)
|
||||
V 393: Pos(0.226274, -0.226274, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.312667)
|
||||
V 394: Pos(0.295641, -0.122459, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.312667)
|
||||
V 395: Pos(0.32, 0.0, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.312667)
|
||||
V 396: Pos(0.324, 0.0, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.599609, 0.694336)
|
||||
V 397: Pos(0.299337, 0.123989, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.693359, 0.694336)
|
||||
V 398: Pos(0.309038, 0.128008, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.547526)
|
||||
V 399: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.589193)
|
||||
V 400: Pos(0.309, 0.0, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.001953, 0.630859)
|
||||
V 401: Pos(0.285479, 0.118249, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.630859)
|
||||
V 402: Pos(0.293332, 0.121502, 0.914722) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.589193)
|
||||
V 403: Pos(0.285479, 0.118249, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.630859)
|
||||
V 404: Pos(0.293332, 0.121502, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.672526)
|
||||
V 405: Pos(0.3175, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.001953, 0.672526)
|
||||
V 406: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.714193)
|
||||
V 407: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.714193)
|
||||
V 408: Pos(0.316891, 0.13126, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.755859)
|
||||
V 409: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.001953, 0.755859)
|
||||
V 410: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.048828, 0.547526)
|
||||
V 411: Pos(0.236527, 0.236527, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.547526)
|
||||
V 412: Pos(0.242538, 0.242538, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.505859)
|
||||
V 413: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 414: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.630859)
|
||||
V 415: Pos(0.224506, 0.224506, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 416: Pos(0.224506, 0.224506, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.672526)
|
||||
V 417: Pos(0.242538, 0.242538, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.755859)
|
||||
V 418: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.095703, 0.547526)
|
||||
V 419: Pos(0.128008, 0.309038, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.547526)
|
||||
V 420: Pos(0.13126, 0.316891, 0.9) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.505859)
|
||||
V 421: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.589193)
|
||||
V 422: Pos(0.118249, 0.285479, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.630859)
|
||||
V 423: Pos(0.121502, 0.293332, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.672526)
|
||||
V 424: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.714193)
|
||||
V 425: Pos(0.13126, 0.316891, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.755859)
|
||||
V 426: Pos(0.0, 0.3345, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 427: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.630859)
|
||||
V 428: Pos(0.0, 0.309, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 429: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 430: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.630859)
|
||||
V 431: Pos(0.0, 0.3175, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.672526)
|
||||
V 432: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.142578, 0.672526)
|
||||
V 433: Pos(0.0, 0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.714193)
|
||||
V 434: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 435: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 436: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.755859)
|
||||
V 437: Pos(-0.128008, 0.309038, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.547526)
|
||||
V 438: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.589193)
|
||||
V 439: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.189453, 0.630859)
|
||||
V 440: Pos(-0.118249, 0.285479, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.630859)
|
||||
V 441: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.589193)
|
||||
V 442: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.630859)
|
||||
V 443: Pos(-0.121502, 0.293332, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.672526)
|
||||
V 444: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.189453, 0.672526)
|
||||
V 445: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.714193)
|
||||
V 446: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.755859)
|
||||
V 447: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.189453, 0.755859)
|
||||
V 448: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.236328, 0.547526)
|
||||
V 449: Pos(-0.236527, 0.236527, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.547526)
|
||||
V 450: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.505859)
|
||||
V 451: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.589193)
|
||||
V 452: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.589193)
|
||||
V 453: Pos(-0.218496, 0.218496, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.630859)
|
||||
V 454: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.236328, 0.630859)
|
||||
V 455: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.672526)
|
||||
V 456: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.714193)
|
||||
V 457: Pos(-0.242538, 0.242538, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.755859)
|
||||
V 458: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.033203, 0.547526)
|
||||
V 459: Pos(-0.309038, 0.128008, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.547526)
|
||||
V 460: Pos(-0.316891, 0.13126, 0.9) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.505859)
|
||||
V 461: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.630859)
|
||||
V 462: Pos(-0.293332, 0.121502, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.672526)
|
||||
V 463: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.714193)
|
||||
V 464: Pos(-0.316891, 0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.755859)
|
||||
V 465: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 466: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.630859)
|
||||
V 467: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 468: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 469: Pos(-0.309, 0.0, 0.9) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 470: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 471: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 472: Pos(-0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.714193)
|
||||
V 473: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 474: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 475: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.755859)
|
||||
V 476: Pos(-0.309038, -0.128008, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.547526)
|
||||
V 477: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.589193)
|
||||
V 478: Pos(-0.309, 0.0, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.126953, 0.630859)
|
||||
V 479: Pos(-0.285479, -0.118249, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.630859)
|
||||
V 480: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.589193)
|
||||
V 481: Pos(-0.285479, -0.118249, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.630859)
|
||||
V 482: Pos(-0.293332, -0.121502, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.672526)
|
||||
V 483: Pos(-0.3175, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 484: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.714193)
|
||||
V 485: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.755859)
|
||||
V 486: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.126953, 0.755859)
|
||||
V 487: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.173828, 0.547526)
|
||||
V 488: Pos(-0.236527, -0.236527, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.547526)
|
||||
V 489: Pos(-0.242538, -0.242538, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.505859)
|
||||
V 490: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 491: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.630859)
|
||||
V 492: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 493: Pos(-0.224506, -0.224506, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.672526)
|
||||
V 494: Pos(-0.242538, -0.242538, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.755859)
|
||||
V 495: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.220703, 0.547526)
|
||||
V 496: Pos(-0.128008, -0.309038, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.547526)
|
||||
V 497: Pos(-0.13126, -0.316891, 0.9) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.505859)
|
||||
V 498: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.589193)
|
||||
V 499: Pos(-0.118249, -0.285479, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.630859)
|
||||
V 500: Pos(-0.121502, -0.293332, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.672526)
|
||||
V 501: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.714193)
|
||||
V 502: Pos(-0.13126, -0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.755859)
|
||||
V 503: Pos(0.0, -0.3345, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 504: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.630859)
|
||||
V 505: Pos(0.0, -0.309, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 506: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 507: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.630859)
|
||||
V 508: Pos(0.0, -0.3175, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.672526)
|
||||
V 509: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.017578, 0.672526)
|
||||
V 510: Pos(0.0, -0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.714193)
|
||||
V 511: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 512: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 513: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.755859)
|
||||
V 514: Pos(0.128008, -0.309038, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.547526)
|
||||
V 515: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.589193)
|
||||
V 516: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.064453, 0.630859)
|
||||
V 517: Pos(0.118249, -0.285479, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.630859)
|
||||
V 518: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.589193)
|
||||
V 519: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.630859)
|
||||
V 520: Pos(0.121502, -0.293332, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.672526)
|
||||
V 521: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.064453, 0.672526)
|
||||
V 522: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.714193)
|
||||
V 523: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.755859)
|
||||
V 524: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.064453, 0.755859)
|
||||
V 525: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.111328, 0.547526)
|
||||
V 526: Pos(0.236527, -0.236527, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.547526)
|
||||
V 527: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.505859)
|
||||
V 528: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.589193)
|
||||
V 529: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.589193)
|
||||
V 530: Pos(0.218496, -0.218496, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.630859)
|
||||
V 531: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.111328, 0.630859)
|
||||
V 532: Pos(0.224506, -0.224506, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.672526)
|
||||
V 533: Pos(0.242538, -0.242538, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.755859)
|
||||
V 534: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.158203, 0.547526)
|
||||
V 535: Pos(0.309038, -0.128008, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.547526)
|
||||
V 536: Pos(0.316891, -0.13126, 0.9) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.505859)
|
||||
V 537: Pos(0.285479, -0.118249, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.630859)
|
||||
V 538: Pos(0.293332, -0.121502, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.672526)
|
||||
V 539: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.714193)
|
||||
V 540: Pos(0.316891, -0.13126, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.755859)
|
||||
V 541: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.547526)
|
||||
V 542: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.547526)
|
||||
V 543: Pos(0.285479, -0.118249, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.630859)
|
||||
V 544: Pos(0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.630859)
|
||||
V 545: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.589193)
|
||||
V 546: Pos(0.309, 0.0, 0.9) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 547: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.672526)
|
||||
V 548: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 549: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.714193)
|
||||
V 550: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.714193)
|
||||
V 551: Pos(0.343, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.755859)
|
||||
V 552: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.755859)
|
||||
V 553: Pos(0.308, 0.0, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 554: Pos(0.284555, 0.117866, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 555: Pos(0.217789, 0.217789, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 556: Pos(0.117866, 0.284555, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 557: Pos(0.0, 0.308, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 558: Pos(-0.117866, 0.284555, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 559: Pos(-0.217789, 0.217789, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 560: Pos(-0.284555, 0.117866, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 561: Pos(-0.308, 0.0, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 562: Pos(-0.284555, -0.117866, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 563: Pos(-0.217789, -0.217789, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 564: Pos(-0.117866, -0.284555, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 565: Pos(0.0, -0.308, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 566: Pos(0.117866, -0.284555, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 567: Pos(0.217789, -0.217789, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 568: Pos(0.284555, -0.117866, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 569: Pos(0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 570: Pos(0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 571: Pos(0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 572: Pos(0.0, -0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 573: Pos(-0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 574: Pos(-0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 575: Pos(-0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 576: Pos(-0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 577: Pos(-0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 578: Pos(-0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 579: Pos(0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 580: Pos(0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 581: Pos(0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
|
||||
===Triangles (Indices): 274
|
||||
Tri: 0 1 2
|
||||
Tri: 3 4 5
|
||||
Tri: 6 7 8
|
||||
Tri: 9 10 11
|
||||
Tri: 12 13 14
|
||||
Tri: 15 16 17
|
||||
Tri: 18 19 20
|
||||
Tri: 21 22 23
|
||||
Tri: 24 25 26
|
||||
Tri: 27 28 29
|
||||
Tri: 30 31 32
|
||||
Tri: 33 34 35
|
||||
Tri: 36 37 38
|
||||
Tri: 39 40 41
|
||||
Tri: 42 43 44
|
||||
Tri: 45 46 47
|
||||
Tri: 48 49 50
|
||||
Tri: 51 52 53
|
||||
Tri: 54 55 56
|
||||
Tri: 57 58 59
|
||||
Tri: 60 61 62
|
||||
Tri: 63 64 65
|
||||
Tri: 66 67 68
|
||||
Tri: 69 70 71
|
||||
Tri: 72 73 74
|
||||
Tri: 75 76 77
|
||||
Tri: 78 79 80
|
||||
Tri: 81 82 83
|
||||
Tri: 84 85 86
|
||||
Tri: 87 88 89
|
||||
Tri: 90 91 92
|
||||
Tri: 93 94 76
|
||||
Tri: 95 96 97
|
||||
Tri: 98 99 100
|
||||
Tri: 101 102 84
|
||||
Tri: 103 104 105
|
||||
Tri: 106 107 108
|
||||
Tri: 109 110 111
|
||||
Tri: 112 113 114
|
||||
Tri: 115 116 117
|
||||
Tri: 118 119 120
|
||||
Tri: 121 122 123
|
||||
Tri: 124 125 126
|
||||
Tri: 127 110 128
|
||||
Tri: 129 130 131
|
||||
Tri: 132 133 134
|
||||
Tri: 119 135 136
|
||||
Tri: 137 138 139
|
||||
Tri: 140 141 142
|
||||
Tri: 143 144 145
|
||||
Tri: 146 147 148
|
||||
Tri: 149 150 151
|
||||
Tri: 152 153 154
|
||||
Tri: 155 156 157
|
||||
Tri: 158 159 160
|
||||
Tri: 161 162 163
|
||||
Tri: 164 165 166
|
||||
Tri: 167 168 169
|
||||
Tri: 170 171 172
|
||||
Tri: 173 174 175
|
||||
Tri: 176 177 178
|
||||
Tri: 161 179 180
|
||||
Tri: 181 182 183
|
||||
Tri: 184 185 186
|
||||
Tri: 187 188 189
|
||||
Tri: 190 191 192
|
||||
Tri: 193 194 195
|
||||
Tri: 196 179 197
|
||||
Tri: 198 199 200
|
||||
Tri: 201 202 203
|
||||
Tri: 188 204 205
|
||||
Tri: 206 207 208
|
||||
Tri: 209 210 211
|
||||
Tri: 212 213 214
|
||||
Tri: 215 216 217
|
||||
Tri: 218 219 220
|
||||
Tri: 221 222 223
|
||||
Tri: 224 225 226
|
||||
Tri: 227 228 229
|
||||
Tri: 230 231 213
|
||||
Tri: 232 233 234
|
||||
Tri: 235 236 237
|
||||
Tri: 238 239 221
|
||||
Tri: 240 241 242
|
||||
Tri: 243 244 245
|
||||
Tri: 246 247 248
|
||||
Tri: 249 250 251
|
||||
Tri: 252 253 254
|
||||
Tri: 255 256 257
|
||||
Tri: 258 259 260
|
||||
Tri: 261 262 263
|
||||
Tri: 264 247 265
|
||||
Tri: 266 267 268
|
||||
Tri: 269 270 271
|
||||
Tri: 256 272 273
|
||||
Tri: 274 275 276
|
||||
Tri: 277 278 279
|
||||
Tri: 280 281 282
|
||||
Tri: 283 284 285
|
||||
Tri: 286 287 288
|
||||
Tri: 289 290 291
|
||||
Tri: 292 293 294
|
||||
Tri: 295 296 297
|
||||
Tri: 298 299 300
|
||||
Tri: 301 302 303
|
||||
Tri: 304 305 306
|
||||
Tri: 307 308 289
|
||||
Tri: 309 310 311
|
||||
Tri: 312 313 314
|
||||
Tri: 298 315 316
|
||||
Tri: 317 318 319
|
||||
Tri: 320 321 322
|
||||
Tri: 323 324 325
|
||||
Tri: 326 327 328
|
||||
Tri: 329 330 331
|
||||
Tri: 332 333 334
|
||||
Tri: 335 336 337
|
||||
Tri: 338 339 340
|
||||
Tri: 341 342 343
|
||||
Tri: 344 345 346
|
||||
Tri: 347 348 349
|
||||
Tri: 350 351 352
|
||||
Tri: 353 354 355
|
||||
Tri: 356 357 358
|
||||
Tri: 359 360 361
|
||||
Tri: 362 363 364
|
||||
Tri: 365 366 367
|
||||
Tri: 368 369 370
|
||||
Tri: 371 372 373
|
||||
Tri: 374 375 376
|
||||
Tri: 377 378 379
|
||||
Tri: 0 380 1
|
||||
Tri: 3 381 4
|
||||
Tri: 6 382 7
|
||||
Tri: 9 383 10
|
||||
Tri: 12 384 13
|
||||
Tri: 15 385 16
|
||||
Tri: 18 386 19
|
||||
Tri: 21 387 22
|
||||
Tri: 24 388 25
|
||||
Tri: 27 389 28
|
||||
Tri: 30 390 31
|
||||
Tri: 33 391 34
|
||||
Tri: 36 392 37
|
||||
Tri: 39 393 40
|
||||
Tri: 42 394 43
|
||||
Tri: 45 395 46
|
||||
Tri: 48 396 49
|
||||
Tri: 51 397 52
|
||||
Tri: 54 398 55
|
||||
Tri: 57 399 58
|
||||
Tri: 400 401 402
|
||||
Tri: 403 404 405
|
||||
Tri: 66 406 67
|
||||
Tri: 407 408 409
|
||||
Tri: 410 411 412
|
||||
Tri: 75 93 76
|
||||
Tri: 413 414 415
|
||||
Tri: 81 416 82
|
||||
Tri: 84 102 85
|
||||
Tri: 87 417 88
|
||||
Tri: 418 419 420
|
||||
Tri: 93 421 94
|
||||
Tri: 95 422 96
|
||||
Tri: 98 423 99
|
||||
Tri: 101 424 102
|
||||
Tri: 103 425 104
|
||||
Tri: 106 426 107
|
||||
Tri: 109 128 110
|
||||
Tri: 427 428 429
|
||||
Tri: 430 431 432
|
||||
Tri: 118 433 119
|
||||
Tri: 434 435 436
|
||||
Tri: 124 437 125
|
||||
Tri: 127 438 110
|
||||
Tri: 439 440 441
|
||||
Tri: 442 443 444
|
||||
Tri: 119 433 135
|
||||
Tri: 445 446 447
|
||||
Tri: 448 449 450
|
||||
Tri: 143 451 144
|
||||
Tri: 452 453 454
|
||||
Tri: 149 455 150
|
||||
Tri: 152 456 153
|
||||
Tri: 155 457 156
|
||||
Tri: 458 459 460
|
||||
Tri: 161 180 162
|
||||
Tri: 164 461 165
|
||||
Tri: 167 462 168
|
||||
Tri: 170 463 171
|
||||
Tri: 173 464 174
|
||||
Tri: 176 465 177
|
||||
Tri: 161 197 179
|
||||
Tri: 466 467 468
|
||||
Tri: 469 470 471
|
||||
Tri: 187 472 188
|
||||
Tri: 473 474 475
|
||||
Tri: 193 476 194
|
||||
Tri: 196 477 179
|
||||
Tri: 478 479 480
|
||||
Tri: 481 482 483
|
||||
Tri: 188 472 204
|
||||
Tri: 484 485 486
|
||||
Tri: 487 488 489
|
||||
Tri: 212 230 213
|
||||
Tri: 490 491 492
|
||||
Tri: 218 493 219
|
||||
Tri: 221 239 222
|
||||
Tri: 224 494 225
|
||||
Tri: 495 496 497
|
||||
Tri: 230 498 231
|
||||
Tri: 232 499 233
|
||||
Tri: 235 500 236
|
||||
Tri: 238 501 239
|
||||
Tri: 240 502 241
|
||||
Tri: 243 503 244
|
||||
Tri: 246 265 247
|
||||
Tri: 504 505 506
|
||||
Tri: 507 508 509
|
||||
Tri: 255 510 256
|
||||
Tri: 511 512 513
|
||||
Tri: 261 514 262
|
||||
Tri: 264 515 247
|
||||
Tri: 516 517 518
|
||||
Tri: 519 520 521
|
||||
Tri: 256 510 272
|
||||
Tri: 522 523 524
|
||||
Tri: 525 526 527
|
||||
Tri: 280 528 281
|
||||
Tri: 529 530 531
|
||||
Tri: 286 532 287
|
||||
Tri: 289 308 290
|
||||
Tri: 292 533 293
|
||||
Tri: 534 535 536
|
||||
Tri: 298 316 299
|
||||
Tri: 301 537 302
|
||||
Tri: 304 538 305
|
||||
Tri: 307 539 308
|
||||
Tri: 309 540 310
|
||||
Tri: 312 541 313
|
||||
Tri: 298 542 315
|
||||
Tri: 543 544 545
|
||||
Tri: 546 547 548
|
||||
Tri: 323 549 324
|
||||
Tri: 550 551 552
|
||||
Tri: 329 553 330
|
||||
Tri: 332 554 333
|
||||
Tri: 335 555 336
|
||||
Tri: 338 556 339
|
||||
Tri: 341 557 342
|
||||
Tri: 344 558 345
|
||||
Tri: 347 559 348
|
||||
Tri: 350 560 351
|
||||
Tri: 353 561 354
|
||||
Tri: 356 562 357
|
||||
Tri: 359 563 360
|
||||
Tri: 362 564 363
|
||||
Tri: 365 565 366
|
||||
Tri: 368 566 369
|
||||
Tri: 371 567 372
|
||||
Tri: 374 568 375
|
||||
Tri: 379 569 570
|
||||
Tri: 570 571 572
|
||||
Tri: 572 573 574
|
||||
Tri: 574 575 377
|
||||
Tri: 377 576 577
|
||||
Tri: 577 578 378
|
||||
Tri: 378 579 580
|
||||
Tri: 580 581 379
|
||||
Tri: 379 570 572
|
||||
Tri: 572 574 377
|
||||
Tri: 377 577 378
|
||||
Tri: 378 580 379
|
||||
Tri: 379 572 377
|
||||
@@ -0,0 +1,859 @@
|
||||
===Vertices (Split by UV/Normal): 582
|
||||
V 0: Pos(0.32, 0.0, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.312667)
|
||||
V 1: Pos(0.295641, 0.122459, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.001953)
|
||||
V 2: Pos(0.32, 0.0, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.001953)
|
||||
V 3: Pos(0.226274, 0.226274, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.001953)
|
||||
V 4: Pos(0.295641, 0.122459, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.312667)
|
||||
V 5: Pos(0.295641, 0.122459, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.001953)
|
||||
V 6: Pos(0.226274, 0.226274, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.312667)
|
||||
V 7: Pos(0.122459, 0.295641, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.001953)
|
||||
V 8: Pos(0.226274, 0.226274, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.001953)
|
||||
V 9: Pos(0.122459, 0.295641, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.312667)
|
||||
V 10: Pos(0.0, 0.32, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 11: Pos(0.122459, 0.295641, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.001953)
|
||||
V 12: Pos(0.0, 0.32, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 13: Pos(-0.122459, 0.295641, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.001953)
|
||||
V 14: Pos(0.0, 0.32, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 15: Pos(-0.226274, 0.226274, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.001953)
|
||||
V 16: Pos(-0.122459, 0.295641, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.312667)
|
||||
V 17: Pos(-0.122459, 0.295641, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.001953)
|
||||
V 18: Pos(-0.226274, 0.226274, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.312667)
|
||||
V 19: Pos(-0.295641, 0.122459, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.001953)
|
||||
V 20: Pos(-0.226274, 0.226274, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.001953)
|
||||
V 21: Pos(-0.295641, 0.122459, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.312667)
|
||||
V 22: Pos(-0.32, 0.0, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 23: Pos(-0.295641, 0.122459, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.001953)
|
||||
V 24: Pos(-0.32, 0.0, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 25: Pos(-0.295641, -0.122459, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.001953)
|
||||
V 26: Pos(-0.32, 0.0, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 27: Pos(-0.226274, -0.226274, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.001953)
|
||||
V 28: Pos(-0.295641, -0.122459, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.312667)
|
||||
V 29: Pos(-0.295641, -0.122459, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.001953)
|
||||
V 30: Pos(-0.226274, -0.226274, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.312667)
|
||||
V 31: Pos(-0.122459, -0.295641, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.001953)
|
||||
V 32: Pos(-0.226274, -0.226274, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.001953)
|
||||
V 33: Pos(-0.122459, -0.295641, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.312667)
|
||||
V 34: Pos(0.0, -0.32, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 35: Pos(-0.122459, -0.295641, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.001953)
|
||||
V 36: Pos(0.0, -0.32, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 37: Pos(0.122459, -0.295641, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.001953)
|
||||
V 38: Pos(0.0, -0.32, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 39: Pos(0.226274, -0.226274, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.001953)
|
||||
V 40: Pos(0.122459, -0.295641, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.312667)
|
||||
V 41: Pos(0.122459, -0.295641, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.001953)
|
||||
V 42: Pos(0.226274, -0.226274, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.312667)
|
||||
V 43: Pos(0.295641, -0.122459, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.001953)
|
||||
V 44: Pos(0.226274, -0.226274, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.001953)
|
||||
V 45: Pos(0.295641, -0.122459, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.312667)
|
||||
V 46: Pos(0.32, 0.0, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.001953)
|
||||
V 47: Pos(0.295641, -0.122459, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.001953)
|
||||
V 48: Pos(0.324, 0.0, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.600586, 0.506836)
|
||||
V 49: Pos(0.299337, -0.123989, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.506836, 0.694336)
|
||||
V 50: Pos(0.299337, -0.123989, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.506836, 0.506836)
|
||||
V 51: Pos(0.299337, 0.123989, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.694336, 0.506836)
|
||||
V 52: Pos(0.324, 0.0, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.600586, 0.694336)
|
||||
V 53: Pos(0.324, 0.0, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.600586, 0.506836)
|
||||
V 54: Pos(0.316891, 0.13126, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.505859)
|
||||
V 55: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.547526)
|
||||
V 56: Pos(0.343, 0.0, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.505859)
|
||||
V 57: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.547526)
|
||||
V 58: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.589193)
|
||||
V 59: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.547526)
|
||||
V 60: Pos(0.309, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.630859)
|
||||
V 61: Pos(0.293332, 0.121502, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.589193)
|
||||
V 62: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.589193)
|
||||
V 63: Pos(0.285479, 0.118249, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.048828, 0.630859)
|
||||
V 64: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.672526)
|
||||
V 65: Pos(0.309, 0.0, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.630859)
|
||||
V 66: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.672526)
|
||||
V 67: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.714193)
|
||||
V 68: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.672526)
|
||||
V 69: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.714193)
|
||||
V 70: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.755859)
|
||||
V 71: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.714193)
|
||||
V 72: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.547526)
|
||||
V 73: Pos(0.242538, 0.242538, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.095703, 0.505859)
|
||||
V 74: Pos(0.316891, 0.13126, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.505859)
|
||||
V 75: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.589193)
|
||||
V 76: Pos(0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.547526)
|
||||
V 77: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.547526)
|
||||
V 78: Pos(0.285479, 0.118249, 0.9) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 79: Pos(0.224506, 0.224506, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 80: Pos(0.293332, 0.121502, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.589193)
|
||||
V 81: Pos(0.293332, 0.121502, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.672526)
|
||||
V 82: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.630859)
|
||||
V 83: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.630859)
|
||||
V 84: Pos(0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.672526)
|
||||
V 85: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.714193)
|
||||
V 86: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.672526)
|
||||
V 87: Pos(0.236527, 0.236527, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.714193)
|
||||
V 88: Pos(0.316891, 0.13126, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.755859)
|
||||
V 89: Pos(0.309038, 0.128008, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.714193)
|
||||
V 90: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.547526)
|
||||
V 91: Pos(0.13126, 0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.142578, 0.505859)
|
||||
V 92: Pos(0.242538, 0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.505859)
|
||||
V 93: Pos(0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.589193)
|
||||
V 94: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.547526)
|
||||
V 95: Pos(0.121502, 0.293332, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.589193)
|
||||
V 96: Pos(0.218496, 0.218496, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.630859)
|
||||
V 97: Pos(0.224506, 0.224506, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.589193)
|
||||
V 98: Pos(0.118249, 0.285479, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.630859)
|
||||
V 99: Pos(0.224506, 0.224506, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.672526)
|
||||
V 100: Pos(0.218496, 0.218496, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.630859)
|
||||
V 101: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.672526)
|
||||
V 102: Pos(0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.714193)
|
||||
V 103: Pos(0.128008, 0.309038, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.714193)
|
||||
V 104: Pos(0.242538, 0.242538, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.755859)
|
||||
V 105: Pos(0.236527, 0.236527, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.714193)
|
||||
V 106: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 107: Pos(0.128008, 0.309038, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.547526)
|
||||
V 108: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.505859)
|
||||
V 109: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.547526)
|
||||
V 110: Pos(0.0, 0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.589193)
|
||||
V 111: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.589193)
|
||||
V 112: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.630859)
|
||||
V 113: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.189453, 0.589193)
|
||||
V 114: Pos(0.121502, 0.293332, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.589193)
|
||||
V 115: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.189453, 0.630859)
|
||||
V 116: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.672526)
|
||||
V 117: Pos(0.118249, 0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.630859)
|
||||
V 118: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.714193)
|
||||
V 119: Pos(0.0, 0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.672526)
|
||||
V 120: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.672526)
|
||||
V 121: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.189453, 0.714193)
|
||||
V 122: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.755859)
|
||||
V 123: Pos(0.128008, 0.309038, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.714193)
|
||||
V 124: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.505859)
|
||||
V 125: Pos(0.0, 0.3345, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 126: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 127: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.547526)
|
||||
V 128: Pos(0.0, 0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.547526)
|
||||
V 129: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 130: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.589193)
|
||||
V 131: Pos(0.0, 0.3175, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 132: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.236328, 0.630859)
|
||||
V 133: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.672526)
|
||||
V 134: Pos(0.0, 0.309, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.630859)
|
||||
V 135: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.714193)
|
||||
V 136: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.672526)
|
||||
V 137: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.714193)
|
||||
V 138: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 139: Pos(0.0, 0.3345, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 140: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.547526)
|
||||
V 141: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.283203, 0.505859)
|
||||
V 142: Pos(-0.13126, 0.316891, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.505859)
|
||||
V 143: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.589193)
|
||||
V 144: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.547526)
|
||||
V 145: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.547526)
|
||||
V 146: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.283203, 0.589193)
|
||||
V 147: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.630859)
|
||||
V 148: Pos(-0.121502, 0.293332, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.589193)
|
||||
V 149: Pos(-0.121502, 0.293332, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.672526)
|
||||
V 150: Pos(-0.218496, 0.218496, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.630859)
|
||||
V 151: Pos(-0.118249, 0.285479, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.630859)
|
||||
V 152: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.672526)
|
||||
V 153: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.714193)
|
||||
V 154: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.672526)
|
||||
V 155: Pos(-0.13126, 0.316891, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.755859)
|
||||
V 156: Pos(-0.236527, 0.236527, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.714193)
|
||||
V 157: Pos(-0.128008, 0.309038, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.714193)
|
||||
V 158: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.547526)
|
||||
V 159: Pos(-0.316891, 0.13126, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.080078, 0.505859)
|
||||
V 160: Pos(-0.242538, 0.242538, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.505859)
|
||||
V 161: Pos(-0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.547526)
|
||||
V 162: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.589193)
|
||||
V 163: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.547526)
|
||||
V 164: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.630859)
|
||||
V 165: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.589193)
|
||||
V 166: Pos(-0.224506, 0.224506, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.589193)
|
||||
V 167: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.672526)
|
||||
V 168: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.630859)
|
||||
V 169: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.630859)
|
||||
V 170: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.672526)
|
||||
V 171: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.714193)
|
||||
V 172: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.672526)
|
||||
V 173: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.714193)
|
||||
V 174: Pos(-0.242538, 0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.755859)
|
||||
V 175: Pos(-0.236527, 0.236527, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.714193)
|
||||
V 176: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 177: Pos(-0.309038, 0.128008, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.547526)
|
||||
V 178: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.505859)
|
||||
V 179: Pos(-0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.589193)
|
||||
V 180: Pos(-0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.589193)
|
||||
V 181: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.630859)
|
||||
V 182: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.126953, 0.589193)
|
||||
V 183: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.589193)
|
||||
V 184: Pos(-0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 185: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 186: Pos(-0.285479, 0.118249, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.630859)
|
||||
V 187: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.714193)
|
||||
V 188: Pos(-0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.672526)
|
||||
V 189: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.672526)
|
||||
V 190: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.126953, 0.714193)
|
||||
V 191: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.755859)
|
||||
V 192: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.714193)
|
||||
V 193: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.505859)
|
||||
V 194: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 195: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 196: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.547526)
|
||||
V 197: Pos(-0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.547526)
|
||||
V 198: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 199: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.589193)
|
||||
V 200: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 201: Pos(-0.285479, -0.118249, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.173828, 0.630859)
|
||||
V 202: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.672526)
|
||||
V 203: Pos(-0.309, 0.0, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.630859)
|
||||
V 204: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.714193)
|
||||
V 205: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.672526)
|
||||
V 206: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.714193)
|
||||
V 207: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 208: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 209: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.547526)
|
||||
V 210: Pos(-0.242538, -0.242538, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.220703, 0.505859)
|
||||
V 211: Pos(-0.316891, -0.13126, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.505859)
|
||||
V 212: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.589193)
|
||||
V 213: Pos(-0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.547526)
|
||||
V 214: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.547526)
|
||||
V 215: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 216: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 217: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.589193)
|
||||
V 218: Pos(-0.293332, -0.121502, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.672526)
|
||||
V 219: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.630859)
|
||||
V 220: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.630859)
|
||||
V 221: Pos(-0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.672526)
|
||||
V 222: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.714193)
|
||||
V 223: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.672526)
|
||||
V 224: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.714193)
|
||||
V 225: Pos(-0.316891, -0.13126, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.755859)
|
||||
V 226: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.714193)
|
||||
V 227: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.547526)
|
||||
V 228: Pos(-0.13126, -0.316891, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.267578, 0.505859)
|
||||
V 229: Pos(-0.242538, -0.242538, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.505859)
|
||||
V 230: Pos(-0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.589193)
|
||||
V 231: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.547526)
|
||||
V 232: Pos(-0.121502, -0.293332, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.589193)
|
||||
V 233: Pos(-0.218496, -0.218496, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.630859)
|
||||
V 234: Pos(-0.224506, -0.224506, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.589193)
|
||||
V 235: Pos(-0.118249, -0.285479, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.630859)
|
||||
V 236: Pos(-0.224506, -0.224506, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.672526)
|
||||
V 237: Pos(-0.218496, -0.218496, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.630859)
|
||||
V 238: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.672526)
|
||||
V 239: Pos(-0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.714193)
|
||||
V 240: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.714193)
|
||||
V 241: Pos(-0.242538, -0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.755859)
|
||||
V 242: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.714193)
|
||||
V 243: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 244: Pos(-0.128008, -0.309038, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.547526)
|
||||
V 245: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.505859)
|
||||
V 246: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.547526)
|
||||
V 247: Pos(0.0, -0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.589193)
|
||||
V 248: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.589193)
|
||||
V 249: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.630859)
|
||||
V 250: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.064453, 0.589193)
|
||||
V 251: Pos(-0.121502, -0.293332, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.589193)
|
||||
V 252: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.064453, 0.630859)
|
||||
V 253: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.672526)
|
||||
V 254: Pos(-0.118249, -0.285479, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.630859)
|
||||
V 255: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.714193)
|
||||
V 256: Pos(0.0, -0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.672526)
|
||||
V 257: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.672526)
|
||||
V 258: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.064453, 0.714193)
|
||||
V 259: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.755859)
|
||||
V 260: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.714193)
|
||||
V 261: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.505859)
|
||||
V 262: Pos(0.0, -0.3345, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 263: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 264: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.547526)
|
||||
V 265: Pos(0.0, -0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.547526)
|
||||
V 266: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 267: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.589193)
|
||||
V 268: Pos(0.0, -0.3175, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 269: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.111328, 0.630859)
|
||||
V 270: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.672526)
|
||||
V 271: Pos(0.0, -0.309, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.630859)
|
||||
V 272: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.714193)
|
||||
V 273: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.672526)
|
||||
V 274: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.714193)
|
||||
V 275: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 276: Pos(0.0, -0.3345, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 277: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.547526)
|
||||
V 278: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.158203, 0.505859)
|
||||
V 279: Pos(0.13126, -0.316891, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.505859)
|
||||
V 280: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.589193)
|
||||
V 281: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.547526)
|
||||
V 282: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.547526)
|
||||
V 283: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.158203, 0.589193)
|
||||
V 284: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.630859)
|
||||
V 285: Pos(0.121502, -0.293332, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.589193)
|
||||
V 286: Pos(0.121502, -0.293332, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.672526)
|
||||
V 287: Pos(0.218496, -0.218496, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.630859)
|
||||
V 288: Pos(0.118249, -0.285479, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.630859)
|
||||
V 289: Pos(0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.672526)
|
||||
V 290: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.714193)
|
||||
V 291: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.672526)
|
||||
V 292: Pos(0.13126, -0.316891, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.755859)
|
||||
V 293: Pos(0.236527, -0.236527, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.714193)
|
||||
V 294: Pos(0.128008, -0.309038, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.714193)
|
||||
V 295: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.547526)
|
||||
V 296: Pos(0.316891, -0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.205078, 0.505859)
|
||||
V 297: Pos(0.242538, -0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.505859)
|
||||
V 298: Pos(0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.547526)
|
||||
V 299: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.589193)
|
||||
V 300: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.547526)
|
||||
V 301: Pos(0.218496, -0.218496, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.630859)
|
||||
V 302: Pos(0.293332, -0.121502, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.589193)
|
||||
V 303: Pos(0.224506, -0.224506, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.589193)
|
||||
V 304: Pos(0.224506, -0.224506, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.672526)
|
||||
V 305: Pos(0.285479, -0.118249, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.630859)
|
||||
V 306: Pos(0.218496, -0.218496, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.630859)
|
||||
V 307: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.672526)
|
||||
V 308: Pos(0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.714193)
|
||||
V 309: Pos(0.309038, -0.128008, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.714193)
|
||||
V 310: Pos(0.242538, -0.242538, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.755859)
|
||||
V 311: Pos(0.236527, -0.236527, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.714193)
|
||||
V 312: Pos(0.343, 0.0, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.505859)
|
||||
V 313: Pos(0.309038, -0.128008, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.547526)
|
||||
V 314: Pos(0.316891, -0.13126, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.505859)
|
||||
V 315: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.589193)
|
||||
V 316: Pos(0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.589193)
|
||||
V 317: Pos(0.285479, -0.118249, 0.9) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.630859)
|
||||
V 318: Pos(0.3175, 0.0, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.251953, 0.589193)
|
||||
V 319: Pos(0.293332, -0.121502, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.589193)
|
||||
V 320: Pos(0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 321: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 322: Pos(0.285479, -0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.630859)
|
||||
V 323: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.714193)
|
||||
V 324: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.672526)
|
||||
V 325: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.672526)
|
||||
V 326: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.251953, 0.714193)
|
||||
V 327: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.755859)
|
||||
V 328: Pos(0.309038, -0.128008, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.714193)
|
||||
V 329: Pos(0.308, 0.0, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 330: Pos(0.284555, 0.117866, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 331: Pos(0.284555, 0.117866, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 332: Pos(0.284555, 0.117866, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 333: Pos(0.217789, 0.217789, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 334: Pos(0.217789, 0.217789, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 335: Pos(0.217789, 0.217789, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 336: Pos(0.117866, 0.284555, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 337: Pos(0.117866, 0.284555, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 338: Pos(0.117866, 0.284555, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 339: Pos(0.0, 0.308, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 340: Pos(0.0, 0.308, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 341: Pos(0.0, 0.308, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 342: Pos(-0.117866, 0.284555, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 343: Pos(-0.117866, 0.284555, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 344: Pos(-0.117866, 0.284555, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 345: Pos(-0.217789, 0.217789, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 346: Pos(-0.217789, 0.217789, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 347: Pos(-0.217789, 0.217789, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 348: Pos(-0.284555, 0.117866, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 349: Pos(-0.284555, 0.117866, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 350: Pos(-0.284555, 0.117866, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 351: Pos(-0.308, 0.0, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 352: Pos(-0.308, 0.0, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 353: Pos(-0.308, 0.0, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 354: Pos(-0.284555, -0.117866, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 355: Pos(-0.284555, -0.117866, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 356: Pos(-0.284555, -0.117866, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 357: Pos(-0.217789, -0.217789, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 358: Pos(-0.217789, -0.217789, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 359: Pos(-0.217789, -0.217789, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 360: Pos(-0.117866, -0.284555, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 361: Pos(-0.117866, -0.284555, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 362: Pos(-0.117866, -0.284555, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 363: Pos(0.0, -0.308, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 364: Pos(0.0, -0.308, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 365: Pos(0.0, -0.308, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 366: Pos(0.117866, -0.284555, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 367: Pos(0.117866, -0.284555, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 368: Pos(0.117866, -0.284555, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 369: Pos(0.217789, -0.217789, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 370: Pos(0.217789, -0.217789, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 371: Pos(0.217789, -0.217789, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 372: Pos(0.284555, -0.117866, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 373: Pos(0.284555, -0.117866, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 374: Pos(0.284555, -0.117866, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 375: Pos(0.308, 0.0, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 376: Pos(0.308, 0.0, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 377: Pos(-0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 378: Pos(0.0, 0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 379: Pos(0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 380: Pos(0.295641, 0.122459, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.312667)
|
||||
V 381: Pos(0.226274, 0.226274, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.312667)
|
||||
V 382: Pos(0.122459, 0.295641, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.312667)
|
||||
V 383: Pos(0.0, 0.32, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 384: Pos(-0.122459, 0.295641, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.312667)
|
||||
V 385: Pos(-0.226274, 0.226274, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.312667)
|
||||
V 386: Pos(-0.295641, 0.122459, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.312667)
|
||||
V 387: Pos(-0.32, 0.0, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 388: Pos(-0.295641, -0.122459, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.312667)
|
||||
V 389: Pos(-0.226274, -0.226274, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.312667)
|
||||
V 390: Pos(-0.122459, -0.295641, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.312667)
|
||||
V 391: Pos(0.0, -0.32, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 392: Pos(0.122459, -0.295641, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.312667)
|
||||
V 393: Pos(0.226274, -0.226274, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.312667)
|
||||
V 394: Pos(0.295641, -0.122459, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.312667)
|
||||
V 395: Pos(0.32, 0.0, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.312667)
|
||||
V 396: Pos(0.324, 0.0, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.600586, 0.694336)
|
||||
V 397: Pos(0.299337, 0.123989, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.694336, 0.694336)
|
||||
V 398: Pos(0.309038, 0.128008, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.547526)
|
||||
V 399: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.589193)
|
||||
V 400: Pos(0.309, 0.0, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.001953, 0.630859)
|
||||
V 401: Pos(0.285479, 0.118249, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.630859)
|
||||
V 402: Pos(0.293332, 0.121502, 0.914722) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.589193)
|
||||
V 403: Pos(0.285479, 0.118249, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.630859)
|
||||
V 404: Pos(0.293332, 0.121502, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.672526)
|
||||
V 405: Pos(0.3175, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.001953, 0.672526)
|
||||
V 406: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.714193)
|
||||
V 407: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.714193)
|
||||
V 408: Pos(0.316891, 0.13126, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.755859)
|
||||
V 409: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.001953, 0.755859)
|
||||
V 410: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.048828, 0.547526)
|
||||
V 411: Pos(0.236527, 0.236527, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.547526)
|
||||
V 412: Pos(0.242538, 0.242538, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.505859)
|
||||
V 413: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 414: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.630859)
|
||||
V 415: Pos(0.224506, 0.224506, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 416: Pos(0.224506, 0.224506, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.672526)
|
||||
V 417: Pos(0.242538, 0.242538, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.755859)
|
||||
V 418: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.095703, 0.547526)
|
||||
V 419: Pos(0.128008, 0.309038, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.547526)
|
||||
V 420: Pos(0.13126, 0.316891, 0.9) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.505859)
|
||||
V 421: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.589193)
|
||||
V 422: Pos(0.118249, 0.285479, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.630859)
|
||||
V 423: Pos(0.121502, 0.293332, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.672526)
|
||||
V 424: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.714193)
|
||||
V 425: Pos(0.13126, 0.316891, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.755859)
|
||||
V 426: Pos(0.0, 0.3345, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 427: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.630859)
|
||||
V 428: Pos(0.0, 0.309, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 429: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 430: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.630859)
|
||||
V 431: Pos(0.0, 0.3175, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.672526)
|
||||
V 432: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.142578, 0.672526)
|
||||
V 433: Pos(0.0, 0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.714193)
|
||||
V 434: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 435: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 436: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.755859)
|
||||
V 437: Pos(-0.128008, 0.309038, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.547526)
|
||||
V 438: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.589193)
|
||||
V 439: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.189453, 0.630859)
|
||||
V 440: Pos(-0.118249, 0.285479, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.630859)
|
||||
V 441: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.589193)
|
||||
V 442: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.630859)
|
||||
V 443: Pos(-0.121502, 0.293332, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.672526)
|
||||
V 444: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.189453, 0.672526)
|
||||
V 445: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.714193)
|
||||
V 446: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.755859)
|
||||
V 447: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.189453, 0.755859)
|
||||
V 448: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.236328, 0.547526)
|
||||
V 449: Pos(-0.236527, 0.236527, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.547526)
|
||||
V 450: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.505859)
|
||||
V 451: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.589193)
|
||||
V 452: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.589193)
|
||||
V 453: Pos(-0.218496, 0.218496, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.630859)
|
||||
V 454: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.236328, 0.630859)
|
||||
V 455: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.672526)
|
||||
V 456: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.714193)
|
||||
V 457: Pos(-0.242538, 0.242538, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.755859)
|
||||
V 458: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.033203, 0.547526)
|
||||
V 459: Pos(-0.309038, 0.128008, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.547526)
|
||||
V 460: Pos(-0.316891, 0.13126, 0.9) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.505859)
|
||||
V 461: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.630859)
|
||||
V 462: Pos(-0.293332, 0.121502, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.672526)
|
||||
V 463: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.714193)
|
||||
V 464: Pos(-0.316891, 0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.755859)
|
||||
V 465: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 466: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.630859)
|
||||
V 467: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 468: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 469: Pos(-0.309, 0.0, 0.9) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 470: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 471: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 472: Pos(-0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.714193)
|
||||
V 473: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 474: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 475: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.755859)
|
||||
V 476: Pos(-0.309038, -0.128008, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.547526)
|
||||
V 477: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.589193)
|
||||
V 478: Pos(-0.309, 0.0, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.126953, 0.630859)
|
||||
V 479: Pos(-0.285479, -0.118249, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.630859)
|
||||
V 480: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.589193)
|
||||
V 481: Pos(-0.285479, -0.118249, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.630859)
|
||||
V 482: Pos(-0.293332, -0.121502, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.672526)
|
||||
V 483: Pos(-0.3175, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 484: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.714193)
|
||||
V 485: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.755859)
|
||||
V 486: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.126953, 0.755859)
|
||||
V 487: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.173828, 0.547526)
|
||||
V 488: Pos(-0.236527, -0.236527, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.547526)
|
||||
V 489: Pos(-0.242538, -0.242538, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.505859)
|
||||
V 490: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 491: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.630859)
|
||||
V 492: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 493: Pos(-0.224506, -0.224506, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.672526)
|
||||
V 494: Pos(-0.242538, -0.242538, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.755859)
|
||||
V 495: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.220703, 0.547526)
|
||||
V 496: Pos(-0.128008, -0.309038, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.547526)
|
||||
V 497: Pos(-0.13126, -0.316891, 0.9) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.505859)
|
||||
V 498: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.589193)
|
||||
V 499: Pos(-0.118249, -0.285479, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.630859)
|
||||
V 500: Pos(-0.121502, -0.293332, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.672526)
|
||||
V 501: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.714193)
|
||||
V 502: Pos(-0.13126, -0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.755859)
|
||||
V 503: Pos(0.0, -0.3345, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 504: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.630859)
|
||||
V 505: Pos(0.0, -0.309, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 506: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 507: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.630859)
|
||||
V 508: Pos(0.0, -0.3175, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.672526)
|
||||
V 509: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.017578, 0.672526)
|
||||
V 510: Pos(0.0, -0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.714193)
|
||||
V 511: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 512: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 513: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.755859)
|
||||
V 514: Pos(0.128008, -0.309038, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.547526)
|
||||
V 515: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.589193)
|
||||
V 516: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.064453, 0.630859)
|
||||
V 517: Pos(0.118249, -0.285479, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.630859)
|
||||
V 518: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.589193)
|
||||
V 519: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.630859)
|
||||
V 520: Pos(0.121502, -0.293332, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.672526)
|
||||
V 521: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.064453, 0.672526)
|
||||
V 522: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.714193)
|
||||
V 523: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.755859)
|
||||
V 524: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.064453, 0.755859)
|
||||
V 525: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.111328, 0.547526)
|
||||
V 526: Pos(0.236527, -0.236527, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.547526)
|
||||
V 527: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.505859)
|
||||
V 528: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.589193)
|
||||
V 529: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.589193)
|
||||
V 530: Pos(0.218496, -0.218496, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.630859)
|
||||
V 531: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.111328, 0.630859)
|
||||
V 532: Pos(0.224506, -0.224506, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.672526)
|
||||
V 533: Pos(0.242538, -0.242538, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.755859)
|
||||
V 534: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.158203, 0.547526)
|
||||
V 535: Pos(0.309038, -0.128008, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.547526)
|
||||
V 536: Pos(0.316891, -0.13126, 0.9) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.505859)
|
||||
V 537: Pos(0.285479, -0.118249, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.630859)
|
||||
V 538: Pos(0.293332, -0.121502, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.672526)
|
||||
V 539: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.714193)
|
||||
V 540: Pos(0.316891, -0.13126, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.755859)
|
||||
V 541: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.547526)
|
||||
V 542: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.547526)
|
||||
V 543: Pos(0.285479, -0.118249, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.630859)
|
||||
V 544: Pos(0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.630859)
|
||||
V 545: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.589193)
|
||||
V 546: Pos(0.309, 0.0, 0.9) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 547: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.672526)
|
||||
V 548: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 549: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.714193)
|
||||
V 550: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.714193)
|
||||
V 551: Pos(0.343, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.755859)
|
||||
V 552: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.755859)
|
||||
V 553: Pos(0.308, 0.0, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 554: Pos(0.284555, 0.117866, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 555: Pos(0.217789, 0.217789, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 556: Pos(0.117866, 0.284555, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 557: Pos(0.0, 0.308, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 558: Pos(-0.117866, 0.284555, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 559: Pos(-0.217789, 0.217789, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 560: Pos(-0.284555, 0.117866, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 561: Pos(-0.308, 0.0, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 562: Pos(-0.284555, -0.117866, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 563: Pos(-0.217789, -0.217789, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 564: Pos(-0.117866, -0.284555, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 565: Pos(0.0, -0.308, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 566: Pos(0.117866, -0.284555, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 567: Pos(0.217789, -0.217789, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 568: Pos(0.284555, -0.117866, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 569: Pos(0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 570: Pos(0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 571: Pos(0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 572: Pos(0.0, -0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 573: Pos(-0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 574: Pos(-0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 575: Pos(-0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 576: Pos(-0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 577: Pos(-0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 578: Pos(-0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 579: Pos(0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 580: Pos(0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 581: Pos(0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
|
||||
===Triangles (Indices): 274
|
||||
Tri: 0 1 2
|
||||
Tri: 3 4 5
|
||||
Tri: 6 7 8
|
||||
Tri: 9 10 11
|
||||
Tri: 12 13 14
|
||||
Tri: 15 16 17
|
||||
Tri: 18 19 20
|
||||
Tri: 21 22 23
|
||||
Tri: 24 25 26
|
||||
Tri: 27 28 29
|
||||
Tri: 30 31 32
|
||||
Tri: 33 34 35
|
||||
Tri: 36 37 38
|
||||
Tri: 39 40 41
|
||||
Tri: 42 43 44
|
||||
Tri: 45 46 47
|
||||
Tri: 48 49 50
|
||||
Tri: 51 52 53
|
||||
Tri: 54 55 56
|
||||
Tri: 57 58 59
|
||||
Tri: 60 61 62
|
||||
Tri: 63 64 65
|
||||
Tri: 66 67 68
|
||||
Tri: 69 70 71
|
||||
Tri: 72 73 74
|
||||
Tri: 75 76 77
|
||||
Tri: 78 79 80
|
||||
Tri: 81 82 83
|
||||
Tri: 84 85 86
|
||||
Tri: 87 88 89
|
||||
Tri: 90 91 92
|
||||
Tri: 93 94 76
|
||||
Tri: 95 96 97
|
||||
Tri: 98 99 100
|
||||
Tri: 101 102 84
|
||||
Tri: 103 104 105
|
||||
Tri: 106 107 108
|
||||
Tri: 109 110 111
|
||||
Tri: 112 113 114
|
||||
Tri: 115 116 117
|
||||
Tri: 118 119 120
|
||||
Tri: 121 122 123
|
||||
Tri: 124 125 126
|
||||
Tri: 127 110 128
|
||||
Tri: 129 130 131
|
||||
Tri: 132 133 134
|
||||
Tri: 119 135 136
|
||||
Tri: 137 138 139
|
||||
Tri: 140 141 142
|
||||
Tri: 143 144 145
|
||||
Tri: 146 147 148
|
||||
Tri: 149 150 151
|
||||
Tri: 152 153 154
|
||||
Tri: 155 156 157
|
||||
Tri: 158 159 160
|
||||
Tri: 161 162 163
|
||||
Tri: 164 165 166
|
||||
Tri: 167 168 169
|
||||
Tri: 170 171 172
|
||||
Tri: 173 174 175
|
||||
Tri: 176 177 178
|
||||
Tri: 161 179 180
|
||||
Tri: 181 182 183
|
||||
Tri: 184 185 186
|
||||
Tri: 187 188 189
|
||||
Tri: 190 191 192
|
||||
Tri: 193 194 195
|
||||
Tri: 196 179 197
|
||||
Tri: 198 199 200
|
||||
Tri: 201 202 203
|
||||
Tri: 188 204 205
|
||||
Tri: 206 207 208
|
||||
Tri: 209 210 211
|
||||
Tri: 212 213 214
|
||||
Tri: 215 216 217
|
||||
Tri: 218 219 220
|
||||
Tri: 221 222 223
|
||||
Tri: 224 225 226
|
||||
Tri: 227 228 229
|
||||
Tri: 230 231 213
|
||||
Tri: 232 233 234
|
||||
Tri: 235 236 237
|
||||
Tri: 238 239 221
|
||||
Tri: 240 241 242
|
||||
Tri: 243 244 245
|
||||
Tri: 246 247 248
|
||||
Tri: 249 250 251
|
||||
Tri: 252 253 254
|
||||
Tri: 255 256 257
|
||||
Tri: 258 259 260
|
||||
Tri: 261 262 263
|
||||
Tri: 264 247 265
|
||||
Tri: 266 267 268
|
||||
Tri: 269 270 271
|
||||
Tri: 256 272 273
|
||||
Tri: 274 275 276
|
||||
Tri: 277 278 279
|
||||
Tri: 280 281 282
|
||||
Tri: 283 284 285
|
||||
Tri: 286 287 288
|
||||
Tri: 289 290 291
|
||||
Tri: 292 293 294
|
||||
Tri: 295 296 297
|
||||
Tri: 298 299 300
|
||||
Tri: 301 302 303
|
||||
Tri: 304 305 306
|
||||
Tri: 307 308 289
|
||||
Tri: 309 310 311
|
||||
Tri: 312 313 314
|
||||
Tri: 298 315 316
|
||||
Tri: 317 318 319
|
||||
Tri: 320 321 322
|
||||
Tri: 323 324 325
|
||||
Tri: 326 327 328
|
||||
Tri: 329 330 331
|
||||
Tri: 332 333 334
|
||||
Tri: 335 336 337
|
||||
Tri: 338 339 340
|
||||
Tri: 341 342 343
|
||||
Tri: 344 345 346
|
||||
Tri: 347 348 349
|
||||
Tri: 350 351 352
|
||||
Tri: 353 354 355
|
||||
Tri: 356 357 358
|
||||
Tri: 359 360 361
|
||||
Tri: 362 363 364
|
||||
Tri: 365 366 367
|
||||
Tri: 368 369 370
|
||||
Tri: 371 372 373
|
||||
Tri: 374 375 376
|
||||
Tri: 377 378 379
|
||||
Tri: 0 380 1
|
||||
Tri: 3 381 4
|
||||
Tri: 6 382 7
|
||||
Tri: 9 383 10
|
||||
Tri: 12 384 13
|
||||
Tri: 15 385 16
|
||||
Tri: 18 386 19
|
||||
Tri: 21 387 22
|
||||
Tri: 24 388 25
|
||||
Tri: 27 389 28
|
||||
Tri: 30 390 31
|
||||
Tri: 33 391 34
|
||||
Tri: 36 392 37
|
||||
Tri: 39 393 40
|
||||
Tri: 42 394 43
|
||||
Tri: 45 395 46
|
||||
Tri: 48 396 49
|
||||
Tri: 51 397 52
|
||||
Tri: 54 398 55
|
||||
Tri: 57 399 58
|
||||
Tri: 400 401 402
|
||||
Tri: 403 404 405
|
||||
Tri: 66 406 67
|
||||
Tri: 407 408 409
|
||||
Tri: 410 411 412
|
||||
Tri: 75 93 76
|
||||
Tri: 413 414 415
|
||||
Tri: 81 416 82
|
||||
Tri: 84 102 85
|
||||
Tri: 87 417 88
|
||||
Tri: 418 419 420
|
||||
Tri: 93 421 94
|
||||
Tri: 95 422 96
|
||||
Tri: 98 423 99
|
||||
Tri: 101 424 102
|
||||
Tri: 103 425 104
|
||||
Tri: 106 426 107
|
||||
Tri: 109 128 110
|
||||
Tri: 427 428 429
|
||||
Tri: 430 431 432
|
||||
Tri: 118 433 119
|
||||
Tri: 434 435 436
|
||||
Tri: 124 437 125
|
||||
Tri: 127 438 110
|
||||
Tri: 439 440 441
|
||||
Tri: 442 443 444
|
||||
Tri: 119 433 135
|
||||
Tri: 445 446 447
|
||||
Tri: 448 449 450
|
||||
Tri: 143 451 144
|
||||
Tri: 452 453 454
|
||||
Tri: 149 455 150
|
||||
Tri: 152 456 153
|
||||
Tri: 155 457 156
|
||||
Tri: 458 459 460
|
||||
Tri: 161 180 162
|
||||
Tri: 164 461 165
|
||||
Tri: 167 462 168
|
||||
Tri: 170 463 171
|
||||
Tri: 173 464 174
|
||||
Tri: 176 465 177
|
||||
Tri: 161 197 179
|
||||
Tri: 466 467 468
|
||||
Tri: 469 470 471
|
||||
Tri: 187 472 188
|
||||
Tri: 473 474 475
|
||||
Tri: 193 476 194
|
||||
Tri: 196 477 179
|
||||
Tri: 478 479 480
|
||||
Tri: 481 482 483
|
||||
Tri: 188 472 204
|
||||
Tri: 484 485 486
|
||||
Tri: 487 488 489
|
||||
Tri: 212 230 213
|
||||
Tri: 490 491 492
|
||||
Tri: 218 493 219
|
||||
Tri: 221 239 222
|
||||
Tri: 224 494 225
|
||||
Tri: 495 496 497
|
||||
Tri: 230 498 231
|
||||
Tri: 232 499 233
|
||||
Tri: 235 500 236
|
||||
Tri: 238 501 239
|
||||
Tri: 240 502 241
|
||||
Tri: 243 503 244
|
||||
Tri: 246 265 247
|
||||
Tri: 504 505 506
|
||||
Tri: 507 508 509
|
||||
Tri: 255 510 256
|
||||
Tri: 511 512 513
|
||||
Tri: 261 514 262
|
||||
Tri: 264 515 247
|
||||
Tri: 516 517 518
|
||||
Tri: 519 520 521
|
||||
Tri: 256 510 272
|
||||
Tri: 522 523 524
|
||||
Tri: 525 526 527
|
||||
Tri: 280 528 281
|
||||
Tri: 529 530 531
|
||||
Tri: 286 532 287
|
||||
Tri: 289 308 290
|
||||
Tri: 292 533 293
|
||||
Tri: 534 535 536
|
||||
Tri: 298 316 299
|
||||
Tri: 301 537 302
|
||||
Tri: 304 538 305
|
||||
Tri: 307 539 308
|
||||
Tri: 309 540 310
|
||||
Tri: 312 541 313
|
||||
Tri: 298 542 315
|
||||
Tri: 543 544 545
|
||||
Tri: 546 547 548
|
||||
Tri: 323 549 324
|
||||
Tri: 550 551 552
|
||||
Tri: 329 553 330
|
||||
Tri: 332 554 333
|
||||
Tri: 335 555 336
|
||||
Tri: 338 556 339
|
||||
Tri: 341 557 342
|
||||
Tri: 344 558 345
|
||||
Tri: 347 559 348
|
||||
Tri: 350 560 351
|
||||
Tri: 353 561 354
|
||||
Tri: 356 562 357
|
||||
Tri: 359 563 360
|
||||
Tri: 362 564 363
|
||||
Tri: 365 565 366
|
||||
Tri: 368 566 369
|
||||
Tri: 371 567 372
|
||||
Tri: 374 568 375
|
||||
Tri: 379 569 570
|
||||
Tri: 570 571 572
|
||||
Tri: 572 573 574
|
||||
Tri: 574 575 377
|
||||
Tri: 377 576 577
|
||||
Tri: 577 578 378
|
||||
Tri: 378 579 580
|
||||
Tri: 580 581 379
|
||||
Tri: 379 570 572
|
||||
Tri: 572 574 377
|
||||
Tri: 377 577 378
|
||||
Tri: 378 580 379
|
||||
Tri: 379 572 377
|
||||
@@ -0,0 +1,859 @@
|
||||
===Vertices (Split by UV/Normal): 582
|
||||
V 0: Pos(0.32, 0.0, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.312667)
|
||||
V 1: Pos(0.295641, 0.122459, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.001953)
|
||||
V 2: Pos(0.32, 0.0, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.001953)
|
||||
V 3: Pos(0.226274, 0.226274, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.001953)
|
||||
V 4: Pos(0.295641, 0.122459, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.312667)
|
||||
V 5: Pos(0.295641, 0.122459, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.001953)
|
||||
V 6: Pos(0.226274, 0.226274, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.312667)
|
||||
V 7: Pos(0.122459, 0.295641, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.001953)
|
||||
V 8: Pos(0.226274, 0.226274, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.001953)
|
||||
V 9: Pos(0.122459, 0.295641, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.312667)
|
||||
V 10: Pos(0.0, 0.32, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 11: Pos(0.122459, 0.295641, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.001953)
|
||||
V 12: Pos(0.0, 0.32, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 13: Pos(-0.122459, 0.295641, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.001953)
|
||||
V 14: Pos(0.0, 0.32, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 15: Pos(-0.226274, 0.226274, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.001953)
|
||||
V 16: Pos(-0.122459, 0.295641, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.312667)
|
||||
V 17: Pos(-0.122459, 0.295641, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.001953)
|
||||
V 18: Pos(-0.226274, 0.226274, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.312667)
|
||||
V 19: Pos(-0.295641, 0.122459, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.001953)
|
||||
V 20: Pos(-0.226274, 0.226274, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.001953)
|
||||
V 21: Pos(-0.295641, 0.122459, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.312667)
|
||||
V 22: Pos(-0.32, 0.0, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 23: Pos(-0.295641, 0.122459, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.001953)
|
||||
V 24: Pos(-0.32, 0.0, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 25: Pos(-0.295641, -0.122459, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.001953)
|
||||
V 26: Pos(-0.32, 0.0, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 27: Pos(-0.226274, -0.226274, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.001953)
|
||||
V 28: Pos(-0.295641, -0.122459, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.312667)
|
||||
V 29: Pos(-0.295641, -0.122459, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.001953)
|
||||
V 30: Pos(-0.226274, -0.226274, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.312667)
|
||||
V 31: Pos(-0.122459, -0.295641, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.001953)
|
||||
V 32: Pos(-0.226274, -0.226274, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.001953)
|
||||
V 33: Pos(-0.122459, -0.295641, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.312667)
|
||||
V 34: Pos(0.0, -0.32, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 35: Pos(-0.122459, -0.295641, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.001953)
|
||||
V 36: Pos(0.0, -0.32, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 37: Pos(0.122459, -0.295641, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.001953)
|
||||
V 38: Pos(0.0, -0.32, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 39: Pos(0.226274, -0.226274, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.001953)
|
||||
V 40: Pos(0.122459, -0.295641, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.312667)
|
||||
V 41: Pos(0.122459, -0.295641, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.001953)
|
||||
V 42: Pos(0.226274, -0.226274, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.312667)
|
||||
V 43: Pos(0.295641, -0.122459, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.001953)
|
||||
V 44: Pos(0.226274, -0.226274, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.001953)
|
||||
V 45: Pos(0.295641, -0.122459, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.312667)
|
||||
V 46: Pos(0.32, 0.0, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.001953)
|
||||
V 47: Pos(0.295641, -0.122459, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.001953)
|
||||
V 48: Pos(0.324, 0.0, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.599609, 0.505859)
|
||||
V 49: Pos(0.299337, -0.123989, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.505859, 0.693359)
|
||||
V 50: Pos(0.299337, -0.123989, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.505859, 0.505859)
|
||||
V 51: Pos(0.299337, 0.123989, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.693359, 0.505859)
|
||||
V 52: Pos(0.324, 0.0, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.599609, 0.693359)
|
||||
V 53: Pos(0.324, 0.0, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.599609, 0.505859)
|
||||
V 54: Pos(0.316891, 0.13126, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.505859)
|
||||
V 55: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.547526)
|
||||
V 56: Pos(0.343, 0.0, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.505859)
|
||||
V 57: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.547526)
|
||||
V 58: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.589193)
|
||||
V 59: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.547526)
|
||||
V 60: Pos(0.309, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.630859)
|
||||
V 61: Pos(0.293332, 0.121502, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.589193)
|
||||
V 62: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.589193)
|
||||
V 63: Pos(0.285479, 0.118249, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.048828, 0.630859)
|
||||
V 64: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.672526)
|
||||
V 65: Pos(0.309, 0.0, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.630859)
|
||||
V 66: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.672526)
|
||||
V 67: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.714193)
|
||||
V 68: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.672526)
|
||||
V 69: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.714193)
|
||||
V 70: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.755859)
|
||||
V 71: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.714193)
|
||||
V 72: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.547526)
|
||||
V 73: Pos(0.242538, 0.242538, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.095703, 0.505859)
|
||||
V 74: Pos(0.316891, 0.13126, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.505859)
|
||||
V 75: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.589193)
|
||||
V 76: Pos(0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.547526)
|
||||
V 77: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.547526)
|
||||
V 78: Pos(0.285479, 0.118249, 0.9) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 79: Pos(0.224506, 0.224506, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 80: Pos(0.293332, 0.121502, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.589193)
|
||||
V 81: Pos(0.293332, 0.121502, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.672526)
|
||||
V 82: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.630859)
|
||||
V 83: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.630859)
|
||||
V 84: Pos(0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.672526)
|
||||
V 85: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.714193)
|
||||
V 86: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.672526)
|
||||
V 87: Pos(0.236527, 0.236527, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.714193)
|
||||
V 88: Pos(0.316891, 0.13126, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.755859)
|
||||
V 89: Pos(0.309038, 0.128008, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.714193)
|
||||
V 90: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.547526)
|
||||
V 91: Pos(0.13126, 0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.142578, 0.505859)
|
||||
V 92: Pos(0.242538, 0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.505859)
|
||||
V 93: Pos(0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.589193)
|
||||
V 94: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.547526)
|
||||
V 95: Pos(0.121502, 0.293332, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.589193)
|
||||
V 96: Pos(0.218496, 0.218496, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.630859)
|
||||
V 97: Pos(0.224506, 0.224506, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.589193)
|
||||
V 98: Pos(0.118249, 0.285479, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.630859)
|
||||
V 99: Pos(0.224506, 0.224506, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.672526)
|
||||
V 100: Pos(0.218496, 0.218496, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.630859)
|
||||
V 101: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.672526)
|
||||
V 102: Pos(0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.714193)
|
||||
V 103: Pos(0.128008, 0.309038, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.714193)
|
||||
V 104: Pos(0.242538, 0.242538, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.755859)
|
||||
V 105: Pos(0.236527, 0.236527, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.714193)
|
||||
V 106: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 107: Pos(0.128008, 0.309038, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.547526)
|
||||
V 108: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.505859)
|
||||
V 109: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.547526)
|
||||
V 110: Pos(0.0, 0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.589193)
|
||||
V 111: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.589193)
|
||||
V 112: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.630859)
|
||||
V 113: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.189453, 0.589193)
|
||||
V 114: Pos(0.121502, 0.293332, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.589193)
|
||||
V 115: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.189453, 0.630859)
|
||||
V 116: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.672526)
|
||||
V 117: Pos(0.118249, 0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.630859)
|
||||
V 118: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.714193)
|
||||
V 119: Pos(0.0, 0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.672526)
|
||||
V 120: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.672526)
|
||||
V 121: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.189453, 0.714193)
|
||||
V 122: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.755859)
|
||||
V 123: Pos(0.128008, 0.309038, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.714193)
|
||||
V 124: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.505859)
|
||||
V 125: Pos(0.0, 0.3345, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 126: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 127: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.547526)
|
||||
V 128: Pos(0.0, 0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.547526)
|
||||
V 129: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 130: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.589193)
|
||||
V 131: Pos(0.0, 0.3175, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 132: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.236328, 0.630859)
|
||||
V 133: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.672526)
|
||||
V 134: Pos(0.0, 0.309, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.630859)
|
||||
V 135: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.714193)
|
||||
V 136: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.672526)
|
||||
V 137: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.714193)
|
||||
V 138: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 139: Pos(0.0, 0.3345, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 140: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.547526)
|
||||
V 141: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.283203, 0.505859)
|
||||
V 142: Pos(-0.13126, 0.316891, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.505859)
|
||||
V 143: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.589193)
|
||||
V 144: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.547526)
|
||||
V 145: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.547526)
|
||||
V 146: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.283203, 0.589193)
|
||||
V 147: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.630859)
|
||||
V 148: Pos(-0.121502, 0.293332, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.589193)
|
||||
V 149: Pos(-0.121502, 0.293332, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.672526)
|
||||
V 150: Pos(-0.218496, 0.218496, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.630859)
|
||||
V 151: Pos(-0.118249, 0.285479, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.630859)
|
||||
V 152: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.672526)
|
||||
V 153: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.714193)
|
||||
V 154: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.672526)
|
||||
V 155: Pos(-0.13126, 0.316891, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.755859)
|
||||
V 156: Pos(-0.236527, 0.236527, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.714193)
|
||||
V 157: Pos(-0.128008, 0.309038, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.714193)
|
||||
V 158: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.547526)
|
||||
V 159: Pos(-0.316891, 0.13126, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.080078, 0.505859)
|
||||
V 160: Pos(-0.242538, 0.242538, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.505859)
|
||||
V 161: Pos(-0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.547526)
|
||||
V 162: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.589193)
|
||||
V 163: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.547526)
|
||||
V 164: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.630859)
|
||||
V 165: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.589193)
|
||||
V 166: Pos(-0.224506, 0.224506, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.589193)
|
||||
V 167: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.672526)
|
||||
V 168: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.630859)
|
||||
V 169: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.630859)
|
||||
V 170: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.672526)
|
||||
V 171: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.714193)
|
||||
V 172: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.672526)
|
||||
V 173: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.714193)
|
||||
V 174: Pos(-0.242538, 0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.755859)
|
||||
V 175: Pos(-0.236527, 0.236527, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.714193)
|
||||
V 176: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 177: Pos(-0.309038, 0.128008, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.547526)
|
||||
V 178: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.505859)
|
||||
V 179: Pos(-0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.589193)
|
||||
V 180: Pos(-0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.589193)
|
||||
V 181: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.630859)
|
||||
V 182: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.126953, 0.589193)
|
||||
V 183: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.589193)
|
||||
V 184: Pos(-0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 185: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 186: Pos(-0.285479, 0.118249, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.630859)
|
||||
V 187: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.714193)
|
||||
V 188: Pos(-0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.672526)
|
||||
V 189: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.672526)
|
||||
V 190: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.126953, 0.714193)
|
||||
V 191: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.755859)
|
||||
V 192: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.714193)
|
||||
V 193: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.505859)
|
||||
V 194: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 195: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 196: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.547526)
|
||||
V 197: Pos(-0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.547526)
|
||||
V 198: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 199: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.589193)
|
||||
V 200: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 201: Pos(-0.285479, -0.118249, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.173828, 0.630859)
|
||||
V 202: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.672526)
|
||||
V 203: Pos(-0.309, 0.0, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.630859)
|
||||
V 204: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.714193)
|
||||
V 205: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.672526)
|
||||
V 206: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.714193)
|
||||
V 207: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 208: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 209: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.547526)
|
||||
V 210: Pos(-0.242538, -0.242538, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.220703, 0.505859)
|
||||
V 211: Pos(-0.316891, -0.13126, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.505859)
|
||||
V 212: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.589193)
|
||||
V 213: Pos(-0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.547526)
|
||||
V 214: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.547526)
|
||||
V 215: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 216: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 217: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.589193)
|
||||
V 218: Pos(-0.293332, -0.121502, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.672526)
|
||||
V 219: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.630859)
|
||||
V 220: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.630859)
|
||||
V 221: Pos(-0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.672526)
|
||||
V 222: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.714193)
|
||||
V 223: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.672526)
|
||||
V 224: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.714193)
|
||||
V 225: Pos(-0.316891, -0.13126, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.755859)
|
||||
V 226: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.714193)
|
||||
V 227: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.547526)
|
||||
V 228: Pos(-0.13126, -0.316891, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.267578, 0.505859)
|
||||
V 229: Pos(-0.242538, -0.242538, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.505859)
|
||||
V 230: Pos(-0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.589193)
|
||||
V 231: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.547526)
|
||||
V 232: Pos(-0.121502, -0.293332, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.589193)
|
||||
V 233: Pos(-0.218496, -0.218496, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.630859)
|
||||
V 234: Pos(-0.224506, -0.224506, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.589193)
|
||||
V 235: Pos(-0.118249, -0.285479, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.630859)
|
||||
V 236: Pos(-0.224506, -0.224506, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.672526)
|
||||
V 237: Pos(-0.218496, -0.218496, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.630859)
|
||||
V 238: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.672526)
|
||||
V 239: Pos(-0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.714193)
|
||||
V 240: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.714193)
|
||||
V 241: Pos(-0.242538, -0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.755859)
|
||||
V 242: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.714193)
|
||||
V 243: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 244: Pos(-0.128008, -0.309038, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.547526)
|
||||
V 245: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.505859)
|
||||
V 246: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.547526)
|
||||
V 247: Pos(0.0, -0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.589193)
|
||||
V 248: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.589193)
|
||||
V 249: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.630859)
|
||||
V 250: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.064453, 0.589193)
|
||||
V 251: Pos(-0.121502, -0.293332, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.589193)
|
||||
V 252: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.064453, 0.630859)
|
||||
V 253: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.672526)
|
||||
V 254: Pos(-0.118249, -0.285479, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.630859)
|
||||
V 255: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.714193)
|
||||
V 256: Pos(0.0, -0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.672526)
|
||||
V 257: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.672526)
|
||||
V 258: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.064453, 0.714193)
|
||||
V 259: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.755859)
|
||||
V 260: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.714193)
|
||||
V 261: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.505859)
|
||||
V 262: Pos(0.0, -0.3345, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 263: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 264: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.547526)
|
||||
V 265: Pos(0.0, -0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.547526)
|
||||
V 266: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 267: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.589193)
|
||||
V 268: Pos(0.0, -0.3175, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 269: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.111328, 0.630859)
|
||||
V 270: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.672526)
|
||||
V 271: Pos(0.0, -0.309, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.630859)
|
||||
V 272: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.714193)
|
||||
V 273: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.672526)
|
||||
V 274: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.714193)
|
||||
V 275: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 276: Pos(0.0, -0.3345, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 277: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.547526)
|
||||
V 278: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.158203, 0.505859)
|
||||
V 279: Pos(0.13126, -0.316891, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.505859)
|
||||
V 280: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.589193)
|
||||
V 281: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.547526)
|
||||
V 282: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.547526)
|
||||
V 283: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.158203, 0.589193)
|
||||
V 284: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.630859)
|
||||
V 285: Pos(0.121502, -0.293332, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.589193)
|
||||
V 286: Pos(0.121502, -0.293332, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.672526)
|
||||
V 287: Pos(0.218496, -0.218496, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.630859)
|
||||
V 288: Pos(0.118249, -0.285479, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.630859)
|
||||
V 289: Pos(0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.672526)
|
||||
V 290: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.714193)
|
||||
V 291: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.672526)
|
||||
V 292: Pos(0.13126, -0.316891, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.755859)
|
||||
V 293: Pos(0.236527, -0.236527, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.714193)
|
||||
V 294: Pos(0.128008, -0.309038, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.714193)
|
||||
V 295: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.547526)
|
||||
V 296: Pos(0.316891, -0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.205078, 0.505859)
|
||||
V 297: Pos(0.242538, -0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.505859)
|
||||
V 298: Pos(0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.547526)
|
||||
V 299: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.589193)
|
||||
V 300: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.547526)
|
||||
V 301: Pos(0.218496, -0.218496, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.630859)
|
||||
V 302: Pos(0.293332, -0.121502, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.589193)
|
||||
V 303: Pos(0.224506, -0.224506, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.589193)
|
||||
V 304: Pos(0.224506, -0.224506, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.672526)
|
||||
V 305: Pos(0.285479, -0.118249, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.630859)
|
||||
V 306: Pos(0.218496, -0.218496, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.630859)
|
||||
V 307: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.672526)
|
||||
V 308: Pos(0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.714193)
|
||||
V 309: Pos(0.309038, -0.128008, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.714193)
|
||||
V 310: Pos(0.242538, -0.242538, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.755859)
|
||||
V 311: Pos(0.236527, -0.236527, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.714193)
|
||||
V 312: Pos(0.343, 0.0, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.505859)
|
||||
V 313: Pos(0.309038, -0.128008, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.547526)
|
||||
V 314: Pos(0.316891, -0.13126, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.505859)
|
||||
V 315: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.589193)
|
||||
V 316: Pos(0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.589193)
|
||||
V 317: Pos(0.285479, -0.118249, 0.9) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.630859)
|
||||
V 318: Pos(0.3175, 0.0, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.251953, 0.589193)
|
||||
V 319: Pos(0.293332, -0.121502, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.589193)
|
||||
V 320: Pos(0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 321: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 322: Pos(0.285479, -0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.630859)
|
||||
V 323: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.714193)
|
||||
V 324: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.672526)
|
||||
V 325: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.672526)
|
||||
V 326: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.251953, 0.714193)
|
||||
V 327: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.755859)
|
||||
V 328: Pos(0.309038, -0.128008, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.714193)
|
||||
V 329: Pos(0.308, 0.0, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 330: Pos(0.284555, 0.117866, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 331: Pos(0.284555, 0.117866, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 332: Pos(0.284555, 0.117866, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 333: Pos(0.217789, 0.217789, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 334: Pos(0.217789, 0.217789, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 335: Pos(0.217789, 0.217789, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 336: Pos(0.117866, 0.284555, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 337: Pos(0.117866, 0.284555, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 338: Pos(0.117866, 0.284555, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 339: Pos(0.0, 0.308, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 340: Pos(0.0, 0.308, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 341: Pos(0.0, 0.308, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 342: Pos(-0.117866, 0.284555, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 343: Pos(-0.117866, 0.284555, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 344: Pos(-0.117866, 0.284555, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 345: Pos(-0.217789, 0.217789, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 346: Pos(-0.217789, 0.217789, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 347: Pos(-0.217789, 0.217789, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 348: Pos(-0.284555, 0.117866, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 349: Pos(-0.284555, 0.117866, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 350: Pos(-0.284555, 0.117866, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 351: Pos(-0.308, 0.0, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 352: Pos(-0.308, 0.0, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 353: Pos(-0.308, 0.0, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 354: Pos(-0.284555, -0.117866, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 355: Pos(-0.284555, -0.117866, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 356: Pos(-0.284555, -0.117866, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 357: Pos(-0.217789, -0.217789, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 358: Pos(-0.217789, -0.217789, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 359: Pos(-0.217789, -0.217789, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 360: Pos(-0.117866, -0.284555, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 361: Pos(-0.117866, -0.284555, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 362: Pos(-0.117866, -0.284555, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 363: Pos(0.0, -0.308, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 364: Pos(0.0, -0.308, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 365: Pos(0.0, -0.308, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 366: Pos(0.117866, -0.284555, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 367: Pos(0.117866, -0.284555, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 368: Pos(0.117866, -0.284555, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 369: Pos(0.217789, -0.217789, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 370: Pos(0.217789, -0.217789, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 371: Pos(0.217789, -0.217789, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 372: Pos(0.284555, -0.117866, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 373: Pos(0.284555, -0.117866, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 374: Pos(0.284555, -0.117866, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 375: Pos(0.308, 0.0, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 376: Pos(0.308, 0.0, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 377: Pos(-0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 378: Pos(0.0, 0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 379: Pos(0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 380: Pos(0.295641, 0.122459, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.312667)
|
||||
V 381: Pos(0.226274, 0.226274, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.312667)
|
||||
V 382: Pos(0.122459, 0.295641, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.312667)
|
||||
V 383: Pos(0.0, 0.32, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 384: Pos(-0.122459, 0.295641, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.312667)
|
||||
V 385: Pos(-0.226274, 0.226274, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.312667)
|
||||
V 386: Pos(-0.295641, 0.122459, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.312667)
|
||||
V 387: Pos(-0.32, 0.0, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 388: Pos(-0.295641, -0.122459, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.312667)
|
||||
V 389: Pos(-0.226274, -0.226274, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.312667)
|
||||
V 390: Pos(-0.122459, -0.295641, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.312667)
|
||||
V 391: Pos(0.0, -0.32, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 392: Pos(0.122459, -0.295641, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.312667)
|
||||
V 393: Pos(0.226274, -0.226274, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.312667)
|
||||
V 394: Pos(0.295641, -0.122459, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.312667)
|
||||
V 395: Pos(0.32, 0.0, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.312667)
|
||||
V 396: Pos(0.324, 0.0, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.599609, 0.693359)
|
||||
V 397: Pos(0.299337, 0.123989, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.693359, 0.693359)
|
||||
V 398: Pos(0.309038, 0.128008, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.547526)
|
||||
V 399: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.589193)
|
||||
V 400: Pos(0.309, 0.0, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.001953, 0.630859)
|
||||
V 401: Pos(0.285479, 0.118249, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.630859)
|
||||
V 402: Pos(0.293332, 0.121502, 0.914722) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.589193)
|
||||
V 403: Pos(0.285479, 0.118249, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.630859)
|
||||
V 404: Pos(0.293332, 0.121502, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.672526)
|
||||
V 405: Pos(0.3175, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.001953, 0.672526)
|
||||
V 406: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.714193)
|
||||
V 407: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.714193)
|
||||
V 408: Pos(0.316891, 0.13126, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.755859)
|
||||
V 409: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.001953, 0.755859)
|
||||
V 410: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.048828, 0.547526)
|
||||
V 411: Pos(0.236527, 0.236527, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.547526)
|
||||
V 412: Pos(0.242538, 0.242538, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.505859)
|
||||
V 413: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 414: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.630859)
|
||||
V 415: Pos(0.224506, 0.224506, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 416: Pos(0.224506, 0.224506, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.672526)
|
||||
V 417: Pos(0.242538, 0.242538, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.755859)
|
||||
V 418: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.095703, 0.547526)
|
||||
V 419: Pos(0.128008, 0.309038, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.547526)
|
||||
V 420: Pos(0.13126, 0.316891, 0.9) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.505859)
|
||||
V 421: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.589193)
|
||||
V 422: Pos(0.118249, 0.285479, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.630859)
|
||||
V 423: Pos(0.121502, 0.293332, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.672526)
|
||||
V 424: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.714193)
|
||||
V 425: Pos(0.13126, 0.316891, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.755859)
|
||||
V 426: Pos(0.0, 0.3345, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 427: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.630859)
|
||||
V 428: Pos(0.0, 0.309, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 429: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 430: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.630859)
|
||||
V 431: Pos(0.0, 0.3175, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.672526)
|
||||
V 432: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.142578, 0.672526)
|
||||
V 433: Pos(0.0, 0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.714193)
|
||||
V 434: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 435: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 436: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.755859)
|
||||
V 437: Pos(-0.128008, 0.309038, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.547526)
|
||||
V 438: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.589193)
|
||||
V 439: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.189453, 0.630859)
|
||||
V 440: Pos(-0.118249, 0.285479, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.630859)
|
||||
V 441: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.589193)
|
||||
V 442: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.630859)
|
||||
V 443: Pos(-0.121502, 0.293332, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.672526)
|
||||
V 444: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.189453, 0.672526)
|
||||
V 445: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.714193)
|
||||
V 446: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.755859)
|
||||
V 447: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.189453, 0.755859)
|
||||
V 448: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.236328, 0.547526)
|
||||
V 449: Pos(-0.236527, 0.236527, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.547526)
|
||||
V 450: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.505859)
|
||||
V 451: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.589193)
|
||||
V 452: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.589193)
|
||||
V 453: Pos(-0.218496, 0.218496, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.630859)
|
||||
V 454: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.236328, 0.630859)
|
||||
V 455: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.672526)
|
||||
V 456: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.714193)
|
||||
V 457: Pos(-0.242538, 0.242538, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.755859)
|
||||
V 458: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.033203, 0.547526)
|
||||
V 459: Pos(-0.309038, 0.128008, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.547526)
|
||||
V 460: Pos(-0.316891, 0.13126, 0.9) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.505859)
|
||||
V 461: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.630859)
|
||||
V 462: Pos(-0.293332, 0.121502, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.672526)
|
||||
V 463: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.714193)
|
||||
V 464: Pos(-0.316891, 0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.755859)
|
||||
V 465: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 466: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.630859)
|
||||
V 467: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 468: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 469: Pos(-0.309, 0.0, 0.9) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 470: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 471: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 472: Pos(-0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.714193)
|
||||
V 473: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 474: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 475: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.755859)
|
||||
V 476: Pos(-0.309038, -0.128008, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.547526)
|
||||
V 477: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.589193)
|
||||
V 478: Pos(-0.309, 0.0, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.126953, 0.630859)
|
||||
V 479: Pos(-0.285479, -0.118249, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.630859)
|
||||
V 480: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.589193)
|
||||
V 481: Pos(-0.285479, -0.118249, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.630859)
|
||||
V 482: Pos(-0.293332, -0.121502, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.672526)
|
||||
V 483: Pos(-0.3175, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 484: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.714193)
|
||||
V 485: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.755859)
|
||||
V 486: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.126953, 0.755859)
|
||||
V 487: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.173828, 0.547526)
|
||||
V 488: Pos(-0.236527, -0.236527, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.547526)
|
||||
V 489: Pos(-0.242538, -0.242538, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.505859)
|
||||
V 490: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 491: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.630859)
|
||||
V 492: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 493: Pos(-0.224506, -0.224506, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.672526)
|
||||
V 494: Pos(-0.242538, -0.242538, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.755859)
|
||||
V 495: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.220703, 0.547526)
|
||||
V 496: Pos(-0.128008, -0.309038, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.547526)
|
||||
V 497: Pos(-0.13126, -0.316891, 0.9) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.505859)
|
||||
V 498: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.589193)
|
||||
V 499: Pos(-0.118249, -0.285479, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.630859)
|
||||
V 500: Pos(-0.121502, -0.293332, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.672526)
|
||||
V 501: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.714193)
|
||||
V 502: Pos(-0.13126, -0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.755859)
|
||||
V 503: Pos(0.0, -0.3345, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 504: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.630859)
|
||||
V 505: Pos(0.0, -0.309, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 506: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 507: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.630859)
|
||||
V 508: Pos(0.0, -0.3175, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.672526)
|
||||
V 509: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.017578, 0.672526)
|
||||
V 510: Pos(0.0, -0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.714193)
|
||||
V 511: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 512: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 513: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.755859)
|
||||
V 514: Pos(0.128008, -0.309038, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.547526)
|
||||
V 515: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.589193)
|
||||
V 516: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.064453, 0.630859)
|
||||
V 517: Pos(0.118249, -0.285479, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.630859)
|
||||
V 518: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.589193)
|
||||
V 519: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.630859)
|
||||
V 520: Pos(0.121502, -0.293332, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.672526)
|
||||
V 521: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.064453, 0.672526)
|
||||
V 522: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.714193)
|
||||
V 523: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.755859)
|
||||
V 524: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.064453, 0.755859)
|
||||
V 525: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.111328, 0.547526)
|
||||
V 526: Pos(0.236527, -0.236527, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.547526)
|
||||
V 527: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.505859)
|
||||
V 528: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.589193)
|
||||
V 529: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.589193)
|
||||
V 530: Pos(0.218496, -0.218496, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.630859)
|
||||
V 531: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.111328, 0.630859)
|
||||
V 532: Pos(0.224506, -0.224506, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.672526)
|
||||
V 533: Pos(0.242538, -0.242538, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.755859)
|
||||
V 534: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.158203, 0.547526)
|
||||
V 535: Pos(0.309038, -0.128008, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.547526)
|
||||
V 536: Pos(0.316891, -0.13126, 0.9) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.505859)
|
||||
V 537: Pos(0.285479, -0.118249, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.630859)
|
||||
V 538: Pos(0.293332, -0.121502, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.672526)
|
||||
V 539: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.714193)
|
||||
V 540: Pos(0.316891, -0.13126, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.755859)
|
||||
V 541: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.547526)
|
||||
V 542: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.547526)
|
||||
V 543: Pos(0.285479, -0.118249, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.630859)
|
||||
V 544: Pos(0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.630859)
|
||||
V 545: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.589193)
|
||||
V 546: Pos(0.309, 0.0, 0.9) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 547: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.672526)
|
||||
V 548: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 549: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.714193)
|
||||
V 550: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.714193)
|
||||
V 551: Pos(0.343, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.755859)
|
||||
V 552: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.755859)
|
||||
V 553: Pos(0.308, 0.0, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 554: Pos(0.284555, 0.117866, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 555: Pos(0.217789, 0.217789, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 556: Pos(0.117866, 0.284555, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 557: Pos(0.0, 0.308, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 558: Pos(-0.117866, 0.284555, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 559: Pos(-0.217789, 0.217789, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 560: Pos(-0.284555, 0.117866, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 561: Pos(-0.308, 0.0, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 562: Pos(-0.284555, -0.117866, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 563: Pos(-0.217789, -0.217789, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 564: Pos(-0.117866, -0.284555, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 565: Pos(0.0, -0.308, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 566: Pos(0.117866, -0.284555, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 567: Pos(0.217789, -0.217789, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 568: Pos(0.284555, -0.117866, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 569: Pos(0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 570: Pos(0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 571: Pos(0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 572: Pos(0.0, -0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 573: Pos(-0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 574: Pos(-0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 575: Pos(-0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 576: Pos(-0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 577: Pos(-0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 578: Pos(-0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 579: Pos(0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 580: Pos(0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 581: Pos(0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
|
||||
===Triangles (Indices): 274
|
||||
Tri: 0 1 2
|
||||
Tri: 3 4 5
|
||||
Tri: 6 7 8
|
||||
Tri: 9 10 11
|
||||
Tri: 12 13 14
|
||||
Tri: 15 16 17
|
||||
Tri: 18 19 20
|
||||
Tri: 21 22 23
|
||||
Tri: 24 25 26
|
||||
Tri: 27 28 29
|
||||
Tri: 30 31 32
|
||||
Tri: 33 34 35
|
||||
Tri: 36 37 38
|
||||
Tri: 39 40 41
|
||||
Tri: 42 43 44
|
||||
Tri: 45 46 47
|
||||
Tri: 48 49 50
|
||||
Tri: 51 52 53
|
||||
Tri: 54 55 56
|
||||
Tri: 57 58 59
|
||||
Tri: 60 61 62
|
||||
Tri: 63 64 65
|
||||
Tri: 66 67 68
|
||||
Tri: 69 70 71
|
||||
Tri: 72 73 74
|
||||
Tri: 75 76 77
|
||||
Tri: 78 79 80
|
||||
Tri: 81 82 83
|
||||
Tri: 84 85 86
|
||||
Tri: 87 88 89
|
||||
Tri: 90 91 92
|
||||
Tri: 93 94 76
|
||||
Tri: 95 96 97
|
||||
Tri: 98 99 100
|
||||
Tri: 101 102 84
|
||||
Tri: 103 104 105
|
||||
Tri: 106 107 108
|
||||
Tri: 109 110 111
|
||||
Tri: 112 113 114
|
||||
Tri: 115 116 117
|
||||
Tri: 118 119 120
|
||||
Tri: 121 122 123
|
||||
Tri: 124 125 126
|
||||
Tri: 127 110 128
|
||||
Tri: 129 130 131
|
||||
Tri: 132 133 134
|
||||
Tri: 119 135 136
|
||||
Tri: 137 138 139
|
||||
Tri: 140 141 142
|
||||
Tri: 143 144 145
|
||||
Tri: 146 147 148
|
||||
Tri: 149 150 151
|
||||
Tri: 152 153 154
|
||||
Tri: 155 156 157
|
||||
Tri: 158 159 160
|
||||
Tri: 161 162 163
|
||||
Tri: 164 165 166
|
||||
Tri: 167 168 169
|
||||
Tri: 170 171 172
|
||||
Tri: 173 174 175
|
||||
Tri: 176 177 178
|
||||
Tri: 161 179 180
|
||||
Tri: 181 182 183
|
||||
Tri: 184 185 186
|
||||
Tri: 187 188 189
|
||||
Tri: 190 191 192
|
||||
Tri: 193 194 195
|
||||
Tri: 196 179 197
|
||||
Tri: 198 199 200
|
||||
Tri: 201 202 203
|
||||
Tri: 188 204 205
|
||||
Tri: 206 207 208
|
||||
Tri: 209 210 211
|
||||
Tri: 212 213 214
|
||||
Tri: 215 216 217
|
||||
Tri: 218 219 220
|
||||
Tri: 221 222 223
|
||||
Tri: 224 225 226
|
||||
Tri: 227 228 229
|
||||
Tri: 230 231 213
|
||||
Tri: 232 233 234
|
||||
Tri: 235 236 237
|
||||
Tri: 238 239 221
|
||||
Tri: 240 241 242
|
||||
Tri: 243 244 245
|
||||
Tri: 246 247 248
|
||||
Tri: 249 250 251
|
||||
Tri: 252 253 254
|
||||
Tri: 255 256 257
|
||||
Tri: 258 259 260
|
||||
Tri: 261 262 263
|
||||
Tri: 264 247 265
|
||||
Tri: 266 267 268
|
||||
Tri: 269 270 271
|
||||
Tri: 256 272 273
|
||||
Tri: 274 275 276
|
||||
Tri: 277 278 279
|
||||
Tri: 280 281 282
|
||||
Tri: 283 284 285
|
||||
Tri: 286 287 288
|
||||
Tri: 289 290 291
|
||||
Tri: 292 293 294
|
||||
Tri: 295 296 297
|
||||
Tri: 298 299 300
|
||||
Tri: 301 302 303
|
||||
Tri: 304 305 306
|
||||
Tri: 307 308 289
|
||||
Tri: 309 310 311
|
||||
Tri: 312 313 314
|
||||
Tri: 298 315 316
|
||||
Tri: 317 318 319
|
||||
Tri: 320 321 322
|
||||
Tri: 323 324 325
|
||||
Tri: 326 327 328
|
||||
Tri: 329 330 331
|
||||
Tri: 332 333 334
|
||||
Tri: 335 336 337
|
||||
Tri: 338 339 340
|
||||
Tri: 341 342 343
|
||||
Tri: 344 345 346
|
||||
Tri: 347 348 349
|
||||
Tri: 350 351 352
|
||||
Tri: 353 354 355
|
||||
Tri: 356 357 358
|
||||
Tri: 359 360 361
|
||||
Tri: 362 363 364
|
||||
Tri: 365 366 367
|
||||
Tri: 368 369 370
|
||||
Tri: 371 372 373
|
||||
Tri: 374 375 376
|
||||
Tri: 377 378 379
|
||||
Tri: 0 380 1
|
||||
Tri: 3 381 4
|
||||
Tri: 6 382 7
|
||||
Tri: 9 383 10
|
||||
Tri: 12 384 13
|
||||
Tri: 15 385 16
|
||||
Tri: 18 386 19
|
||||
Tri: 21 387 22
|
||||
Tri: 24 388 25
|
||||
Tri: 27 389 28
|
||||
Tri: 30 390 31
|
||||
Tri: 33 391 34
|
||||
Tri: 36 392 37
|
||||
Tri: 39 393 40
|
||||
Tri: 42 394 43
|
||||
Tri: 45 395 46
|
||||
Tri: 48 396 49
|
||||
Tri: 51 397 52
|
||||
Tri: 54 398 55
|
||||
Tri: 57 399 58
|
||||
Tri: 400 401 402
|
||||
Tri: 403 404 405
|
||||
Tri: 66 406 67
|
||||
Tri: 407 408 409
|
||||
Tri: 410 411 412
|
||||
Tri: 75 93 76
|
||||
Tri: 413 414 415
|
||||
Tri: 81 416 82
|
||||
Tri: 84 102 85
|
||||
Tri: 87 417 88
|
||||
Tri: 418 419 420
|
||||
Tri: 93 421 94
|
||||
Tri: 95 422 96
|
||||
Tri: 98 423 99
|
||||
Tri: 101 424 102
|
||||
Tri: 103 425 104
|
||||
Tri: 106 426 107
|
||||
Tri: 109 128 110
|
||||
Tri: 427 428 429
|
||||
Tri: 430 431 432
|
||||
Tri: 118 433 119
|
||||
Tri: 434 435 436
|
||||
Tri: 124 437 125
|
||||
Tri: 127 438 110
|
||||
Tri: 439 440 441
|
||||
Tri: 442 443 444
|
||||
Tri: 119 433 135
|
||||
Tri: 445 446 447
|
||||
Tri: 448 449 450
|
||||
Tri: 143 451 144
|
||||
Tri: 452 453 454
|
||||
Tri: 149 455 150
|
||||
Tri: 152 456 153
|
||||
Tri: 155 457 156
|
||||
Tri: 458 459 460
|
||||
Tri: 161 180 162
|
||||
Tri: 164 461 165
|
||||
Tri: 167 462 168
|
||||
Tri: 170 463 171
|
||||
Tri: 173 464 174
|
||||
Tri: 176 465 177
|
||||
Tri: 161 197 179
|
||||
Tri: 466 467 468
|
||||
Tri: 469 470 471
|
||||
Tri: 187 472 188
|
||||
Tri: 473 474 475
|
||||
Tri: 193 476 194
|
||||
Tri: 196 477 179
|
||||
Tri: 478 479 480
|
||||
Tri: 481 482 483
|
||||
Tri: 188 472 204
|
||||
Tri: 484 485 486
|
||||
Tri: 487 488 489
|
||||
Tri: 212 230 213
|
||||
Tri: 490 491 492
|
||||
Tri: 218 493 219
|
||||
Tri: 221 239 222
|
||||
Tri: 224 494 225
|
||||
Tri: 495 496 497
|
||||
Tri: 230 498 231
|
||||
Tri: 232 499 233
|
||||
Tri: 235 500 236
|
||||
Tri: 238 501 239
|
||||
Tri: 240 502 241
|
||||
Tri: 243 503 244
|
||||
Tri: 246 265 247
|
||||
Tri: 504 505 506
|
||||
Tri: 507 508 509
|
||||
Tri: 255 510 256
|
||||
Tri: 511 512 513
|
||||
Tri: 261 514 262
|
||||
Tri: 264 515 247
|
||||
Tri: 516 517 518
|
||||
Tri: 519 520 521
|
||||
Tri: 256 510 272
|
||||
Tri: 522 523 524
|
||||
Tri: 525 526 527
|
||||
Tri: 280 528 281
|
||||
Tri: 529 530 531
|
||||
Tri: 286 532 287
|
||||
Tri: 289 308 290
|
||||
Tri: 292 533 293
|
||||
Tri: 534 535 536
|
||||
Tri: 298 316 299
|
||||
Tri: 301 537 302
|
||||
Tri: 304 538 305
|
||||
Tri: 307 539 308
|
||||
Tri: 309 540 310
|
||||
Tri: 312 541 313
|
||||
Tri: 298 542 315
|
||||
Tri: 543 544 545
|
||||
Tri: 546 547 548
|
||||
Tri: 323 549 324
|
||||
Tri: 550 551 552
|
||||
Tri: 329 553 330
|
||||
Tri: 332 554 333
|
||||
Tri: 335 555 336
|
||||
Tri: 338 556 339
|
||||
Tri: 341 557 342
|
||||
Tri: 344 558 345
|
||||
Tri: 347 559 348
|
||||
Tri: 350 560 351
|
||||
Tri: 353 561 354
|
||||
Tri: 356 562 357
|
||||
Tri: 359 563 360
|
||||
Tri: 362 564 363
|
||||
Tri: 365 565 366
|
||||
Tri: 368 566 369
|
||||
Tri: 371 567 372
|
||||
Tri: 374 568 375
|
||||
Tri: 379 569 570
|
||||
Tri: 570 571 572
|
||||
Tri: 572 573 574
|
||||
Tri: 574 575 377
|
||||
Tri: 377 576 577
|
||||
Tri: 577 578 378
|
||||
Tri: 378 579 580
|
||||
Tri: 580 581 379
|
||||
Tri: 379 570 572
|
||||
Tri: 572 574 377
|
||||
Tri: 377 577 378
|
||||
Tri: 378 580 379
|
||||
Tri: 379 572 377
|
||||
@@ -0,0 +1,859 @@
|
||||
===Vertices (Split by UV/Normal): 582
|
||||
V 0: Pos(0.32, 0.0, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.312667)
|
||||
V 1: Pos(0.295641, 0.122459, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.001953)
|
||||
V 2: Pos(0.32, 0.0, 0.0) Norm(-0.980785, -0.19509, 0.0) UV(0.001953, 0.001953)
|
||||
V 3: Pos(0.226274, 0.226274, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.001953)
|
||||
V 4: Pos(0.295641, 0.122459, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.312667)
|
||||
V 5: Pos(0.295641, 0.122459, 0.0) Norm(0.83147, 0.55557, 0.0) UV(0.048828, 0.001953)
|
||||
V 6: Pos(0.226274, 0.226274, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.312667)
|
||||
V 7: Pos(0.122459, 0.295641, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.001953)
|
||||
V 8: Pos(0.226274, 0.226274, 0.0) Norm(-0.55557, -0.83147, 0.0) UV(0.095703, 0.001953)
|
||||
V 9: Pos(0.122459, 0.295641, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.312667)
|
||||
V 10: Pos(0.0, 0.32, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 11: Pos(0.122459, 0.295641, 0.0) Norm(-0.19509, -0.980785, 0.0) UV(0.142578, 0.001953)
|
||||
V 12: Pos(0.0, 0.32, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 13: Pos(-0.122459, 0.295641, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.001953)
|
||||
V 14: Pos(0.0, 0.32, 0.0) Norm(0.19509, -0.980785, 0.0) UV(0.189453, 0.001953)
|
||||
V 15: Pos(-0.226274, 0.226274, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.001953)
|
||||
V 16: Pos(-0.122459, 0.295641, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.312667)
|
||||
V 17: Pos(-0.122459, 0.295641, 0.0) Norm(-0.55557, 0.83147, 0.0) UV(0.236328, 0.001953)
|
||||
V 18: Pos(-0.226274, 0.226274, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.312667)
|
||||
V 19: Pos(-0.295641, 0.122459, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.001953)
|
||||
V 20: Pos(-0.226274, 0.226274, 0.0) Norm(0.83147, -0.55557, 0.0) UV(0.033203, 0.001953)
|
||||
V 21: Pos(-0.295641, 0.122459, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.312667)
|
||||
V 22: Pos(-0.32, 0.0, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 23: Pos(-0.295641, 0.122459, 0.0) Norm(0.980785, -0.19509, 0.0) UV(0.080078, 0.001953)
|
||||
V 24: Pos(-0.32, 0.0, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 25: Pos(-0.295641, -0.122459, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.001953)
|
||||
V 26: Pos(-0.32, 0.0, 0.0) Norm(0.980785, 0.19509, 0.0) UV(0.126953, 0.001953)
|
||||
V 27: Pos(-0.226274, -0.226274, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.001953)
|
||||
V 28: Pos(-0.295641, -0.122459, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.312667)
|
||||
V 29: Pos(-0.295641, -0.122459, 0.0) Norm(-0.83147, -0.55557, 0.0) UV(0.173828, 0.001953)
|
||||
V 30: Pos(-0.226274, -0.226274, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.312667)
|
||||
V 31: Pos(-0.122459, -0.295641, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.001953)
|
||||
V 32: Pos(-0.226274, -0.226274, 0.0) Norm(0.55557, 0.83147, 0.0) UV(0.220703, 0.001953)
|
||||
V 33: Pos(-0.122459, -0.295641, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.312667)
|
||||
V 34: Pos(0.0, -0.32, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 35: Pos(-0.122459, -0.295641, 0.0) Norm(0.19509, 0.980785, 0.0) UV(0.017578, 0.001953)
|
||||
V 36: Pos(0.0, -0.32, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 37: Pos(0.122459, -0.295641, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.001953)
|
||||
V 38: Pos(0.0, -0.32, 0.0) Norm(-0.19509, 0.980785, 0.0) UV(0.064453, 0.001953)
|
||||
V 39: Pos(0.226274, -0.226274, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.001953)
|
||||
V 40: Pos(0.122459, -0.295641, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.312667)
|
||||
V 41: Pos(0.122459, -0.295641, 0.0) Norm(0.55557, -0.83147, 0.0) UV(0.111328, 0.001953)
|
||||
V 42: Pos(0.226274, -0.226274, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.312667)
|
||||
V 43: Pos(0.295641, -0.122459, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.001953)
|
||||
V 44: Pos(0.226274, -0.226274, 0.0) Norm(-0.83147, 0.55557, 0.0) UV(0.158203, 0.001953)
|
||||
V 45: Pos(0.295641, -0.122459, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.312667)
|
||||
V 46: Pos(0.32, 0.0, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.001953)
|
||||
V 47: Pos(0.295641, -0.122459, 0.0) Norm(-0.980785, 0.19509, 0.0) UV(0.205078, 0.001953)
|
||||
V 48: Pos(0.324, 0.0, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.600586, 0.505859)
|
||||
V 49: Pos(0.299337, -0.123989, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.506836, 0.693359)
|
||||
V 50: Pos(0.299337, -0.123989, 0.36) Norm(0.980785, -0.19509, 0.0) UV(0.506836, 0.505859)
|
||||
V 51: Pos(0.299337, 0.123989, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.694336, 0.505859)
|
||||
V 52: Pos(0.324, 0.0, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.600586, 0.693359)
|
||||
V 53: Pos(0.324, 0.0, 0.36) Norm(0.980785, 0.19509, 0.0) UV(0.600586, 0.505859)
|
||||
V 54: Pos(0.316891, 0.13126, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.505859)
|
||||
V 55: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.547526)
|
||||
V 56: Pos(0.343, 0.0, 0.9) Norm(0.853456, 0.169763, 0.492741) UV(0.001953, 0.505859)
|
||||
V 57: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.547526)
|
||||
V 58: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.589193)
|
||||
V 59: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.547526)
|
||||
V 60: Pos(0.309, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.630859)
|
||||
V 61: Pos(0.293332, 0.121502, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.589193)
|
||||
V 62: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.589193)
|
||||
V 63: Pos(0.285479, 0.118249, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.048828, 0.630859)
|
||||
V 64: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.672526)
|
||||
V 65: Pos(0.309, 0.0, 0.9) Norm(-0.853454, -0.169763, -0.492744) UV(0.001953, 0.630859)
|
||||
V 66: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.672526)
|
||||
V 67: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.714193)
|
||||
V 68: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.672526)
|
||||
V 69: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.048828, 0.714193)
|
||||
V 70: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.755859)
|
||||
V 71: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492742) UV(0.001953, 0.714193)
|
||||
V 72: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.547526)
|
||||
V 73: Pos(0.242538, 0.242538, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.095703, 0.505859)
|
||||
V 74: Pos(0.316891, 0.13126, 0.9) Norm(-0.723525, -0.483444, -0.492741) UV(0.048828, 0.505859)
|
||||
V 75: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.589193)
|
||||
V 76: Pos(0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.547526)
|
||||
V 77: Pos(0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.547526)
|
||||
V 78: Pos(0.285479, 0.118249, 0.9) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 79: Pos(0.224506, 0.224506, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 80: Pos(0.293332, 0.121502, 0.914722) Norm(0.723525, 0.483444, -0.492742) UV(0.048828, 0.589193)
|
||||
V 81: Pos(0.293332, 0.121502, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.672526)
|
||||
V 82: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.630859)
|
||||
V 83: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483443, 0.492743) UV(0.048828, 0.630859)
|
||||
V 84: Pos(0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.672526)
|
||||
V 85: Pos(0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.714193)
|
||||
V 86: Pos(0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.048828, 0.672526)
|
||||
V 87: Pos(0.236527, 0.236527, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.714193)
|
||||
V 88: Pos(0.316891, 0.13126, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.755859)
|
||||
V 89: Pos(0.309038, 0.128008, 0.885278) Norm(0.723524, 0.483443, -0.492743) UV(0.048828, 0.714193)
|
||||
V 90: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.547526)
|
||||
V 91: Pos(0.13126, 0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.142578, 0.505859)
|
||||
V 92: Pos(0.242538, 0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492742) UV(0.095703, 0.505859)
|
||||
V 93: Pos(0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.589193)
|
||||
V 94: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.547526)
|
||||
V 95: Pos(0.121502, 0.293332, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.589193)
|
||||
V 96: Pos(0.218496, 0.218496, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.630859)
|
||||
V 97: Pos(0.224506, 0.224506, 0.914722) Norm(-0.483444, -0.723525, 0.492742) UV(0.095703, 0.589193)
|
||||
V 98: Pos(0.118249, 0.285479, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.630859)
|
||||
V 99: Pos(0.224506, 0.224506, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.672526)
|
||||
V 100: Pos(0.218496, 0.218496, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.095703, 0.630859)
|
||||
V 101: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.672526)
|
||||
V 102: Pos(0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.095703, 0.714193)
|
||||
V 103: Pos(0.128008, 0.309038, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.714193)
|
||||
V 104: Pos(0.242538, 0.242538, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.755859)
|
||||
V 105: Pos(0.236527, 0.236527, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.095703, 0.714193)
|
||||
V 106: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 107: Pos(0.128008, 0.309038, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.547526)
|
||||
V 108: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853456, 0.492741) UV(0.142578, 0.505859)
|
||||
V 109: Pos(0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.547526)
|
||||
V 110: Pos(0.0, 0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.589193)
|
||||
V 111: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.589193)
|
||||
V 112: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.630859)
|
||||
V 113: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.189453, 0.589193)
|
||||
V 114: Pos(0.121502, 0.293332, 0.914722) Norm(0.169763, 0.853456, -0.492741) UV(0.142578, 0.589193)
|
||||
V 115: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.189453, 0.630859)
|
||||
V 116: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.672526)
|
||||
V 117: Pos(0.118249, 0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.142578, 0.630859)
|
||||
V 118: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.714193)
|
||||
V 119: Pos(0.0, 0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.672526)
|
||||
V 120: Pos(0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.142578, 0.672526)
|
||||
V 121: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.189453, 0.714193)
|
||||
V 122: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.755859)
|
||||
V 123: Pos(0.128008, 0.309038, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.142578, 0.714193)
|
||||
V 124: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.505859)
|
||||
V 125: Pos(0.0, 0.3345, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 126: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853456, 0.492741) UV(0.189453, 0.505859)
|
||||
V 127: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.547526)
|
||||
V 128: Pos(0.0, 0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.547526)
|
||||
V 129: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 130: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.589193)
|
||||
V 131: Pos(0.0, 0.3175, 0.914722) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 132: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.236328, 0.630859)
|
||||
V 133: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.672526)
|
||||
V 134: Pos(0.0, 0.309, 0.9) Norm(0.169763, -0.853454, -0.492744) UV(0.189453, 0.630859)
|
||||
V 135: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.714193)
|
||||
V 136: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.672526)
|
||||
V 137: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.236328, 0.714193)
|
||||
V 138: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 139: Pos(0.0, 0.3345, 0.885278) Norm(-0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 140: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.547526)
|
||||
V 141: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.283203, 0.505859)
|
||||
V 142: Pos(-0.13126, 0.316891, 0.9) Norm(0.483444, -0.723525, -0.492741) UV(0.236328, 0.505859)
|
||||
V 143: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.589193)
|
||||
V 144: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.547526)
|
||||
V 145: Pos(-0.128008, 0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.547526)
|
||||
V 146: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.283203, 0.589193)
|
||||
V 147: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.630859)
|
||||
V 148: Pos(-0.121502, 0.293332, 0.914722) Norm(0.483444, -0.723525, 0.492742) UV(0.236328, 0.589193)
|
||||
V 149: Pos(-0.121502, 0.293332, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.672526)
|
||||
V 150: Pos(-0.218496, 0.218496, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.630859)
|
||||
V 151: Pos(-0.118249, 0.285479, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.236328, 0.630859)
|
||||
V 152: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.672526)
|
||||
V 153: Pos(-0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.714193)
|
||||
V 154: Pos(-0.121502, 0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.236328, 0.672526)
|
||||
V 155: Pos(-0.13126, 0.316891, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.755859)
|
||||
V 156: Pos(-0.236527, 0.236527, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.714193)
|
||||
V 157: Pos(-0.128008, 0.309038, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.236328, 0.714193)
|
||||
V 158: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.547526)
|
||||
V 159: Pos(-0.316891, 0.13126, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.080078, 0.505859)
|
||||
V 160: Pos(-0.242538, 0.242538, 0.9) Norm(0.723524, -0.483443, -0.492742) UV(0.033203, 0.505859)
|
||||
V 161: Pos(-0.309038, 0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.547526)
|
||||
V 162: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.589193)
|
||||
V 163: Pos(-0.236527, 0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.033203, 0.547526)
|
||||
V 164: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.630859)
|
||||
V 165: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.589193)
|
||||
V 166: Pos(-0.224506, 0.224506, 0.914722) Norm(-0.723525, 0.483444, -0.492742) UV(0.033203, 0.589193)
|
||||
V 167: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.672526)
|
||||
V 168: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.630859)
|
||||
V 169: Pos(-0.218496, 0.218496, 0.9) Norm(-0.723524, 0.483443, 0.492743) UV(0.033203, 0.630859)
|
||||
V 170: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.672526)
|
||||
V 171: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.714193)
|
||||
V 172: Pos(-0.224506, 0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.033203, 0.672526)
|
||||
V 173: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.714193)
|
||||
V 174: Pos(-0.242538, 0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.755859)
|
||||
V 175: Pos(-0.236527, 0.236527, 0.885278) Norm(-0.723524, 0.483443, -0.492743) UV(0.033203, 0.714193)
|
||||
V 176: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 177: Pos(-0.309038, 0.128008, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.547526)
|
||||
V 178: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853456, 0.169763, 0.492741) UV(0.080078, 0.505859)
|
||||
V 179: Pos(-0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.589193)
|
||||
V 180: Pos(-0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.589193)
|
||||
V 181: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.630859)
|
||||
V 182: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.126953, 0.589193)
|
||||
V 183: Pos(-0.293332, 0.121502, 0.914722) Norm(-0.853456, 0.169763, -0.492741) UV(0.080078, 0.589193)
|
||||
V 184: Pos(-0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 185: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 186: Pos(-0.285479, 0.118249, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.080078, 0.630859)
|
||||
V 187: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.714193)
|
||||
V 188: Pos(-0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.672526)
|
||||
V 189: Pos(-0.293332, 0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.080078, 0.672526)
|
||||
V 190: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.126953, 0.714193)
|
||||
V 191: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.755859)
|
||||
V 192: Pos(-0.309038, 0.128008, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.080078, 0.714193)
|
||||
V 193: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.505859)
|
||||
V 194: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 195: Pos(-0.343, 0.0, 0.9) Norm(-0.853456, -0.169763, 0.492741) UV(0.126953, 0.505859)
|
||||
V 196: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.547526)
|
||||
V 197: Pos(-0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.547526)
|
||||
V 198: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 199: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.589193)
|
||||
V 200: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 201: Pos(-0.285479, -0.118249, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.173828, 0.630859)
|
||||
V 202: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.672526)
|
||||
V 203: Pos(-0.309, 0.0, 0.9) Norm(0.853454, 0.169763, -0.492744) UV(0.126953, 0.630859)
|
||||
V 204: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.714193)
|
||||
V 205: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.672526)
|
||||
V 206: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.173828, 0.714193)
|
||||
V 207: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 208: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 209: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.547526)
|
||||
V 210: Pos(-0.242538, -0.242538, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.220703, 0.505859)
|
||||
V 211: Pos(-0.316891, -0.13126, 0.9) Norm(0.723525, 0.483444, -0.492741) UV(0.173828, 0.505859)
|
||||
V 212: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.589193)
|
||||
V 213: Pos(-0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.547526)
|
||||
V 214: Pos(-0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.547526)
|
||||
V 215: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 216: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 217: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.723525, -0.483444, -0.492742) UV(0.173828, 0.589193)
|
||||
V 218: Pos(-0.293332, -0.121502, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.672526)
|
||||
V 219: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.630859)
|
||||
V 220: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483443, 0.492743) UV(0.173828, 0.630859)
|
||||
V 221: Pos(-0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.672526)
|
||||
V 222: Pos(-0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.714193)
|
||||
V 223: Pos(-0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.173828, 0.672526)
|
||||
V 224: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.714193)
|
||||
V 225: Pos(-0.316891, -0.13126, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.755859)
|
||||
V 226: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.723524, -0.483443, -0.492743) UV(0.173828, 0.714193)
|
||||
V 227: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.547526)
|
||||
V 228: Pos(-0.13126, -0.316891, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.267578, 0.505859)
|
||||
V 229: Pos(-0.242538, -0.242538, 0.9) Norm(0.483443, 0.723524, -0.492742) UV(0.220703, 0.505859)
|
||||
V 230: Pos(-0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.589193)
|
||||
V 231: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.547526)
|
||||
V 232: Pos(-0.121502, -0.293332, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.589193)
|
||||
V 233: Pos(-0.218496, -0.218496, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.630859)
|
||||
V 234: Pos(-0.224506, -0.224506, 0.914722) Norm(0.483444, 0.723525, 0.492742) UV(0.220703, 0.589193)
|
||||
V 235: Pos(-0.118249, -0.285479, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.630859)
|
||||
V 236: Pos(-0.224506, -0.224506, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.672526)
|
||||
V 237: Pos(-0.218496, -0.218496, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.220703, 0.630859)
|
||||
V 238: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.672526)
|
||||
V 239: Pos(-0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.220703, 0.714193)
|
||||
V 240: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.714193)
|
||||
V 241: Pos(-0.242538, -0.242538, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.755859)
|
||||
V 242: Pos(-0.236527, -0.236527, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.220703, 0.714193)
|
||||
V 243: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 244: Pos(-0.128008, -0.309038, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.547526)
|
||||
V 245: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853456, 0.492741) UV(0.017578, 0.505859)
|
||||
V 246: Pos(-0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.547526)
|
||||
V 247: Pos(0.0, -0.3175, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.589193)
|
||||
V 248: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.589193)
|
||||
V 249: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.630859)
|
||||
V 250: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.064453, 0.589193)
|
||||
V 251: Pos(-0.121502, -0.293332, 0.914722) Norm(-0.169763, -0.853456, -0.492741) UV(0.017578, 0.589193)
|
||||
V 252: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.064453, 0.630859)
|
||||
V 253: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.672526)
|
||||
V 254: Pos(-0.118249, -0.285479, 0.9) Norm(0.169763, 0.853455, -0.492743) UV(0.017578, 0.630859)
|
||||
V 255: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.714193)
|
||||
V 256: Pos(0.0, -0.3175, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.672526)
|
||||
V 257: Pos(-0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.017578, 0.672526)
|
||||
V 258: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.064453, 0.714193)
|
||||
V 259: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.755859)
|
||||
V 260: Pos(-0.128008, -0.309038, 0.885278) Norm(-0.169763, -0.853455, -0.492743) UV(0.017578, 0.714193)
|
||||
V 261: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.505859)
|
||||
V 262: Pos(0.0, -0.3345, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 263: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853456, 0.492741) UV(0.064453, 0.505859)
|
||||
V 264: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.547526)
|
||||
V 265: Pos(0.0, -0.3345, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.547526)
|
||||
V 266: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 267: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.589193)
|
||||
V 268: Pos(0.0, -0.3175, 0.914722) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 269: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.111328, 0.630859)
|
||||
V 270: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.672526)
|
||||
V 271: Pos(0.0, -0.309, 0.9) Norm(-0.169763, 0.853454, -0.492744) UV(0.064453, 0.630859)
|
||||
V 272: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.714193)
|
||||
V 273: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.672526)
|
||||
V 274: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.111328, 0.714193)
|
||||
V 275: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 276: Pos(0.0, -0.3345, 0.885278) Norm(0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 277: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.547526)
|
||||
V 278: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.158203, 0.505859)
|
||||
V 279: Pos(0.13126, -0.316891, 0.9) Norm(-0.483444, 0.723525, -0.492741) UV(0.111328, 0.505859)
|
||||
V 280: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.589193)
|
||||
V 281: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.547526)
|
||||
V 282: Pos(0.128008, -0.309038, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.547526)
|
||||
V 283: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.158203, 0.589193)
|
||||
V 284: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.630859)
|
||||
V 285: Pos(0.121502, -0.293332, 0.914722) Norm(-0.483444, 0.723525, 0.492742) UV(0.111328, 0.589193)
|
||||
V 286: Pos(0.121502, -0.293332, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.672526)
|
||||
V 287: Pos(0.218496, -0.218496, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.630859)
|
||||
V 288: Pos(0.118249, -0.285479, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.111328, 0.630859)
|
||||
V 289: Pos(0.224506, -0.224506, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.672526)
|
||||
V 290: Pos(0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.714193)
|
||||
V 291: Pos(0.121502, -0.293332, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.111328, 0.672526)
|
||||
V 292: Pos(0.13126, -0.316891, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.755859)
|
||||
V 293: Pos(0.236527, -0.236527, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.714193)
|
||||
V 294: Pos(0.128008, -0.309038, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.111328, 0.714193)
|
||||
V 295: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.547526)
|
||||
V 296: Pos(0.316891, -0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.205078, 0.505859)
|
||||
V 297: Pos(0.242538, -0.242538, 0.9) Norm(-0.723524, 0.483443, -0.492742) UV(0.158203, 0.505859)
|
||||
V 298: Pos(0.309038, -0.128008, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.547526)
|
||||
V 299: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.589193)
|
||||
V 300: Pos(0.236527, -0.236527, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.158203, 0.547526)
|
||||
V 301: Pos(0.218496, -0.218496, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.630859)
|
||||
V 302: Pos(0.293332, -0.121502, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.589193)
|
||||
V 303: Pos(0.224506, -0.224506, 0.914722) Norm(0.723525, -0.483444, -0.492742) UV(0.158203, 0.589193)
|
||||
V 304: Pos(0.224506, -0.224506, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.672526)
|
||||
V 305: Pos(0.285479, -0.118249, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.630859)
|
||||
V 306: Pos(0.218496, -0.218496, 0.9) Norm(0.723524, -0.483443, 0.492743) UV(0.158203, 0.630859)
|
||||
V 307: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.672526)
|
||||
V 308: Pos(0.236527, -0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.714193)
|
||||
V 309: Pos(0.309038, -0.128008, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.714193)
|
||||
V 310: Pos(0.242538, -0.242538, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.755859)
|
||||
V 311: Pos(0.236527, -0.236527, 0.885278) Norm(0.723524, -0.483443, -0.492743) UV(0.158203, 0.714193)
|
||||
V 312: Pos(0.343, 0.0, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.505859)
|
||||
V 313: Pos(0.309038, -0.128008, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.547526)
|
||||
V 314: Pos(0.316891, -0.13126, 0.9) Norm(0.853456, -0.169763, 0.492741) UV(0.205078, 0.505859)
|
||||
V 315: Pos(0.3175, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.589193)
|
||||
V 316: Pos(0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.589193)
|
||||
V 317: Pos(0.285479, -0.118249, 0.9) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.630859)
|
||||
V 318: Pos(0.3175, 0.0, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.251953, 0.589193)
|
||||
V 319: Pos(0.293332, -0.121502, 0.914722) Norm(0.853456, -0.169763, -0.492741) UV(0.205078, 0.589193)
|
||||
V 320: Pos(0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 321: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 322: Pos(0.285479, -0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492743) UV(0.205078, 0.630859)
|
||||
V 323: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.714193)
|
||||
V 324: Pos(0.3175, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.672526)
|
||||
V 325: Pos(0.293332, -0.121502, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.205078, 0.672526)
|
||||
V 326: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.251953, 0.714193)
|
||||
V 327: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.755859)
|
||||
V 328: Pos(0.309038, -0.128008, 0.885278) Norm(0.853455, -0.169763, -0.492743) UV(0.205078, 0.714193)
|
||||
V 329: Pos(0.308, 0.0, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 330: Pos(0.284555, 0.117866, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 331: Pos(0.284555, 0.117866, 0.35) Norm(-0.980785, -0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 332: Pos(0.284555, 0.117866, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 333: Pos(0.217789, 0.217789, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 334: Pos(0.217789, 0.217789, 0.35) Norm(-0.83147, -0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 335: Pos(0.217789, 0.217789, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 336: Pos(0.117866, 0.284555, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 337: Pos(0.117866, 0.284555, 0.35) Norm(-0.55557, -0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 338: Pos(0.117866, 0.284555, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 339: Pos(0.0, 0.308, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 340: Pos(0.0, 0.308, 0.35) Norm(-0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 341: Pos(0.0, 0.308, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 342: Pos(-0.117866, 0.284555, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 343: Pos(-0.117866, 0.284555, 0.35) Norm(0.19509, -0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 344: Pos(-0.117866, 0.284555, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 345: Pos(-0.217789, 0.217789, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 346: Pos(-0.217789, 0.217789, 0.35) Norm(0.55557, -0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 347: Pos(-0.217789, 0.217789, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 348: Pos(-0.284555, 0.117866, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 349: Pos(-0.284555, 0.117866, 0.35) Norm(0.83147, -0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 350: Pos(-0.284555, 0.117866, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 351: Pos(-0.308, 0.0, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 352: Pos(-0.308, 0.0, 0.35) Norm(0.980785, -0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 353: Pos(-0.308, 0.0, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.001953)
|
||||
V 354: Pos(-0.284555, -0.117866, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.345703)
|
||||
V 355: Pos(-0.284555, -0.117866, 0.35) Norm(0.980785, 0.19509, 0.0) UV(0.552734, 0.001953)
|
||||
V 356: Pos(-0.284555, -0.117866, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.001953)
|
||||
V 357: Pos(-0.217789, -0.217789, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.345703)
|
||||
V 358: Pos(-0.217789, -0.217789, 0.35) Norm(0.83147, 0.55557, 0.0) UV(0.599609, 0.001953)
|
||||
V 359: Pos(-0.217789, -0.217789, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.001953)
|
||||
V 360: Pos(-0.117866, -0.284555, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.345703)
|
||||
V 361: Pos(-0.117866, -0.284555, 0.35) Norm(0.55557, 0.83147, 0.0) UV(0.646484, 0.001953)
|
||||
V 362: Pos(-0.117866, -0.284555, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.001953)
|
||||
V 363: Pos(0.0, -0.308, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 364: Pos(0.0, -0.308, 0.35) Norm(0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 365: Pos(0.0, -0.308, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.001953)
|
||||
V 366: Pos(0.117866, -0.284555, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.345703)
|
||||
V 367: Pos(0.117866, -0.284555, 0.35) Norm(-0.19509, 0.980785, 0.0) UV(0.740234, 0.001953)
|
||||
V 368: Pos(0.117866, -0.284555, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.001953)
|
||||
V 369: Pos(0.217789, -0.217789, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.345703)
|
||||
V 370: Pos(0.217789, -0.217789, 0.35) Norm(-0.55557, 0.83147, 0.0) UV(0.787109, 0.001953)
|
||||
V 371: Pos(0.217789, -0.217789, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.001953)
|
||||
V 372: Pos(0.284555, -0.117866, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.345703)
|
||||
V 373: Pos(0.284555, -0.117866, 0.35) Norm(-0.83147, 0.55557, 0.0) UV(0.833984, 0.001953)
|
||||
V 374: Pos(0.284555, -0.117866, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.001953)
|
||||
V 375: Pos(0.308, 0.0, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.345703)
|
||||
V 376: Pos(0.308, 0.0, 0.35) Norm(-0.980785, 0.19509, 0.0) UV(0.880859, 0.001953)
|
||||
V 377: Pos(-0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 378: Pos(0.0, 0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 379: Pos(0.308, 0.0, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 380: Pos(0.295641, 0.122459, 0.87) Norm(-0.980785, -0.19509, 0.0) UV(0.048828, 0.312667)
|
||||
V 381: Pos(0.226274, 0.226274, 0.87) Norm(0.83147, 0.55557, 0.0) UV(0.095703, 0.312667)
|
||||
V 382: Pos(0.122459, 0.295641, 0.87) Norm(-0.55557, -0.83147, 0.0) UV(0.142578, 0.312667)
|
||||
V 383: Pos(0.0, 0.32, 0.87) Norm(-0.19509, -0.980785, 0.0) UV(0.189453, 0.312667)
|
||||
V 384: Pos(-0.122459, 0.295641, 0.87) Norm(0.19509, -0.980785, 0.0) UV(0.236328, 0.312667)
|
||||
V 385: Pos(-0.226274, 0.226274, 0.87) Norm(-0.55557, 0.83147, 0.0) UV(0.283203, 0.312667)
|
||||
V 386: Pos(-0.295641, 0.122459, 0.87) Norm(0.83147, -0.55557, 0.0) UV(0.080078, 0.312667)
|
||||
V 387: Pos(-0.32, 0.0, 0.87) Norm(0.980785, -0.19509, 0.0) UV(0.126953, 0.312667)
|
||||
V 388: Pos(-0.295641, -0.122459, 0.87) Norm(0.980785, 0.19509, 0.0) UV(0.173828, 0.312667)
|
||||
V 389: Pos(-0.226274, -0.226274, 0.87) Norm(-0.83147, -0.55557, 0.0) UV(0.220703, 0.312667)
|
||||
V 390: Pos(-0.122459, -0.295641, 0.87) Norm(0.55557, 0.83147, 0.0) UV(0.267578, 0.312667)
|
||||
V 391: Pos(0.0, -0.32, 0.87) Norm(0.19509, 0.980785, 0.0) UV(0.064453, 0.312667)
|
||||
V 392: Pos(0.122459, -0.295641, 0.87) Norm(-0.19509, 0.980785, 0.0) UV(0.111328, 0.312667)
|
||||
V 393: Pos(0.226274, -0.226274, 0.87) Norm(0.55557, -0.83147, 0.0) UV(0.158203, 0.312667)
|
||||
V 394: Pos(0.295641, -0.122459, 0.87) Norm(-0.83147, 0.55557, 0.0) UV(0.205078, 0.312667)
|
||||
V 395: Pos(0.32, 0.0, 0.87) Norm(-0.980785, 0.19509, 0.0) UV(0.251953, 0.312667)
|
||||
V 396: Pos(0.324, 0.0, 0.62) Norm(0.980785, -0.19509, 0.0) UV(0.600586, 0.693359)
|
||||
V 397: Pos(0.299337, 0.123989, 0.62) Norm(0.980785, 0.19509, 0.0) UV(0.694336, 0.693359)
|
||||
V 398: Pos(0.309038, 0.128008, 0.914722) Norm(0.853456, 0.169763, 0.492741) UV(0.048828, 0.547526)
|
||||
V 399: Pos(0.293332, 0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.048828, 0.589193)
|
||||
V 400: Pos(0.309, 0.0, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.001953, 0.630859)
|
||||
V 401: Pos(0.285479, 0.118249, 0.9) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.630859)
|
||||
V 402: Pos(0.293332, 0.121502, 0.914722) Norm(0.853456, 0.169763, -0.492741) UV(0.048828, 0.589193)
|
||||
V 403: Pos(0.285479, 0.118249, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.630859)
|
||||
V 404: Pos(0.293332, 0.121502, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.048828, 0.672526)
|
||||
V 405: Pos(0.3175, 0.0, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.001953, 0.672526)
|
||||
V 406: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.001953, 0.714193)
|
||||
V 407: Pos(0.309038, 0.128008, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.714193)
|
||||
V 408: Pos(0.316891, 0.13126, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.048828, 0.755859)
|
||||
V 409: Pos(0.343, 0.0, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.001953, 0.755859)
|
||||
V 410: Pos(0.309038, 0.128008, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.048828, 0.547526)
|
||||
V 411: Pos(0.236527, 0.236527, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.547526)
|
||||
V 412: Pos(0.242538, 0.242538, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.095703, 0.505859)
|
||||
V 413: Pos(0.285479, 0.118249, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.048828, 0.630859)
|
||||
V 414: Pos(0.218496, 0.218496, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.630859)
|
||||
V 415: Pos(0.224506, 0.224506, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.095703, 0.589193)
|
||||
V 416: Pos(0.224506, 0.224506, 0.885278) Norm(0.723524, 0.483443, 0.492743) UV(0.095703, 0.672526)
|
||||
V 417: Pos(0.242538, 0.242538, 0.9) Norm(0.723524, 0.483443, -0.492743) UV(0.095703, 0.755859)
|
||||
V 418: Pos(0.236527, 0.236527, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.095703, 0.547526)
|
||||
V 419: Pos(0.128008, 0.309038, 0.914722) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.547526)
|
||||
V 420: Pos(0.13126, 0.316891, 0.9) Norm(-0.483444, -0.723525, -0.492741) UV(0.142578, 0.505859)
|
||||
V 421: Pos(0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.589193)
|
||||
V 422: Pos(0.118249, 0.285479, 0.9) Norm(-0.483444, -0.723525, 0.492742) UV(0.142578, 0.630859)
|
||||
V 423: Pos(0.121502, 0.293332, 0.885278) Norm(-0.483443, -0.723524, -0.492743) UV(0.142578, 0.672526)
|
||||
V 424: Pos(0.128008, 0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.142578, 0.714193)
|
||||
V 425: Pos(0.13126, 0.316891, 0.9) Norm(0.483443, 0.723524, -0.492743) UV(0.142578, 0.755859)
|
||||
V 426: Pos(0.0, 0.3345, 0.914722) Norm(0.169763, 0.853456, 0.492741) UV(0.189453, 0.547526)
|
||||
V 427: Pos(0.118249, 0.285479, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.630859)
|
||||
V 428: Pos(0.0, 0.309, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.630859)
|
||||
V 429: Pos(0.0, 0.3175, 0.914722) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.589193)
|
||||
V 430: Pos(0.0, 0.309, 0.9) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.630859)
|
||||
V 431: Pos(0.0, 0.3175, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.189453, 0.672526)
|
||||
V 432: Pos(0.121502, 0.293332, 0.885278) Norm(-0.169763, -0.853454, -0.492743) UV(0.142578, 0.672526)
|
||||
V 433: Pos(0.0, 0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.189453, 0.714193)
|
||||
V 434: Pos(0.0, 0.3345, 0.885278) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.714193)
|
||||
V 435: Pos(0.0, 0.343, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.189453, 0.755859)
|
||||
V 436: Pos(0.13126, 0.316891, 0.9) Norm(0.169763, 0.853455, -0.492742) UV(0.142578, 0.755859)
|
||||
V 437: Pos(-0.128008, 0.309038, 0.914722) Norm(-0.169763, 0.853456, 0.492741) UV(0.236328, 0.547526)
|
||||
V 438: Pos(-0.121502, 0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.236328, 0.589193)
|
||||
V 439: Pos(0.0, 0.309, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.189453, 0.630859)
|
||||
V 440: Pos(-0.118249, 0.285479, 0.9) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.630859)
|
||||
V 441: Pos(-0.121502, 0.293332, 0.914722) Norm(-0.169763, 0.853456, -0.492741) UV(0.236328, 0.589193)
|
||||
V 442: Pos(-0.118249, 0.285479, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.630859)
|
||||
V 443: Pos(-0.121502, 0.293332, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.236328, 0.672526)
|
||||
V 444: Pos(0.0, 0.3175, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.189453, 0.672526)
|
||||
V 445: Pos(-0.128008, 0.309038, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.714193)
|
||||
V 446: Pos(-0.13126, 0.316891, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.236328, 0.755859)
|
||||
V 447: Pos(0.0, 0.343, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.189453, 0.755859)
|
||||
V 448: Pos(-0.128008, 0.309038, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.236328, 0.547526)
|
||||
V 449: Pos(-0.236527, 0.236527, 0.914722) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.547526)
|
||||
V 450: Pos(-0.242538, 0.242538, 0.9) Norm(0.483444, -0.723524, -0.492742) UV(0.283203, 0.505859)
|
||||
V 451: Pos(-0.224506, 0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.589193)
|
||||
V 452: Pos(-0.224506, 0.224506, 0.914722) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.589193)
|
||||
V 453: Pos(-0.218496, 0.218496, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.283203, 0.630859)
|
||||
V 454: Pos(-0.118249, 0.285479, 0.9) Norm(0.483444, -0.723524, 0.492742) UV(0.236328, 0.630859)
|
||||
V 455: Pos(-0.224506, 0.224506, 0.885278) Norm(-0.483443, 0.723524, 0.492743) UV(0.283203, 0.672526)
|
||||
V 456: Pos(-0.236527, 0.236527, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.283203, 0.714193)
|
||||
V 457: Pos(-0.242538, 0.242538, 0.9) Norm(0.483443, -0.723524, 0.492743) UV(0.283203, 0.755859)
|
||||
V 458: Pos(-0.236527, 0.236527, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.033203, 0.547526)
|
||||
V 459: Pos(-0.309038, 0.128008, 0.914722) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.547526)
|
||||
V 460: Pos(-0.316891, 0.13126, 0.9) Norm(0.723525, -0.483444, -0.492741) UV(0.080078, 0.505859)
|
||||
V 461: Pos(-0.285479, 0.118249, 0.9) Norm(-0.723525, 0.483444, -0.492742) UV(0.080078, 0.630859)
|
||||
V 462: Pos(-0.293332, 0.121502, 0.885278) Norm(-0.723524, 0.483443, 0.492743) UV(0.080078, 0.672526)
|
||||
V 463: Pos(-0.309038, 0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.080078, 0.714193)
|
||||
V 464: Pos(-0.316891, 0.13126, 0.9) Norm(-0.723524, 0.483443, -0.492743) UV(0.080078, 0.755859)
|
||||
V 465: Pos(-0.3345, 0.0, 0.914722) Norm(-0.853456, 0.169763, 0.492741) UV(0.126953, 0.547526)
|
||||
V 466: Pos(-0.285479, 0.118249, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.630859)
|
||||
V 467: Pos(-0.309, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.630859)
|
||||
V 468: Pos(-0.3175, 0.0, 0.914722) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.589193)
|
||||
V 469: Pos(-0.309, 0.0, 0.9) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.630859)
|
||||
V 470: Pos(-0.3175, 0.0, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 471: Pos(-0.293332, 0.121502, 0.885278) Norm(0.853454, -0.169763, -0.492743) UV(0.080078, 0.672526)
|
||||
V 472: Pos(-0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.126953, 0.714193)
|
||||
V 473: Pos(-0.3345, 0.0, 0.885278) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.714193)
|
||||
V 474: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.126953, 0.755859)
|
||||
V 475: Pos(-0.316891, 0.13126, 0.9) Norm(-0.853455, 0.169763, -0.492742) UV(0.080078, 0.755859)
|
||||
V 476: Pos(-0.309038, -0.128008, 0.914722) Norm(-0.853456, -0.169763, 0.492741) UV(0.173828, 0.547526)
|
||||
V 477: Pos(-0.293332, -0.121502, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.173828, 0.589193)
|
||||
V 478: Pos(-0.309, 0.0, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.126953, 0.630859)
|
||||
V 479: Pos(-0.285479, -0.118249, 0.9) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.630859)
|
||||
V 480: Pos(-0.293332, -0.121502, 0.914722) Norm(-0.853456, -0.169763, -0.492741) UV(0.173828, 0.589193)
|
||||
V 481: Pos(-0.285479, -0.118249, 0.9) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.630859)
|
||||
V 482: Pos(-0.293332, -0.121502, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.173828, 0.672526)
|
||||
V 483: Pos(-0.3175, 0.0, 0.885278) Norm(0.853455, 0.169763, -0.492743) UV(0.126953, 0.672526)
|
||||
V 484: Pos(-0.309038, -0.128008, 0.885278) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.714193)
|
||||
V 485: Pos(-0.316891, -0.13126, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.173828, 0.755859)
|
||||
V 486: Pos(-0.343, 0.0, 0.9) Norm(-0.853455, -0.169763, -0.492743) UV(0.126953, 0.755859)
|
||||
V 487: Pos(-0.309038, -0.128008, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.173828, 0.547526)
|
||||
V 488: Pos(-0.236527, -0.236527, 0.914722) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.547526)
|
||||
V 489: Pos(-0.242538, -0.242538, 0.9) Norm(0.723524, 0.483444, -0.492742) UV(0.220703, 0.505859)
|
||||
V 490: Pos(-0.285479, -0.118249, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.173828, 0.630859)
|
||||
V 491: Pos(-0.218496, -0.218496, 0.9) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.630859)
|
||||
V 492: Pos(-0.224506, -0.224506, 0.914722) Norm(-0.723524, -0.483444, -0.492742) UV(0.220703, 0.589193)
|
||||
V 493: Pos(-0.224506, -0.224506, 0.885278) Norm(-0.723524, -0.483443, 0.492743) UV(0.220703, 0.672526)
|
||||
V 494: Pos(-0.242538, -0.242538, 0.9) Norm(-0.723524, -0.483443, -0.492743) UV(0.220703, 0.755859)
|
||||
V 495: Pos(-0.236527, -0.236527, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.220703, 0.547526)
|
||||
V 496: Pos(-0.128008, -0.309038, 0.914722) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.547526)
|
||||
V 497: Pos(-0.13126, -0.316891, 0.9) Norm(0.483444, 0.723525, -0.492741) UV(0.267578, 0.505859)
|
||||
V 498: Pos(-0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.589193)
|
||||
V 499: Pos(-0.118249, -0.285479, 0.9) Norm(0.483444, 0.723525, 0.492742) UV(0.267578, 0.630859)
|
||||
V 500: Pos(-0.121502, -0.293332, 0.885278) Norm(0.483443, 0.723524, -0.492743) UV(0.267578, 0.672526)
|
||||
V 501: Pos(-0.128008, -0.309038, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.267578, 0.714193)
|
||||
V 502: Pos(-0.13126, -0.316891, 0.9) Norm(-0.483443, -0.723524, -0.492743) UV(0.267578, 0.755859)
|
||||
V 503: Pos(0.0, -0.3345, 0.914722) Norm(-0.169763, -0.853456, 0.492741) UV(0.064453, 0.547526)
|
||||
V 504: Pos(-0.118249, -0.285479, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.630859)
|
||||
V 505: Pos(0.0, -0.309, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.630859)
|
||||
V 506: Pos(0.0, -0.3175, 0.914722) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.589193)
|
||||
V 507: Pos(0.0, -0.309, 0.9) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.630859)
|
||||
V 508: Pos(0.0, -0.3175, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.064453, 0.672526)
|
||||
V 509: Pos(-0.121502, -0.293332, 0.885278) Norm(0.169763, 0.853454, -0.492743) UV(0.017578, 0.672526)
|
||||
V 510: Pos(0.0, -0.3345, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.064453, 0.714193)
|
||||
V 511: Pos(0.0, -0.3345, 0.885278) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.714193)
|
||||
V 512: Pos(0.0, -0.343, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.064453, 0.755859)
|
||||
V 513: Pos(-0.13126, -0.316891, 0.9) Norm(-0.169763, -0.853455, -0.492742) UV(0.017578, 0.755859)
|
||||
V 514: Pos(0.128008, -0.309038, 0.914722) Norm(0.169763, -0.853456, 0.492741) UV(0.111328, 0.547526)
|
||||
V 515: Pos(0.121502, -0.293332, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.111328, 0.589193)
|
||||
V 516: Pos(0.0, -0.309, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.064453, 0.630859)
|
||||
V 517: Pos(0.118249, -0.285479, 0.9) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.630859)
|
||||
V 518: Pos(0.121502, -0.293332, 0.914722) Norm(0.169763, -0.853456, -0.492741) UV(0.111328, 0.589193)
|
||||
V 519: Pos(0.118249, -0.285479, 0.9) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.630859)
|
||||
V 520: Pos(0.121502, -0.293332, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.111328, 0.672526)
|
||||
V 521: Pos(0.0, -0.3175, 0.885278) Norm(-0.169763, 0.853455, -0.492743) UV(0.064453, 0.672526)
|
||||
V 522: Pos(0.128008, -0.309038, 0.885278) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.714193)
|
||||
V 523: Pos(0.13126, -0.316891, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.111328, 0.755859)
|
||||
V 524: Pos(0.0, -0.343, 0.9) Norm(0.169763, -0.853455, -0.492743) UV(0.064453, 0.755859)
|
||||
V 525: Pos(0.128008, -0.309038, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.111328, 0.547526)
|
||||
V 526: Pos(0.236527, -0.236527, 0.914722) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.547526)
|
||||
V 527: Pos(0.242538, -0.242538, 0.9) Norm(-0.483444, 0.723524, -0.492742) UV(0.158203, 0.505859)
|
||||
V 528: Pos(0.224506, -0.224506, 0.914722) Norm(0.0, 0.0, -1.0) UV(0.158203, 0.589193)
|
||||
V 529: Pos(0.224506, -0.224506, 0.914722) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.589193)
|
||||
V 530: Pos(0.218496, -0.218496, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.158203, 0.630859)
|
||||
V 531: Pos(0.118249, -0.285479, 0.9) Norm(-0.483444, 0.723524, 0.492742) UV(0.111328, 0.630859)
|
||||
V 532: Pos(0.224506, -0.224506, 0.885278) Norm(0.483443, -0.723524, 0.492743) UV(0.158203, 0.672526)
|
||||
V 533: Pos(0.242538, -0.242538, 0.9) Norm(-0.483443, 0.723524, 0.492743) UV(0.158203, 0.755859)
|
||||
V 534: Pos(0.236527, -0.236527, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.158203, 0.547526)
|
||||
V 535: Pos(0.309038, -0.128008, 0.914722) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.547526)
|
||||
V 536: Pos(0.316891, -0.13126, 0.9) Norm(-0.723525, 0.483444, -0.492741) UV(0.205078, 0.505859)
|
||||
V 537: Pos(0.285479, -0.118249, 0.9) Norm(0.723525, -0.483444, -0.492742) UV(0.205078, 0.630859)
|
||||
V 538: Pos(0.293332, -0.121502, 0.885278) Norm(0.723524, -0.483443, 0.492743) UV(0.205078, 0.672526)
|
||||
V 539: Pos(0.309038, -0.128008, 0.885278) Norm(0.0, 0.0, -1.0) UV(0.205078, 0.714193)
|
||||
V 540: Pos(0.316891, -0.13126, 0.9) Norm(0.723524, -0.483443, -0.492743) UV(0.205078, 0.755859)
|
||||
V 541: Pos(0.3345, 0.0, 0.914722) Norm(0.853456, -0.169763, 0.492741) UV(0.251953, 0.547526)
|
||||
V 542: Pos(0.3345, 0.0, 0.914722) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.547526)
|
||||
V 543: Pos(0.285479, -0.118249, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.630859)
|
||||
V 544: Pos(0.309, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.630859)
|
||||
V 545: Pos(0.3175, 0.0, 0.914722) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.589193)
|
||||
V 546: Pos(0.309, 0.0, 0.9) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.630859)
|
||||
V 547: Pos(0.3175, 0.0, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.251953, 0.672526)
|
||||
V 548: Pos(0.293332, -0.121502, 0.885278) Norm(-0.853454, 0.169763, -0.492743) UV(0.205078, 0.672526)
|
||||
V 549: Pos(0.3345, 0.0, 0.885278) Norm(0.0, 0.0, 1.0) UV(0.251953, 0.714193)
|
||||
V 550: Pos(0.3345, 0.0, 0.885278) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.714193)
|
||||
V 551: Pos(0.343, 0.0, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.251953, 0.755859)
|
||||
V 552: Pos(0.316891, -0.13126, 0.9) Norm(0.853455, -0.169763, -0.492742) UV(0.205078, 0.755859)
|
||||
V 553: Pos(0.308, 0.0, 0.88) Norm(-0.980785, -0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 554: Pos(0.284555, 0.117866, 0.88) Norm(-0.83147, -0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 555: Pos(0.217789, 0.217789, 0.88) Norm(-0.55557, -0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 556: Pos(0.117866, 0.284555, 0.88) Norm(-0.19509, -0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 557: Pos(0.0, 0.308, 0.88) Norm(0.19509, -0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 558: Pos(-0.117866, 0.284555, 0.88) Norm(0.55557, -0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 559: Pos(-0.217789, 0.217789, 0.88) Norm(0.83147, -0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 560: Pos(-0.284555, 0.117866, 0.88) Norm(0.980785, -0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 561: Pos(-0.308, 0.0, 0.88) Norm(0.980785, 0.19509, 0.0) UV(0.505859, 0.345703)
|
||||
V 562: Pos(-0.284555, -0.117866, 0.88) Norm(0.83147, 0.55557, 0.0) UV(0.552734, 0.345703)
|
||||
V 563: Pos(-0.217789, -0.217789, 0.88) Norm(0.55557, 0.83147, 0.0) UV(0.599609, 0.345703)
|
||||
V 564: Pos(-0.117866, -0.284555, 0.88) Norm(0.19509, 0.980785, 0.0) UV(0.646484, 0.345703)
|
||||
V 565: Pos(0.0, -0.308, 0.88) Norm(-0.19509, 0.980785, 0.0) UV(0.693359, 0.345703)
|
||||
V 566: Pos(0.117866, -0.284555, 0.88) Norm(-0.55557, 0.83147, 0.0) UV(0.740234, 0.345703)
|
||||
V 567: Pos(0.217789, -0.217789, 0.88) Norm(-0.83147, 0.55557, 0.0) UV(0.787109, 0.345703)
|
||||
V 568: Pos(0.284555, -0.117866, 0.88) Norm(-0.980785, 0.19509, 0.0) UV(0.833984, 0.345703)
|
||||
V 569: Pos(0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 570: Pos(0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 571: Pos(0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 572: Pos(0.0, -0.308, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 573: Pos(-0.117866, -0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 574: Pos(-0.217789, -0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 575: Pos(-0.284555, -0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 576: Pos(-0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 577: Pos(-0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 578: Pos(-0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 579: Pos(0.117866, 0.284555, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 580: Pos(0.217789, 0.217789, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
V 581: Pos(0.284555, 0.117866, 0.35) Norm(0.0, 0.0, -1.0) UV(0.702148, 0.509766)
|
||||
|
||||
===Triangles (Indices): 274
|
||||
Tri: 0 1 2
|
||||
Tri: 3 4 5
|
||||
Tri: 6 7 8
|
||||
Tri: 9 10 11
|
||||
Tri: 12 13 14
|
||||
Tri: 15 16 17
|
||||
Tri: 18 19 20
|
||||
Tri: 21 22 23
|
||||
Tri: 24 25 26
|
||||
Tri: 27 28 29
|
||||
Tri: 30 31 32
|
||||
Tri: 33 34 35
|
||||
Tri: 36 37 38
|
||||
Tri: 39 40 41
|
||||
Tri: 42 43 44
|
||||
Tri: 45 46 47
|
||||
Tri: 48 49 50
|
||||
Tri: 51 52 53
|
||||
Tri: 54 55 56
|
||||
Tri: 57 58 59
|
||||
Tri: 60 61 62
|
||||
Tri: 63 64 65
|
||||
Tri: 66 67 68
|
||||
Tri: 69 70 71
|
||||
Tri: 72 73 74
|
||||
Tri: 75 76 77
|
||||
Tri: 78 79 80
|
||||
Tri: 81 82 83
|
||||
Tri: 84 85 86
|
||||
Tri: 87 88 89
|
||||
Tri: 90 91 92
|
||||
Tri: 93 94 76
|
||||
Tri: 95 96 97
|
||||
Tri: 98 99 100
|
||||
Tri: 101 102 84
|
||||
Tri: 103 104 105
|
||||
Tri: 106 107 108
|
||||
Tri: 109 110 111
|
||||
Tri: 112 113 114
|
||||
Tri: 115 116 117
|
||||
Tri: 118 119 120
|
||||
Tri: 121 122 123
|
||||
Tri: 124 125 126
|
||||
Tri: 127 110 128
|
||||
Tri: 129 130 131
|
||||
Tri: 132 133 134
|
||||
Tri: 119 135 136
|
||||
Tri: 137 138 139
|
||||
Tri: 140 141 142
|
||||
Tri: 143 144 145
|
||||
Tri: 146 147 148
|
||||
Tri: 149 150 151
|
||||
Tri: 152 153 154
|
||||
Tri: 155 156 157
|
||||
Tri: 158 159 160
|
||||
Tri: 161 162 163
|
||||
Tri: 164 165 166
|
||||
Tri: 167 168 169
|
||||
Tri: 170 171 172
|
||||
Tri: 173 174 175
|
||||
Tri: 176 177 178
|
||||
Tri: 161 179 180
|
||||
Tri: 181 182 183
|
||||
Tri: 184 185 186
|
||||
Tri: 187 188 189
|
||||
Tri: 190 191 192
|
||||
Tri: 193 194 195
|
||||
Tri: 196 179 197
|
||||
Tri: 198 199 200
|
||||
Tri: 201 202 203
|
||||
Tri: 188 204 205
|
||||
Tri: 206 207 208
|
||||
Tri: 209 210 211
|
||||
Tri: 212 213 214
|
||||
Tri: 215 216 217
|
||||
Tri: 218 219 220
|
||||
Tri: 221 222 223
|
||||
Tri: 224 225 226
|
||||
Tri: 227 228 229
|
||||
Tri: 230 231 213
|
||||
Tri: 232 233 234
|
||||
Tri: 235 236 237
|
||||
Tri: 238 239 221
|
||||
Tri: 240 241 242
|
||||
Tri: 243 244 245
|
||||
Tri: 246 247 248
|
||||
Tri: 249 250 251
|
||||
Tri: 252 253 254
|
||||
Tri: 255 256 257
|
||||
Tri: 258 259 260
|
||||
Tri: 261 262 263
|
||||
Tri: 264 247 265
|
||||
Tri: 266 267 268
|
||||
Tri: 269 270 271
|
||||
Tri: 256 272 273
|
||||
Tri: 274 275 276
|
||||
Tri: 277 278 279
|
||||
Tri: 280 281 282
|
||||
Tri: 283 284 285
|
||||
Tri: 286 287 288
|
||||
Tri: 289 290 291
|
||||
Tri: 292 293 294
|
||||
Tri: 295 296 297
|
||||
Tri: 298 299 300
|
||||
Tri: 301 302 303
|
||||
Tri: 304 305 306
|
||||
Tri: 307 308 289
|
||||
Tri: 309 310 311
|
||||
Tri: 312 313 314
|
||||
Tri: 298 315 316
|
||||
Tri: 317 318 319
|
||||
Tri: 320 321 322
|
||||
Tri: 323 324 325
|
||||
Tri: 326 327 328
|
||||
Tri: 329 330 331
|
||||
Tri: 332 333 334
|
||||
Tri: 335 336 337
|
||||
Tri: 338 339 340
|
||||
Tri: 341 342 343
|
||||
Tri: 344 345 346
|
||||
Tri: 347 348 349
|
||||
Tri: 350 351 352
|
||||
Tri: 353 354 355
|
||||
Tri: 356 357 358
|
||||
Tri: 359 360 361
|
||||
Tri: 362 363 364
|
||||
Tri: 365 366 367
|
||||
Tri: 368 369 370
|
||||
Tri: 371 372 373
|
||||
Tri: 374 375 376
|
||||
Tri: 377 378 379
|
||||
Tri: 0 380 1
|
||||
Tri: 3 381 4
|
||||
Tri: 6 382 7
|
||||
Tri: 9 383 10
|
||||
Tri: 12 384 13
|
||||
Tri: 15 385 16
|
||||
Tri: 18 386 19
|
||||
Tri: 21 387 22
|
||||
Tri: 24 388 25
|
||||
Tri: 27 389 28
|
||||
Tri: 30 390 31
|
||||
Tri: 33 391 34
|
||||
Tri: 36 392 37
|
||||
Tri: 39 393 40
|
||||
Tri: 42 394 43
|
||||
Tri: 45 395 46
|
||||
Tri: 48 396 49
|
||||
Tri: 51 397 52
|
||||
Tri: 54 398 55
|
||||
Tri: 57 399 58
|
||||
Tri: 400 401 402
|
||||
Tri: 403 404 405
|
||||
Tri: 66 406 67
|
||||
Tri: 407 408 409
|
||||
Tri: 410 411 412
|
||||
Tri: 75 93 76
|
||||
Tri: 413 414 415
|
||||
Tri: 81 416 82
|
||||
Tri: 84 102 85
|
||||
Tri: 87 417 88
|
||||
Tri: 418 419 420
|
||||
Tri: 93 421 94
|
||||
Tri: 95 422 96
|
||||
Tri: 98 423 99
|
||||
Tri: 101 424 102
|
||||
Tri: 103 425 104
|
||||
Tri: 106 426 107
|
||||
Tri: 109 128 110
|
||||
Tri: 427 428 429
|
||||
Tri: 430 431 432
|
||||
Tri: 118 433 119
|
||||
Tri: 434 435 436
|
||||
Tri: 124 437 125
|
||||
Tri: 127 438 110
|
||||
Tri: 439 440 441
|
||||
Tri: 442 443 444
|
||||
Tri: 119 433 135
|
||||
Tri: 445 446 447
|
||||
Tri: 448 449 450
|
||||
Tri: 143 451 144
|
||||
Tri: 452 453 454
|
||||
Tri: 149 455 150
|
||||
Tri: 152 456 153
|
||||
Tri: 155 457 156
|
||||
Tri: 458 459 460
|
||||
Tri: 161 180 162
|
||||
Tri: 164 461 165
|
||||
Tri: 167 462 168
|
||||
Tri: 170 463 171
|
||||
Tri: 173 464 174
|
||||
Tri: 176 465 177
|
||||
Tri: 161 197 179
|
||||
Tri: 466 467 468
|
||||
Tri: 469 470 471
|
||||
Tri: 187 472 188
|
||||
Tri: 473 474 475
|
||||
Tri: 193 476 194
|
||||
Tri: 196 477 179
|
||||
Tri: 478 479 480
|
||||
Tri: 481 482 483
|
||||
Tri: 188 472 204
|
||||
Tri: 484 485 486
|
||||
Tri: 487 488 489
|
||||
Tri: 212 230 213
|
||||
Tri: 490 491 492
|
||||
Tri: 218 493 219
|
||||
Tri: 221 239 222
|
||||
Tri: 224 494 225
|
||||
Tri: 495 496 497
|
||||
Tri: 230 498 231
|
||||
Tri: 232 499 233
|
||||
Tri: 235 500 236
|
||||
Tri: 238 501 239
|
||||
Tri: 240 502 241
|
||||
Tri: 243 503 244
|
||||
Tri: 246 265 247
|
||||
Tri: 504 505 506
|
||||
Tri: 507 508 509
|
||||
Tri: 255 510 256
|
||||
Tri: 511 512 513
|
||||
Tri: 261 514 262
|
||||
Tri: 264 515 247
|
||||
Tri: 516 517 518
|
||||
Tri: 519 520 521
|
||||
Tri: 256 510 272
|
||||
Tri: 522 523 524
|
||||
Tri: 525 526 527
|
||||
Tri: 280 528 281
|
||||
Tri: 529 530 531
|
||||
Tri: 286 532 287
|
||||
Tri: 289 308 290
|
||||
Tri: 292 533 293
|
||||
Tri: 534 535 536
|
||||
Tri: 298 316 299
|
||||
Tri: 301 537 302
|
||||
Tri: 304 538 305
|
||||
Tri: 307 539 308
|
||||
Tri: 309 540 310
|
||||
Tri: 312 541 313
|
||||
Tri: 298 542 315
|
||||
Tri: 543 544 545
|
||||
Tri: 546 547 548
|
||||
Tri: 323 549 324
|
||||
Tri: 550 551 552
|
||||
Tri: 329 553 330
|
||||
Tri: 332 554 333
|
||||
Tri: 335 555 336
|
||||
Tri: 338 556 339
|
||||
Tri: 341 557 342
|
||||
Tri: 344 558 345
|
||||
Tri: 347 559 348
|
||||
Tri: 350 560 351
|
||||
Tri: 353 561 354
|
||||
Tri: 356 562 357
|
||||
Tri: 359 563 360
|
||||
Tri: 362 564 363
|
||||
Tri: 365 565 366
|
||||
Tri: 368 566 369
|
||||
Tri: 371 567 372
|
||||
Tri: 374 568 375
|
||||
Tri: 379 569 570
|
||||
Tri: 570 571 572
|
||||
Tri: 572 573 574
|
||||
Tri: 574 575 377
|
||||
Tri: 377 576 577
|
||||
Tri: 577 578 378
|
||||
Tri: 378 579 580
|
||||
Tri: 580 581 379
|
||||
Tri: 379 570 572
|
||||
Tri: 572 574 377
|
||||
Tri: 377 577 378
|
||||
Tri: 378 580 379
|
||||
Tri: 379 572 377
|
||||
@@ -0,0 +1,183 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(GameServer LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||
if(NOT EXISTS "${EIGEN_BUNDLED_DIR}/Eigen/Core")
|
||||
message(FATAL_ERROR
|
||||
"Eigen3 was not found. Install Eigen3 or configure the graphical "
|
||||
"project once to populate thirdparty/eigen-5.0.0.")
|
||||
endif()
|
||||
add_library(game_server_eigen INTERFACE)
|
||||
target_include_directories(game_server_eigen INTERFACE "${EIGEN_BUNDLED_DIR}")
|
||||
add_library(Eigen3::Eigen ALIAS game_server_eigen)
|
||||
endif()
|
||||
|
||||
add_library(game_server_core STATIC
|
||||
"${GAME_ROOT}/src/Campaign.cpp"
|
||||
"${GAME_ROOT}/src/Campaign.h"
|
||||
"${GAME_ROOT}/src/CharacterState.cpp"
|
||||
"${GAME_ROOT}/src/CharacterState.h"
|
||||
"${GAME_ROOT}/src/GameState.cpp"
|
||||
"${GAME_ROOT}/src/GameState.h"
|
||||
"${GAME_ROOT}/src/ISaveable.h"
|
||||
"${GAME_ROOT}/src/Localization.cpp"
|
||||
"${GAME_ROOT}/src/Localization.h"
|
||||
"${GAME_ROOT}/src/LocationGameState.cpp"
|
||||
"${GAME_ROOT}/src/LocationGameState.h"
|
||||
"${GAME_ROOT}/src/LocationState.cpp"
|
||||
"${GAME_ROOT}/src/LocationState.h"
|
||||
"${GAME_ROOT}/src/NpcCar.cpp"
|
||||
"${GAME_ROOT}/src/NpcCar.h"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.cpp"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServer.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServerLifecycle.h"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.h"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.cpp"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.h"
|
||||
"${GAME_ROOT}/src/items/Inventory.cpp"
|
||||
"${GAME_ROOT}/src/items/Item.h"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.cpp"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.h"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.cpp"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.h"
|
||||
"${GAME_ROOT}/src/quest/QuestTypes.h"
|
||||
"${GAME_ROOT}/src/utils/HeadlessUtils.cpp"
|
||||
"${GAME_ROOT}/src/utils/Utils.h"
|
||||
)
|
||||
|
||||
source_group(TREE "${GAME_ROOT}/src" PREFIX "Source" FILES
|
||||
"${GAME_ROOT}/src/Campaign.cpp"
|
||||
"${GAME_ROOT}/src/Campaign.h"
|
||||
"${GAME_ROOT}/src/CharacterState.cpp"
|
||||
"${GAME_ROOT}/src/CharacterState.h"
|
||||
"${GAME_ROOT}/src/GameState.cpp"
|
||||
"${GAME_ROOT}/src/GameState.h"
|
||||
"${GAME_ROOT}/src/ISaveable.h"
|
||||
"${GAME_ROOT}/src/Localization.cpp"
|
||||
"${GAME_ROOT}/src/Localization.h"
|
||||
"${GAME_ROOT}/src/LocationGameState.cpp"
|
||||
"${GAME_ROOT}/src/LocationGameState.h"
|
||||
"${GAME_ROOT}/src/LocationState.cpp"
|
||||
"${GAME_ROOT}/src/LocationState.h"
|
||||
"${GAME_ROOT}/src/NpcCar.cpp"
|
||||
"${GAME_ROOT}/src/NpcCar.h"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.cpp"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServer.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServerLifecycle.h"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.h"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.cpp"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.h"
|
||||
"${GAME_ROOT}/src/items/Inventory.cpp"
|
||||
"${GAME_ROOT}/src/items/Item.h"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.cpp"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.h"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.cpp"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.h"
|
||||
"${GAME_ROOT}/src/quest/QuestTypes.h"
|
||||
"${GAME_ROOT}/src/utils/HeadlessUtils.cpp"
|
||||
"${GAME_ROOT}/src/utils/Utils.h"
|
||||
)
|
||||
|
||||
target_include_directories(game_server_core PUBLIC "${GAME_ROOT}/src")
|
||||
target_link_libraries(game_server_core PUBLIC Eigen3::Eigen Threads::Threads)
|
||||
target_compile_definitions(game_server_core PRIVATE FRG_HEADLESS_SERVER=1)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(game_server_core PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(game_server_core PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
add_executable(GameServer src/main.cpp README.md)
|
||||
source_group("Server" FILES src/main.cpp README.md)
|
||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||
if(WIN32)
|
||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||
message(FATAL_ERROR
|
||||
"Boost was not found at thirdparty/boost_1_90_0. Configure the "
|
||||
"graphical project once to populate it.")
|
||||
endif()
|
||||
target_sources(GameServer PRIVATE
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(
|
||||
GameServer PRIVATE NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
||||
endif()
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(
|
||||
GameServerPathFinderTests tests/PathFinderTests.cpp)
|
||||
target_link_libraries(GameServerPathFinderTests PRIVATE game_server_core)
|
||||
source_group(
|
||||
"Server\\Tests" FILES tests/PathFinderTests.cpp)
|
||||
add_test(
|
||||
NAME GameServer.PathFinder
|
||||
COMMAND GameServerPathFinderTests)
|
||||
add_executable(
|
||||
GameServerStateTests tests/LocationGameStateTests.cpp)
|
||||
target_link_libraries(GameServerStateTests PRIVATE game_server_core)
|
||||
source_group(
|
||||
"Server\\Tests" FILES tests/LocationGameStateTests.cpp)
|
||||
add_test(
|
||||
NAME GameServer.LocationState
|
||||
COMMAND GameServerStateTests)
|
||||
if(WIN32)
|
||||
add_executable(
|
||||
GameServerNetworkProtocolTests
|
||||
tests/NetworkProtocolTests.cpp
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h")
|
||||
target_link_libraries(
|
||||
GameServerNetworkProtocolTests PRIVATE game_server_core)
|
||||
add_test(
|
||||
NAME GameServer.NetworkProtocol
|
||||
COMMAND GameServerNetworkProtocolTests)
|
||||
add_executable(
|
||||
GameServerWebsocketLockstepTests
|
||||
tests/WebsocketLockstepTests.cpp
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameClient.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp")
|
||||
target_include_directories(
|
||||
GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
game_server_core ws2_32 mswsock)
|
||||
add_test(
|
||||
NAME GameServer.WebsocketLockstep
|
||||
COMMAND GameServerWebsocketLockstepTests)
|
||||
endif()
|
||||
add_test(
|
||||
NAME GameServer.IdleSmoke
|
||||
COMMAND GameServer --ticks 2 --no-sleep)
|
||||
add_test(
|
||||
NAME GameServer.BattleZoneSharedPlayer
|
||||
COMMAND GameServer --campaign battle_zone --location uni_exterior
|
||||
--ticks 2 --no-sleep)
|
||||
set_tests_properties(
|
||||
GameServer.BattleZoneSharedPlayer
|
||||
PROPERTIES WORKING_DIRECTORY "${GAME_ROOT}")
|
||||
endif()
|
||||
@@ -0,0 +1,104 @@
|
||||
# Headless GameServer
|
||||
|
||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||
audio, Lua, or rendering dependencies. On Windows it exposes a deterministic
|
||||
lockstep session through a Boost.Beast WebSocket listener.
|
||||
|
||||
## Build
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
cmake -S server -B server/build
|
||||
cmake --build server/build --config Release --target GameServer
|
||||
ctest --test-dir server/build -C Release --output-on-failure
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
```
|
||||
|
||||
Eigen3 must either be installed or be available at
|
||||
`thirdparty/eigen-5.0.0`. The headless target reads ordinary filesystem resources
|
||||
only; ZIP-backed campaign loading belongs to the later content-loading
|
||||
milestone.
|
||||
|
||||
Run until interrupted:
|
||||
|
||||
```bash
|
||||
./server/build/GameServer
|
||||
```
|
||||
|
||||
For a multi-configuration Windows build, the executable is normally
|
||||
`server/build/Release/GameServer.exe`.
|
||||
|
||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
||||
real-time delay.
|
||||
|
||||
Start the multiplayer lockstep server:
|
||||
|
||||
```powershell
|
||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
||||
```
|
||||
|
||||
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
|
||||
afterward and select `battle_zone_multiplayer`; that campaign connects to
|
||||
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
|
||||
|
||||
The server sends one state snapshot when a player connects, then broadcasts one
|
||||
numbered command bundle every 100 ms. Bundles may be empty. Destination input is
|
||||
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
||||
client wall-clock values, define simulation time.
|
||||
|
||||
If `--location` is omitted, the campaign's configured start location is used.
|
||||
`--campaigns PATH` selects a different campaign registry. Content paths are
|
||||
resolved from the process working directory, so run from the repository root
|
||||
when using the bundled `resources/` tree.
|
||||
|
||||
Available startup options:
|
||||
|
||||
```text
|
||||
--campaign ID
|
||||
--location ID
|
||||
--campaigns PATH
|
||||
--address IP
|
||||
--port N
|
||||
--tick-ms 100
|
||||
--ticks N
|
||||
--no-sleep
|
||||
```
|
||||
|
||||
Network mode requires a 100 ms tick; another `--tick-ms` value is rejected.
|
||||
The selected campaign's `networkPlayerSlots` lists pre-existing character IDs
|
||||
assigned to connections in order. `networkCompatibility` is a release token
|
||||
that must be changed whenever deterministic content changes; clients reject a
|
||||
different token. The bundled multiplayer Battle Zone has two slots (`player`
|
||||
and `player2`) and rejects additional clients. Disconnecting
|
||||
releases the slot. The bot's random autonomous controller is disabled in
|
||||
multiplayer.
|
||||
|
||||
The graphical client executes presentation behavior after building its runtime
|
||||
`Location`; the headless content pass loads authoritative navigation, NPC,
|
||||
interactive-object, trigger, teleport, taxi, and animation metadata. Lua and
|
||||
dialogue-driven shared multiplayer behavior is not yet supported.
|
||||
|
||||
## Tests
|
||||
|
||||
CTest covers:
|
||||
|
||||
- repeated `LocationGameState::setup()` preserving mutable runtime state;
|
||||
- ticking a prepared location with zero players;
|
||||
- deterministic fixed-100 ms replay behavior;
|
||||
- protocol-v2 parsing, validation, target normalization, state hashing, and
|
||||
two-tick command ordering;
|
||||
- a WebSocket initial-state/slot/destination/tick integration test;
|
||||
- idle server startup;
|
||||
- headless loading of `battle_zone_multiplayer/uni_exterior`.
|
||||
|
||||
The lockstep implementation currently targets identical executable and content
|
||||
builds. Diagnostic state hashes detect divergence, but there is no automatic
|
||||
checkpoint resynchronization or reconnect input history yet.
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include "game_server/NetworkProtocol.h"
|
||||
#if defined(NETWORK)
|
||||
#include "game_server/WebsocketGameServer.h"
|
||||
#endif
|
||||
#include "Campaign.h"
|
||||
#include "LocationState.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic_bool stopRequested{false};
|
||||
|
||||
void requestStop(int)
|
||||
{
|
||||
stopRequested.store(true);
|
||||
}
|
||||
|
||||
std::int64_t parsePositiveNumber(const char* value, const char* option)
|
||||
{
|
||||
try {
|
||||
const std::string text(value);
|
||||
std::size_t consumed = 0;
|
||||
const long long parsed = std::stoll(text, &consumed);
|
||||
if (consumed != text.size() || parsed <= 0)
|
||||
throw std::invalid_argument("not positive");
|
||||
return static_cast<std::int64_t>(parsed);
|
||||
} catch (const std::exception&) {
|
||||
throw std::runtime_error(
|
||||
std::string("Invalid value for ") + option + ": " + value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::int64_t tickMs = FRG::kSimulationTickMs;
|
||||
std::int64_t maxTicks = (std::numeric_limits<std::int64_t>::max)();
|
||||
bool sleepBetweenTicks = true;
|
||||
std::string campaignId;
|
||||
std::string locationId;
|
||||
std::string campaignsPath = "resources/config/campaigns.json";
|
||||
std::string listenAddress = "0.0.0.0";
|
||||
std::int64_t listenPort = 8080;
|
||||
|
||||
try {
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string option(argv[index]);
|
||||
if (option == "--ticks" && index + 1 < argc) {
|
||||
maxTicks = parsePositiveNumber(argv[++index], "--ticks");
|
||||
} else if (option == "--tick-ms" && index + 1 < argc) {
|
||||
tickMs = parsePositiveNumber(argv[++index], "--tick-ms");
|
||||
} else if (option == "--no-sleep") {
|
||||
sleepBetweenTicks = false;
|
||||
} else if (option == "--campaign" && index + 1 < argc) {
|
||||
campaignId = argv[++index];
|
||||
} else if (option == "--location" && index + 1 < argc) {
|
||||
locationId = argv[++index];
|
||||
} else if (option == "--campaigns" && index + 1 < argc) {
|
||||
campaignsPath = argv[++index];
|
||||
} else if (option == "--address" && index + 1 < argc) {
|
||||
listenAddress = argv[++index];
|
||||
} else if (option == "--port" && index + 1 < argc) {
|
||||
listenPort = parsePositiveNumber(argv[++index], "--port");
|
||||
if (listenPort > 65535)
|
||||
throw std::runtime_error("Invalid value for --port");
|
||||
} else if (option == "--help") {
|
||||
std::cout
|
||||
<< "Usage: GameServer [--ticks N] [--tick-ms N] "
|
||||
"[--no-sleep] [--campaign ID] [--location ID] "
|
||||
"[--campaigns PATH] [--address IP] [--port N]\n";
|
||||
return EXIT_SUCCESS;
|
||||
} else {
|
||||
throw std::runtime_error("Unknown or incomplete option: " + option);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (tickMs != FRG::kSimulationTickMs) {
|
||||
std::cerr << "Network lockstep requires --tick-ms "
|
||||
<< FRG::kSimulationTickMs << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::signal(SIGINT, requestStop);
|
||||
std::signal(SIGTERM, requestStop);
|
||||
|
||||
FRG::InternalGameServer server;
|
||||
std::vector<std::string> playerSlots;
|
||||
std::string compatibilityFingerprint = "lockstep-v2";
|
||||
if (!campaignId.empty()) {
|
||||
try {
|
||||
FRG::CampaignRegistry::instance().loadFromJson(campaignsPath);
|
||||
std::cout << "[server] Loading...\n" << std::flush;
|
||||
const FRG::CampaignConfig* campaign =
|
||||
FRG::CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign)
|
||||
throw std::runtime_error(
|
||||
"Unknown campaign '" + campaignId + "'");
|
||||
const std::string selectedLocation = locationId.empty()
|
||||
? campaign->startLocation : locationId;
|
||||
const FRG::LocationConfig* location =
|
||||
campaign->findLocation(selectedLocation);
|
||||
if (!location)
|
||||
throw std::runtime_error(
|
||||
"Campaign '" + campaignId +
|
||||
"' has no location '" + selectedLocation + "'");
|
||||
server.startCampaign(*campaign, std::string(), true, false);
|
||||
server.prepareLocation(*location);
|
||||
const std::string startState =
|
||||
FRG::readTextFile(campaign->startStatePath);
|
||||
if (!startState.empty()) {
|
||||
try {
|
||||
nlohmann::json persisted =
|
||||
nlohmann::json::parse(startState);
|
||||
nlohmann::json networkState;
|
||||
FRG::ClientLocationState ignoredLocalState;
|
||||
FRG::loadSinglePlayerPersistence(
|
||||
persisted, networkState, ignoredLocalState);
|
||||
networkState["campaignId"] = campaignId;
|
||||
networkState["currentLocationId"] = selectedLocation;
|
||||
server.load(networkState);
|
||||
std::cout << "Loaded initial state '"
|
||||
<< campaign->startStatePath << "'\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Ignoring invalid initial state '"
|
||||
<< campaign->startStatePath << "': "
|
||||
<< error.what() << '\n';
|
||||
}
|
||||
}
|
||||
server.activateLocation(selectedLocation);
|
||||
playerSlots = campaign->networkPlayerSlots;
|
||||
for (const std::string& slot : playerSlots) {
|
||||
if (server.snapshot()->players.count(slot) == 0)
|
||||
throw std::runtime_error(
|
||||
"Multiplayer slot '" + slot +
|
||||
"' is missing from the initial state");
|
||||
}
|
||||
compatibilityFingerprint =
|
||||
campaign->networkCompatibility.empty()
|
||||
? "lockstep-v2:" + campaignId
|
||||
: campaign->networkCompatibility;
|
||||
if (!campaign->tutorial) {
|
||||
server.submit(FRG::SetTutorialStepCommand{
|
||||
FRG::TutorialStep::Completed});
|
||||
}
|
||||
std::cout << "Loaded campaign '" << campaignId
|
||||
<< "', location '" << selectedLocation
|
||||
<< "' with shared player '" << server.localPlayerId() << "'\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Server content setup failed: "
|
||||
<< error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
const auto tickDuration = std::chrono::milliseconds(tickMs);
|
||||
auto nextTick = std::chrono::steady_clock::now();
|
||||
|
||||
#if defined(NETWORK)
|
||||
std::unique_ptr<FRG::WebsocketGameServer> websocketServer;
|
||||
try {
|
||||
websocketServer = std::make_unique<FRG::WebsocketGameServer>(
|
||||
server, playerSlots, compatibilityFingerprint,
|
||||
listenAddress, static_cast<std::uint16_t>(listenPort));
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "WebSocket listener setup failed: "
|
||||
<< error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "GameServer listening on ws://" << listenAddress << ':'
|
||||
<< websocketServer->port() << " (" << tickMs << " ms tick)\n";
|
||||
#else
|
||||
std::cout << "GameServer started (network disabled, "
|
||||
<< tickMs << " ms tick)\n";
|
||||
#endif
|
||||
for (std::int64_t tick = 0;
|
||||
tick < maxTicks && !stopRequested.load(); ++tick) {
|
||||
nextTick += tickDuration;
|
||||
#if defined(NETWORK)
|
||||
websocketServer->update();
|
||||
#else
|
||||
server.update(tickMs);
|
||||
#endif
|
||||
if (sleepBetweenTicks) std::this_thread::sleep_until(nextTick);
|
||||
}
|
||||
std::cout << "GameServer stopped\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
#include "LocationGameState.h"
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
int fail(const char* message)
|
||||
{
|
||||
std::cout << "LocationGameStateTests: " << message << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
FRG::ClientLocationState localState;
|
||||
auto camera = localState.getOrCreate("test_location");
|
||||
camera->cameraAzimuth = 0.75f;
|
||||
camera->cameraInclination = 1.25f;
|
||||
nlohmann::json localJson;
|
||||
localState.save(localJson);
|
||||
FRG::ClientLocationState restoredLocalState;
|
||||
restoredLocalState.load(localJson);
|
||||
const auto restoredCamera =
|
||||
restoredLocalState.getOrCreate("test_location");
|
||||
if (restoredCamera->cameraAzimuth != 0.75f ||
|
||||
restoredCamera->cameraInclination != 1.25f) {
|
||||
return fail("Local camera state did not round trip");
|
||||
}
|
||||
|
||||
FRG::LocationNetworkState networkLocationState;
|
||||
nlohmann::json networkLocationJson;
|
||||
networkLocationState.save(networkLocationJson);
|
||||
if (networkLocationJson.contains("cameraAzimuth") ||
|
||||
networkLocationJson.contains("cameraInclination")) {
|
||||
return fail("Network location state contains local camera fields");
|
||||
}
|
||||
|
||||
const nlohmann::json authoritativeJson{
|
||||
{"schemaVersion", FRG::GameState::kSchemaVersion},
|
||||
{"campaignId", "test"}
|
||||
};
|
||||
nlohmann::json persistenceJson;
|
||||
FRG::saveSinglePlayerPersistence(
|
||||
authoritativeJson, localState, persistenceJson);
|
||||
nlohmann::json restoredAuthoritativeJson;
|
||||
FRG::ClientLocationState restoredPersistenceLocalState;
|
||||
FRG::loadSinglePlayerPersistence(
|
||||
persistenceJson, restoredAuthoritativeJson,
|
||||
restoredPersistenceLocalState);
|
||||
if (restoredAuthoritativeJson != authoritativeJson ||
|
||||
restoredPersistenceLocalState.getOrCreate("test_location")
|
||||
->cameraAzimuth != 0.75f) {
|
||||
return fail("Composed single-player state did not round trip");
|
||||
}
|
||||
if (persistenceJson.at("networkState").contains("localState")) {
|
||||
return fail("Local state leaked into authoritative state");
|
||||
}
|
||||
|
||||
const std::filesystem::path repositoryRoot =
|
||||
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
|
||||
std::filesystem::current_path(repositoryRoot);
|
||||
|
||||
FRG::CampaignRegistry::instance().loadFromJson(
|
||||
"resources/config/campaigns.json");
|
||||
const FRG::CampaignConfig* singlePlayerBattleZone =
|
||||
FRG::CampaignRegistry::instance().find("battle_zone");
|
||||
const FRG::CampaignConfig* multiplayerBattleZoneConfig =
|
||||
FRG::CampaignRegistry::instance().find("battle_zone_multiplayer");
|
||||
if (FRG::CampaignRegistry::instance().all().size() != 3 ||
|
||||
!singlePlayerBattleZone || !multiplayerBattleZoneConfig) {
|
||||
return fail("Expected three campaigns including both Battle Zone modes");
|
||||
}
|
||||
if (singlePlayerBattleZone->startStatePath !=
|
||||
multiplayerBattleZoneConfig->startStatePath) {
|
||||
return fail("Battle Zone modes do not share their initial state");
|
||||
}
|
||||
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
|
||||
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player" ||
|
||||
multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
|
||||
"player2" ||
|
||||
multiplayerBattleZoneConfig->networkCompatibility.empty()) {
|
||||
return fail("Multiplayer player slots were not loaded");
|
||||
}
|
||||
|
||||
FRG::LocationDefinition definition;
|
||||
definition.config.id = "test_location";
|
||||
|
||||
FRG::NpcData npc;
|
||||
npc.id = "npc";
|
||||
npc.name = "Test NPC";
|
||||
npc.hp = 75.f;
|
||||
definition.npcs.push_back(npc);
|
||||
|
||||
FRG::InteractiveObjectData object;
|
||||
object.id = "object";
|
||||
object.base.name = "Test Object";
|
||||
object.base.meshPath = "unused.mesh";
|
||||
object.isActive = true;
|
||||
definition.interactiveObjects.push_back(object);
|
||||
|
||||
FRG::TriggerZoneDefinition trigger;
|
||||
trigger.id = "trigger";
|
||||
trigger.enabled = true;
|
||||
definition.triggerZones.push_back(trigger);
|
||||
|
||||
FRG::TeleportDefinition teleport;
|
||||
teleport.id = "teleport";
|
||||
teleport.active = false;
|
||||
definition.teleports.push_back(teleport);
|
||||
|
||||
FRG::LocationGameState state;
|
||||
if (!state.setup(definition, {}))
|
||||
return fail("First setup must initialize runtime fields");
|
||||
if (state.npcs.size() != 1 || state.interactiveObjects.size() != 1)
|
||||
return fail("Definitions were not applied");
|
||||
|
||||
state.npcs.at("npc")->hp = 12.f;
|
||||
state.interactiveObjects.at("object")->isActive = false;
|
||||
state.triggerZones.at("trigger").enabled = false;
|
||||
state.teleportZones.at("teleport") = true;
|
||||
state.hasPersistentState = true;
|
||||
|
||||
if (state.setup(definition, {}))
|
||||
return fail("Persistent setup must not reinitialize runtime fields");
|
||||
if (state.npcs.at("npc")->hp != 12.f ||
|
||||
state.interactiveObjects.at("object")->isActive ||
|
||||
state.triggerZones.at("trigger").enabled ||
|
||||
!state.teleportZones.at("teleport")) {
|
||||
return fail("Repeated setup did not preserve runtime state");
|
||||
}
|
||||
|
||||
FRG::PlayerStateMap noPlayers;
|
||||
state.update(50, noPlayers, false);
|
||||
auto disabledPlayer = std::make_shared<FRG::CharacterState>();
|
||||
disabledPlayer->configureAsPlayer();
|
||||
disabledPlayer->enabled = false;
|
||||
state.location->targetInteractiveObjectId = "object";
|
||||
state.location->targetInteractNpcId = "npc";
|
||||
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}};
|
||||
const FRG::LocationSimulationEvents disabledEvents =
|
||||
state.update(50, disabledPlayers, false);
|
||||
if (!disabledEvents.interactiveObjects.empty() ||
|
||||
!disabledEvents.npcInteractionId.empty()) {
|
||||
return fail("Disabled player interacted with location entities");
|
||||
}
|
||||
|
||||
auto& hostileNpc = *state.npcs.at("npc");
|
||||
hostileNpc.enabled = true;
|
||||
hostileNpc.canAttack = true;
|
||||
hostileNpc.hp = hostileNpc.initialHp = 75.f;
|
||||
hostileNpc.position = Eigen::Vector3f::Zero();
|
||||
hostileNpc.homePosition = {5.f, 0.f, 0.f};
|
||||
hostileNpc.stopInPlace();
|
||||
|
||||
auto playerOne = std::make_shared<FRG::CharacterState>();
|
||||
playerOne->configureAsPlayer();
|
||||
playerOne->enabled = true;
|
||||
playerOne->position = {3.f, 0.f, 0.f};
|
||||
auto playerTwo = std::make_shared<FRG::CharacterState>();
|
||||
playerTwo->configureAsPlayer();
|
||||
playerTwo->enabled = true;
|
||||
playerTwo->position = {2.f, 0.f, 0.f};
|
||||
FRG::PlayerStateMap combatPlayers{
|
||||
{"player", playerOne}, {"player2", playerTwo}};
|
||||
|
||||
state.update(100, combatPlayers, false);
|
||||
if (hostileNpc.attackTargetId != "player2") {
|
||||
return fail("Hostile NPC did not select the nearest player");
|
||||
}
|
||||
|
||||
playerOne->position = {1.f, 0.f, 0.f};
|
||||
playerTwo->position = {4.f, 0.f, 0.f};
|
||||
state.update(100, combatPlayers, false);
|
||||
if (hostileNpc.attackTargetId != "player2") {
|
||||
return fail("Hostile NPC did not retain its living target lock");
|
||||
}
|
||||
|
||||
playerTwo->hp = 0.f;
|
||||
state.update(100, combatPlayers, false);
|
||||
if (hostileNpc.attackTargetId != "player") {
|
||||
return fail("Hostile NPC did not acquire a replacement after target death");
|
||||
}
|
||||
|
||||
playerOne->enabled = false;
|
||||
state.update(100, combatPlayers, false);
|
||||
if (!hostileNpc.attackTargetId.empty() ||
|
||||
hostileNpc.battle_state != 0 ||
|
||||
(hostileNpc.requestedWalkTarget - hostileNpc.homePosition).norm() >
|
||||
FRG::CharacterState::kWalkThreshold) {
|
||||
return fail("Hostile NPC did not return home after losing its target");
|
||||
}
|
||||
|
||||
FRG::CampaignConfig battleZone;
|
||||
battleZone.id = "battle_zone";
|
||||
battleZone.questsPath = "resources/config/quests_battle_zone.json";
|
||||
FRG::InternalGameServer server;
|
||||
server.startCampaign(battleZone, {}, false, true);
|
||||
const auto snapshot = server.snapshot();
|
||||
const auto bot = snapshot->players.find("player2");
|
||||
if (bot == snapshot->players.end())
|
||||
return fail("Battle Zone test bot was not created");
|
||||
const auto stand = static_cast<std::size_t>(FRG::AnimationState::STAND);
|
||||
const auto walk = static_cast<std::size_t>(FRG::AnimationState::WALK);
|
||||
if (bot->second.state.animationFrameCounts[stand] <= 1 ||
|
||||
bot->second.state.animationFrameCounts[walk] <= 1) {
|
||||
return fail("Battle Zone test bot animation metadata was not loaded");
|
||||
}
|
||||
|
||||
FRG::InternalGameServer networkServer;
|
||||
FRG::CampaignConfig multiplayerBattleZone = battleZone;
|
||||
multiplayerBattleZone.id = "battle_zone_multiplayer";
|
||||
multiplayerBattleZone.networkPlayerSlots = {
|
||||
"player", "player2"};
|
||||
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
|
||||
const auto networkSnapshot = networkServer.snapshot();
|
||||
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
|
||||
return fail("Network campaign ID was not retained");
|
||||
if (networkSnapshot->players.count("player") != 1)
|
||||
return fail("Network campaign shared player was not created");
|
||||
if (networkSnapshot->players.count("player2") != 0)
|
||||
return fail("Network campaign unexpectedly created the test bot");
|
||||
if (networkSnapshot->players.at("player").state.enabled)
|
||||
return fail("Network campaign player slot was enabled before connection");
|
||||
|
||||
nlohmann::json loadedNetworkState;
|
||||
networkServer.save(loadedNetworkState);
|
||||
loadedNetworkState["players"]["player2"] =
|
||||
loadedNetworkState["players"]["player"];
|
||||
loadedNetworkState["players"]["player2"]["position"] =
|
||||
{4.f, 0.f, 5.f};
|
||||
networkServer.load(loadedNetworkState);
|
||||
const auto loadedNetworkSnapshot = networkServer.snapshot();
|
||||
const auto loadedNetworkBot =
|
||||
loadedNetworkSnapshot->players.find("player2");
|
||||
if (loadedNetworkBot == loadedNetworkSnapshot->players.end() ||
|
||||
loadedNetworkBot->second.state.animationFrameCounts[stand] <= 1 ||
|
||||
loadedNetworkBot->second.state.animationFrameCounts[walk] <= 1) {
|
||||
return fail(
|
||||
"Loaded multiplayer slot did not restore animation metadata");
|
||||
}
|
||||
if (!loadedNetworkBot->second.state.position.isApprox(
|
||||
Eigen::Vector3f(4.f, 0.f, 5.f))) {
|
||||
return fail("Loaded multiplayer slot position was overwritten");
|
||||
}
|
||||
if (loadedNetworkBot->second.state.enabled)
|
||||
return fail("Loaded multiplayer slot was enabled before connection");
|
||||
|
||||
const std::string locationId = loadedNetworkSnapshot->currentLocationId;
|
||||
networkServer.submit(FRG::SetCharacterPositionCommand{
|
||||
locationId, "player2", Eigen::Vector3f(7.f, 0.f, 8.f), true});
|
||||
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
|
||||
locationId, "player2", 1.5f, true});
|
||||
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
|
||||
locationId, "player2", "player"});
|
||||
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||
locationId, "player2", false});
|
||||
const auto commandSnapshot = networkServer.snapshot();
|
||||
const auto commandCharacter = commandSnapshot->players.at("player2").state;
|
||||
if (!commandCharacter.position.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
|
||||
!commandCharacter.homePosition.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
|
||||
commandCharacter.facingAngle != 1.5f ||
|
||||
commandCharacter.targetFacingAngle != 1.5f ||
|
||||
!commandCharacter.faceTargetId.empty() ||
|
||||
commandCharacter.enabled) {
|
||||
return fail("Semantic character commands were not applied");
|
||||
}
|
||||
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||
locationId, "player2", true});
|
||||
const auto enabledPlayer =
|
||||
networkServer.snapshot()->players.at("player2").state;
|
||||
if (!enabledPlayer.enabled || !enabledPlayer.attackTargetId.empty()) {
|
||||
return fail("Enabled player acquired an autonomous attack target");
|
||||
}
|
||||
|
||||
FRG::CharacterState guardedMover;
|
||||
guardedMover.configureAsPlayer();
|
||||
guardedMover.enabled = true;
|
||||
guardedMover.position = Eigen::Vector3f::Zero();
|
||||
guardedMover.walkTarget = Eigen::Vector3f(2.f, 0.f, 0.f);
|
||||
guardedMover.requestedWalkTarget = guardedMover.walkTarget;
|
||||
guardedMover.pathWaypoints = {guardedMover.walkTarget};
|
||||
const std::vector<FRG::CharacterMovementObstacle> movementObstacles{
|
||||
{"mover", Eigen::Vector3f::Zero(), 0.45f},
|
||||
{"blocker", Eigen::Vector3f(0.8f, 0.f, 0.f), 0.45f}};
|
||||
FRG::CharacterUpdateContext guardedContext;
|
||||
guardedContext.characterId = "mover";
|
||||
guardedContext.movementObstacles = &movementObstacles;
|
||||
FRG::CharacterUpdateEvents blockedEvents;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
blockedEvents = guardedMover.update(100, guardedContext);
|
||||
if (!guardedMover.position.isZero() ||
|
||||
blockedEvents.forceNavigationReplan ||
|
||||
!guardedMover.pathWaypoints.empty()) {
|
||||
return fail("Movement guard did not cancel the blocked player action");
|
||||
}
|
||||
nlohmann::json guardedJson;
|
||||
guardedMover.save(guardedJson);
|
||||
FRG::CharacterState restoredGuardedMover;
|
||||
restoredGuardedMover.load(guardedJson);
|
||||
if (restoredGuardedMover.movementBlockedTicks !=
|
||||
guardedMover.movementBlockedTicks ||
|
||||
restoredGuardedMover.navigationRetryTicks !=
|
||||
guardedMover.navigationRetryTicks) {
|
||||
return fail("Movement blockage state did not round trip");
|
||||
}
|
||||
|
||||
FRG::LocationGameState occupancyState;
|
||||
auto stationary = std::make_shared<FRG::CharacterState>();
|
||||
stationary->enabled = true;
|
||||
stationary->hp = 10.f;
|
||||
stationary->position = Eigen::Vector3f(4.f, 0.f, 0.f);
|
||||
auto moving = std::make_shared<FRG::CharacterState>();
|
||||
moving->enabled = true;
|
||||
moving->hp = 10.f;
|
||||
moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)};
|
||||
moving->movedLastTick = true;
|
||||
FRG::PlayerStateMap occupancyPlayers{
|
||||
{"stationary", stationary}, {"moving", moving}};
|
||||
if (occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers).size() != 1 ||
|
||||
occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers, "moving").size() != 2 ||
|
||||
!occupancyState.collectPathObstacles(
|
||||
"stationary", occupancyPlayers).empty()) {
|
||||
return fail("Stationary obstacle collection policy is incorrect");
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
#include "game_server/NetworkProtocol.h"
|
||||
#include "LocationState.h"
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(bool condition, const char* message)
|
||||
{
|
||||
if (condition) return;
|
||||
std::cerr << "NetworkProtocolTests: " << message << '\n';
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
FRG::LocationNetworkState locationState;
|
||||
nlohmann::json savedLocationState;
|
||||
locationState.save(savedLocationState);
|
||||
const nlohmann::json original{
|
||||
{"schemaVersion", 2},
|
||||
{"campaignId", "battle_zone_multiplayer"},
|
||||
{"players", {{"player", {{"hp", 200.0}}}}},
|
||||
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
|
||||
};
|
||||
FRG::InitialStateMessage initial;
|
||||
initial.state = original;
|
||||
initial.assignedPlayerId = "player";
|
||||
initial.snapshotTick = 42;
|
||||
initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer";
|
||||
const std::string message = FRG::makeInitialStateMessage(initial);
|
||||
FRG::InitialStateMessage parsed;
|
||||
std::string error;
|
||||
require(
|
||||
FRG::parseInitialStateMessage(message, parsed, error),
|
||||
"valid initial-state envelope was rejected");
|
||||
require(parsed.state == original, "state payload did not round trip");
|
||||
require(parsed.snapshotTick == 42, "snapshot tick did not round trip");
|
||||
require(
|
||||
parsed.assignedPlayerId == "player",
|
||||
"assigned player did not round trip");
|
||||
require(
|
||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") &&
|
||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraInclination"),
|
||||
"local camera state leaked into the network payload");
|
||||
|
||||
FRG::DestinationInputMessage input;
|
||||
input.clientSequence = 7;
|
||||
input.target = Eigen::Vector3f(1.234f, 0.f, -5.678f);
|
||||
FRG::DestinationInputMessage parsedInput;
|
||||
require(
|
||||
FRG::parseDestinationInputMessage(
|
||||
FRG::makeDestinationInputMessage(input), parsedInput, error),
|
||||
"destination input did not round trip");
|
||||
require(
|
||||
parsedInput.clientSequence == 7 &&
|
||||
parsedInput.target.isApprox(Eigen::Vector3f(1.23f, 0.f, -5.68f)),
|
||||
"destination input was not normalized");
|
||||
|
||||
FRG::CharacterActionInputMessage actionInput{
|
||||
8, "set_target", "hostile_npc"};
|
||||
FRG::CharacterActionInputMessage parsedActionInput;
|
||||
require(
|
||||
FRG::parseCharacterActionInputMessage(
|
||||
FRG::makeCharacterActionInputMessage(actionInput),
|
||||
parsedActionInput, error),
|
||||
"character action input did not round trip");
|
||||
require(
|
||||
parsedActionInput.clientSequence == 8 &&
|
||||
parsedActionInput.action == "set_target" &&
|
||||
parsedActionInput.targetId == "hostile_npc",
|
||||
"character action input fields did not round trip");
|
||||
require(
|
||||
!FRG::parseCharacterActionInputMessage(
|
||||
R"({"protocolVersion":2,"type":"character_action","payload":{"clientSequence":1,"action":"unknown","targetId":""}})",
|
||||
parsedActionInput, error),
|
||||
"unknown character action was accepted");
|
||||
require(
|
||||
!FRG::parseCharacterActionInputMessage(
|
||||
R"({"protocolVersion":2,"type":"character_action","payload":{"clientSequence":1,"action":"set_target","targetId":""}})",
|
||||
parsedActionInput, error),
|
||||
"empty attack target was accepted");
|
||||
|
||||
FRG::TickMessage tick;
|
||||
tick.tickId = 43;
|
||||
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
||||
1, "player", parsedInput.target});
|
||||
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
||||
2, "uni_exterior", "player2", true});
|
||||
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
|
||||
3, "player", "set_target", "hostile_npc"});
|
||||
tick.stateHash = "0123456789abcdef";
|
||||
FRG::TickMessage parsedTick;
|
||||
require(
|
||||
FRG::parseTickMessage(
|
||||
FRG::makeTickMessage(tick), parsedTick, error),
|
||||
"tick message did not round trip");
|
||||
require(
|
||||
parsedTick.tickId == 43 && parsedTick.commands.size() == 3 &&
|
||||
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||
parsedTick.commands[1]) &&
|
||||
std::get<FRG::ScheduledCharacterEnabledCommand>(
|
||||
parsedTick.commands[1]).enabled &&
|
||||
std::holds_alternative<FRG::ScheduledCharacterActionCommand>(
|
||||
parsedTick.commands[2]) &&
|
||||
std::get<FRG::ScheduledCharacterActionCommand>(
|
||||
parsedTick.commands[2]).targetId == "hostile_npc",
|
||||
"tick contents did not round trip");
|
||||
|
||||
require(
|
||||
!FRG::parseInitialStateMessage(
|
||||
R"({"protocolVersion":1,"type":"initial_state","payload":{}})",
|
||||
parsed, error),
|
||||
"unknown protocol version was accepted");
|
||||
require(
|
||||
!FRG::parseDestinationInputMessage(
|
||||
R"({"protocolVersion":2,"type":"command","payload":{}})",
|
||||
parsedInput, error),
|
||||
"unknown message type was accepted");
|
||||
require(
|
||||
!FRG::parseInitialStateMessage("not json", parsed, error),
|
||||
"malformed JSON was accepted");
|
||||
require(
|
||||
!FRG::parseDestinationInputMessage(
|
||||
R"({"protocolVersion":2,"type":"destination_input","payload":{"clientSequence":1,"target":[1e999,0,0]}})",
|
||||
parsedInput, error),
|
||||
"non-finite destination was accepted");
|
||||
std::string errorCode;
|
||||
std::string errorDetail;
|
||||
require(
|
||||
FRG::parseErrorMessage(
|
||||
FRG::makeErrorMessage("server_full", "No slot"),
|
||||
errorCode, errorDetail) &&
|
||||
errorCode == "server_full" && errorDetail == "No slot",
|
||||
"error message did not round trip");
|
||||
|
||||
nlohmann::json hashState = original;
|
||||
hashState["savedAt"] = "first";
|
||||
const std::string firstHash = FRG::canonicalStateHash(hashState);
|
||||
hashState["savedAt"] = "second";
|
||||
require(
|
||||
firstHash == FRG::canonicalStateHash(hashState),
|
||||
"persistence timestamp changed deterministic hash");
|
||||
|
||||
FRG::LockstepTimeline timeline(2);
|
||||
const std::uint64_t firstSequence =
|
||||
timeline.schedule("player", Eigen::Vector3f(1.f, 0.f, 1.f));
|
||||
const std::uint64_t secondSequence =
|
||||
timeline.schedule("player_2", Eigen::Vector3f(2.f, 0.f, 2.f));
|
||||
const FRG::TickMessage emptyTick = timeline.advance();
|
||||
require(
|
||||
emptyTick.tickId == 1 && emptyTick.commands.empty(),
|
||||
"input-delay tick was not empty");
|
||||
const FRG::TickMessage commandTick = timeline.advance();
|
||||
require(
|
||||
commandTick.tickId == 2 && commandTick.commands.size() == 2 &&
|
||||
std::get<FRG::ScheduledDestinationCommand>(
|
||||
commandTick.commands[0]).commandSequence == firstSequence &&
|
||||
std::get<FRG::ScheduledDestinationCommand>(
|
||||
commandTick.commands[1]).commandSequence == secondSequence,
|
||||
"scheduled commands were not finalized in stable order");
|
||||
|
||||
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
||||
timeline.scheduleCharacterAction("player", "clear_attack_target");
|
||||
const FRG::TickMessage presenceTick = timeline.advance();
|
||||
require(
|
||||
presenceTick.commands.size() == 1 &&
|
||||
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||
presenceTick.commands[0]),
|
||||
"character enabled command was not scheduled for the next tick");
|
||||
const FRG::TickMessage actionTick =
|
||||
timeline.advance();
|
||||
require(
|
||||
actionTick.commands.size() == 1 &&
|
||||
std::holds_alternative<FRG::ScheduledCharacterActionCommand>(
|
||||
actionTick.commands[0]),
|
||||
"character action was not scheduled with input delay");
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
#include "CharacterState.h"
|
||||
#include "LocationGameState.h"
|
||||
#include "navigation/PathFinder.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int fail(const char* message)
|
||||
{
|
||||
std::cout << "PathFinderTests: " << message << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::filesystem::path writeGrid(
|
||||
const std::string& name, int width, int depth, bool corridor)
|
||||
{
|
||||
const auto path = std::filesystem::temp_directory_path() / name;
|
||||
std::ofstream output(path);
|
||||
output << "cellSize 1\nagentRadius 0.1\nfloorY 0\n"
|
||||
<< "minX 0\nminZ 0\ngridWidth " << width
|
||||
<< "\ngridDepth " << depth << "\n";
|
||||
for (int z = 0; z < depth; ++z) {
|
||||
for (int x = 0; x < width; ++x)
|
||||
output << (!corridor || z == depth / 2 ? '1' : '0');
|
||||
output << '\n';
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
bool samePath(
|
||||
const std::vector<Eigen::Vector3f>& lhs,
|
||||
const std::vector<Eigen::Vector3f>& rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size()) return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
if (lhs[i] != rhs[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
float segmentDistanceSquared(
|
||||
const Eigen::Vector3f& point,
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end)
|
||||
{
|
||||
const Eigen::Vector2f p(point.x(), point.z());
|
||||
const Eigen::Vector2f a(start.x(), start.z());
|
||||
const Eigen::Vector2f segment(end.x() - start.x(), end.z() - start.z());
|
||||
const float lengthSquared = segment.squaredNorm();
|
||||
const float t = lengthSquared > 1e-8f
|
||||
? (std::max)(0.f, (std::min)(1.f, (p - a).dot(segment) / lengthSquared))
|
||||
: 0.f;
|
||||
return (p - (a + segment * t)).squaredNorm();
|
||||
}
|
||||
|
||||
void runBenchmark(FRG::PathFinder& pathFinder, int queries)
|
||||
{
|
||||
std::vector<FRG::PathFinder::DynamicObstacle> obstacles;
|
||||
for (int i = 0; i < queries; ++i) {
|
||||
const float x = 4.5f + static_cast<float>((i * 7) % 70);
|
||||
const float z = 4.5f + static_cast<float>((i * 13) % 70);
|
||||
obstacles.push_back({Eigen::Vector3f(x, 0.f, z), 0.25f});
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
size_t waypointCount = 0;
|
||||
for (int i = 0; i < queries; ++i) {
|
||||
const Eigen::Vector3f from(
|
||||
1.5f, 0.f, 1.5f + static_cast<float>(i % 30));
|
||||
const Eigen::Vector3f to(
|
||||
78.5f, 0.f, 78.5f - static_cast<float>(i % 30));
|
||||
waypointCount +=
|
||||
pathFinder.findPathToNearest(from, to, obstacles).size();
|
||||
}
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
std::cout << "PathFinder benchmark " << queries << " characters/queries: "
|
||||
<< elapsed.count() << " ms, " << waypointCount
|
||||
<< " waypoints" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const bool benchmark = argc > 1 && std::string(argv[1]) == "--benchmark";
|
||||
const auto openGrid = writeGrid("frg_pathfinder_open.txt", 80, 80, false);
|
||||
FRG::PathFinder pathFinder;
|
||||
pathFinder.build(openGrid.string());
|
||||
if (!pathFinder.isReady()) return fail("Open test grid did not load");
|
||||
|
||||
const FRG::PathFinder::DynamicObstacle first{
|
||||
Eigen::Vector3f(8.5f, 0.f, 8.5f), 0.75f};
|
||||
const FRG::PathFinder::DynamicObstacle second{
|
||||
Eigen::Vector3f(12.5f, 0.f, 10.5f), 0.75f};
|
||||
const Eigen::Vector3f start(2.5f, 0.f, 8.5f);
|
||||
const Eigen::Vector3f end(18.5f, 0.f, 8.5f);
|
||||
const auto ordered = pathFinder.findPathToNearest(
|
||||
start, end, {first, second});
|
||||
const auto reversed = pathFinder.findPathToNearest(
|
||||
start, end, {second, first});
|
||||
if (ordered.empty() || !samePath(ordered, reversed))
|
||||
return fail("Obstacle insertion order changed the route");
|
||||
Eigen::Vector3f previous = start;
|
||||
for (const Eigen::Vector3f& point : ordered) {
|
||||
if (segmentDistanceSquared(
|
||||
first.position, previous, point) < 0.85f * 0.85f)
|
||||
return fail("Smoothed path crossed a dynamic obstacle");
|
||||
previous = point;
|
||||
}
|
||||
|
||||
const auto occupiedDestination =
|
||||
pathFinder.findPathToNearest(start, first.position, {first});
|
||||
if (occupiedDestination.empty())
|
||||
return fail("Occupied destination did not produce a nearest route");
|
||||
const Eigen::Vector3f occupiedEnd = occupiedDestination.back();
|
||||
const Eigen::Vector2f occupiedDelta(
|
||||
occupiedEnd.x() - first.position.x(),
|
||||
occupiedEnd.z() - first.position.z());
|
||||
if (occupiedDelta.squaredNorm() < 0.85f * 0.85f)
|
||||
return fail("Occupied destination ended inside the obstacle");
|
||||
|
||||
const auto corridorGrid =
|
||||
writeGrid("frg_pathfinder_corridor.txt", 20, 3, true);
|
||||
FRG::PathFinder corridor;
|
||||
corridor.build(corridorGrid.string());
|
||||
const FRG::PathFinder::DynamicObstacle blocker{
|
||||
Eigen::Vector3f(10.5f, 0.f, 1.5f), 0.45f};
|
||||
const auto nearest = corridor.findPathToNearest(
|
||||
Eigen::Vector3f(1.5f, 0.f, 1.5f),
|
||||
Eigen::Vector3f(18.5f, 0.f, 1.5f), {blocker});
|
||||
if (nearest.empty() || nearest.back().x() >= blocker.position.x())
|
||||
return fail("Nearest fallback ignored a dynamic blocker");
|
||||
|
||||
FRG::CharacterState guardedMover;
|
||||
guardedMover.configureAsPlayer();
|
||||
guardedMover.enabled = true;
|
||||
guardedMover.walkTarget = Eigen::Vector3f(2.f, 0.f, 0.f);
|
||||
guardedMover.requestedWalkTarget = guardedMover.walkTarget;
|
||||
guardedMover.pathWaypoints = {guardedMover.walkTarget};
|
||||
const std::vector<FRG::CharacterMovementObstacle> movementObstacles{
|
||||
{"mover", Eigen::Vector3f::Zero(), 0.45f},
|
||||
{"blocker", Eigen::Vector3f(0.8f, 0.f, 0.f), 0.45f}};
|
||||
FRG::CharacterUpdateContext guardedContext;
|
||||
guardedContext.characterId = "mover";
|
||||
guardedContext.movementObstacles = &movementObstacles;
|
||||
FRG::CharacterUpdateEvents blockedEvents;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
blockedEvents = guardedMover.update(100, guardedContext);
|
||||
if (!guardedMover.position.isZero() ||
|
||||
blockedEvents.forceNavigationReplan ||
|
||||
!guardedMover.pathWaypoints.empty() ||
|
||||
guardedMover.requestedWalkTarget != guardedMover.position ||
|
||||
guardedMover.currentState != FRG::AnimationState::STAND)
|
||||
return fail("Blocked player action was not cancelled");
|
||||
|
||||
FRG::CharacterState retryingNpc;
|
||||
retryingNpc.enabled = true;
|
||||
retryingNpc.configureNavigationRetryCooldowns(
|
||||
"retrying_npc", {2, 3, 4, 5, 6, 7, 8, 9, 10, 11});
|
||||
FRG::CharacterState otherNpc;
|
||||
otherNpc.configureNavigationRetryCooldowns(
|
||||
"other_npc", {2, 3, 4, 5, 6, 7, 8, 9, 10, 11});
|
||||
if (otherNpc.navigationRetryCooldownTicks ==
|
||||
retryingNpc.navigationRetryCooldownTicks)
|
||||
return fail("Stable NPC IDs produced the same retry sequence");
|
||||
const int firstCooldown =
|
||||
retryingNpc.navigationRetryCooldownTicks.front();
|
||||
retryingNpc.walkTarget = Eigen::Vector3f(2.f, 0.f, 0.f);
|
||||
retryingNpc.requestedWalkTarget = retryingNpc.walkTarget;
|
||||
retryingNpc.pathWaypoints = {retryingNpc.walkTarget};
|
||||
const std::vector<FRG::CharacterMovementObstacle> movingBlocker{
|
||||
{"b", Eigen::Vector3f(0.8f, 0.f, 0.f), 0.45f, true,
|
||||
Eigen::Vector3f(-1.f, 0.f, 0.f)}};
|
||||
FRG::CharacterUpdateContext retryContext;
|
||||
retryContext.characterId = "retrying_npc";
|
||||
retryContext.movementObstacles = &movingBlocker;
|
||||
FRG::CharacterUpdateEvents retryEvents;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
retryEvents = retryingNpc.update(100, retryContext);
|
||||
if (retryEvents.forceNavigationReplan ||
|
||||
!retryingNpc.pathWaypoints.empty() ||
|
||||
retryingNpc.navigationRetryTicks != firstCooldown ||
|
||||
retryingNpc.requestedWalkTarget != Eigen::Vector3f(2.f, 0.f, 0.f))
|
||||
return fail("Blocked NPC did not stop with a deterministic cooldown");
|
||||
for (int i = 0; i < firstCooldown; ++i)
|
||||
retryEvents = retryingNpc.update(100, retryContext);
|
||||
if (!retryEvents.forceNavigationReplan ||
|
||||
retryEvents.blockingCharacterId != "b")
|
||||
return fail("Blocked NPC did not retry after its cooldown");
|
||||
|
||||
retryingNpc.movedLastTick = true;
|
||||
nlohmann::json blockedState;
|
||||
retryingNpc.save(blockedState);
|
||||
FRG::CharacterState restoredMover;
|
||||
restoredMover.load(blockedState);
|
||||
if (restoredMover.movementBlockedTicks !=
|
||||
retryingNpc.movementBlockedTicks ||
|
||||
restoredMover.navigationRetryTicks !=
|
||||
retryingNpc.navigationRetryTicks ||
|
||||
restoredMover.navigationRetryCooldownTicks !=
|
||||
retryingNpc.navigationRetryCooldownTicks ||
|
||||
restoredMover.navigationRetryCooldownIndex !=
|
||||
retryingNpc.navigationRetryCooldownIndex ||
|
||||
!restoredMover.movedLastTick)
|
||||
return fail("Blocked movement state did not round trip");
|
||||
|
||||
FRG::LocationGameState occupancyState;
|
||||
auto stationary = std::make_shared<FRG::CharacterState>();
|
||||
stationary->enabled = true;
|
||||
stationary->hp = 10.f;
|
||||
auto moving = std::make_shared<FRG::CharacterState>();
|
||||
moving->enabled = true;
|
||||
moving->hp = 10.f;
|
||||
moving->pathWaypoints = {Eigen::Vector3f(4.f, 0.f, 0.f)};
|
||||
moving->movedLastTick = true;
|
||||
FRG::PlayerStateMap occupancyPlayers{
|
||||
{"stationary", stationary}, {"moving", moving}};
|
||||
if (occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers).size() != 1 ||
|
||||
occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers, "moving").size() != 2 ||
|
||||
!occupancyState.collectPathObstacles(
|
||||
"stationary", occupancyPlayers).empty())
|
||||
return fail("Stationary occupancy policy is incorrect");
|
||||
moving->movedLastTick = false;
|
||||
if (occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers).size() != 2)
|
||||
return fail("Physically stalled character was omitted from planning");
|
||||
|
||||
FRG::LocationGameState approachingState;
|
||||
auto left = std::make_shared<FRG::CharacterState>();
|
||||
left->configureAsPlayer();
|
||||
left->enabled = true;
|
||||
left->position = Eigen::Vector3f(0.f, 0.f, 0.f);
|
||||
left->walkTarget = Eigen::Vector3f(2.f, 0.f, 0.f);
|
||||
left->requestedWalkTarget = left->walkTarget;
|
||||
left->pathWaypoints = {left->walkTarget};
|
||||
auto right = std::make_shared<FRG::CharacterState>();
|
||||
right->configureAsPlayer();
|
||||
right->enabled = true;
|
||||
right->position = Eigen::Vector3f(1.4f, 0.f, 0.f);
|
||||
right->walkTarget = Eigen::Vector3f(-1.f, 0.f, 0.f);
|
||||
right->requestedWalkTarget = right->walkTarget;
|
||||
right->pathWaypoints = {right->walkTarget};
|
||||
FRG::PlayerStateMap approachingPlayers{{"a", left}, {"b", right}};
|
||||
for (int i = 0; i < 5; ++i)
|
||||
approachingState.update(100, approachingPlayers, false);
|
||||
const float separation = (right->position - left->position).norm();
|
||||
if (separation + 1e-5f <
|
||||
left->movementContactRadius + right->movementContactRadius ||
|
||||
!left->pathWaypoints.empty() || !right->pathWaypoints.empty())
|
||||
return fail("Colliding players did not stop without overlap");
|
||||
|
||||
if (benchmark) {
|
||||
runBenchmark(pathFinder, 100);
|
||||
runBenchmark(pathFinder, 1000);
|
||||
}
|
||||
std::filesystem::remove(openGrid);
|
||||
std::filesystem::remove(corridorGrid);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include "game_server/WebsocketGameClient.h"
|
||||
#include "game_server/WebsocketGameServer.h"
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(bool condition, const char* message)
|
||||
{
|
||||
if (condition) return;
|
||||
std::cerr << "WebsocketLockstepTests: " << message << '\n';
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
template <class Predicate>
|
||||
bool waitFor(Predicate predicate)
|
||||
{
|
||||
const auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (predicate()) return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
FRG::CampaignConfig campaign;
|
||||
campaign.id = "test_lockstep";
|
||||
campaign.networkPlayerSlots = {"player", "player2"};
|
||||
|
||||
FRG::InternalGameServer authoritative;
|
||||
authoritative.startCampaign(campaign, {}, true, false);
|
||||
nlohmann::json initialState;
|
||||
authoritative.save(initialState);
|
||||
initialState["players"]["player2"] = initialState["players"]["player"];
|
||||
FRG::LocationGameState testLocation;
|
||||
auto hostileNpc = std::make_shared<FRG::CharacterState>();
|
||||
hostileNpc->enabled = true;
|
||||
hostileNpc->hp = 200.f;
|
||||
testLocation.npcs.emplace("hostile_npc", std::move(hostileNpc));
|
||||
nlohmann::json testLocationState;
|
||||
testLocation.save(testLocationState);
|
||||
initialState["currentLocationId"] = "test_location";
|
||||
initialState["locations"]["test_location"] = std::move(testLocationState);
|
||||
authoritative.load(initialState);
|
||||
FRG::WebsocketGameServer server(
|
||||
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep",
|
||||
"127.0.0.1", 0);
|
||||
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
|
||||
require(
|
||||
waitFor([&]() { return client->connected(); }),
|
||||
"client did not connect");
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
return client->initialized();
|
||||
}),
|
||||
"client did not receive initial state");
|
||||
require(client->localPlayerId() == "player", "wrong player slot assigned");
|
||||
|
||||
auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
require(
|
||||
waitFor([&]() {
|
||||
secondClient->update(0);
|
||||
return secondClient->initialized();
|
||||
}),
|
||||
"second client did not receive initial state");
|
||||
require(
|
||||
secondClient->localPlayerId() == "player2",
|
||||
"second client received the wrong player slot");
|
||||
|
||||
FRG::WebsocketGameClient overflow(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
require(
|
||||
waitFor([&]() {
|
||||
overflow.update(0);
|
||||
return overflow.rejected();
|
||||
}),
|
||||
"connection beyond configured slots was not rejected");
|
||||
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player2").state.enabled &&
|
||||
secondClient->snapshot()->players.at("player").state.enabled;
|
||||
}) &&
|
||||
authoritative.snapshot()->players.at("player").state.enabled &&
|
||||
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||
"connection presence was not broadcast to all replicas");
|
||||
|
||||
client->submit(FRG::SetCharacterDestinationCommand{
|
||||
{}, "spoofed", Eigen::Vector3f(1.234f, 0.f, 2.345f), "ignored"});
|
||||
secondClient->submit(FRG::SetCharacterDestinationCommand{
|
||||
{}, "spoofed", Eigen::Vector3f(-1.234f, 0.f, 3.456f), "ignored"});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
server.update();
|
||||
server.update();
|
||||
server.update();
|
||||
const bool destinationsConverged = waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
const auto first = client->snapshot();
|
||||
const auto second = secondClient->snapshot();
|
||||
const auto host = authoritative.snapshot();
|
||||
return first->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
|
||||
second->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
|
||||
first->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
second->players.at("player2").state.requestedWalkTarget) &&
|
||||
second->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player").state.requestedWalkTarget) &&
|
||||
host->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player").state.requestedWalkTarget) &&
|
||||
host->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
second->players.at("player2").state.requestedWalkTarget);
|
||||
});
|
||||
if (!destinationsConverged) {
|
||||
const auto first = client->snapshot();
|
||||
const auto second = secondClient->snapshot();
|
||||
const auto host = authoritative.snapshot();
|
||||
std::cerr << "destination revisions " << first->revision << ", "
|
||||
<< second->revision << ", " << host->revision << "\n"
|
||||
<< "player targets "
|
||||
<< first->players.at("player").state.requestedWalkTarget.transpose()
|
||||
<< " / "
|
||||
<< host->players.at("player").state.requestedWalkTarget.transpose()
|
||||
<< "\nplayer2 targets "
|
||||
<< second->players.at("player2").state.requestedWalkTarget.transpose()
|
||||
<< " / "
|
||||
<< host->players.at("player2").state.requestedWalkTarget.transpose()
|
||||
<< std::endl;
|
||||
}
|
||||
require(
|
||||
destinationsConverged,
|
||||
"simultaneous destinations did not converge across lockstep replicas");
|
||||
|
||||
client->submit(FRG::SetCharacterTargetCommand{
|
||||
"stale_location", "spoofed_character", "hostile_npc"});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
server.update();
|
||||
server.update();
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player").state.attackTargetId ==
|
||||
"hostile_npc" &&
|
||||
secondClient->snapshot()->players.at("player").state.attackTargetId ==
|
||||
"hostile_npc" &&
|
||||
authoritative.snapshot()->players.at("player").state.attackTargetId ==
|
||||
"hostile_npc";
|
||||
}),
|
||||
"target command was not delayed and rebroadcast");
|
||||
|
||||
client->submit(FRG::ClearCharacterAttackTargetCommand{
|
||||
"stale_location", "spoofed_character"});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
server.update();
|
||||
server.update();
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||
authoritative.snapshot()->players.at("player").state.attackTargetId.empty();
|
||||
}),
|
||||
"clear attack target command was not delayed and rebroadcast");
|
||||
|
||||
client->submit(FRG::TryCharacterAttackComboCommand{
|
||||
"stale_location", "spoofed_character"});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
server.update();
|
||||
server.update();
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->revision ==
|
||||
secondClient->snapshot()->revision;
|
||||
}),
|
||||
"attack combo command was not rebroadcast in lockstep");
|
||||
|
||||
secondClient.reset();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
return !client->snapshot()->players.at("player2").state.enabled;
|
||||
}) &&
|
||||
!authoritative.snapshot()->players.at("player2").state.enabled,
|
||||
"disconnect presence was not propagated");
|
||||
|
||||
FRG::WebsocketGameClient replacement(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
require(
|
||||
waitFor([&]() {
|
||||
replacement.update(0);
|
||||
return replacement.initialized();
|
||||
}),
|
||||
"disconnected player slot was not reusable");
|
||||
server.update();
|
||||
require(
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
replacement.update(0);
|
||||
return client->snapshot()->players.at("player2").state.enabled;
|
||||
}) &&
|
||||
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||
"reconnected player was not enabled");
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "BoneAnimatedModelNew.h"
|
||||
#include "BoneAnimatedModelNew.h"
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace FRG
|
||||
{
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
using std::min;
|
||||
using std::max;
|
||||
#endif
|
||||
@@ -722,9 +722,10 @@ namespace FRG
|
||||
return -1;
|
||||
}
|
||||
|
||||
void BoneSystemNew::Interpolate(int frame)
|
||||
void BoneSystemNew::Interpolate(float frame)
|
||||
{
|
||||
int startingKeyFrame = -1;
|
||||
int nextKeyFrame = -1;
|
||||
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
||||
{
|
||||
int oldFrame = animations[0].keyFrames[i].frame;
|
||||
@@ -732,22 +733,29 @@ namespace FRG
|
||||
if (frame >= oldFrame && frame < nextFrame)
|
||||
{
|
||||
startingKeyFrame = i;
|
||||
nextKeyFrame = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startingKeyFrame == -1)
|
||||
{
|
||||
logger() << "Exception here: frame number is out of range of keyframes. Frame: " << frame << std::endl;
|
||||
throw std::runtime_error("Exception here");
|
||||
if (animations[0].keyFrames.size() < 2)
|
||||
return;
|
||||
startingKeyFrame = static_cast<int>(animations[0].keyFrames.size()) - 1;
|
||||
nextKeyFrame = 0;
|
||||
}
|
||||
|
||||
int modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = animations[0].keyFrames[startingKeyFrame + 1].frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
||||
float modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = nextKeyFrame == 0
|
||||
? animations[0].keyFrames.back().frame -
|
||||
animations[0].keyFrames.front().frame + 1
|
||||
: animations[0].keyFrames[nextKeyFrame].frame -
|
||||
animations[0].keyFrames[startingKeyFrame].frame;
|
||||
float t = diffFrames > 0 ? modifiedFrameNumber / diffFrames : 0.0f;
|
||||
|
||||
std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingKeyFrame].bones;
|
||||
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingKeyFrame + 1].bones;
|
||||
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[nextKeyFrame].bones;
|
||||
|
||||
std::vector<Matrix4f> skinningMatrixForEachBone(currentBones.size());
|
||||
|
||||
@@ -906,9 +914,10 @@ namespace FRG
|
||||
}
|
||||
}
|
||||
|
||||
void GpuSkinningShaderDataNew::ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame)
|
||||
void GpuSkinningShaderDataNew::ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, float frame)
|
||||
{
|
||||
int startingKeyFrame = -1;
|
||||
int nextKeyFrame = -1;
|
||||
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
||||
{
|
||||
int oldFrame = keyFrames[i].frame;
|
||||
@@ -916,6 +925,7 @@ namespace FRG
|
||||
if (frame >= oldFrame && frame < nextFrame)
|
||||
{
|
||||
startingKeyFrame = static_cast<int>(i);
|
||||
nextKeyFrame = static_cast<int>(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -924,16 +934,23 @@ namespace FRG
|
||||
|
||||
if (startingKeyFrame == -1)
|
||||
{
|
||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||
return;
|
||||
if (keyFrames.size() < 2)
|
||||
{
|
||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||
return;
|
||||
}
|
||||
startingKeyFrame = static_cast<int>(keyFrames.size()) - 1;
|
||||
nextKeyFrame = 0;
|
||||
}
|
||||
|
||||
int modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = keyFrames[startingKeyFrame + 1].frame - keyFrames[startingKeyFrame].frame;
|
||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
||||
float modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = nextKeyFrame == 0
|
||||
? keyFrames.back().frame - keyFrames.front().frame + 1
|
||||
: keyFrames[nextKeyFrame].frame - keyFrames[startingKeyFrame].frame;
|
||||
float t = diffFrames > 0 ? modifiedFrameNumber / diffFrames : 0.0f;
|
||||
|
||||
const std::vector<Bone>& oneFrameBones = keyFrames[startingKeyFrame].bones;
|
||||
const std::vector<Bone>& nextFrameBones = keyFrames[startingKeyFrame + 1].bones;
|
||||
const std::vector<Bone>& nextFrameBones = keyFrames[nextKeyFrame].bones;
|
||||
|
||||
for (size_t i = 0; i < startBones.size(); i++)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace FRG
|
||||
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||
void LoadFromBinaryFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||
|
||||
void Interpolate(int frame);
|
||||
void Interpolate(float frame);
|
||||
|
||||
int findBoneIndex(const std::string& name) const;
|
||||
};
|
||||
@@ -94,14 +94,14 @@ namespace FRG
|
||||
|
||||
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
||||
void RenderVBO(Renderer& renderer, const std::vector<std::string>& meshNamesOrdered);
|
||||
void ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame);
|
||||
void ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, float frame);
|
||||
};
|
||||
|
||||
struct BoneAnimationDataNew
|
||||
{
|
||||
BoneSystemNew model;
|
||||
float currentFrame = 0.f;
|
||||
int lastFrame = -1;
|
||||
float lastFrame = -1.0f;
|
||||
int totalFrames = 1;
|
||||
|
||||
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <iostream>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
|
||||
using json = nlohmann::json;
|
||||
@@ -109,6 +110,10 @@ namespace FRG {
|
||||
c.startStatePath = entry.value("startState", "");
|
||||
c.questsPath = entry.value("quests", "");
|
||||
c.tutorial = entry.value("tutorial", false);
|
||||
c.networkPlayerSlots =
|
||||
entry.value("networkPlayerSlots", std::vector<std::string>{});
|
||||
c.networkCompatibility =
|
||||
entry.value("networkCompatibility", std::string());
|
||||
c.loadingScreenPath = entry.value("loadingScreen", std::string(CampaignConfig::kDefaultLoadingScreen));
|
||||
for (const std::string& path : c.locationConfigPaths) {
|
||||
LocationConfig loc;
|
||||
|
||||
@@ -55,6 +55,10 @@ namespace FRG {
|
||||
std::string startStatePath;
|
||||
std::string questsPath;
|
||||
bool tutorial = false;
|
||||
// Existing character IDs assigned to multiplayer connections in order.
|
||||
std::vector<std::string> networkPlayerSlots;
|
||||
// Version token shared by clients with identical deterministic content.
|
||||
std::string networkCompatibility;
|
||||
// UI layout shown while the campaign loads (preloaded with the rest of the UI).
|
||||
std::string loadingScreenPath = kDefaultLoadingScreen;
|
||||
|
||||
|
||||
+274
-444
File diff suppressed because it is too large.
Load diff
+65
-53
@@ -11,30 +11,28 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <chrono>
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class TextRenderer;
|
||||
class IGameServer;
|
||||
|
||||
class Character {
|
||||
public:
|
||||
Character(IGameServer& server, std::string locationId, std::string entityId)
|
||||
: gameServer_(server), locationId_(std::move(locationId)),
|
||||
entityId_(std::move(entityId)) {}
|
||||
const std::string& entityId() const { return entityId_; }
|
||||
void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); }
|
||||
// The aliasing shared_ptr keeps the owning GameSnapshot alive for as long
|
||||
// as the returned CharacterState is being read.
|
||||
std::shared_ptr<const CharacterState> stateSnapshot() const;
|
||||
static constexpr float kAttackRange = 1.25f;
|
||||
|
||||
// Resolves a character index to a live Character pointer.
|
||||
// kNoTarget (-1) → nullptr, kPlayerIndex (-2) → Location::player, 0..n → npcs[n].
|
||||
using CharacterResolver = std::function<Character*(int index)>;
|
||||
|
||||
using PathPlanner = std::function<std::vector<Eigen::Vector3f>(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end)>;
|
||||
|
||||
// All serializable game state (position, hp, flags, pathfinding, etc.)
|
||||
CharacterState state;
|
||||
|
||||
int index = CharacterState::kNoTarget;
|
||||
|
||||
// ---- Asset loading ----
|
||||
void loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
void loadBinaryAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
@@ -46,31 +44,19 @@ public:
|
||||
|
||||
// Creates a fully-loaded Character from a CharacterState (including its creationInfo).
|
||||
// Loads all animations, textures, and weapon mesh/texture from the paths stored in creationInfo.
|
||||
static std::unique_ptr<Character> createFromState(CharacterState st, Renderer& renderer, const char* zipFile);
|
||||
static std::unique_ptr<Character> createFromState(
|
||||
const CharacterState& initialState, IGameServer& server,
|
||||
const std::string& locationId, const std::string& entityId,
|
||||
Renderer& renderer, const char* zipFile);
|
||||
|
||||
// ---- Per-frame update ----
|
||||
// resolver maps character indices to live pointers so attackTargetIndex / faceTargetIndex
|
||||
// can be dereferenced inside update(). Pass nullptr only if no targets are set.
|
||||
void update(int64_t deltaMs, const CharacterResolver& resolver = nullptr);
|
||||
|
||||
void setPathPlanner(PathPlanner planner);
|
||||
|
||||
// ---- Navigation (path data lives in state; pathPlanner lives here) ----
|
||||
// callbackName, if given, is the global Lua function name backing onArrived —
|
||||
// stored in state.onArrivedCallbackName so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
|
||||
void setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived = nullptr, std::string callbackName = "");
|
||||
void stopAndRotateToTarget(int npcId);
|
||||
|
||||
void forceReplan();
|
||||
void stopInPlace() { state.stopInPlace(); }
|
||||
|
||||
// ---- Reset ----
|
||||
void resetPlayerAfterDeath();
|
||||
|
||||
void tryAttackCombo();
|
||||
// Applies authoritative animation state to runtime resources and consumes
|
||||
// presentation-only events emitted by CharacterState::update.
|
||||
void update(int64_t deltaMs, const CharacterUpdateEvents* events = nullptr);
|
||||
|
||||
// ---- Damage ----
|
||||
void applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection = Eigen::Vector3f::Zero(), int attackerIndex = CharacterState::kNoTarget);
|
||||
void presentDamage(float hp, float maxHp, const Eigen::Vector3f& attackDirection,
|
||||
const std::string& attackerId);
|
||||
|
||||
// ---- Spark setup ----
|
||||
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
|
||||
@@ -80,6 +66,10 @@ public:
|
||||
void drawName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
void drawPlayerName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix,
|
||||
const std::string& displayName);
|
||||
void drawHealthBar(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
@@ -101,14 +91,12 @@ public:
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
|
||||
// ---- Query helpers (delegate to state) ----
|
||||
bool isMoving() const { return state.isMoving(); }
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return state.getRequestedWalkTarget(); }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const { return state.getCurrentNavigationTarget(); }
|
||||
float getHp() const { return state.getHp(); }
|
||||
void setHp(float newHp) { state.setHp(newHp); }
|
||||
|
||||
float getSpeedMultiplier(float transitionProgress) const;
|
||||
// ---- Query helpers (read immutable snapshots) ----
|
||||
bool isMoving() const;
|
||||
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
Eigen::Vector3f renderPosition() const;
|
||||
float getHp() const;
|
||||
|
||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||
VertexRenderStruct weaponMesh;
|
||||
@@ -123,38 +111,62 @@ public:
|
||||
static Dialogue::TranslationDatabase nameTranslations;
|
||||
|
||||
private:
|
||||
friend class Game;
|
||||
friend class Location;
|
||||
friend class ScriptEngine;
|
||||
friend class LocationEditor;
|
||||
friend class PathVisualization;
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
std::string entityId_;
|
||||
std::function<void()> onDeathAnimComplete;
|
||||
std::function<void(float, float, const std::string&)> onHpChanged;
|
||||
std::map<AnimationState, BoneAnimationDataNew> animations;
|
||||
VertexRenderStruct modelMutable;
|
||||
VertexRenderStruct healthBarMesh;
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
||||
|
||||
PathPlanner pathPlanner;
|
||||
struct PresentationSample {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float facingAngle = 0.0f;
|
||||
float animationFrame = 0.0f;
|
||||
AnimationState currentState = AnimationState::STAND;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
static constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
static constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||||
PresentationSample previousPresentationSample_;
|
||||
PresentationSample currentPresentationSample_;
|
||||
std::chrono::steady_clock::time_point lastPresentationUpdate_;
|
||||
bool hasPresentationSample_ = false;
|
||||
|
||||
AnimationState resolveActiveState() const;
|
||||
bool prepareGpuSkinning();
|
||||
void drawAttachedWeapon(Renderer& renderer);
|
||||
AnimationState resolveActiveState(const CharacterState& state) const;
|
||||
CharacterState interpolatedState(const CharacterState& state) const;
|
||||
void updateAnimationForRender(const CharacterState& state);
|
||||
bool prepareGpuSkinning(const CharacterState& state);
|
||||
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
||||
|
||||
void drawGpuSkinning(Renderer& renderer);
|
||||
void drawShadowDepthGpuSkinning(Renderer& renderer);
|
||||
void drawShadowDepthCpu(Renderer& renderer);
|
||||
void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||
void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||
void drawGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||
void drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||
void drawShadowDepthCpu(Renderer& renderer, const CharacterState& state);
|
||||
void drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||
void drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||
void drawNightGpuSkinning(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
void drawNightCpu(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
void drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||
void drawNightCpuWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||
|
||||
+461
-6
@@ -1,7 +1,70 @@
|
||||
#include "CharacterState.h"
|
||||
#include "LocationState.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
// Tick counts are authoritative and therefore independent of render rate.
|
||||
constexpr float ATTACK_COOLDOWN_TIME = 1.6f;
|
||||
constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||||
constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
constexpr int BLOCKED_REPLAN_TICKS = 3;
|
||||
|
||||
std::uint32_t stableIdHash(const std::string& value)
|
||||
{
|
||||
std::uint32_t hash = 2166136261u;
|
||||
for (const unsigned char byte : value) {
|
||||
hash ^= byte;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterState::configureAsPlayer() {
|
||||
walkSpeed = 3.0f;
|
||||
rotationSpeed = 8.0f;
|
||||
modelScale = 1.f;
|
||||
modelCorrectionRotation = Eigen::Quaternionf(
|
||||
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
|
||||
Eigen::AngleAxisf(-PI * 0.5f, Eigen::Vector3f::UnitX()));
|
||||
canAttack = true;
|
||||
isPlayer = true;
|
||||
}
|
||||
|
||||
void CharacterState::configureNavigationRetryCooldowns(
|
||||
const std::string& entityId, std::vector<int> cooldownTicks)
|
||||
{
|
||||
if (cooldownTicks.empty()) {
|
||||
cooldownTicks = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
}
|
||||
for (int& ticks : cooldownTicks)
|
||||
ticks = (std::max)(1, (std::min)(600, ticks));
|
||||
|
||||
std::uint32_t state = stableIdHash(entityId);
|
||||
for (std::size_t i = cooldownTicks.size(); i > 1; --i) {
|
||||
state = state * 1664525u + 1013904223u;
|
||||
const std::size_t other = state % i;
|
||||
std::swap(cooldownTicks[i - 1], cooldownTicks[other]);
|
||||
}
|
||||
navigationRetryCooldownTicks = std::move(cooldownTicks);
|
||||
navigationRetryCooldownIndex = 0;
|
||||
}
|
||||
|
||||
void CharacterState::scheduleNextNavigationRetry()
|
||||
{
|
||||
if (navigationRetryCooldownTicks.empty())
|
||||
configureNavigationRetryCooldowns(npcName);
|
||||
navigationRetryTicks = navigationRetryCooldownTicks[
|
||||
navigationRetryCooldownIndex % navigationRetryCooldownTicks.size()];
|
||||
navigationRetryCooldownIndex =
|
||||
(navigationRetryCooldownIndex + 1) %
|
||||
navigationRetryCooldownTicks.size();
|
||||
}
|
||||
|
||||
bool CharacterState::isMoving() const {
|
||||
Eigen::Vector3f toTarget = walkTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
@@ -20,14 +83,386 @@ void CharacterState::stopInPlace() {
|
||||
requestedWalkTarget = walkTarget;
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
onArrivedCallback = nullptr;
|
||||
onArrivedCallbackName.clear();
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
}
|
||||
|
||||
void CharacterState::setTarget(
|
||||
const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||
const bool sameRequestedTarget =
|
||||
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool alreadyMovingToTarget =
|
||||
!pathWaypoints.empty() ||
|
||||
(walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool stoppedAfterFailedPath =
|
||||
pathWaypoints.empty() &&
|
||||
(walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
if (callbackName.empty() && sameRequestedTarget &&
|
||||
(alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestedWalkTarget = normalizedTarget;
|
||||
onArrivedCallbackName = std::move(callbackName);
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
pathWaypoints = location.navigationMaps.empty()
|
||||
? std::vector<Eigen::Vector3f>{}
|
||||
: location.planPathExceptMyself(
|
||||
position, normalizedTarget, entityId, dynamicObstacles);
|
||||
currentWaypointIndex = 0;
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
walkTarget = pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
if (!isPlayer) scheduleNextNavigationRetry();
|
||||
}
|
||||
|
||||
void CharacterState::forceReplan(
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(
|
||||
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||
pathWaypoints = location.navigationMaps.empty()
|
||||
? std::vector<Eigen::Vector3f>{}
|
||||
: location.planPathExceptMyself(
|
||||
position, normalizedTarget, entityId, dynamicObstacles);
|
||||
currentWaypointIndex = 0;
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
walkTarget = pathWaypoints.back();
|
||||
blockingCharacterId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
if (!isPlayer) scheduleNextNavigationRetry();
|
||||
}
|
||||
|
||||
void CharacterState::stopAndRotateToTarget(std::string characterId)
|
||||
{
|
||||
stopInPlace();
|
||||
faceTargetId = std::move(characterId);
|
||||
}
|
||||
|
||||
void CharacterState::resetPlayerAfterDeath()
|
||||
{
|
||||
if (!isPlayer) return;
|
||||
currentState = AnimationState::STAND;
|
||||
resetAnim = true;
|
||||
showWeapon = false;
|
||||
battle_state = 0;
|
||||
attack = 0;
|
||||
}
|
||||
|
||||
void CharacterState::tryAttackCombo()
|
||||
{
|
||||
if (static_cast<int>(animationFrame) < 20 ||
|
||||
(currentState != AnimationState::ACTION_ATTACK &&
|
||||
currentState != AnimationState::ACTION_ATTACK_2)) {
|
||||
return;
|
||||
}
|
||||
const auto index = static_cast<std::size_t>(currentState);
|
||||
animationFrame = index < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||||
}
|
||||
|
||||
void CharacterState::setHp(float newHp) {
|
||||
hp = newHp;
|
||||
if (hp < 0.f) hp = 0.f;
|
||||
if (onHpChanged) onHpChanged(hp, initialHp, CharacterState::kNoTarget);
|
||||
}
|
||||
|
||||
void CharacterState::setAnimationMetadata(
|
||||
AnimationState state, int startFrame, int frameCount)
|
||||
{
|
||||
const auto index = static_cast<std::size_t>(state);
|
||||
if (index < animationFrameCounts.size()) {
|
||||
animationStartFrames[index] = (std::max)(0, startFrame);
|
||||
animationFrameCounts[index] = (std::max)(1, frameCount);
|
||||
}
|
||||
}
|
||||
|
||||
CharacterUpdateEvents CharacterState::update(
|
||||
std::int64_t deltaMs, const CharacterUpdateContext& context)
|
||||
{
|
||||
CharacterUpdateEvents events;
|
||||
movedLastTick = false;
|
||||
if (!enabled) return events;
|
||||
bool navigationPausedThisTick = false;
|
||||
|
||||
const CharacterObservation* attackTarget = context.attackTarget;
|
||||
const CharacterObservation* faceTarget = context.faceTarget;
|
||||
if (!attackTarget || attackTarget->hp <= 0.f) {
|
||||
battle_state = 0;
|
||||
attack = 0;
|
||||
attackTargetId.clear();
|
||||
} else {
|
||||
const float distance = (attackTarget->position - position).norm();
|
||||
if (distance >= visibleRange) {
|
||||
if ((attackTarget->position - requestedWalkTarget).norm() >
|
||||
TARGET_REPLAN_THRESHOLD) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else if (distance >= 1.25f) {
|
||||
if ((attackTarget->position - requestedWalkTarget).norm() > TARGET_REPLAN_THRESHOLD) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else {
|
||||
battle_state = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const Eigen::Vector3f activeTarget = getCurrentNavigationTarget();
|
||||
const Eigen::Vector3f lookTarget =
|
||||
battle_state == 1 && attackTarget ? attackTarget->position : activeTarget;
|
||||
|
||||
if (currentState == AnimationState::WALK || currentState == AnimationState::STAND) {
|
||||
Eigen::Vector3f toTarget = activeTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
const float distance = toTarget.norm();
|
||||
if (distance > kWalkThreshold) {
|
||||
const Eigen::Vector3f direction = toTarget / distance;
|
||||
const float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
Eigen::Vector3f candidate =
|
||||
moveAmount >= distance ? activeTarget : position + direction * moveAmount;
|
||||
candidate.y() = 0.f;
|
||||
const CharacterMovementObstacle* blocker = nullptr;
|
||||
if (context.movementObstacles) {
|
||||
for (const CharacterMovementObstacle& obstacle :
|
||||
*context.movementObstacles) {
|
||||
if (obstacle.id == context.characterId) continue;
|
||||
Eigen::Vector3f startDelta = position - obstacle.position;
|
||||
Eigen::Vector3f endDelta = candidate - obstacle.position;
|
||||
startDelta.y() = 0.f;
|
||||
endDelta.y() = 0.f;
|
||||
const float minimumDistance =
|
||||
movementContactRadius + obstacle.radius;
|
||||
const float minimumDistanceSq =
|
||||
minimumDistance * minimumDistance;
|
||||
const Eigen::Vector3f segment = candidate - position;
|
||||
const float segmentLengthSq =
|
||||
segment.x() * segment.x() + segment.z() * segment.z();
|
||||
float t = 0.f;
|
||||
if (segmentLengthSq > 1e-8f) {
|
||||
t = -(startDelta.x() * segment.x() +
|
||||
startDelta.z() * segment.z()) / segmentLengthSq;
|
||||
t = (std::max)(0.f, (std::min)(1.f, t));
|
||||
}
|
||||
Eigen::Vector3f closest = startDelta + segment * t;
|
||||
closest.y() = 0.f;
|
||||
const bool movingCloser =
|
||||
endDelta.squaredNorm() + 1e-6f < startDelta.squaredNorm();
|
||||
if (movingCloser &&
|
||||
closest.squaredNorm() < minimumDistanceSq) {
|
||||
blocker = &obstacle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blocker) {
|
||||
blockingCharacterId = blocker->id;
|
||||
currentState = AnimationState::STAND;
|
||||
const bool sameDirection =
|
||||
blocker->moving &&
|
||||
blocker->movementDirection.dot(direction) > 0.5f;
|
||||
if (sameDirection) {
|
||||
movementBlockedTicks = 0;
|
||||
} else if (++movementBlockedTicks >= BLOCKED_REPLAN_TICKS) {
|
||||
if (isPlayer) {
|
||||
stopInPlace();
|
||||
} else {
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
walkTarget =
|
||||
Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
movementBlockedTicks = 0;
|
||||
scheduleNextNavigationRetry();
|
||||
navigationPausedThisTick = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
movedLastTick =
|
||||
(candidate - position).squaredNorm() > 1e-8f;
|
||||
position = candidate;
|
||||
movementBlockedTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
}
|
||||
targetFacingAngle = std::atan2(direction.x(), -direction.z());
|
||||
if (!blocker) currentState = AnimationState::WALK;
|
||||
} else {
|
||||
currentState = AnimationState::STAND;
|
||||
const bool completedPath = !pathWaypoints.empty();
|
||||
const bool hasNextWaypoint =
|
||||
!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
||||
if (hasNextWaypoint) {
|
||||
++currentWaypointIndex;
|
||||
} else {
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
if (completedPath &&
|
||||
(requestedWalkTarget - position).norm() >
|
||||
TARGET_REPLAN_THRESHOLD &&
|
||||
!isPlayer)
|
||||
scheduleNextNavigationRetry();
|
||||
if (completedPath && !onArrivedCallbackName.empty()) {
|
||||
events.arrivedCallbackName = onArrivedCallbackName;
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (faceTarget) {
|
||||
Eigen::Vector3f toFace = faceTarget->position - position;
|
||||
toFace.y() = 0.f;
|
||||
const float distanceToFace = toFace.norm();
|
||||
targetFacingAngle = distanceToFace > 1e-3f
|
||||
? std::atan2(toFace.x() / distanceToFace, -toFace.z() / distanceToFace)
|
||||
: facingAngle;
|
||||
} else if (!faceTargetId.empty()) {
|
||||
faceTargetId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPlayer && !navigationPausedThisTick && pathWaypoints.empty() &&
|
||||
(requestedWalkTarget - position).norm() > TARGET_REPLAN_THRESHOLD &&
|
||||
!events.navigationRequested) {
|
||||
if (navigationRetryTicks > 0) --navigationRetryTicks;
|
||||
if (navigationRetryTicks == 0) {
|
||||
events.navigationRequested = true;
|
||||
events.forceNavigationReplan = true;
|
||||
events.navigationTarget = requestedWalkTarget;
|
||||
events.blockingCharacterId = blockingCharacterId;
|
||||
}
|
||||
}
|
||||
|
||||
if (hp <= 0.f) {
|
||||
if (currentState != AnimationState::ACTION_TO_DEATH &&
|
||||
currentState != AnimationState::DEATH_IDLE) {
|
||||
currentState = AnimationState::ACTION_TO_DEATH;
|
||||
resetAnim = true;
|
||||
attackTargetId.clear();
|
||||
}
|
||||
} else if (battle_state == 1 && attackTarget) {
|
||||
targetFacingAngle = std::atan2(
|
||||
lookTarget.x() - position.x(), -(lookTarget.z() - position.z()));
|
||||
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
|
||||
currentState = AnimationState::STAND_TO_ACTION;
|
||||
resetAnim = true;
|
||||
}
|
||||
if (canAttack && attack == 0 && attack_cooldown < 0.f &&
|
||||
currentState == AnimationState::ACTION_IDLE) {
|
||||
attack = 1;
|
||||
attack_cooldown = ATTACK_COOLDOWN_TIME;
|
||||
}
|
||||
if (canAttack && attack_cooldown >= 0.f)
|
||||
attack_cooldown -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (attack == 1 && currentState == AnimationState::ACTION_IDLE &&
|
||||
attackTarget->hp > 0.f) {
|
||||
currentState = AnimationState::ACTION_ATTACK;
|
||||
resetAnim = true;
|
||||
}
|
||||
} else if (currentState == AnimationState::STAND_TO_ACTION ||
|
||||
currentState == AnimationState::ACTION_IDLE ||
|
||||
currentState == AnimationState::ACTION_ATTACK) {
|
||||
currentState = AnimationState::ACTION_TO_STAND;
|
||||
resetAnim = true;
|
||||
}
|
||||
|
||||
float angleDiff = targetFacingAngle - facingAngle;
|
||||
while (angleDiff > PI) angleDiff -= 2.f * PI;
|
||||
while (angleDiff < -PI) angleDiff += 2.f * PI;
|
||||
const float rotationStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
if (std::fabs(angleDiff) <= rotationStep) {
|
||||
facingAngle = targetFacingAngle;
|
||||
faceTargetId.clear();
|
||||
} else {
|
||||
facingAngle += angleDiff > 0.f ? rotationStep : -rotationStep;
|
||||
}
|
||||
|
||||
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDrift > 0.f) {
|
||||
homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (homeDriftCheckTimer <= 0.f) {
|
||||
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL;
|
||||
Eigen::Vector3f toHome = homePosition - position;
|
||||
toHome.y() = 0.f;
|
||||
if (!isMoving() && toHome.norm() > maxHomeDrift) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = homePosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resetAnim) {
|
||||
resetAnim = false;
|
||||
const auto index = static_cast<std::size_t>(currentState);
|
||||
animationFrame = index < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||||
}
|
||||
const int previousFrame = static_cast<int>(animationFrame);
|
||||
animationFrame += static_cast<float>(deltaMs) / 24.f;
|
||||
|
||||
if (previousFrame < 19 && static_cast<int>(animationFrame) >= 19 &&
|
||||
(currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2) &&
|
||||
attackTarget) {
|
||||
events.attackImpact = true;
|
||||
events.attackTargetId = attackTargetId;
|
||||
events.attackDirection = attackTarget->position - position;
|
||||
events.attackDirection.y() = 0.f;
|
||||
const float length = events.attackDirection.norm();
|
||||
if (length > 1e-3f) events.attackDirection /= length;
|
||||
else events.attackDirection = Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
showWeapon =
|
||||
(static_cast<int>(animationFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION) ||
|
||||
(static_cast<int>(animationFrame) <= 32 && currentState == AnimationState::ACTION_TO_STAND) ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2 ||
|
||||
currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_IDLE;
|
||||
|
||||
const auto animationIndex = static_cast<std::size_t>(currentState);
|
||||
const int frameCount = animationIndex < animationFrameCounts.size()
|
||||
? animationFrameCounts[animationIndex] : 1;
|
||||
if (frameCount > 1 && static_cast<int>(animationFrame) >= frameCount - 1) {
|
||||
if (currentState == AnimationState::STAND_TO_ACTION)
|
||||
currentState = AnimationState::ACTION_IDLE;
|
||||
else if (currentState == AnimationState::ACTION_TO_STAND)
|
||||
currentState = AnimationState::STAND;
|
||||
else if (currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2) {
|
||||
currentState = AnimationState::ACTION_IDLE;
|
||||
attack = 0;
|
||||
} else if (currentState == AnimationState::ACTION_TO_DEATH) {
|
||||
currentState = AnimationState::DEATH_IDLE;
|
||||
events.deathAnimationComplete = true;
|
||||
}
|
||||
const auto nextIndex = static_cast<std::size_t>(currentState);
|
||||
animationFrame = nextIndex < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[nextIndex]) : 0.f;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
void CharacterState::save(nlohmann::json& out) const
|
||||
@@ -41,8 +476,8 @@ void CharacterState::save(nlohmann::json& out) const
|
||||
out["currentState"] = static_cast<int>(currentState);
|
||||
out["attack_cooldown"] = attack_cooldown;
|
||||
out["showWeapon"] = showWeapon;
|
||||
out["attackTargetIndex"] = attackTargetIndex;
|
||||
out["faceTargetIndex"] = faceTargetIndex;
|
||||
out["attackTargetId"] = attackTargetId;
|
||||
out["faceTargetId"] = faceTargetId;
|
||||
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() };
|
||||
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() };
|
||||
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() };
|
||||
@@ -53,6 +488,14 @@ void CharacterState::save(nlohmann::json& out) const
|
||||
out["pathWaypoints"] = std::move(wpArr);
|
||||
out["currentWaypointIndex"] = currentWaypointIndex;
|
||||
out["homeDriftCheckTimer"] = homeDriftCheckTimer;
|
||||
out["movementBlockedTicks"] = movementBlockedTicks;
|
||||
out["navigationRetryTicks"] = navigationRetryTicks;
|
||||
out["movedLastTick"] = movedLastTick;
|
||||
out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks;
|
||||
out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex;
|
||||
if (!blockingCharacterId.empty())
|
||||
out["blockingCharacterId"] = blockingCharacterId;
|
||||
out["animationFrame"] = animationFrame;
|
||||
out["visibleRange"] = visibleRange;
|
||||
if (!onArrivedCallbackName.empty())
|
||||
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
||||
@@ -76,8 +519,8 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
||||
attack_cooldown = in.value("attack_cooldown", attack_cooldown);
|
||||
showWeapon = in.value("showWeapon", showWeapon);
|
||||
attackTargetIndex = in.value("attackTargetIndex", attackTargetIndex);
|
||||
faceTargetIndex = in.value("faceTargetIndex", faceTargetIndex);
|
||||
attackTargetId = in.value("attackTargetId", attackTargetId);
|
||||
faceTargetId = in.value("faceTargetId", faceTargetId);
|
||||
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition);
|
||||
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget);
|
||||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget);
|
||||
@@ -89,6 +532,18 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
}
|
||||
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
||||
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
|
||||
movementBlockedTicks = in.value("movementBlockedTicks", movementBlockedTicks);
|
||||
navigationRetryTicks = in.value("navigationRetryTicks", navigationRetryTicks);
|
||||
movedLastTick = in.value("movedLastTick", movedLastTick);
|
||||
navigationRetryCooldownTicks = in.value(
|
||||
"navigationRetryCooldownTicks", navigationRetryCooldownTicks);
|
||||
navigationRetryCooldownIndex = in.value(
|
||||
"navigationRetryCooldownIndex", navigationRetryCooldownIndex);
|
||||
if (!navigationRetryCooldownTicks.empty())
|
||||
navigationRetryCooldownIndex %= navigationRetryCooldownTicks.size();
|
||||
blockingCharacterId =
|
||||
in.value("blockingCharacterId", std::string());
|
||||
animationFrame = in.value("animationFrame", animationFrame);
|
||||
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
||||
}
|
||||
|
||||
|
||||
+92
-25
@@ -2,16 +2,23 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include "navigation/PathFinder.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
constexpr float VISIBLE_RANGE = 6.f;
|
||||
|
||||
struct CharacterUpdateContext;
|
||||
struct CharacterUpdateEvents;
|
||||
struct LocationNetworkState;
|
||||
|
||||
enum class AnimationState {
|
||||
STAND = 0,
|
||||
WALK = 1,
|
||||
@@ -24,6 +31,8 @@ enum class AnimationState {
|
||||
DEATH_IDLE = 8,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Paths and fixed parameters needed to recreate a Character's visual assets
|
||||
// from scratch (e.g. after loading a saved game).
|
||||
struct CharacterCreationInfo {
|
||||
@@ -45,22 +54,17 @@ struct CharacterCreationInfo {
|
||||
float weaponMeshScale = 1.0f;
|
||||
};
|
||||
|
||||
// All serializable game state for a character.
|
||||
// This is a member of Character; rendering-only data lives in Character itself.
|
||||
// All serializable authoritative state for a character. GameState or
|
||||
// LocationGameState owns it; rendering-only data lives in Character.
|
||||
class CharacterState : public ISaveable {
|
||||
public:
|
||||
// Special index values for attackTargetIndex / faceTargetIndex
|
||||
static constexpr int kNoTarget = -1; // no target (was nullptr)
|
||||
static constexpr int kPlayerIndex = -2; // refers to Location::player
|
||||
|
||||
// Distance below which the character is considered to have reached its target.
|
||||
static constexpr float kWalkThreshold = 0.05f;
|
||||
|
||||
// --- Identity ---
|
||||
std::string npcId;
|
||||
std::string npcName;
|
||||
bool isPlayer = false;
|
||||
bool enabled = true;
|
||||
bool enabled = false;
|
||||
bool canAttack = false;
|
||||
|
||||
// --- Position / orientation ---
|
||||
@@ -84,18 +88,22 @@ public:
|
||||
// Weapon visibility toggle (changes during animation)
|
||||
bool showWeapon = true;
|
||||
|
||||
// --- Index-based character references (replace raw pointers) ---
|
||||
int attackTargetIndex = kNoTarget;
|
||||
int faceTargetIndex = kNoTarget;
|
||||
// --- Stable character references ---
|
||||
// Empty means no target. "player" is the campaign player; NPC IDs are
|
||||
// location-local and validated while loading location content.
|
||||
std::string attackTargetId;
|
||||
std::string faceTargetId;
|
||||
|
||||
// --- Home position for drift recovery ---
|
||||
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
||||
float maxHomeDrift = 3.0f;
|
||||
|
||||
// --- Slow-motion effect ---
|
||||
float slowMoTimeRemaining = 0.0f;
|
||||
float slowMoActiveTime = 6.0f;
|
||||
float slowMoTransitionTime = 0.5f;
|
||||
// --- Authoritative logical animation clock ---
|
||||
// Frame counts are immutable metadata copied from loaded clips during
|
||||
// character construction. Simulation advances animationFrame itself.
|
||||
float animationFrame = 0.0f;
|
||||
std::array<int, 9> animationStartFrames = {{0, 0, 0, 0, 0, 0, 0, 0, 0}};
|
||||
std::array<int, 9> animationFrameCounts = {{1, 1, 1, 1, 1, 1, 1, 1, 1}};
|
||||
|
||||
// --- Movement / visual parameters ---
|
||||
float walkSpeed = 3.0f;
|
||||
@@ -103,8 +111,12 @@ public:
|
||||
float modelScale = 0.12f;
|
||||
Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity();
|
||||
bool useGpuSkinning = true;
|
||||
// Full clearance used while planning around stationary characters.
|
||||
float collisionRadius = 0.45f;
|
||||
// Smaller physical contact threshold used by the movement guard.
|
||||
float movementContactRadius = 0.32f;
|
||||
float interactionRadius = 0.0f;
|
||||
bool movedLastTick = false;
|
||||
|
||||
// --- Pathfinding data (serializable parts) ---
|
||||
Eigen::Vector3f walkTarget = Eigen::Vector3f::Zero();
|
||||
@@ -114,31 +126,86 @@ public:
|
||||
|
||||
// Internal drift-check timer (serializable so it survives save/load).
|
||||
float homeDriftCheckTimer = 5.0f;
|
||||
int movementBlockedTicks = 0;
|
||||
int navigationRetryTicks = 0;
|
||||
std::string blockingCharacterId;
|
||||
std::vector<int> navigationRetryCooldownTicks;
|
||||
std::size_t navigationRetryCooldownIndex = 0;
|
||||
|
||||
// --- Non-serializable callbacks (re-wired after each load) ---
|
||||
std::function<void()> onArrivedCallback;
|
||||
std::function<void()> onDeathAnimComplete;
|
||||
std::function<void(float, float, int)> onHpChanged;
|
||||
|
||||
// Global Lua function name backing onArrivedCallback, if it was sourced from a
|
||||
// script (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback
|
||||
// can be re-looked-up by name after a save/load; empty if none or non-Lua.
|
||||
// Global Lua function called when the character reaches the final waypoint.
|
||||
// The name is serialized, so no runtime callback needs to be rehydrated.
|
||||
std::string onArrivedCallbackName;
|
||||
|
||||
// --- Asset / creation info (used to reload visuals from a saved state) ---
|
||||
CharacterCreationInfo creationInfo;
|
||||
|
||||
// --- Pure-state methods ---
|
||||
void configureAsPlayer();
|
||||
void configureNavigationRetryCooldowns(
|
||||
const std::string& entityId,
|
||||
std::vector<int> cooldownTicks = {});
|
||||
bool isMoving() const;
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
void stopInPlace();
|
||||
void setTarget(const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>&
|
||||
dynamicObstacles = {});
|
||||
void forceReplan(const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>&
|
||||
dynamicObstacles = {});
|
||||
void stopAndRotateToTarget(std::string characterId);
|
||||
void resetPlayerAfterDeath();
|
||||
void tryAttackCombo();
|
||||
float getHp() const { return hp; }
|
||||
void setHp(float newHp);
|
||||
|
||||
CharacterUpdateEvents update(std::int64_t deltaMs, const CharacterUpdateContext& context);
|
||||
void setAnimationMetadata(AnimationState state, int startFrame, int frameCount);
|
||||
// --- Serialisation (runtime mutable fields only; creation info is reloaded from config) ---
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
void scheduleNextNavigationRetry();
|
||||
};
|
||||
|
||||
using PlayerStateMap =
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>;
|
||||
|
||||
struct CharacterObservation {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float hp = 0.f;
|
||||
};
|
||||
|
||||
struct CharacterMovementObstacle {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 0.f;
|
||||
bool moving = false;
|
||||
Eigen::Vector3f movementDirection = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct CharacterUpdateContext {
|
||||
std::string characterId;
|
||||
const CharacterObservation* attackTarget = nullptr;
|
||||
const CharacterObservation* faceTarget = nullptr;
|
||||
const std::vector<CharacterMovementObstacle>* movementObstacles = nullptr;
|
||||
};
|
||||
|
||||
struct CharacterUpdateEvents {
|
||||
bool attackImpact = false;
|
||||
bool deathAnimationComplete = false;
|
||||
bool navigationRequested = false;
|
||||
bool forceNavigationReplan = false;
|
||||
Eigen::Vector3f navigationTarget = Eigen::Vector3f::Zero();
|
||||
std::string blockingCharacterId;
|
||||
std::string arrivedCallbackName;
|
||||
std::string attackTargetId;
|
||||
Eigen::Vector3f attackDirection = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
#include "Environment.h"
|
||||
#include "Environment.h"
|
||||
|
||||
#include "utils/Utils.h"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <GL/gl.h>
|
||||
#endif
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,7 @@ bool Environment::enableLogging = true;
|
||||
|
||||
SDL_Window* Environment::window = nullptr;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
bool Environment::isFullscreen = false;
|
||||
#else
|
||||
bool Environment::isFullscreen = true;
|
||||
@@ -71,7 +71,7 @@ void Environment::computeProjectionDimensions()
|
||||
}
|
||||
|
||||
void Environment::setFullscreen(bool enable) {
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
if (enable) {
|
||||
EmscriptenFullscreenStrategy strategy{};
|
||||
strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
#ifdef __linux__
|
||||
#include <SDL2/SDL.h>
|
||||
#endif
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace FRG {
|
||||
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
using std::min;
|
||||
using std::max;
|
||||
#endif
|
||||
|
||||
+348
-218
@@ -1,4 +1,5 @@
|
||||
#include "Game.h"
|
||||
#include "game_server/NetworkProtocol.h"
|
||||
#include "AnimatedModel.h"
|
||||
#include "Localization.h"
|
||||
#include "utils/Utils.h"
|
||||
@@ -13,12 +14,13 @@
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
@@ -35,7 +37,7 @@ namespace FRG
|
||||
static const float zoomMax = 20.0f;
|
||||
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
const char* CONST_ZIP_FILE = "resources.zip";
|
||||
const char* CONST_MUSIC_ZIP_FILE = "music.zip";
|
||||
#else
|
||||
@@ -47,7 +49,7 @@ namespace FRG
|
||||
float y = 0;
|
||||
float z = 0;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
Game* Game::s_instance = nullptr;
|
||||
|
||||
// Коллбэк успешной загрузки resources.zip
|
||||
@@ -76,16 +78,19 @@ namespace FRG
|
||||
#endif
|
||||
|
||||
Location* Game::currentLocation() const {
|
||||
if (gameState.currentLocationName.empty()) return nullptr;
|
||||
auto it = gameState.locations.find(gameState.currentLocationName);
|
||||
return (it != gameState.locations.end()) ? it->second.get() : nullptr;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state || state->currentLocationId.empty()) return nullptr;
|
||||
return gameServerHost.runtimeLocation(state->currentLocationId);
|
||||
}
|
||||
|
||||
Game::Game()
|
||||
: newTickCount(0)
|
||||
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, internalGameServer()
|
||||
, localGameServerHost(internalGameServer)
|
||||
, gameServer(internalGameServer)
|
||||
, gameServerHost(localGameServerHost)
|
||||
, menuManager(renderer, gameServer, *audioPlayer)
|
||||
, lastTickCount(0)
|
||||
, audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, menuManager(renderer, gameState, *audioPlayer)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -104,7 +109,7 @@ namespace FRG
|
||||
renderer.InitOpenGL();
|
||||
//logger() << "Android test step 1 " << std::endl;
|
||||
|
||||
#if defined(EMSCRIPTEN) || defined(__ANDROID__)
|
||||
#if defined(__EMSCRIPTEN__) || defined(__ANDROID__)
|
||||
// These shaders and loading.png are preloaded separately (not from zip),
|
||||
// so they are available immediately without waiting for resources.zip.
|
||||
//logger() << "Android test step 1 " << std::endl;
|
||||
@@ -144,7 +149,7 @@ namespace FRG
|
||||
loadingMesh.data = CreateRect2D({ 0.0f, 0.0f }, { minDimension * 0.5f, minDimension * 0.5f }, 3);
|
||||
loadingMesh.RefreshVBO();
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
s_instance = this;
|
||||
|
||||
std::string resPath = FRG::getPersistentResourcePath("resources.zip");
|
||||
@@ -163,7 +168,7 @@ namespace FRG
|
||||
|
||||
// Скачиваем сразу в постоянный путь /offline/resources.zip
|
||||
emscripten_async_wget2(
|
||||
"resources.zip", // URL на сервере
|
||||
"resources.zip?v=2", // Cache-bust stale browser responses after resource updates
|
||||
resPath.c_str(), // Назначение в VFS Emscripten
|
||||
"GET",
|
||||
nullptr,
|
||||
@@ -218,7 +223,7 @@ namespace FRG
|
||||
loadSteps.push([this]() {
|
||||
|
||||
|
||||
#if defined(EMSCRIPTEN) || defined(__ANDROID__)
|
||||
#if defined(__EMSCRIPTEN__) || defined(__ANDROID__)
|
||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
||||
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/defaultAtmosphere.vertex", "resources/shaders/defaultAtmosphere_web.fragment", CONST_ZIP_FILE);
|
||||
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/planet_bake.vertex", "resources/shaders/planet_bake_web.fragment", CONST_ZIP_FILE);
|
||||
@@ -357,19 +362,6 @@ namespace FRG
|
||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||
|
||||
loadSteps.push([this]() {
|
||||
// Wire inventory callbacks: tutorial tracking + toast notifications.
|
||||
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
|
||||
menuManager.onItemPickedUp(itemId);
|
||||
const Item* item = ItemRegistry::instance().findById(itemId);
|
||||
if (item) menuManager.showToast(menuManager.toastTextureItemAdded, item->name);
|
||||
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
|
||||
};
|
||||
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
|
||||
const Item* item = ItemRegistry::instance().findById(itemId);
|
||||
if (item) menuManager.showToast(menuManager.toastTextureItemRemoved, item->name);
|
||||
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
|
||||
};
|
||||
|
||||
// Wire phone dialogue start function so MenuManager can trigger dialogues.
|
||||
menuManager.startDialogueFunc = [this](const std::string& id) {
|
||||
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
|
||||
@@ -391,15 +383,11 @@ namespace FRG
|
||||
menuManager.skipCutsceneFunc = [this]() {
|
||||
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
||||
};
|
||||
|
||||
menuManager.callTaxiFunc = [this]() {
|
||||
if (currentLocation()) currentLocation()->onTaxiCalled();
|
||||
};
|
||||
});
|
||||
|
||||
loadSteps.push([this]() {
|
||||
try {
|
||||
menuManager.setup(gameState.inventory, CONST_ZIP_FILE);
|
||||
menuManager.setup(CONST_ZIP_FILE);
|
||||
logger() << "UI loaded successfully" << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
@@ -414,7 +402,8 @@ namespace FRG
|
||||
performResetToInitialState();
|
||||
applyTutorialSetting();
|
||||
const CampaignConfig& campaign = activeCampaign();
|
||||
gameState.currentLocationName = campaign.startLocation;
|
||||
server().submit(
|
||||
SetCurrentLocationCommand{campaign.startLocation});
|
||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
||||
@@ -505,7 +494,10 @@ namespace FRG
|
||||
}
|
||||
|
||||
const CampaignConfig& Game::activeCampaign() const {
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
|
||||
const auto state = stateSnapshot();
|
||||
const CampaignConfig* campaign = state
|
||||
? CampaignRegistry::instance().find(state->campaignId)
|
||||
: nullptr;
|
||||
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||
if (!campaign) {
|
||||
static const CampaignConfig empty;
|
||||
@@ -515,7 +507,8 @@ namespace FRG
|
||||
}
|
||||
|
||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||
return player && loadedCampaignId == campaignId && gameState.locations.count(locationId) > 0;
|
||||
return player && loadedCampaignId == campaignId
|
||||
&& gameServerHost.hasRuntimeLocation(locationId);
|
||||
}
|
||||
|
||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||
@@ -555,48 +548,91 @@ namespace FRG
|
||||
|
||||
void Game::loadCampaign(const std::string& campaignId) {
|
||||
loadedCampaignId = campaignId;
|
||||
gameState.campaignId = campaignId;
|
||||
gameState.storedLocationStates.clear();
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
const CampaignConfig* campaign =
|
||||
CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign) return;
|
||||
#if defined(NETWORK)
|
||||
remoteCampaignActive = campaignId == "battle_zone_multiplayer";
|
||||
remoteInitialStateBound = false;
|
||||
if (remoteCampaignActive) {
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' selected; creating WebSocket client" << std::endl;
|
||||
// Use "ws://127.0.0.1:8080/" for a local development server.
|
||||
static const char* const kMultiplayerServerUrl =
|
||||
"wss://api.shadowoverbishkek.com/server001/";
|
||||
WebsocketEndpoint endpoint;
|
||||
if (!endpoint.parse(kMultiplayerServerUrl)) {
|
||||
logger() << "[network] Invalid multiplayer server URL: "
|
||||
<< kMultiplayerServerUrl << std::endl;
|
||||
}
|
||||
websocketGameClient = std::make_unique<WebsocketGameClient>(endpoint);
|
||||
remoteGameServerHost =
|
||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||
gameServer.use(*websocketGameClient);
|
||||
gameServerHost.use(*remoteGameServerHost);
|
||||
logger() << "[network] Remote game server routes installed"
|
||||
<< std::endl;
|
||||
} else {
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' uses the in-process server" << std::endl;
|
||||
gameServer.use(internalGameServer);
|
||||
gameServerHost.use(localGameServerHost);
|
||||
}
|
||||
#else
|
||||
remoteCampaignActive = false;
|
||||
remoteInitialStateBound = false;
|
||||
logger() << "[network] Networking was not compiled into this client; "
|
||||
"campaign '" << campaignId << "' uses the in-process server"
|
||||
<< std::endl;
|
||||
#endif
|
||||
gameServerHost.initializeCampaign(
|
||||
campaignId, campaign->questsPath, CONST_ZIP_FILE);
|
||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||
player = Location::createPlayer(renderer, audioPlayer.get());
|
||||
player = Location::createPlayer(
|
||||
renderer, audioPlayer.get(), server(),
|
||||
server().localPlayerId());
|
||||
if (player)
|
||||
player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
}
|
||||
|
||||
void Game::unloadCampaign() {
|
||||
unloadLocation(false);
|
||||
player.reset();
|
||||
loadedCampaignId.clear();
|
||||
gameState.storedLocationStates.clear();
|
||||
gameServerHost.clearCampaign();
|
||||
#if defined(NETWORK)
|
||||
gameServer.use(internalGameServer);
|
||||
gameServerHost.use(localGameServerHost);
|
||||
remoteGameServerHost.reset();
|
||||
websocketGameClient.reset();
|
||||
#endif
|
||||
remoteCampaignActive = false;
|
||||
remoteInitialStateBound = false;
|
||||
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||
}
|
||||
|
||||
void Game::loadLocation(const std::string& locationId) {
|
||||
const LocationConfig* config = activeCampaign().findLocation(locationId);
|
||||
if (!config) {
|
||||
std::cerr << "[campaign] Campaign '" << gameState.campaignId << "' has no location '"
|
||||
const auto state = stateSnapshot();
|
||||
std::cerr << "[campaign] Campaign '"
|
||||
<< (state ? state->campaignId : std::string()) << "' has no location '"
|
||||
<< locationId << "'" << std::endl;
|
||||
return;
|
||||
}
|
||||
SDL_PumpEvents();
|
||||
const bool initializeLocation = gameServerHost.prepareLocation(*config);
|
||||
std::shared_ptr<Location> loc = createLocation(*config);
|
||||
wireLocation(loc.get());
|
||||
if (shadowMap) loc->shadowMap = shadowMap;
|
||||
if (initializeLocation)
|
||||
loc->scriptEngine.callLocationInitCallback();
|
||||
// Back to how the player left it (or to the start state's snapshot of it).
|
||||
auto stored = gameState.storedLocationStates.find(locationId);
|
||||
if (stored != gameState.storedLocationStates.end()) loc->load(stored->second);
|
||||
gameState.locations[locationId] = loc;
|
||||
gameServerHost.bindRuntimeLocation(locationId, loc);
|
||||
}
|
||||
|
||||
void Game::unloadLocation(bool storeState) {
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
if (!storeState) continue;
|
||||
nlohmann::json locJson;
|
||||
loc->save(locJson);
|
||||
gameState.storedLocationStates[name] = std::move(locJson);
|
||||
}
|
||||
gameState.locations.clear();
|
||||
gameServerHost.clearRuntimeLocations(storeState);
|
||||
// Textures only the location's models and characters used are freed now.
|
||||
renderer.textureManager.UnloadUnused();
|
||||
}
|
||||
@@ -614,68 +650,46 @@ namespace FRG
|
||||
}
|
||||
// Runs from inside the current location's update(); the location itself is
|
||||
// released by a later loading step.
|
||||
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() {
|
||||
gameState.currentLocationName = destName;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
|
||||
server().submit(SetCurrentLocationCommand{destName});
|
||||
Location* loc = currentLocation();
|
||||
if (!loc) return;
|
||||
if (loc->player) {
|
||||
loc->player->state.position = destPos;
|
||||
loc->player->setTarget(destPos);
|
||||
loc->player->stopInPlace();
|
||||
loc->player->state.facingAngle = destRotY;
|
||||
loc->player->state.targetFacingAngle = destRotY;
|
||||
server().submit(SetCharacterPositionCommand{
|
||||
destName, loc->player->entityId(), destPos});
|
||||
server().submit(SetCharacterFacingAngleCommand{
|
||||
destName, loc->player->entityId(), destRotY, true});
|
||||
loc->stopCharacter(*loc->player);
|
||||
}
|
||||
loc->state.cameraAzimuth = destRotY;
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
loc->localState().cameraAzimuth = destRotY;
|
||||
loc->scriptEngine.callLocationEnterCallback();
|
||||
|
||||
//Reset taxi
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
loc->state.taxiArrivedCallbackCalled = false;
|
||||
gameState.taxiIsCalled = false;
|
||||
server().submit(ResetTaxiCommand{destName});
|
||||
server().submit(SetTaxiCalledCommand{false});
|
||||
|
||||
menuManager.onLocationChanged(destName);
|
||||
});
|
||||
}
|
||||
|
||||
void Game::performFullReset() {
|
||||
gameState.currentLocationName.clear();
|
||||
gameState.isDarklands = false;
|
||||
gameState.isNight = false;
|
||||
gameState.isDawn = false;
|
||||
gameState.playerHp = 200.f;
|
||||
gameState.playerMaxHp = 200.f;
|
||||
gameState.money = 5500;
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.globalFloats["player_hp"] = 200;
|
||||
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
gameState.tutorialPhoneChatScreenOpened = false;
|
||||
gameState.tutorialJournalScreenOpened = false;
|
||||
gameState.tutorialNeedOpenTaxiScreen = false;
|
||||
gameState.tutorialTaxiHintDone = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gameState.chatUnread[i] = true;
|
||||
gameState.chatPreviewMsg[i].clear();
|
||||
gameState.chatHistory[i].clear();
|
||||
}
|
||||
gameState.inventory.clear();
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
|
||||
void Game::performFullReset() {
|
||||
// Rebuild the loaded location from its config; the others have no stored state
|
||||
// any more, so they are built fresh when the player first goes there.
|
||||
gameState.storedLocationStates.clear();
|
||||
std::vector<std::string> loadedIds;
|
||||
for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name);
|
||||
std::vector<std::string> loadedIds =
|
||||
gameServerHost.loadedRuntimeLocationIds();
|
||||
gameServerHost.resetCampaign(
|
||||
activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
unloadLocation(false);
|
||||
for (const std::string& id : loadedIds) loadLocation(id);
|
||||
|
||||
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||
// Re-comment before shipping.
|
||||
//saveInitialState();
|
||||
saveInitialState();
|
||||
|
||||
}
|
||||
|
||||
void Game::performResetToInitialState() {
|
||||
@@ -698,15 +712,8 @@ namespace FRG
|
||||
if (!content.empty()) {
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
const std::string campaignId = gameState.campaignId;
|
||||
// GameState::load() merges into existing maps rather than replacing them,
|
||||
// so any Lua globals set during play that aren't in the pristine snapshot
|
||||
// must be cleared explicitly before loading it.
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.load(root); // applies to the loaded location in place and stores
|
||||
gameServerHost.load(root); // applies to the loaded location in place and stores
|
||||
// the other locations' states for when they load.
|
||||
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||
@@ -724,12 +731,13 @@ namespace FRG
|
||||
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
|
||||
void Game::applyTutorialSetting() {
|
||||
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
||||
gameState.tutorialStep = TutorialStep::Completed;
|
||||
server().submit(
|
||||
SetTutorialStepCommand{TutorialStep::Completed});
|
||||
}
|
||||
|
||||
void Game::saveInitialState() {
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
gameServerHost.save(root);
|
||||
// Written next to the executable; copy it over the campaign's start-state file.
|
||||
const std::string startStatePath = activeCampaign().startStatePath;
|
||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||
@@ -752,7 +760,7 @@ namespace FRG
|
||||
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
||||
params.npcsJsonPath = config.npcsJsonPath;
|
||||
params.dialoguesJsonPath = config.dialoguesJsonPath;
|
||||
params.navigationJsonPaths = config.navigationJsonPaths;
|
||||
//params.navigationJsonPaths = config.navigationJsonPaths;
|
||||
params.scriptPath = config.scriptPath;
|
||||
params.teleportsJsonPath = config.teleportsJsonPath;
|
||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||
@@ -760,27 +768,30 @@ namespace FRG
|
||||
params.playerPosition = config.playerPosition;
|
||||
params.player = player;
|
||||
|
||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
auto loc = std::make_shared<Location>(
|
||||
renderer, server(), config.id);
|
||||
gameServerHost.setupRuntimeLocation(
|
||||
config.id, loc, std::move(params), audioPlayer.get());
|
||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||
gameState.isNight = isNight;
|
||||
gameState.isDawn = isDawn;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
server().submit(SetWorldModeCommand{
|
||||
state->isDarklands, isNight, isDawn});
|
||||
};
|
||||
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
loc->requestReturnToMainMenu = [this]() {
|
||||
gameState.currentLocationName.clear();
|
||||
server().submit(SetCurrentLocationCommand{std::string()});
|
||||
menuManager.showMenuAfterGameCompleted();
|
||||
};
|
||||
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
for (auto& npc : loc->npcs) {
|
||||
if (npc && npc->state.canAttack) {
|
||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
auto state = npc ? npc->stateSnapshot() : nullptr;
|
||||
if (state && state->canAttack) {
|
||||
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
}
|
||||
}
|
||||
loc->requestDarklandsPlayBattleMusic = [this]() {
|
||||
@@ -792,26 +803,6 @@ namespace FRG
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
};
|
||||
|
||||
if (config.lockInteractiveObjectsDuringTutorial) {
|
||||
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||
loc->areInteractiveObjectsLocked = [this]() {
|
||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
||||
};
|
||||
}
|
||||
|
||||
if (config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||
loc->state.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
loc->state.taxiCarWaypoints = taxi.waypoints;
|
||||
loc->state.taxiCarInitialState.position = taxi.initialPosition;
|
||||
loc->state.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
loc->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
loc->state.taxiCarInitialState.velocity = 0.f;
|
||||
loc->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||
loc->state.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
}
|
||||
|
||||
return loc;
|
||||
}
|
||||
|
||||
@@ -826,20 +817,16 @@ namespace FRG
|
||||
teleportPlayer(destName, destPos, destRotY);
|
||||
};
|
||||
|
||||
// Share the global int store with all dialogue runtimes so flags set via
|
||||
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
|
||||
{
|
||||
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
||||
}
|
||||
|
||||
// Wire tutorial advance callbacks.
|
||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||
{
|
||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||
server().submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
|
||||
});
|
||||
loc->onPlayerFloorWalk = [this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::Walk);
|
||||
server().submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::Walk});
|
||||
menuManager.onPlayerStartedWalking();
|
||||
};
|
||||
}
|
||||
@@ -866,24 +853,34 @@ namespace FRG
|
||||
if (loc->player) {
|
||||
// Raw pointer: the player belongs to the location (and outlives it), so a
|
||||
// shared_ptr here would keep unloaded locations alive. Rewired on every load.
|
||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||
loc->player->onHpChanged = [this, loc](float hp, float maxHp, const std::string& attackerId) {
|
||||
menuManager.updateHealthBar(hp, maxHp);
|
||||
|
||||
if (attackerIndex != CharacterState::kNoTarget) {
|
||||
if (!attackerId.empty()) {
|
||||
//Someone attacked, cancel dialogue and agro the player
|
||||
|
||||
if (loc->dialogueSystem.isDialogueActive()) {
|
||||
loc->dialogueSystem.stopDialogue();
|
||||
}
|
||||
|
||||
if ((loc->player->state.attackTargetIndex == CharacterState::kNoTarget ||
|
||||
((loc->player->state.attackTargetIndex >= 0) &&
|
||||
(loc->npcs[loc->player->state.attackTargetIndex]->state.hp <= 0)))
|
||||
bool currentTargetDead = false;
|
||||
auto playerState = loc->player->stateSnapshot();
|
||||
if (!playerState) return;
|
||||
for (const auto& npc : loc->npcs) {
|
||||
if (npc && npc->entityId() == playerState->attackTargetId) {
|
||||
auto npcState = npc->stateSnapshot();
|
||||
currentTargetDead = !npcState || npcState->hp <= 0.f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((playerState->attackTargetId.empty() || currentTargetDead)
|
||||
&&
|
||||
loc->player->isMoving() == false
|
||||
&&
|
||||
loc->player->getHp() > 0) {
|
||||
loc->player->state.attackTargetIndex = attackerIndex;
|
||||
server().submit(SetCharacterTargetCommand{
|
||||
loc->locationId(),
|
||||
server().localPlayerId(), attackerId});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -905,7 +902,9 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
logger() << "Shadow map initialized" << std::endl;
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap = shadowMap;
|
||||
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||
loc->shadowMap = shadowMap;
|
||||
|
||||
}
|
||||
void Game::destroyShadowMapAndProparateToLocations()
|
||||
@@ -915,7 +914,9 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap.reset();
|
||||
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||
loc->shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
}
|
||||
@@ -988,16 +989,12 @@ namespace FRG
|
||||
Location* loc = currentLocation();
|
||||
if (loc)
|
||||
{
|
||||
// Sync global flags so Location's draw functions see them.
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
|
||||
if (gameState.isDarklands) {
|
||||
const auto state = stateSnapshot();
|
||||
if (state && state->isDarklands) {
|
||||
loc->drawGameDarklands();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
}
|
||||
else if (gameState.isNight) {
|
||||
else if (state && state->isNight) {
|
||||
loc->drawGameNight();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
}
|
||||
@@ -1142,7 +1139,7 @@ namespace FRG
|
||||
|
||||
int64_t Game::getSyncTimeMs() {
|
||||
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
return localNow;
|
||||
}
|
||||
|
||||
@@ -1151,21 +1148,15 @@ namespace FRG
|
||||
|
||||
if (lastTickCount == 0) {
|
||||
lastTickCount = getSyncTimeMs();
|
||||
|
||||
lastTickCount = (lastTickCount / 50) * 50;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
newTickCount = getSyncTimeMs();
|
||||
|
||||
newTickCount = (newTickCount / 50) * 50;
|
||||
|
||||
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
||||
|
||||
int64_t delta = newTickCount - lastTickCount;
|
||||
|
||||
lastTickCount = newTickCount;
|
||||
const int64_t now = getSyncTimeMs();
|
||||
const int64_t delta =
|
||||
(std::min)(now - lastTickCount,
|
||||
static_cast<int64_t>(CONST_MAX_TIME_INTERVAL));
|
||||
lastTickCount = now;
|
||||
if (delta > 0) {
|
||||
|
||||
updateDarklandsFlash(delta);
|
||||
menuManager.uiManager.update(static_cast<float>(delta));
|
||||
@@ -1175,14 +1166,115 @@ namespace FRG
|
||||
if (!menuManager.isMainMenuOpen())
|
||||
menuManager.update(static_cast<float>(delta));
|
||||
|
||||
if (loadingCompleted && currentLocation())
|
||||
if (loadingCompleted)
|
||||
{
|
||||
currentLocation()->update(delta);
|
||||
if (remoteCampaignActive) {
|
||||
// Remote authoritative time comes only from contiguous server ticks.
|
||||
gameServerHost.update(delta);
|
||||
#if defined(NETWORK)
|
||||
// The connection-assigned slot is known only after initial_state.
|
||||
// Always rebuild once: replica_.load() replaces authoritative
|
||||
// location state even when the assigned ID remains "player".
|
||||
if (websocketGameClient && websocketGameClient->initialized() &&
|
||||
(!remoteInitialStateBound || !player ||
|
||||
player->entityId() != server().localPlayerId())) {
|
||||
const auto assignedState = stateSnapshot();
|
||||
const std::string locationId = assignedState
|
||||
? assignedState->currentLocationId : std::string();
|
||||
unloadLocation(false);
|
||||
player = Location::createPlayer(
|
||||
renderer, audioPlayer.get(), server(),
|
||||
server().localPlayerId());
|
||||
if (player)
|
||||
player->onDeathAnimComplete =
|
||||
[this]() { startDarklandsTransition(); };
|
||||
if (!locationId.empty()) loadLocation(locationId);
|
||||
remoteInitialStateBound = true;
|
||||
logger() << "[network] Runtime rebound to assigned player '"
|
||||
<< server().localPlayerId() << "'" << std::endl;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
simulationAccumulatorMs += delta;
|
||||
while (simulationAccumulatorMs >= kSimulationTickMs) {
|
||||
gameServerHost.update(kSimulationTickMs);
|
||||
simulationAccumulatorMs -= kSimulationTickMs;
|
||||
}
|
||||
}
|
||||
if (Location* location = currentLocation())
|
||||
location->updatePresentation(delta);
|
||||
#if defined(NETWORK)
|
||||
if (remoteCampaignActive && websocketGameClient &&
|
||||
websocketGameClient->connectionFailed()) {
|
||||
logger() << "[network] Connection failed; returning to campaign selection"
|
||||
<< std::endl;
|
||||
unloadCampaign();
|
||||
menuManager.showMainMenu();
|
||||
menuManager.showCampaignScreen();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
processServerEvents();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void Game::processServerEvents()
|
||||
{
|
||||
for (GameServerEvent& event : server().drainEvents()) {
|
||||
std::visit([this](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, InventoryItemAddedEvent>) {
|
||||
menuManager.onItemPickedUp(value.itemId);
|
||||
const Item* item =
|
||||
ItemRegistry::instance().findById(value.itemId);
|
||||
if (item)
|
||||
menuManager.showToast(
|
||||
menuManager.toastTextureItemAdded, item->name);
|
||||
audioPlayer->playSoundAsync(
|
||||
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
|
||||
} else if constexpr (std::is_same_v<T, InventoryItemRemovedEvent>) {
|
||||
const Item* item =
|
||||
ItemRegistry::instance().findById(value.itemId);
|
||||
if (item)
|
||||
menuManager.showToast(
|
||||
menuManager.toastTextureItemRemoved, item->name);
|
||||
audioPlayer->playSoundAsync(
|
||||
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
|
||||
} else if constexpr (std::is_same_v<T, Quest::QuestEvent>) {
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
auto quest = std::find_if(
|
||||
state->quests.begin(), state->quests.end(),
|
||||
[&value](const Quest::QuestState& candidate) {
|
||||
return candidate.definition.id == value.questId;
|
||||
});
|
||||
if (quest == state->quests.end()) return;
|
||||
switch (value.type) {
|
||||
case Quest::QuestEventType::Unlocked:
|
||||
menuManager.showToast(
|
||||
menuManager.toastTextureQuestNew,
|
||||
quest->definition.title);
|
||||
break;
|
||||
case Quest::QuestEventType::Completed:
|
||||
menuManager.showToast(
|
||||
menuManager.toastTextureQuestCompleted,
|
||||
quest->definition.title);
|
||||
break;
|
||||
case Quest::QuestEventType::Failed:
|
||||
menuManager.showToast(
|
||||
menuManager.toastTextureQuestFailed,
|
||||
quest->definition.title);
|
||||
break;
|
||||
case Quest::QuestEventType::ObjectiveCompleted:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, event);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::render() {
|
||||
FRG::CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
@@ -1228,6 +1320,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
void Game::update() {
|
||||
processServerEvents();
|
||||
SDL_Event event;
|
||||
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
|
||||
while (SDL_PollEvent(&event)) {
|
||||
@@ -1332,8 +1425,9 @@ namespace FRG
|
||||
}
|
||||
else if (event.button.button == SDL_BUTTON_RIGHT
|
||||
&& event.type == SDL_MOUSEBUTTONUP
|
||||
&& editorMode == EditorMode::Navigation
|
||||
&& currentLocation()) {
|
||||
&& currentLocation()
|
||||
&& currentLocation()->editorMode == EditorMode::Navigation
|
||||
) {
|
||||
currentLocation()->editor.handleRightClick();
|
||||
}
|
||||
}
|
||||
@@ -1361,7 +1455,8 @@ namespace FRG
|
||||
}
|
||||
//logger() << "Current zoom: " << Environment::zoom << std::endl;
|
||||
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
|
||||
gameState.completeTutorialStep(TutorialStep::Zoom);
|
||||
server().submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::Zoom});
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
|
||||
@@ -1378,15 +1473,29 @@ namespace FRG
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_1)
|
||||
{
|
||||
startNightTransition();
|
||||
currentLocation()->switchNavigation(0);
|
||||
//startNightTransition();
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_2)
|
||||
{
|
||||
startDarklandsTransition();
|
||||
currentLocation()->switchNavigation(1);
|
||||
//startDarklandsTransition();
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_3)
|
||||
{
|
||||
logger() << "Player position: " << currentLocation()->player->state.position.transpose() << std::endl;
|
||||
currentLocation()->switchNavigation(2);
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_n)
|
||||
{
|
||||
|
||||
if (currentLocation()->editorMode == EditorMode::None)
|
||||
{
|
||||
currentLocation()->editorMode = EditorMode::Navigation;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLocation()->editorMode = EditorMode::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1434,20 +1543,6 @@ namespace FRG
|
||||
return n;
|
||||
}
|
||||
|
||||
void Game::activateSlowMoEffect() {
|
||||
if (!currentLocation()) return;
|
||||
|
||||
if (currentLocation()->player) {
|
||||
currentLocation()->player->state.slowMoTimeRemaining = currentLocation()->player->state.slowMoActiveTime;
|
||||
}
|
||||
|
||||
for (auto& npc : currentLocation()->npcs) {
|
||||
if (npc) {
|
||||
npc->state.slowMoTimeRemaining = npc->state.slowMoActiveTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Game::enterCameraDragMode(int eventX, int eventY)
|
||||
{
|
||||
cameraDragging = true;
|
||||
@@ -1460,8 +1555,9 @@ namespace FRG
|
||||
currentLocation()->lastMouseY = eventY;
|
||||
|
||||
// Snapshot current angles so we can measure how far the user rotates.
|
||||
dragStartAzimuth = currentLocation()->state.cameraAzimuth;
|
||||
dragStartInclination = currentLocation()->state.cameraInclination;
|
||||
const LocationLocalState& state = currentLocation()->localState();
|
||||
dragStartAzimuth = state.cameraAzimuth;
|
||||
dragStartInclination = state.cameraInclination;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1521,7 +1617,8 @@ namespace FRG
|
||||
|
||||
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
|
||||
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
|
||||
gameState.completeTutorialStep(TutorialStep::Zoom);
|
||||
server().submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::Zoom});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1664,12 +1761,16 @@ namespace FRG
|
||||
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
|
||||
// not just nudged a single axis by accident.
|
||||
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
||||
const auto game = stateSnapshot();
|
||||
if (cameraDragging
|
||||
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
|
||||
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
|
||||
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
|
||||
&& game
|
||||
&& game->tutorialStep == TutorialStep::RotateCamera) {
|
||||
const LocationLocalState& state = currentLocation()->localState();
|
||||
float deltaAz = std::abs(state.cameraAzimuth - dragStartAzimuth);
|
||||
float deltaInc = std::abs(state.cameraInclination - dragStartInclination);
|
||||
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
|
||||
gameState.completeTutorialStep(TutorialStep::RotateCamera);
|
||||
server().submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1677,13 +1778,14 @@ namespace FRG
|
||||
|
||||
bool Game::startDarklandsTransition()
|
||||
{
|
||||
if (!gameState.isNight)
|
||||
const auto state = stateSnapshot();
|
||||
if (!state || !state->isNight)
|
||||
{
|
||||
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gameState.isDawn)
|
||||
if (state->isDawn)
|
||||
{
|
||||
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
|
||||
return false;
|
||||
@@ -1717,11 +1819,13 @@ namespace FRG
|
||||
if (darklandsFlashFadingIn) {
|
||||
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
|
||||
if (darklandsFlashAlpha >= 1.0f) {
|
||||
const auto before = stateSnapshot();
|
||||
if (!before) return;
|
||||
if (isNightTransition) {
|
||||
gameState.isNight = !gameState.isNight;
|
||||
server().submit(SetWorldModeCommand{
|
||||
before->isDarklands, !before->isNight, before->isDawn});
|
||||
if (currentLocation())
|
||||
{
|
||||
currentLocation()->state.isNight = gameState.isNight;
|
||||
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
|
||||
}
|
||||
|
||||
@@ -1729,12 +1833,15 @@ namespace FRG
|
||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||
|
||||
} else {
|
||||
gameState.isDarklands = !gameState.isDarklands;
|
||||
server().submit(SetWorldModeCommand{
|
||||
!before->isDarklands, before->isNight, before->isDawn});
|
||||
const auto after = stateSnapshot();
|
||||
if (!after) return;
|
||||
|
||||
// Change HUD
|
||||
menuManager.setDarklandsMode(gameState.isDarklands);
|
||||
menuManager.setDarklandsMode(after->isDarklands);
|
||||
|
||||
if (gameState.isDarklands)
|
||||
if (after->isDarklands)
|
||||
{
|
||||
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
@@ -1746,8 +1853,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
if (currentLocation()) {
|
||||
currentLocation()->state.isDarklands = gameState.isDarklands;
|
||||
if (gameState.isDarklands)
|
||||
if (after->isDarklands)
|
||||
currentLocation()->scriptEngine.callDarklandsEnterCallback();
|
||||
else
|
||||
currentLocation()->scriptEngine.callDarklandsExitCallback();
|
||||
@@ -1816,11 +1922,18 @@ namespace FRG
|
||||
if (content.empty()) return {};
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
if (root.value("persistenceVersion", -1) !=
|
||||
kSinglePlayerPersistenceVersion) return {};
|
||||
const auto& networkState = root.at("networkState");
|
||||
if (networkState.value("schemaVersion", -1) !=
|
||||
GameState::kSchemaVersion) return {};
|
||||
SaveSlotInfo info;
|
||||
info.empty = false;
|
||||
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
info.locationName = root.value("currentLocationName", "");
|
||||
info.savedAt = root.value("savedAt", "");
|
||||
info.campaignId =
|
||||
networkState.at("campaignId").get<std::string>();
|
||||
info.locationName =
|
||||
networkState.at("currentLocationId").get<std::string>();
|
||||
info.savedAt = networkState.value("savedAt", "");
|
||||
return info;
|
||||
} catch (...) {
|
||||
return {};
|
||||
@@ -1830,7 +1943,7 @@ namespace FRG
|
||||
void Game::saveGame(int slot)
|
||||
{
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
gameServerHost.save(root);
|
||||
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
||||
|
||||
/*std::ofstream file(path);
|
||||
@@ -1858,11 +1971,26 @@ namespace FRG
|
||||
return;
|
||||
}
|
||||
|
||||
// The save's campaign and location may not be loaded yet; the state is applied once they are.
|
||||
const std::string campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
const std::string locationId = root.value("currentLocationName", std::string());
|
||||
if (root.value("persistenceVersion", -1) !=
|
||||
kSinglePlayerPersistenceVersion ||
|
||||
!root.contains("networkState") ||
|
||||
root["networkState"].value("schemaVersion", -1) !=
|
||||
GameState::kSchemaVersion) {
|
||||
std::cerr << "[save] Unsupported save schema in " << path << std::endl;
|
||||
return;
|
||||
}
|
||||
const auto& networkState = root["networkState"];
|
||||
const std::string campaignId =
|
||||
networkState.value("campaignId", std::string());
|
||||
const std::string locationId =
|
||||
networkState.value("currentLocationId", std::string());
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign || !campaign->findLocation(locationId)) {
|
||||
std::cerr << "[save] Unknown campaign/location in " << path << std::endl;
|
||||
return;
|
||||
}
|
||||
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||
gameState.load(root);
|
||||
gameServerHost.load(root);
|
||||
playMusicForLoadedGame();
|
||||
menuManager.enterGameplay(true);
|
||||
});
|
||||
@@ -1870,19 +1998,21 @@ namespace FRG
|
||||
|
||||
void Game::playMusicForLoadedGame()
|
||||
{
|
||||
if (gameState.isDarklands)
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
if (state->isDarklands)
|
||||
{
|
||||
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
}
|
||||
else if (gameState.isNight)
|
||||
else if (state->isNight)
|
||||
{
|
||||
audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameState.currentLocationName == "location_dorm") {
|
||||
if (state->currentLocationId == "location_dorm") {
|
||||
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
||||
}
|
||||
|
||||
+37
-17
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
#include "Character.h"
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
@@ -21,7 +21,14 @@
|
||||
#include <unordered_set>
|
||||
#include "Location.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "GameState.h"
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include "game_server/LocalGameServerHost.h"
|
||||
#include "game_server/GameServerRouter.h"
|
||||
#include "game_server/GameServerHostRouter.h"
|
||||
#if defined(NETWORK)
|
||||
#include "game_server/WebsocketGameClient.h"
|
||||
#include "game_server/RemoteGameServerHost.h"
|
||||
#endif
|
||||
namespace FRG {
|
||||
|
||||
struct LocationConfig;
|
||||
@@ -73,9 +80,7 @@ namespace FRG {
|
||||
// content reload, don't have a meaningful denominator, so the bar is hidden.
|
||||
bool showLoadingProgressBar = true;
|
||||
|
||||
GameState gameState;
|
||||
|
||||
EditorMode editorMode = EditorMode::None;
|
||||
//EditorMode editorMode = EditorMode::None;
|
||||
|
||||
// Returns false if a transition is already in progress.
|
||||
bool startDarklandsTransition();
|
||||
@@ -83,11 +88,25 @@ namespace FRG {
|
||||
|
||||
std::unique_ptr<AudioPlayerAsync> audioPlayer;
|
||||
|
||||
MenuManager menuManager;
|
||||
|
||||
void activateSlowMoEffect();
|
||||
private:
|
||||
InternalGameServer internalGameServer;
|
||||
LocalGameServerHost localGameServerHost;
|
||||
#if defined(NETWORK)
|
||||
std::unique_ptr<WebsocketGameClient> websocketGameClient;
|
||||
std::unique_ptr<RemoteGameServerHost> remoteGameServerHost;
|
||||
#endif
|
||||
GameServerRouter gameServer;
|
||||
GameServerHostRouter gameServerHost;
|
||||
bool remoteCampaignActive = false;
|
||||
bool remoteInitialStateBound = false;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
public:
|
||||
IGameServer& server() { return gameServer; }
|
||||
const IGameServer& server() const { return gameServer; }
|
||||
|
||||
MenuManager menuManager;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
static Game* s_instance;
|
||||
#endif
|
||||
|
||||
@@ -123,15 +142,16 @@ namespace FRG {
|
||||
|
||||
|
||||
Location* currentLocation() const;
|
||||
std::shared_ptr<const GameSnapshot> stateSnapshot() const {
|
||||
return gameServer.snapshot();
|
||||
}
|
||||
|
||||
void saveGame(int slot);
|
||||
void loadGame(int slot);
|
||||
void playMusicForLoadedGame();
|
||||
SaveSlotInfo readSlotInfo(int slot) const;
|
||||
// Loading model: one campaign at a time, and within it only the location the player
|
||||
// is in (gameState.locations). Leaving a location stores its state in
|
||||
// gameState.storedLocationStates and releases it; the player is created once per
|
||||
// campaign and shared by its locations.
|
||||
// Loading model: one campaign at a time. InternalGameServer privately owns the
|
||||
// one live Location while GameState retains pure value state for every location.
|
||||
std::string loadedCampaignId;
|
||||
std::shared_ptr<Character> player;
|
||||
|
||||
@@ -146,7 +166,7 @@ namespace FRG {
|
||||
void unloadCampaign();
|
||||
// Builds the campaign's location and restores its stored state, if any.
|
||||
void loadLocation(const std::string& locationId);
|
||||
// Releases the loaded location, keeping its state in storedLocationStates if asked.
|
||||
// Releases the loaded runtime location, retaining its value state when requested.
|
||||
void unloadLocation(bool storeState);
|
||||
// Teleport zones: moves the player to another location of the campaign.
|
||||
void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY);
|
||||
@@ -174,6 +194,7 @@ namespace FRG {
|
||||
|
||||
int64_t getSyncTimeMs();
|
||||
void processTickCount();
|
||||
void processServerEvents();
|
||||
void drawScene();
|
||||
void drawUI();
|
||||
|
||||
@@ -191,7 +212,7 @@ namespace FRG {
|
||||
void endPinch();
|
||||
int countNonUiPointers() const;
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#ifdef __EMSCRIPTEN__
|
||||
static void onResourcesZipLoaded(unsigned handle, void* userData, const char* filename);
|
||||
static void onResourcesZipError(unsigned handle, void* userData, int httpStatus);
|
||||
static void onResourcesZipProgress(unsigned handle, void* userData, int percentComplete);
|
||||
@@ -230,13 +251,12 @@ namespace FRG {
|
||||
*/
|
||||
//void drawMobilePortraitOverlay();
|
||||
|
||||
int64_t newTickCount;
|
||||
int64_t lastTickCount;
|
||||
int64_t simulationAccumulatorMs = 0;
|
||||
|
||||
int lastDrawableW = -1;
|
||||
int lastDrawableH = -1;
|
||||
|
||||
static const size_t CONST_TIMER_INTERVAL = 10;
|
||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||
};
|
||||
|
||||
|
||||
+76
-46
@@ -1,24 +1,12 @@
|
||||
#include "GameState.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Saves made before the tutorial rework kept the dorm and the university tutorials
|
||||
// as two state machines (tutorialStep 0..6 + uniIntTutorialState 0..4); map them
|
||||
// onto the single TutorialStep sequence.
|
||||
static TutorialStep migrateLegacyTutorialStep(int dormStep, int uniIntState)
|
||||
{
|
||||
if (dormStep <= 4) return static_cast<TutorialStep>(dormStep); // Step0..Step4 kept their values
|
||||
switch (uniIntState) {
|
||||
case 2: return TutorialStep::DarklandsHealth; // DarklandsActive
|
||||
case 3: return TutorialStep::DarklandsAttack; // DarklandsStep13
|
||||
case 4: return TutorialStep::Completed; // DarklandsFull
|
||||
default: return TutorialStep::EnterDarklands; // Step10 / Step11
|
||||
}
|
||||
}
|
||||
|
||||
bool GameState::completeTutorialStep(TutorialStep step)
|
||||
{
|
||||
if (tutorialStep != step || step == TutorialStep::Completed) return false;
|
||||
@@ -26,8 +14,29 @@ bool GameState::completeTutorialStep(TutorialStep step)
|
||||
return true;
|
||||
}
|
||||
|
||||
CharacterState* GameState::findCharacter(
|
||||
const std::string& locationId, const std::string& characterId)
|
||||
{
|
||||
auto player = players.find(characterId);
|
||||
if (player != players.end() && player->second) return player->second.get();
|
||||
auto location = locations.find(locationId);
|
||||
return location == locations.end()
|
||||
? nullptr : location->second.findCharacter(characterId, players);
|
||||
}
|
||||
|
||||
const CharacterState* GameState::findCharacter(
|
||||
const std::string& locationId, const std::string& characterId) const
|
||||
{
|
||||
auto player = players.find(characterId);
|
||||
if (player != players.end() && player->second) return player->second.get();
|
||||
auto location = locations.find(locationId);
|
||||
return location == locations.end()
|
||||
? nullptr : location->second.findCharacter(characterId, players);
|
||||
}
|
||||
|
||||
void GameState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["schemaVersion"] = kSchemaVersion;
|
||||
// Timestamp
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
@@ -37,13 +46,13 @@ void GameState::save(nlohmann::json& out) const
|
||||
|
||||
// --- World ---
|
||||
out["campaignId"] = campaignId;
|
||||
out["currentLocationName"] = currentLocationName;
|
||||
out["currentLocationId"] = currentLocationName;
|
||||
out["isDarklands"] = isDarklands;
|
||||
out["isNight"] = isNight;
|
||||
out["isDawn"] = isDawn;
|
||||
|
||||
// --- Player resources ---
|
||||
out["playerHp"] = playerHp;
|
||||
//out["playerHp"] = playerHp;
|
||||
out["playerMaxHp"] = playerMaxHp;
|
||||
out["money"] = money;
|
||||
|
||||
@@ -87,16 +96,21 @@ void GameState::save(nlohmann::json& out) const
|
||||
questJournal.save(questJson);
|
||||
out["questJournal"] = std::move(questJson);
|
||||
|
||||
// --- Players ---
|
||||
out["players"] = nlohmann::json::object();
|
||||
for (const auto& [id, player] : players) {
|
||||
if (!player) continue;
|
||||
nlohmann::json playerJson;
|
||||
player->save(playerJson);
|
||||
out["players"][id] = std::move(playerJson);
|
||||
}
|
||||
|
||||
// --- Locations ---
|
||||
// Unloaded locations keep their last stored state; loaded ones are saved live.
|
||||
nlohmann::json locsJson = nlohmann::json::object();
|
||||
for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored;
|
||||
for (const auto& [name, loc] : locations) {
|
||||
if (loc) {
|
||||
nlohmann::json locJson;
|
||||
loc->save(locJson);
|
||||
locsJson[name] = std::move(locJson);
|
||||
}
|
||||
for (const auto& [name, location] : locations) {
|
||||
nlohmann::json locJson;
|
||||
location.save(locJson);
|
||||
locsJson[name] = std::move(locJson);
|
||||
}
|
||||
out["locations"] = std::move(locsJson);
|
||||
|
||||
@@ -105,12 +119,16 @@ void GameState::save(nlohmann::json& out) const
|
||||
|
||||
void GameState::load(const nlohmann::json& in)
|
||||
{
|
||||
const int schemaVersion = in.value("schemaVersion", -1);
|
||||
if (schemaVersion != kSchemaVersion)
|
||||
throw std::runtime_error(
|
||||
"Unsupported game-state schema version: " +
|
||||
std::to_string(schemaVersion));
|
||||
//logger() << "GameState::load step 1" << std::endl;
|
||||
|
||||
// --- World ---
|
||||
// Literal default: saves made before campaigns existed belong to the main game.
|
||||
campaignId = in.value("campaignId", std::string("main"));
|
||||
currentLocationName = in.value("currentLocationName", currentLocationName);
|
||||
campaignId = in.at("campaignId").get<std::string>();
|
||||
currentLocationName = in.at("currentLocationId").get<std::string>();
|
||||
isDarklands = in.value("isDarklands", isDarklands);
|
||||
isNight = in.value("isNight", isNight);
|
||||
isDawn = in.value("isDawn", isDawn);
|
||||
@@ -119,7 +137,7 @@ void GameState::load(const nlohmann::json& in)
|
||||
|
||||
|
||||
// --- Player resources ---
|
||||
playerHp = in.value("playerHp", playerHp);
|
||||
//playerHp = in.value("playerHp", playerHp);
|
||||
playerMaxHp = in.value("playerMaxHp", playerMaxHp);
|
||||
money = in.value("money", money);
|
||||
|
||||
@@ -142,11 +160,8 @@ void GameState::load(const nlohmann::json& in)
|
||||
//logger() << "GameState::load step 5" << std::endl;
|
||||
|
||||
// --- Tutorial ---
|
||||
if (in.contains("uniIntTutorialState")) {
|
||||
tutorialStep = migrateLegacyTutorialStep(in.value("tutorialStep", 0), in["uniIntTutorialState"].get<int>());
|
||||
} else {
|
||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
||||
}
|
||||
tutorialStep = static_cast<TutorialStep>(
|
||||
in.at("tutorialStep").get<int>());
|
||||
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
||||
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
||||
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
||||
@@ -193,20 +208,35 @@ void GameState::load(const nlohmann::json& in)
|
||||
|
||||
logger() << "GameState::load step 10" << std::endl;
|
||||
|
||||
// --- Locations ---
|
||||
// Every location's state is stored (applied when the location loads); the ones
|
||||
// already loaded get it right away.
|
||||
storedLocationStates.clear();
|
||||
if (in.contains("locations") && in["locations"].is_object()) {
|
||||
for (const auto& [name, locJson] : in["locations"].items()) {
|
||||
//logger() << "GameState::load step 10.1: " << name << std::endl;
|
||||
storedLocationStates[name] = locJson;
|
||||
// --- Players ---
|
||||
const auto& playerValues = in.at("players");
|
||||
if (!playerValues.is_object())
|
||||
throw std::runtime_error("Game state players must be an object");
|
||||
for (const auto& [id, value] : playerValues.items()) {
|
||||
auto& player = players[id];
|
||||
if (!player) player = std::make_shared<CharacterState>();
|
||||
player->load(value);
|
||||
player->configureAsPlayer();
|
||||
}
|
||||
for (auto it = players.begin(); it != players.end();) {
|
||||
if (!playerValues.contains(it->first)) it = players.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
|
||||
auto it = locations.find(name);
|
||||
if (it != locations.end() && it->second) {
|
||||
it->second->load(locJson);
|
||||
}
|
||||
}
|
||||
// --- Locations ---
|
||||
const auto& locationValues = in.at("locations");
|
||||
if (!locationValues.is_object())
|
||||
throw std::runtime_error("Game state locations must be an object");
|
||||
if (!currentLocationName.empty() &&
|
||||
!locationValues.contains(currentLocationName))
|
||||
throw std::runtime_error(
|
||||
"Game state does not contain current location '" +
|
||||
currentLocationName + "'");
|
||||
for (const auto& [id, value] : locationValues.items())
|
||||
locations[id].load(value);
|
||||
for (auto it = locations.begin(); it != locations.end();) {
|
||||
if (!locationValues.contains(it->first)) it = locations.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
//logger() << "GameState::load step 11" << std::endl;
|
||||
|
||||
|
||||
+14
-9
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include "items/Item.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "Location.h"
|
||||
#include "LocationGameState.h"
|
||||
#include "ISaveable.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <unordered_map>
|
||||
@@ -32,17 +32,17 @@ struct StoredChatMessage {
|
||||
bool incoming;
|
||||
};
|
||||
|
||||
using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
||||
|
||||
struct GameState : public ISaveable {
|
||||
static constexpr int kSchemaVersion = 5;
|
||||
|
||||
// --- World ---
|
||||
// Campaign being played (see CampaignRegistry).
|
||||
std::string campaignId = "main";
|
||||
// The campaign's loaded location(s); only the one the player is in is kept loaded.
|
||||
LocationMap locations;
|
||||
// Saved state (Location::save) of the campaign's locations that are not loaded,
|
||||
// applied when one is loaded again. save()/load() cover both maps.
|
||||
std::unordered_map<std::string, nlohmann::json> storedLocationStates;
|
||||
// Pure authoritative location data. Live Location/render/script instances are
|
||||
// private runtime storage in InternalGameServer and never enter GameState.
|
||||
std::unordered_map<std::string, LocationGameState> locations;
|
||||
// Players are campaign-level entities and remain stable across locations.
|
||||
PlayerStateMap players;
|
||||
std::string currentLocationName;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
@@ -50,7 +50,7 @@ struct GameState : public ISaveable {
|
||||
|
||||
// --- Player resources ---
|
||||
Inventory inventory;
|
||||
float playerHp = 200.f;
|
||||
//float playerHp = 200.f;
|
||||
float playerMaxHp = 200.f;
|
||||
int money = 5500;
|
||||
|
||||
@@ -73,6 +73,11 @@ struct GameState : public ISaveable {
|
||||
// Moves to the next step if `step` is the current one; returns whether it did.
|
||||
bool completeTutorialStep(TutorialStep step);
|
||||
|
||||
CharacterState* findCharacter(
|
||||
const std::string& locationId, const std::string& characterId);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& locationId, const std::string& characterId) const;
|
||||
|
||||
// --- Phone / chat ---
|
||||
bool chatUnread[3] = { true, true, true };
|
||||
std::string chatPreviewMsg[3];
|
||||
|
||||
+810
-881
File diff suppressed because it is too large.
Load diff
+84
-47
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
@@ -16,13 +16,16 @@
|
||||
#include "TeleportZone.h"
|
||||
#include "LocationEditor.h"
|
||||
#include "LocationState.h"
|
||||
#include "ISaveable.h"
|
||||
#include "LocationGameState.h"
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include "Character.h"
|
||||
|
||||
namespace FRG
|
||||
{
|
||||
class IGameServer;
|
||||
|
||||
struct PointLight
|
||||
{
|
||||
@@ -35,7 +38,7 @@ namespace FRG
|
||||
// Rectangular limits — keep light confined to a room-sized AABB centred on the light
|
||||
float limitX = 99999.f; // shader fragment limit: half-width in world X
|
||||
float limitY = 99999.f; // shader fragment limit: half-width in world Z
|
||||
float autoLightLimitX = -1.0f; // activation rect half-width X; <0 = use legacy nearest-light logic
|
||||
float autoLightLimitX = -1.0f; // activation rect half-width X; <0 = use nearest-light logic
|
||||
float autoLightLimitY = -1.0f; // activation rect half-width Z
|
||||
};
|
||||
|
||||
@@ -46,16 +49,17 @@ namespace FRG
|
||||
float radius = 1.5f;
|
||||
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
|
||||
bool enabled = true;
|
||||
bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationState)
|
||||
bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationNetworkState)
|
||||
};
|
||||
|
||||
struct LocationSetup
|
||||
{
|
||||
std::shared_ptr<const LocationDefinition> definition;
|
||||
std::string gameObjectsJsonPath;
|
||||
std::string interactiveObjectsJsonPath;
|
||||
std::string npcsJsonPath;
|
||||
std::string dialoguesJsonPath;
|
||||
std::vector<std::string> navigationJsonPaths;
|
||||
//std::vector<std::string> navigationJsonPaths;
|
||||
std::string scriptPath;
|
||||
std::string teleportsJsonPath;
|
||||
std::string triggerZonesJsonPath;
|
||||
@@ -64,17 +68,29 @@ namespace FRG
|
||||
// Player shared by all locations of a campaign (see Location::createPlayer);
|
||||
// setup() creates a private one when this is null.
|
||||
std::shared_ptr<Character> player;
|
||||
LocationGameState* gameState = nullptr;
|
||||
std::shared_ptr<LocationLocalState> localState;
|
||||
};
|
||||
|
||||
class Location : public ISaveable
|
||||
class Location
|
||||
{
|
||||
public:
|
||||
Location(Renderer& iRenderer, Inventory& iInventory);
|
||||
Location(Renderer& iRenderer, IGameServer& server,
|
||||
std::string locationId);
|
||||
// Detaches the (shared, longer-lived) player from this location.
|
||||
~Location();
|
||||
|
||||
// ---- All serialisable runtime state ----
|
||||
LocationState state;
|
||||
const std::string& locationId() const { return locationId_; }
|
||||
IGameServer& gameServer() { return gameServer_; }
|
||||
std::shared_ptr<const LocationNetworkState> stateSnapshot() const;
|
||||
LocationLocalState& localState() { return *localState_; }
|
||||
const LocationLocalState& localState() const { return *localState_; }
|
||||
bool isDarklands() const;
|
||||
bool isNight() const;
|
||||
bool isDawn() const;
|
||||
int globalInt(const std::string& key) const;
|
||||
float globalFloat(const std::string& key) const;
|
||||
void setGlobalInt(const std::string& key, int value);
|
||||
void setGlobalFloat(const std::string& key, float value);
|
||||
|
||||
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
||||
|
||||
@@ -88,10 +104,8 @@ namespace FRG
|
||||
std::vector<std::unique_ptr<Character>> npcs;
|
||||
|
||||
// Builds the player character (models, animations, weapon, textures).
|
||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer);
|
||||
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
PathFinder* navigation = nullptr;
|
||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
|
||||
IGameServer& server, const std::string& playerId);
|
||||
|
||||
std::shared_ptr<ShadowMap> shadowMap;
|
||||
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
|
||||
@@ -114,10 +128,6 @@ namespace FRG
|
||||
|
||||
std::function<void()> onPlayerTaxiRequired;
|
||||
|
||||
// Optional, set by Game. While it returns true the player can't pick up /
|
||||
// use interactive objects (the dorm tutorial unlocks them step by step).
|
||||
std::function<bool()> areInteractiveObjectsLocked;
|
||||
|
||||
// Set by Game after setup(). Lua and onDeathAnimComplete call this to
|
||||
// request the transition without coupling Location to Game directly.
|
||||
std::function<bool()> requestDarklandsTransition;
|
||||
@@ -140,7 +150,7 @@ namespace FRG
|
||||
EditorMode editorMode = EditorMode::None;
|
||||
LocationEditor editor;
|
||||
|
||||
std::vector<std::string> navigationMapPaths;
|
||||
//std::vector<std::string> navigationMapPaths;
|
||||
|
||||
// Set by Game when the user's primary pointer (left mouse / single touch)
|
||||
// has crossed the tap-vs-drag threshold and is now rotating the camera.
|
||||
@@ -161,9 +171,31 @@ namespace FRG
|
||||
|
||||
|
||||
|
||||
void setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer);
|
||||
void setupNavigation(const std::vector<std::string>& paths);
|
||||
void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
|
||||
//void setupNavigation(const std::vector<std::string>& paths);
|
||||
bool switchNavigation(int index);
|
||||
void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
|
||||
std::string callbackName = "");
|
||||
void clearCharacterAttackTarget(Character& character);
|
||||
void stopCharacter(Character& character);
|
||||
void faceCharacter(Character& character, std::string targetId);
|
||||
void replanCharacter(Character& character);
|
||||
void resetCharacterAfterDeath(Character& character);
|
||||
void tryCharacterAttackCombo(Character& character);
|
||||
void setCharacterHp(Character& character, float hp);
|
||||
void refillCharacterHp(Character& character);
|
||||
bool hasInventoryItem(const std::string& itemId) const;
|
||||
std::size_t inventoryItemCount() const;
|
||||
void addInventoryItem(std::string itemId);
|
||||
void removeInventoryItem(std::string itemId);
|
||||
bool unlockQuest(const std::string& questId);
|
||||
bool completeQuest(const std::string& questId);
|
||||
bool failQuest(const std::string& questId);
|
||||
bool setQuestObjectiveCompleted(
|
||||
const std::string& questId, const std::string& objectiveId, bool completed);
|
||||
bool setQuestObjectiveVisible(
|
||||
const std::string& questId, const std::string& objectiveId, bool visible);
|
||||
bool setQuestActiveObjective(const std::string& questId, int objectiveIndex);
|
||||
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
||||
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
||||
|
||||
@@ -177,8 +209,8 @@ namespace FRG
|
||||
|
||||
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||
|
||||
void update(int64_t deltaMs);
|
||||
void updateNpcCar(int64_t deltaMs);
|
||||
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
||||
void updatePresentation(int64_t deltaMs);
|
||||
// Purely visual feedback for click-triggered player routes. Disabling it
|
||||
// also cancels an animation that is currently playing.
|
||||
void setPathVisualizationEnabled(bool enabled);
|
||||
@@ -194,26 +226,26 @@ namespace FRG
|
||||
void setDialogueFlag(const std::string& flag, int value);
|
||||
int getDialogueFlag(const std::string& flag) const;
|
||||
|
||||
void onTaxiCalled();
|
||||
|
||||
// ---- Save / Load ----
|
||||
// Serialises LocationState + all Character/InteractiveObject sub-states + Lua globals.
|
||||
void save(nlohmann::json& out) const override;
|
||||
// Restores state from JSON written by save.
|
||||
// Navigation pointer and triggerZone.playerInside are re-synced automatically.
|
||||
void load(const nlohmann::json& in) override;
|
||||
protected:
|
||||
friend class LocationEditor;
|
||||
friend class Game;
|
||||
friend class ScriptEngine;
|
||||
friend class LocalGameServerHost;
|
||||
friend class RemoteGameServerHost;
|
||||
Renderer& renderer;
|
||||
Inventory& inventory;
|
||||
AudioPlayerAsync* audioPlayer;
|
||||
|
||||
private:
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
std::shared_ptr<LocationLocalState> localState_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_;
|
||||
void synchronizeRemotePlayers();
|
||||
void drawRemotePlayerNames(
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
// ---- Pointer-to-index helpers ----
|
||||
InteractiveObject* getTargetInteractiveObject();
|
||||
TeleportZone* getTargetTeleportZone();
|
||||
int findInteractiveObjectIndex(const InteractiveObject* obj) const;
|
||||
int findTeleportZoneIndex(const TeleportZone* tz) const;
|
||||
TeleportZone* getTargetTeleportZone(const LocationNetworkState& state);
|
||||
|
||||
enum class InstancedTexture { None, Color, Darklands };
|
||||
|
||||
@@ -223,26 +255,31 @@ namespace FRG
|
||||
void drawInstancedStatics(const std::string& instancedShaderName, InstancedTexture textureKind,
|
||||
const std::function<void()>& setUniforms = {});
|
||||
|
||||
void resolveCharacterCollisions();
|
||||
void updateDynamicReplans(int64_t deltaMs);
|
||||
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
|
||||
void loadTriggerZones(const std::string& jsonPath, const char* zipFile);
|
||||
void loadPointLights(const std::string& jsonPath, const char* zipFile);
|
||||
void updateTriggerZones(const Eigen::Vector3f& playerPos);
|
||||
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
|
||||
int findNpcIndex(const Character* npc) const;
|
||||
void fireBumpCallbacks(Character* mover, Character* standing);
|
||||
void captureAuxiliaryState(LocationGameState& out) const;
|
||||
void applyAuxiliaryState(const LocationGameState& in);
|
||||
|
||||
PathVisualization pathVisualization;
|
||||
|
||||
std::unordered_map<Character*, Eigen::Vector3f> lastCharacterPositions;
|
||||
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;
|
||||
std::unordered_map<Character*, int> stuckFrameCounts;
|
||||
std::unordered_map<Character*, float> stuckPauseRemainingS;
|
||||
struct NpcCarPresentationSample {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float rotation = 0.f;
|
||||
float wheelAngle = 0.f;
|
||||
float steeringAngle = 0.f;
|
||||
};
|
||||
|
||||
NpcCarPresentationSample previousNpcCarPresentation_;
|
||||
NpcCarPresentationSample currentNpcCarPresentation_;
|
||||
std::chrono::steady_clock::time_point lastNpcCarPresentationUpdate_;
|
||||
bool hasNpcCarPresentationSample_ = false;
|
||||
|
||||
void captureNpcCarPresentation(const NpcCar& car);
|
||||
NpcCarPresentationSample interpolatedNpcCarPresentation(const NpcCar& car) const;
|
||||
|
||||
static constexpr float NPC_BUMP_CALLBACK_COOLDOWN = 5.0f;
|
||||
std::unordered_map<int, float> npcBumpedByPlayerCooldown;
|
||||
std::unordered_map<int, float> npcBumpsPlayerCooldown;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+45
-15
@@ -1,6 +1,7 @@
|
||||
#include "LocationEditor.h"
|
||||
#include "Location.h"
|
||||
#include "Character.h"
|
||||
#include "game_server/IGameServer.h"
|
||||
#include "utils/Utils.h"
|
||||
#include "render/OpenGlExtensions.h"
|
||||
#include "TextModel.h"
|
||||
@@ -24,10 +25,14 @@ namespace FRG
|
||||
void LocationEditor::buildNavMeshes()
|
||||
{
|
||||
navigationEditorNavMeshes.clear();
|
||||
if (!loc.navigation) return;
|
||||
const float y = loc.navigation->getFloorY() + 0.02f;
|
||||
auto state = loc.stateSnapshot();
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const PathFinder& navigation =
|
||||
state->navigationMaps[state->activeNavigationIndex];
|
||||
const float y = navigation.getFloorY() + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
for (const auto& obs : loc.navigation->getObstaclePolygons()) {
|
||||
for (const auto& obs : navigation.getObstaclePolygons()) {
|
||||
if (obs.polygon.size() < 3) continue;
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
||||
@@ -51,7 +56,11 @@ namespace FRG
|
||||
{
|
||||
VertexDataStruct data;
|
||||
const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
|
||||
const float y = loc.navigation ? loc.navigation->getFloorY() + 0.05f : 0.05f;
|
||||
auto state = loc.stateSnapshot();
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const float y =
|
||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f;
|
||||
const float s = 0.2f;
|
||||
|
||||
for (const auto& pt : navigationEditorPoints) {
|
||||
@@ -68,12 +77,19 @@ namespace FRG
|
||||
}
|
||||
|
||||
navigationEditorPointsMesh.data = std::move(data);
|
||||
navigationEditorPointsMesh.RefreshVBO();
|
||||
if (navigationEditorPointsMesh.data.PositionData.size() > 0)
|
||||
{
|
||||
navigationEditorPointsMesh.RefreshVBO();
|
||||
}
|
||||
}
|
||||
|
||||
void LocationEditor::drawPoints()
|
||||
{
|
||||
if (navigationEditorPoints.empty()) return;
|
||||
if (navigationEditorPointsMesh.data.PositionData.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
loc.renderer.shaderManager.PushShader("defaultColor");
|
||||
loc.renderer.SetMatrix();
|
||||
loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh);
|
||||
@@ -120,12 +136,17 @@ namespace FRG
|
||||
poly.polygon.emplace_back(pt.x(), pt.z());
|
||||
}
|
||||
|
||||
if (loc.navigation) loc.navigation->addObstaclePolygon(poly);
|
||||
loc.gameServer_.submit(AddNavigationObstacleCommand{
|
||||
loc.locationId_, poly.name, poly.polygon});
|
||||
logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
|
||||
<< poly.polygon.size() << " vertices\n";
|
||||
|
||||
{
|
||||
const float y = loc.navigation ? loc.navigation->getFloorY() + 0.02f : 0.02f;
|
||||
auto state = loc.stateSnapshot();
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const float y =
|
||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
||||
@@ -147,14 +168,21 @@ namespace FRG
|
||||
if (!std::filesystem::exists(baseName + ".json")) break;
|
||||
}
|
||||
|
||||
if (!loc.navigation) return;
|
||||
auto state = loc.stateSnapshot();
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
|
||||
std::cerr << "[NAV_EDITOR] No active navigation map to save\n";
|
||||
return;
|
||||
}
|
||||
const PathFinder& navigation =
|
||||
state->navigationMaps[state->activeNavigationIndex];
|
||||
|
||||
if (loc.navigation->saveConfig(baseName + ".json"))
|
||||
if (navigation.saveConfig(baseName + ".json"))
|
||||
logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
|
||||
else
|
||||
std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";
|
||||
|
||||
if (loc.navigation->saveGrid(baseName + ".txt"))
|
||||
if (navigation.saveGrid(baseName + ".txt"))
|
||||
logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
|
||||
else
|
||||
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
|
||||
@@ -162,8 +190,8 @@ namespace FRG
|
||||
|
||||
void LocationEditor::reload()
|
||||
{
|
||||
loc.setupNavigation(loc.navigationMapPaths);
|
||||
logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
||||
//loc.setupNavigation(loc.navigationMapPaths);
|
||||
//logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
||||
}
|
||||
|
||||
static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color)
|
||||
@@ -454,9 +482,11 @@ namespace FRG
|
||||
data.rotationX = 0.0f;
|
||||
data.rotationY = rotDist(rng);
|
||||
data.rotationZ = 0.0f;
|
||||
data.positionX = loc.player->state.position.x();
|
||||
data.positionY = loc.player->state.position.y();//-0.1;
|
||||
data.positionZ = loc.player->state.position.z();
|
||||
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
|
||||
if (!playerState) return;
|
||||
data.positionX = playerState->position.x();
|
||||
data.positionY = playerState->position.y();//-0.1;
|
||||
data.positionZ = playerState->position.z();
|
||||
data.scale = scaleDist(rng);
|
||||
|
||||
GameObjectData data2;
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
#include "LocationGameState.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
constexpr float NPC_TALK_DISTANCE = 1.35f;
|
||||
}
|
||||
|
||||
bool LocationGameState::setup(
|
||||
LocationDefinition content, const std::string& zipFile)
|
||||
{
|
||||
const bool initializeRuntimeFields = !hasPersistentState;
|
||||
if (!location) location = std::make_shared<LocationNetworkState>();
|
||||
if (location->navigationMaps.empty())
|
||||
location->initNavigation(content.config.navigationJsonPaths, zipFile);
|
||||
lockInteractiveObjectsDuringTutorial =
|
||||
content.config.lockInteractiveObjectsDuringTutorial;
|
||||
|
||||
if (content.config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *content.config.taxi;
|
||||
location->taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
location->taxiCarWaypoints = taxi.waypoints;
|
||||
location->taxiCarInitialState.position = taxi.initialPosition;
|
||||
location->taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
location->taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
location->taxiCarInitialState.velocity = 0.f;
|
||||
location->taxiCarInitialState.steeringAngle = 0.f;
|
||||
location->taxiCarInitialState.waypointReachRadius =
|
||||
taxi.waypointReachRadius;
|
||||
location->taxiCar.waypointReachRadius = taxi.waypointReachRadius;
|
||||
if (initializeRuntimeFields) resetTaxi();
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, bool> configuredNpcs;
|
||||
for (const NpcData& data : content.npcs) {
|
||||
configuredNpcs.emplace(data.id, true);
|
||||
CharacterState defaults = data.toCharacterState();
|
||||
loadCharacterAnimationMetadata(defaults, zipFile);
|
||||
auto& state = npcs[data.id];
|
||||
if (!state) {
|
||||
state = std::make_shared<CharacterState>(std::move(defaults));
|
||||
} else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(defaults);
|
||||
state->load(runtime);
|
||||
}
|
||||
}
|
||||
for (const auto& [id, state] : npcs)
|
||||
if (configuredNpcs.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown NPC '" + id + "'");
|
||||
|
||||
std::unordered_map<std::string, bool> configuredObjects;
|
||||
for (const InteractiveObjectData& data : content.interactiveObjects) {
|
||||
configuredObjects.emplace(data.id, true);
|
||||
InteractiveObjectState defaults = data.toInteractiveObjectState();
|
||||
auto& state = interactiveObjects[data.id];
|
||||
if (!state) {
|
||||
state =
|
||||
std::make_shared<InteractiveObjectState>(std::move(defaults));
|
||||
} else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(defaults);
|
||||
state->load(runtime);
|
||||
}
|
||||
}
|
||||
for (const auto& [id, state] : interactiveObjects)
|
||||
if (configuredObjects.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown interactive object '" +
|
||||
id + "'");
|
||||
|
||||
std::unordered_map<std::string, TriggerZoneGameState> newTriggers;
|
||||
for (const TriggerZoneDefinition& data : content.triggerZones) {
|
||||
auto existing = triggerZones.find(data.id);
|
||||
newTriggers[data.id] = existing != triggerZones.end()
|
||||
? existing->second
|
||||
: TriggerZoneGameState{data.enabled, false};
|
||||
}
|
||||
if (hasPersistentState && newTriggers.size() != triggerZones.size())
|
||||
throw std::runtime_error(
|
||||
"Saved trigger-zone IDs do not match location content");
|
||||
triggerZones = std::move(newTriggers);
|
||||
|
||||
std::unordered_map<std::string, bool> newTeleports;
|
||||
for (const TeleportDefinition& data : content.teleports) {
|
||||
auto existing = teleportZones.find(data.id);
|
||||
newTeleports[data.id] =
|
||||
existing != teleportZones.end() ? existing->second : data.active;
|
||||
}
|
||||
if (hasPersistentState && newTeleports.size() != teleportZones.size())
|
||||
throw std::runtime_error(
|
||||
"Saved teleport IDs do not match location content");
|
||||
teleportZones = std::move(newTeleports);
|
||||
|
||||
definition =
|
||||
std::make_shared<const LocationDefinition>(std::move(content));
|
||||
return initializeRuntimeFields;
|
||||
}
|
||||
|
||||
CharacterState* LocationGameState::findCharacter(
|
||||
const std::string& id, PlayerStateMap& players)
|
||||
{
|
||||
if (id.empty()) return nullptr;
|
||||
auto entity = players.find(id);
|
||||
if (entity != players.end() && entity->second) return entity->second.get();
|
||||
auto npc = npcs.find(id);
|
||||
if (npc != npcs.end() && npc->second) return npc->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterState* LocationGameState::findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const
|
||||
{
|
||||
if (id.empty()) return nullptr;
|
||||
auto entity = players.find(id);
|
||||
if (entity != players.end() && entity->second) return entity->second.get();
|
||||
auto npc = npcs.find(id);
|
||||
if (npc != npcs.end() && npc->second) return npc->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles(
|
||||
const std::string& requesterId,
|
||||
const PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId) const
|
||||
{
|
||||
std::vector<std::pair<std::string, const CharacterState*>> characters;
|
||||
characters.reserve(players.size() + npcs.size());
|
||||
for (const auto& [id, character] : players)
|
||||
if (character) characters.emplace_back(id, character.get());
|
||||
for (const auto& [id, character] : npcs)
|
||||
if (character) characters.emplace_back(id, character.get());
|
||||
std::sort(characters.begin(), characters.end(),
|
||||
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
|
||||
|
||||
std::vector<PathFinder::DynamicObstacle> obstacles;
|
||||
obstacles.reserve(characters.size());
|
||||
for (const auto& [id, character] : characters) {
|
||||
if (id == requesterId || !character->enabled || character->hp <= 0.f)
|
||||
continue;
|
||||
if (character->movedLastTick && id != forcedObstacleId) continue;
|
||||
obstacles.push_back({character->position, character->collisionRadius});
|
||||
}
|
||||
return obstacles;
|
||||
}
|
||||
|
||||
bool LocationGameState::setCharacterDestination(
|
||||
const std::string& id,
|
||||
const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId)
|
||||
{
|
||||
CharacterState* character = findCharacter(id, players);
|
||||
if (!character || !location) return false;
|
||||
character->setTarget(
|
||||
target, std::move(callbackName), *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocationGameState::replanCharacter(
|
||||
const std::string& id,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId)
|
||||
{
|
||||
CharacterState* character = findCharacter(id, players);
|
||||
if (!character || !location) return false;
|
||||
character->forceReplan(
|
||||
*location, id, collectPathObstacles(id, players, forcedObstacleId));
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocationGameState::startTaxi()
|
||||
{
|
||||
location->taxiCar.waypoints = location->taxiCarWaypoints;
|
||||
location->taxiCar.currentWaypoint = 0;
|
||||
location->taxiCar.mode = NpcCar::Mode::FOLLOW_WAYPOINTS;
|
||||
}
|
||||
|
||||
void LocationGameState::resetTaxi()
|
||||
{
|
||||
location->taxiCar = location->taxiCarInitialState;
|
||||
location->taxiArrivedCallbackCalled = false;
|
||||
}
|
||||
|
||||
bool LocationGameState::updateNpcCar(std::int64_t deltaMs)
|
||||
{
|
||||
const float dt = static_cast<float>(deltaMs) / 1000.0f;
|
||||
NpcCar& car = location->taxiCar;
|
||||
|
||||
Eigen::Vector3f target = car.position;
|
||||
float throttle = 0.f;
|
||||
bool hasTarget = false;
|
||||
bool waypointsOver = false;
|
||||
|
||||
if (car.mode == NpcCar::Mode::FOLLOW_WAYPOINTS) {
|
||||
if (!car.waypoints.empty()) {
|
||||
if (car.currentWaypoint >= car.waypoints.size())
|
||||
car.currentWaypoint = 0;
|
||||
|
||||
Eigen::Vector3f toTarget =
|
||||
car.waypoints[car.currentWaypoint] - car.position;
|
||||
toTarget.y() = 0.f;
|
||||
if (toTarget.norm() < car.waypointReachRadius) {
|
||||
++car.currentWaypoint;
|
||||
if (car.currentWaypoint == car.waypoints.size())
|
||||
waypointsOver = true;
|
||||
}
|
||||
|
||||
if (waypointsOver) {
|
||||
car.mode = NpcCar::Mode::NONE;
|
||||
} else {
|
||||
target = car.waypoints[car.currentWaypoint];
|
||||
throttle = 1.f;
|
||||
hasTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float targetHeading = car.rotation;
|
||||
if (hasTarget) {
|
||||
Eigen::Vector3f toTarget = target - car.position;
|
||||
toTarget.y() = 0.f;
|
||||
if (toTarget.squaredNorm() > 1e-4f)
|
||||
targetHeading = std::atan2(-toTarget.x(), -toTarget.z());
|
||||
}
|
||||
|
||||
float angleDiff = targetHeading - car.rotation;
|
||||
while (angleDiff > PI) angleDiff -= 2.f * PI;
|
||||
while (angleDiff < -PI) angleDiff += 2.f * PI;
|
||||
|
||||
const float turnIntent = (std::max)(-1.f, (std::min)(1.f, angleDiff * 2.f));
|
||||
const float targetSteer = turnIntent * NpcCar::maxSteerAngle;
|
||||
const float steerLerp = (std::min)(1.f, dt * 8.f);
|
||||
car.steeringAngle += (targetSteer - car.steeringAngle) * steerLerp;
|
||||
|
||||
if (throttle > 0.f) {
|
||||
car.velocity += NpcCar::acceleration * dt * throttle;
|
||||
} else {
|
||||
const float resistance = NpcCar::friction * dt;
|
||||
if (car.velocity > 0.f)
|
||||
car.velocity = (std::max)(0.f, car.velocity - resistance);
|
||||
else if (car.velocity < 0.f)
|
||||
car.velocity = (std::min)(0.f, car.velocity + resistance);
|
||||
}
|
||||
car.velocity = (std::max)(
|
||||
-NpcCar::maxReverseSpeed, (std::min)(NpcCar::maxSpeed, car.velocity));
|
||||
|
||||
if (std::abs(car.velocity) > 0.01f) {
|
||||
const float speedFactor = car.velocity / NpcCar::maxSpeed;
|
||||
car.rotation += turnIntent * NpcCar::turnRate * dt * speedFactor;
|
||||
}
|
||||
|
||||
const Eigen::Vector3f forward(
|
||||
-std::sin(car.rotation), 0.f, -std::cos(car.rotation));
|
||||
const Eigen::Vector3f totalDelta = forward * car.velocity * dt;
|
||||
const float totalDist = totalDelta.norm();
|
||||
constexpr float maxSubstep = 0.2f;
|
||||
const int substeps = (std::max)(
|
||||
1, static_cast<int>(std::ceil(totalDist / maxSubstep)));
|
||||
const Eigen::Vector3f stepDelta = totalDelta / static_cast<float>(substeps);
|
||||
for (int step = 0; step < substeps; ++step)
|
||||
car.position += stepDelta;
|
||||
|
||||
car.wheelAngle += car.velocity * 4.f * dt;
|
||||
while (car.wheelAngle > 2.f * PI)
|
||||
car.wheelAngle -= 2.f * PI;
|
||||
|
||||
if (!location->taxiArrivedCallbackCalled) {
|
||||
const Eigen::Vector3f toArrival =
|
||||
car.position - location->taxiCarTriggerPosition;
|
||||
if (toArrival.squaredNorm() < 9.f) {
|
||||
location->taxiArrivedCallbackCalled = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LocationSimulationEvents LocationGameState::update(
|
||||
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked)
|
||||
{
|
||||
LocationSimulationEvents result;
|
||||
result.taxiArrived = updateNpcCar(deltaMs);
|
||||
std::vector<std::string> objectIds;
|
||||
objectIds.reserve(interactiveObjects.size());
|
||||
for (const auto& [id, object] : interactiveObjects)
|
||||
if (object) objectIds.push_back(id);
|
||||
std::sort(objectIds.begin(), objectIds.end());
|
||||
for (const std::string& id : objectIds) {
|
||||
InteractiveObjectUpdateEvents events =
|
||||
interactiveObjects.at(id)->update(deltaMs);
|
||||
if (events.onCompleteName.empty()) continue;
|
||||
result.interactiveObjects.push_back(
|
||||
{id, std::move(events.onCompleteName), false, {}});
|
||||
}
|
||||
|
||||
// All characters observe the same tick-start state. Damage is applied only
|
||||
// after every state has advanced, so vector order cannot suppress an attack.
|
||||
std::unordered_map<std::string, CharacterObservation> observations;
|
||||
for (const auto& [id, player] : players)
|
||||
if (player && player->enabled)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{player->position, player->hp});
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc && npc->enabled)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{npc->position, npc->hp});
|
||||
|
||||
std::vector<std::string> playerIds;
|
||||
playerIds.reserve(players.size());
|
||||
for (const auto& [id, player] : players)
|
||||
if (player) playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
|
||||
std::vector<std::string> eligiblePlayerIds;
|
||||
for (const std::string& id : playerIds) {
|
||||
const auto& player = players.at(id);
|
||||
if (player->enabled && player->hp > 0.f)
|
||||
eligiblePlayerIds.push_back(id);
|
||||
}
|
||||
|
||||
std::vector<std::string> npcIds;
|
||||
npcIds.reserve(npcs.size());
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc) npcIds.push_back(id);
|
||||
std::sort(npcIds.begin(), npcIds.end());
|
||||
|
||||
// Target selection is authoritative and runs before all characters observe
|
||||
// the tick. A valid target is a lock: it is retained until the player dies
|
||||
// or becomes disabled, even when the player leaves visibleRange.
|
||||
for (const std::string& id : npcIds) {
|
||||
CharacterState& npc = *npcs.at(id);
|
||||
if (!npc.enabled || !npc.canAttack || npc.hp <= 0.f)
|
||||
continue;
|
||||
|
||||
const std::string previousTarget = npc.attackTargetId;
|
||||
const auto locked = players.find(previousTarget);
|
||||
const bool lockValid =
|
||||
locked != players.end() && locked->second &&
|
||||
locked->second->enabled && locked->second->hp > 0.f;
|
||||
std::string selectedTarget = lockValid ? previousTarget : std::string();
|
||||
|
||||
if (!lockValid) {
|
||||
float nearestDistanceSquared = npc.visibleRange * npc.visibleRange;
|
||||
for (const std::string& playerId : eligiblePlayerIds) {
|
||||
const Eigen::Vector3f delta =
|
||||
players.at(playerId)->position - npc.position;
|
||||
const float distanceSquared =
|
||||
delta.x() * delta.x() + delta.z() * delta.z();
|
||||
if (distanceSquared <= nearestDistanceSquared) {
|
||||
if (selectedTarget.empty() ||
|
||||
distanceSquared < nearestDistanceSquared ||
|
||||
(distanceSquared == nearestDistanceSquared &&
|
||||
playerId < selectedTarget)) {
|
||||
selectedTarget = playerId;
|
||||
nearestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTarget != previousTarget) {
|
||||
npc.stopInPlace();
|
||||
npc.attackTargetId = selectedTarget;
|
||||
}
|
||||
|
||||
if (selectedTarget.empty()) {
|
||||
npc.battle_state = 0;
|
||||
npc.attack = 0;
|
||||
const Eigen::Vector3f homeTarget(
|
||||
npc.homePosition.x(), 0.f, npc.homePosition.z());
|
||||
const Eigen::Vector3f toHome = homeTarget - npc.position;
|
||||
if (toHome.norm() > CharacterState::kWalkThreshold &&
|
||||
(npc.requestedWalkTarget - homeTarget).norm() >
|
||||
CharacterState::kWalkThreshold) {
|
||||
setCharacterDestination(id, homeTarget, {}, players);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CharacterMovementObstacle> movementObstacles;
|
||||
movementObstacles.reserve(playerIds.size() + npcIds.size());
|
||||
auto addMovementObstacle =
|
||||
[&movementObstacles](const std::string& id,
|
||||
const CharacterState& character) {
|
||||
Eigen::Vector3f direction =
|
||||
character.getCurrentNavigationTarget() - character.position;
|
||||
direction.y() = 0.f;
|
||||
const float length = direction.norm();
|
||||
if (length > CharacterState::kWalkThreshold) direction /= length;
|
||||
else direction = Eigen::Vector3f::Zero();
|
||||
movementObstacles.push_back({
|
||||
id,
|
||||
character.position,
|
||||
character.movementContactRadius,
|
||||
character.movedLastTick,
|
||||
direction});
|
||||
};
|
||||
for (const std::string& id : playerIds) {
|
||||
const CharacterState& character = *players.at(id);
|
||||
if (character.enabled && character.hp > 0.f)
|
||||
addMovementObstacle(id, character);
|
||||
}
|
||||
for (const std::string& id : npcIds) {
|
||||
const CharacterState& character = *npcs.at(id);
|
||||
if (character.enabled && character.hp > 0.f)
|
||||
addMovementObstacle(id, character);
|
||||
}
|
||||
// One global ID order also makes blocker selection deterministic.
|
||||
std::sort(
|
||||
movementObstacles.begin(), movementObstacles.end(),
|
||||
[](const CharacterMovementObstacle& lhs,
|
||||
const CharacterMovementObstacle& rhs) { return lhs.id < rhs.id; });
|
||||
|
||||
auto advance = [&](const std::string& id, CharacterState& state) {
|
||||
CharacterUpdateContext context;
|
||||
context.characterId = id;
|
||||
context.movementObstacles = &movementObstacles;
|
||||
auto attack = observations.find(state.attackTargetId);
|
||||
if (attack != observations.end()) context.attackTarget = &attack->second;
|
||||
auto face = observations.find(state.faceTargetId);
|
||||
if (face != observations.end()) context.faceTarget = &face->second;
|
||||
CharacterUpdateEvents events = state.update(deltaMs, context);
|
||||
auto reservation = std::lower_bound(
|
||||
movementObstacles.begin(), movementObstacles.end(), id,
|
||||
[](const CharacterMovementObstacle& obstacle,
|
||||
const std::string& value) { return obstacle.id < value; });
|
||||
if (reservation != movementObstacles.end() && reservation->id == id) {
|
||||
reservation->position = state.position;
|
||||
reservation->moving = state.movedLastTick;
|
||||
Eigen::Vector3f direction =
|
||||
state.getCurrentNavigationTarget() - state.position;
|
||||
direction.y() = 0.f;
|
||||
const float length = direction.norm();
|
||||
if (length > CharacterState::kWalkThreshold)
|
||||
reservation->movementDirection = direction / length;
|
||||
else
|
||||
reservation->movementDirection = Eigen::Vector3f::Zero();
|
||||
}
|
||||
if (!events.arrivedCallbackName.empty() || events.attackImpact ||
|
||||
events.deathAnimationComplete || events.navigationRequested) {
|
||||
result.characters.push_back({id, std::move(events)});
|
||||
}
|
||||
};
|
||||
|
||||
for (const std::string& id : playerIds)
|
||||
if (players.at(id)->enabled)
|
||||
advance(id, *players.at(id));
|
||||
for (const std::string& id : npcIds)
|
||||
if (npcs.at(id)->enabled)
|
||||
advance(id, *npcs.at(id));
|
||||
|
||||
for (const CharacterSimulationEvent& source : result.characters) {
|
||||
if (!source.events.attackImpact) continue;
|
||||
CharacterState* target = findCharacter(source.events.attackTargetId, players);
|
||||
if (!target || !target->enabled || target->hp <= 0.f) continue;
|
||||
constexpr float damage = 10.f;
|
||||
target->setHp(target->hp - damage);
|
||||
result.damage.push_back({
|
||||
source.events.attackTargetId,
|
||||
source.characterId,
|
||||
damage,
|
||||
target->hp,
|
||||
target->initialHp,
|
||||
source.events.attackDirection});
|
||||
}
|
||||
|
||||
auto player = players.find("player");
|
||||
if (!interactiveObjectsLocked &&
|
||||
!location->targetInteractiveObjectId.empty() &&
|
||||
player != players.end() && player->second && player->second->enabled) {
|
||||
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
|
||||
const auto object =
|
||||
target == interactiveObjects.end() ? nullptr : target->second;
|
||||
if (object && !object->isAnimating) {
|
||||
const Eigen::Vector3f& approachTarget = object->hasInteractionPosition
|
||||
? object->interactionPosition : object->position;
|
||||
if ((player->second->position - approachTarget).norm() <= object->approachRadius) {
|
||||
result.interactiveObjects.push_back(
|
||||
{location->targetInteractiveObjectId, {}, true,
|
||||
object->activateFunctionName});
|
||||
location->targetInteractiveObjectId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!location->targetInteractNpcId.empty() &&
|
||||
player != players.end() && player->second && player->second->enabled &&
|
||||
npcs.count(location->targetInteractNpcId) != 0 &&
|
||||
npcs.at(location->targetInteractNpcId)) {
|
||||
const auto& npc = npcs.at(location->targetInteractNpcId);
|
||||
const float distance =
|
||||
(player->second->position - npc->position).norm();
|
||||
if (distance <= NPC_TALK_DISTANCE) {
|
||||
player->second->stopInPlace();
|
||||
npc->faceTargetId = "player";
|
||||
result.npcInteractionId = location->targetInteractNpcId;
|
||||
location->targetInteractNpcId.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void LocationGameState::save(nlohmann::json& out) const
|
||||
{
|
||||
location->save(out["state"]);
|
||||
|
||||
out["npcs"] = nlohmann::json::object();
|
||||
for (const auto& [id, npc] : npcs) {
|
||||
if (npc) {
|
||||
nlohmann::json value;
|
||||
npc->save(value);
|
||||
out["npcs"][id] = std::move(value);
|
||||
}
|
||||
}
|
||||
|
||||
out["interactiveObjects"] = nlohmann::json::object();
|
||||
for (const auto& [id, object] : interactiveObjects) {
|
||||
if (!object) continue;
|
||||
nlohmann::json value;
|
||||
object->save(value);
|
||||
out["interactiveObjects"][id] = std::move(value);
|
||||
}
|
||||
|
||||
out["dialogue"] = dialogue;
|
||||
out["scriptGlobals"] = scriptGlobals;
|
||||
out["triggerZones"] = nlohmann::json::object();
|
||||
for (const auto& [id, zone] : triggerZones)
|
||||
out["triggerZones"][id] = {{"enabled", zone.enabled}, {"playerInside", zone.playerInside}};
|
||||
out["teleportZones"] = nlohmann::json::object();
|
||||
for (const auto& [id, active] : teleportZones)
|
||||
out["teleportZones"][id] = active;
|
||||
}
|
||||
|
||||
void LocationGameState::load(const nlohmann::json& in)
|
||||
{
|
||||
hasPersistentState = true;
|
||||
if (!location) location = std::make_shared<LocationNetworkState>();
|
||||
location->load(in.at("state"));
|
||||
|
||||
const auto& npcValues = in.at("npcs");
|
||||
if (!npcValues.is_object())
|
||||
throw std::runtime_error("Location npcs must be an object");
|
||||
{
|
||||
const auto& values = npcValues;
|
||||
if (!npcs.empty()) {
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (!values.contains(id))
|
||||
throw std::runtime_error(
|
||||
"Saved location is missing NPC '" + id + "'");
|
||||
for (const auto& [id, value] : values.items())
|
||||
if (npcs.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown NPC '" + id + "'");
|
||||
}
|
||||
for (const auto& [id, value] : values.items()) {
|
||||
auto& npc = npcs[id];
|
||||
if (!npc) npc = std::make_shared<CharacterState>();
|
||||
npc->load(value);
|
||||
}
|
||||
}
|
||||
|
||||
const auto& objectValues = in.at("interactiveObjects");
|
||||
if (!objectValues.is_object())
|
||||
throw std::runtime_error("Location interactiveObjects must be an object");
|
||||
{
|
||||
const auto& values = objectValues;
|
||||
if (!interactiveObjects.empty()) {
|
||||
for (const auto& [id, object] : interactiveObjects)
|
||||
if (!values.contains(id))
|
||||
throw std::runtime_error(
|
||||
"Saved location is missing interactive object '" + id + "'");
|
||||
for (const auto& [id, value] : values.items())
|
||||
if (interactiveObjects.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown interactive object '" + id + "'");
|
||||
}
|
||||
for (const auto& [id, value] : values.items()) {
|
||||
auto& object = interactiveObjects[id];
|
||||
if (!object) object = std::make_shared<InteractiveObjectState>();
|
||||
object->load(value);
|
||||
}
|
||||
}
|
||||
|
||||
dialogue = in.at("dialogue");
|
||||
scriptGlobals = in.at("scriptGlobals");
|
||||
triggerZones.clear();
|
||||
const auto& triggerValues = in.at("triggerZones");
|
||||
if (!triggerValues.is_object())
|
||||
throw std::runtime_error("Location triggerZones must be an object");
|
||||
for (const auto& [id, value] : triggerValues.items())
|
||||
triggerZones[id] = {
|
||||
value.at("enabled").get<bool>(),
|
||||
value.at("playerInside").get<bool>()};
|
||||
teleportZones.clear();
|
||||
const auto& teleportValues = in.at("teleportZones");
|
||||
if (!teleportValues.is_object())
|
||||
throw std::runtime_error("Location teleportZones must be an object");
|
||||
for (const auto& [id, value] : teleportValues.items())
|
||||
teleportZones[id] = value.get<bool>();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "CharacterState.h"
|
||||
#include "LocationState.h"
|
||||
#include "items/InteractiveObjectState.h"
|
||||
#include "content/LocationDefinition.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct TriggerZoneGameState {
|
||||
bool enabled = true;
|
||||
bool playerInside = false;
|
||||
};
|
||||
|
||||
struct CharacterSimulationEvent {
|
||||
std::string characterId;
|
||||
CharacterUpdateEvents events;
|
||||
};
|
||||
|
||||
struct CharacterDamageEvent {
|
||||
std::string targetId;
|
||||
std::string attackerId;
|
||||
float amount = 0.f;
|
||||
float hp = 0.f;
|
||||
float maxHp = 0.f;
|
||||
Eigen::Vector3f direction = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct InteractiveObjectSimulationEvent {
|
||||
std::string objectId;
|
||||
std::string animationCompleteFunctionName;
|
||||
bool activated = false;
|
||||
std::string activateFunctionName;
|
||||
};
|
||||
|
||||
struct LocationSimulationEvents {
|
||||
std::vector<CharacterSimulationEvent> characters;
|
||||
std::vector<CharacterDamageEvent> damage;
|
||||
std::vector<InteractiveObjectSimulationEvent> interactiveObjects;
|
||||
std::string npcInteractionId;
|
||||
bool taxiArrived = false;
|
||||
};
|
||||
|
||||
// Pure authoritative state for one location. Shared ownership gives bound local
|
||||
// simulation objects stable addresses; runtime renderers and Location pointers
|
||||
// are deliberately absent, and public clients receive value-only snapshots.
|
||||
struct LocationGameState {
|
||||
std::shared_ptr<LocationNetworkState> location = std::make_shared<LocationNetworkState>();
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>> npcs;
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>> interactiveObjects;
|
||||
bool lockInteractiveObjectsDuringTutorial = false;
|
||||
bool hasPersistentState = false;
|
||||
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue = nlohmann::json::object();
|
||||
nlohmann::json scriptGlobals = nlohmann::json::object();
|
||||
std::shared_ptr<const LocationDefinition> definition;
|
||||
|
||||
bool setup(LocationDefinition content, const std::string& zipFile);
|
||||
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const;
|
||||
std::vector<PathFinder::DynamicObstacle> collectPathObstacles(
|
||||
const std::string& requesterId,
|
||||
const PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {}) const;
|
||||
bool setCharacterDestination(
|
||||
const std::string& id,
|
||||
const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {});
|
||||
bool replanCharacter(
|
||||
const std::string& id,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {});
|
||||
void startTaxi();
|
||||
void resetTaxi();
|
||||
LocationSimulationEvents update(
|
||||
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked);
|
||||
void save(nlohmann::json& out) const;
|
||||
void load(const nlohmann::json& in);
|
||||
|
||||
private:
|
||||
bool updateNpcCar(std::int64_t deltaMs);
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+171
-218
@@ -1,234 +1,187 @@
|
||||
#include "LocationState.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
void LocationState::save(nlohmann::json& out) const
|
||||
std::shared_ptr<LocationLocalState> ClientLocationState::getOrCreate(
|
||||
const std::string& locationId)
|
||||
{
|
||||
out["cameraAzimuth"] = cameraAzimuth;
|
||||
out["cameraInclination"] = cameraInclination;
|
||||
|
||||
out["activeNavigationIndex"] = activeNavigationIndex;
|
||||
|
||||
out["targetInteractiveObjectIndex"] = targetInteractiveObjectIndex;
|
||||
out["targetInteractNpcIndex"] = targetInteractNpcIndex;
|
||||
out["targetTeleportZoneIndex"] = targetTeleportZoneIndex;
|
||||
|
||||
out["isDarklands"] = isDarklands;
|
||||
out["isNight"] = isNight;
|
||||
out["isDawn"] = isDawn;
|
||||
|
||||
|
||||
|
||||
// Taxi stuff:
|
||||
nlohmann::json taxiJson;
|
||||
|
||||
taxiJson["taxiCar"] = {
|
||||
{"positionX", taxiCar.position.x()},
|
||||
{"positionY", taxiCar.position.y()},
|
||||
{"positionZ", taxiCar.position.z()},
|
||||
{"rotation", taxiCar.rotation},
|
||||
{"velocity", taxiCar.velocity},
|
||||
{"steeringAngle", taxiCar.steeringAngle},
|
||||
{"mode", static_cast<int>(taxiCar.mode)},
|
||||
{"waypoints", nlohmann::json::array()},
|
||||
{"currentWaypoint", taxiCar.currentWaypoint},
|
||||
{"waypointReachRadius", taxiCar.waypointReachRadius}
|
||||
};
|
||||
|
||||
nlohmann::json taxiWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCar.waypoints.size(); ++i) {
|
||||
taxiWaypoints.push_back({
|
||||
{"x", taxiCar.waypoints[i].x()},
|
||||
{"y", taxiCar.waypoints[i].y()},
|
||||
{"z", taxiCar.waypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiCar"]["waypoints"] = std::move(taxiWaypoints);
|
||||
|
||||
taxiJson["taxiCarInitialState"] = {
|
||||
{"positionX", taxiCarInitialState.position.x()},
|
||||
{"positionY", taxiCarInitialState.position.y()},
|
||||
{"positionZ", taxiCarInitialState.position.z()},
|
||||
{"rotation", taxiCarInitialState.rotation},
|
||||
{"velocity", taxiCarInitialState.velocity},
|
||||
{"steeringAngle", taxiCarInitialState.steeringAngle},
|
||||
{"mode", static_cast<int>(taxiCarInitialState.mode)},
|
||||
{"waypoints", nlohmann::json::array()},
|
||||
{"currentWaypoint", taxiCarInitialState.currentWaypoint},
|
||||
{"waypointReachRadius", taxiCarInitialState.waypointReachRadius}
|
||||
};
|
||||
|
||||
nlohmann::json taxiInitialWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCarInitialState.waypoints.size(); ++i) {
|
||||
taxiInitialWaypoints.push_back({
|
||||
{"x", taxiCarInitialState.waypoints[i].x()},
|
||||
{"y", taxiCarInitialState.waypoints[i].y()},
|
||||
{"z", taxiCarInitialState.waypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiCarInitialState"]["waypoints"] = std::move(taxiInitialWaypoints);
|
||||
|
||||
nlohmann::json taxiDefaultWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCarWaypoints.size(); ++i) {
|
||||
taxiDefaultWaypoints.push_back({
|
||||
{"x", taxiCarWaypoints[i].x()},
|
||||
{"y", taxiCarWaypoints[i].y()},
|
||||
{"z", taxiCarWaypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiDefaultWaypoints"] = taxiDefaultWaypoints;
|
||||
|
||||
/* taxiCarTriggerPosition = Eigen::Vector3f(
|
||||
taxiJson.value("taxiCarTriggerPositionX", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionY", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionZ", 0.f)
|
||||
);*/
|
||||
|
||||
taxiJson["taxiCarTriggerPositionX"] = taxiCarTriggerPosition.x();
|
||||
taxiJson["taxiCarTriggerPositionY"] = taxiCarTriggerPosition.y();
|
||||
taxiJson["taxiCarTriggerPositionZ"] = taxiCarTriggerPosition.z();
|
||||
|
||||
// {"y", taxiCarTriggerPosition.y()},
|
||||
// {"z", taxiCarTriggerPosition.z()}
|
||||
//};
|
||||
|
||||
taxiJson["taxiArrivedCallbackCalled"] = taxiArrivedCallbackCalled;
|
||||
|
||||
//questJournal.save(questJson);
|
||||
out["taxi"] = std::move(taxiJson);
|
||||
auto& state = locations[locationId];
|
||||
if (!state) state = std::make_shared<LocationLocalState>();
|
||||
return state;
|
||||
}
|
||||
|
||||
void LocationState::load(const nlohmann::json& in)
|
||||
void ClientLocationState::clear()
|
||||
{
|
||||
//logger() << "LocationState::load step 1" << std::endl;
|
||||
locations.clear();
|
||||
}
|
||||
|
||||
cameraAzimuth = in.value("cameraAzimuth", cameraAzimuth);
|
||||
cameraInclination = in.value("cameraInclination", cameraInclination);
|
||||
void LocationLocalState::save(nlohmann::json& out) const
|
||||
{
|
||||
out = {
|
||||
{"cameraAzimuth", cameraAzimuth},
|
||||
{"cameraInclination", cameraInclination}
|
||||
};
|
||||
}
|
||||
|
||||
void LocationLocalState::load(const nlohmann::json& in)
|
||||
{
|
||||
cameraAzimuth = in.at("cameraAzimuth").get<float>();
|
||||
cameraInclination = in.at("cameraInclination").get<float>();
|
||||
}
|
||||
|
||||
void ClientLocationState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["locations"] = nlohmann::json::object();
|
||||
for (const auto& [id, state] : locations) {
|
||||
if (!state) continue;
|
||||
nlohmann::json value;
|
||||
state->save(value);
|
||||
out["locations"][id] = std::move(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientLocationState::load(const nlohmann::json& in)
|
||||
{
|
||||
const auto& values = in.at("locations");
|
||||
if (!values.is_object())
|
||||
throw std::runtime_error("Client location state locations must be an object");
|
||||
for (auto& [id, state] : locations)
|
||||
if (state) *state = LocationLocalState{};
|
||||
for (const auto& [id, value] : values.items())
|
||||
getOrCreate(id)->load(value);
|
||||
}
|
||||
|
||||
void saveSinglePlayerPersistence(
|
||||
const nlohmann::json& networkState,
|
||||
const ClientLocationState& localState,
|
||||
nlohmann::json& out)
|
||||
{
|
||||
nlohmann::json savedLocalState;
|
||||
localState.save(savedLocalState);
|
||||
out = {
|
||||
{"persistenceVersion", kSinglePlayerPersistenceVersion},
|
||||
{"networkState", networkState},
|
||||
{"localState", std::move(savedLocalState)}
|
||||
};
|
||||
}
|
||||
|
||||
void loadSinglePlayerPersistence(
|
||||
const nlohmann::json& in,
|
||||
nlohmann::json& networkState,
|
||||
ClientLocationState& localState)
|
||||
{
|
||||
const int version = in.value("persistenceVersion", -1);
|
||||
if (version != kSinglePlayerPersistenceVersion)
|
||||
throw std::runtime_error(
|
||||
"Unsupported single-player persistence version: " +
|
||||
std::to_string(version));
|
||||
networkState = in.at("networkState");
|
||||
localState.load(in.at("localState"));
|
||||
}
|
||||
|
||||
namespace {
|
||||
nlohmann::json saveCar(const NpcCar& car)
|
||||
{
|
||||
nlohmann::json waypoints = nlohmann::json::array();
|
||||
for (const auto& waypoint : car.waypoints)
|
||||
waypoints.push_back(
|
||||
{{"x", waypoint.x()}, {"y", waypoint.y()}, {"z", waypoint.z()}});
|
||||
return {
|
||||
{"position", {car.position.x(), car.position.y(), car.position.z()}},
|
||||
{"rotation", car.rotation},
|
||||
{"velocity", car.velocity},
|
||||
{"steeringAngle", car.steeringAngle},
|
||||
{"wheelAngle", car.wheelAngle},
|
||||
{"mode", static_cast<int>(car.mode)},
|
||||
{"waypoints", std::move(waypoints)},
|
||||
{"currentWaypoint", car.currentWaypoint}
|
||||
};
|
||||
}
|
||||
|
||||
void loadCar(NpcCar& car, const nlohmann::json& value)
|
||||
{
|
||||
const auto& position = value.at("position");
|
||||
car.position = {
|
||||
position.at(0).get<float>(),
|
||||
position.at(1).get<float>(),
|
||||
position.at(2).get<float>()};
|
||||
car.rotation = value.at("rotation").get<float>();
|
||||
car.velocity = value.at("velocity").get<float>();
|
||||
car.steeringAngle = value.at("steeringAngle").get<float>();
|
||||
car.wheelAngle = value.at("wheelAngle").get<float>();
|
||||
car.mode = static_cast<NpcCar::Mode>(value.at("mode").get<int>());
|
||||
car.waypoints.clear();
|
||||
for (const auto& waypoint : value.at("waypoints"))
|
||||
car.waypoints.push_back({
|
||||
waypoint.at("x").get<float>(),
|
||||
waypoint.at("y").get<float>(),
|
||||
waypoint.at("z").get<float>()});
|
||||
car.currentWaypoint = value.at("currentWaypoint").get<std::size_t>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Entity ownership stays above this navigation-only layer.
|
||||
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles) const
|
||||
{
|
||||
(void)entityId;
|
||||
if (activeNavigationIndex < 0 ||
|
||||
activeNavigationIndex >= static_cast<int>(navigationMaps.size()))
|
||||
return {};
|
||||
return navigationMaps[activeNavigationIndex].findPathToNearest(
|
||||
start, end, dynamicObstacles);
|
||||
}
|
||||
|
||||
void LocationNetworkState::initNavigation(
|
||||
const std::vector<std::string>& paths, const std::string& zipFile)
|
||||
{
|
||||
const int savedActiveNavigationIndex = activeNavigationIndex;
|
||||
//navigationMapPaths = paths;
|
||||
navigationMaps.clear();
|
||||
navigationMaps.resize(paths.size());
|
||||
|
||||
for (size_t i = 0; i < paths.size(); ++i) {
|
||||
navigationMaps[i].build(paths[i], zipFile);
|
||||
}
|
||||
|
||||
activeNavigationIndex =
|
||||
savedActiveNavigationIndex >= 0 &&
|
||||
savedActiveNavigationIndex < static_cast<int>(navigationMaps.size())
|
||||
? savedActiveNavigationIndex
|
||||
: 0;
|
||||
}
|
||||
|
||||
void LocationNetworkState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["activeNavigationIndex"] = activeNavigationIndex;
|
||||
|
||||
out["targetInteractiveObjectId"] = targetInteractiveObjectId;
|
||||
out["targetInteractNpcId"] = targetInteractNpcId;
|
||||
out["targetTeleportZoneId"] = targetTeleportZoneId;
|
||||
|
||||
out["taxi"] = {
|
||||
{"car", saveCar(taxiCar)},
|
||||
{"arrivedCallbackCalled", taxiArrivedCallbackCalled}
|
||||
};
|
||||
}
|
||||
|
||||
void LocationNetworkState::load(const nlohmann::json& in)
|
||||
{
|
||||
activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex);
|
||||
|
||||
targetInteractiveObjectIndex = in.value("targetInteractiveObjectIndex", -1);
|
||||
targetInteractNpcIndex = in.value("targetInteractNpcIndex", -1);
|
||||
targetTeleportZoneIndex = in.value("targetTeleportZoneIndex", -1);
|
||||
|
||||
isDarklands = in.value("isDarklands", false);
|
||||
isNight = in.value("isNight", false);
|
||||
isDawn = in.value("isDawn", false);
|
||||
|
||||
|
||||
//logger() << "LocationState::load step 2" << std::endl;
|
||||
|
||||
nlohmann::json taxiJson = in["taxi"];
|
||||
|
||||
taxiArrivedCallbackCalled = taxiJson.value("taxiArrivedCallbackCalled", false);
|
||||
|
||||
//----- Taxi State ----
|
||||
|
||||
nlohmann::json taxiCarJson = taxiJson["taxiCar"];
|
||||
|
||||
taxiCar.position = Eigen::Vector3f(
|
||||
taxiCarJson.value("positionX", 0.f),
|
||||
taxiCarJson.value("positionY", 0.f),
|
||||
taxiCarJson.value("positionZ", 0.f)
|
||||
);
|
||||
|
||||
//logger() << "LocationState::load step 3" << std::endl;
|
||||
|
||||
taxiCar.rotation = taxiCarJson.value("rotation", 0.f);
|
||||
|
||||
taxiCar.velocity = taxiCarJson.value("velocity", 0.f);
|
||||
|
||||
taxiCar.steeringAngle = taxiCarJson.value("steeringAngle", 0.f);
|
||||
|
||||
taxiCar.mode = static_cast<NpcCar::Mode>(taxiCarJson.value("mode", 0));
|
||||
|
||||
//logger() << "LocationState::load step 4" << std::endl;
|
||||
|
||||
taxiCar.waypoints.clear();
|
||||
if (taxiCarJson.contains("waypoints") && taxiCarJson["waypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiCarJson["waypoints"].size()); ++i)
|
||||
taxiCar.waypoints.push_back(Eigen::Vector3f(
|
||||
taxiCarJson["waypoints"][i].value("x", 0.f),
|
||||
taxiCarJson["waypoints"][i].value("y", 0.f),
|
||||
taxiCarJson["waypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 5" << std::endl;
|
||||
|
||||
taxiCar.currentWaypoint = taxiCarJson.value("currentWaypoint", 0);
|
||||
taxiCar.waypointReachRadius = taxiCarJson.value("waypointReachRadius", 3.f);
|
||||
taxiCar.wheelAngle = taxiCarJson.value("wheelAngle", 0.f);
|
||||
|
||||
|
||||
//--------- Taxi initial state -----
|
||||
|
||||
//logger() << "LocationState::load step 6" << std::endl;
|
||||
|
||||
nlohmann::json taxiCarInitialStateJson = taxiJson["taxiCarInitialState"];
|
||||
|
||||
taxiCarInitialState.position = Eigen::Vector3f(
|
||||
taxiCarInitialStateJson.value("positionX", 0.f),
|
||||
taxiCarInitialStateJson.value("positionY", 0.f),
|
||||
taxiCarInitialStateJson.value("positionZ", 0.f)
|
||||
);
|
||||
|
||||
taxiCarInitialState.rotation = taxiCarInitialStateJson.value("rotation", 0.f);
|
||||
|
||||
taxiCarInitialState.velocity = taxiCarInitialStateJson.value("velocity", 0.f);
|
||||
|
||||
taxiCarInitialState.steeringAngle = taxiCarInitialStateJson.value("steeringAngle", 0.f);
|
||||
|
||||
taxiCarInitialState.mode = static_cast<NpcCar::Mode>(taxiCarInitialStateJson.value("mode", 0));
|
||||
|
||||
//logger() << "LocationState::load step 7" << std::endl;
|
||||
|
||||
|
||||
taxiCarInitialState.waypoints.clear();
|
||||
if (taxiCarInitialStateJson.contains("waypoints") && taxiCarInitialStateJson["waypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiCarInitialStateJson["waypoints"].size()); ++i)
|
||||
taxiCarInitialState.waypoints.push_back(Eigen::Vector3f(
|
||||
taxiCarInitialStateJson["waypoints"][i].value("x", 0.f),
|
||||
taxiCarInitialStateJson["waypoints"][i].value("y", 0.f),
|
||||
taxiCarInitialStateJson["waypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 8" << std::endl;
|
||||
|
||||
taxiCarInitialState.currentWaypoint = taxiCarInitialStateJson.value("currentWaypoint", 0);
|
||||
taxiCarInitialState.waypointReachRadius = taxiCarInitialStateJson.value("waypointReachRadius", 3.f);
|
||||
taxiCarInitialState.wheelAngle = taxiCarInitialStateJson.value("wheelAngle", 0.f);
|
||||
|
||||
|
||||
|
||||
taxiCarWaypoints.clear();
|
||||
if (taxiJson.contains("taxiDefaultWaypoints") && taxiJson["taxiDefaultWaypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiJson["taxiDefaultWaypoints"].size()); ++i)
|
||||
taxiCarWaypoints.push_back(Eigen::Vector3f(
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("x", 0.f),
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("y", 0.f),
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 9" << std::endl;
|
||||
|
||||
taxiCarTriggerPosition = Eigen::Vector3f(
|
||||
taxiJson.value("taxiCarTriggerPositionX", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionY", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionZ", 0.f)
|
||||
);
|
||||
|
||||
//logger() << "LocationState::load step 10" << std::endl;
|
||||
|
||||
targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string());
|
||||
targetInteractNpcId = in.value("targetInteractNpcId", std::string());
|
||||
targetTeleportZoneId = in.value("targetTeleportZoneId", std::string());
|
||||
|
||||
const auto& taxi = in.at("taxi");
|
||||
loadCar(taxiCar, taxi.at("car"));
|
||||
taxiArrivedCallbackCalled =
|
||||
taxi.at("arrivedCallbackCalled").get<bool>();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
+50
-17
@@ -2,34 +2,67 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include "NpcCar.h"
|
||||
#include "navigation/PathFinder.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct LocationState : public ISaveable {
|
||||
// ---- Camera ----
|
||||
float cameraAzimuth = -2.35f;
|
||||
float cameraInclination = 1.1036f;
|
||||
inline constexpr int kSinglePlayerPersistenceVersion = 1;
|
||||
|
||||
struct LocationLocalState : public ISaveable {
|
||||
float cameraAzimuth = -2.35f;
|
||||
float cameraInclination = 1.1036f;
|
||||
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
};
|
||||
|
||||
struct ClientLocationState : public ISaveable {
|
||||
std::unordered_map<std::string, std::shared_ptr<LocationLocalState>> locations;
|
||||
|
||||
std::shared_ptr<LocationLocalState> getOrCreate(const std::string& locationId);
|
||||
void clear();
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
};
|
||||
|
||||
void saveSinglePlayerPersistence(
|
||||
const nlohmann::json& networkState,
|
||||
const ClientLocationState& localState,
|
||||
nlohmann::json& out);
|
||||
void loadSinglePlayerPersistence(
|
||||
const nlohmann::json& in,
|
||||
nlohmann::json& networkState,
|
||||
ClientLocationState& localState);
|
||||
|
||||
struct LocationNetworkState : public ISaveable {
|
||||
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
|
||||
//It might be changed only in the editor mode during development.
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
std::vector<Eigen::Vector3f> planPathExceptMyself(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;
|
||||
void initNavigation(
|
||||
const std::vector<std::string>& paths,
|
||||
const std::string& zipFile = std::string());
|
||||
|
||||
// ---- Navigation ----
|
||||
int activeNavigationIndex = 0;
|
||||
|
||||
// ---- Walk-to targets (-1 = none) ----
|
||||
// Mirrors CharacterState's kNoTarget convention; raw pointers are not serialisable.
|
||||
int targetInteractiveObjectIndex = -1;
|
||||
int targetInteractNpcIndex = -1;
|
||||
int targetTeleportZoneIndex = -1;
|
||||
|
||||
// ---- World-mode flags (synced from Game after each transition) ----
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
|
||||
// ---- Walk-to targets (empty = none) ----
|
||||
std::string targetInteractiveObjectId;
|
||||
std::string targetInteractNpcId;
|
||||
std::string targetTeleportZoneId;
|
||||
|
||||
NpcCar taxiCarInitialState;
|
||||
NpcCar taxiCar;
|
||||
bool taxiArrivedCallbackCalled = false;
|
||||
Vector3f taxiCarTriggerPosition = Vector3f(0,0,0);
|
||||
std::vector<Vector3f> taxiCarWaypoints;
|
||||
Eigen::Vector3f taxiCarTriggerPosition = Eigen::Vector3f(0,0,0);
|
||||
std::vector<Eigen::Vector3f> taxiCarWaypoints;
|
||||
|
||||
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
||||
|
||||
|
||||
+159
-119
@@ -1,4 +1,4 @@
|
||||
#include "MenuManager.h"
|
||||
#include "MenuManager.h"
|
||||
#include "Game.h"
|
||||
#include "Localization.h"
|
||||
#include "items/ItemRegistry.h"
|
||||
@@ -201,9 +201,9 @@ namespace FRG {
|
||||
}
|
||||
#endif
|
||||
|
||||
MenuManager::MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer) :
|
||||
MenuManager::MenuManager(Renderer& iRenderer, IGameServer& server, AudioPlayerAsync& audioPlayer) :
|
||||
renderer(iRenderer),
|
||||
gameState_(gameState),
|
||||
server_(server),
|
||||
audioPlayer_(audioPlayer)
|
||||
{
|
||||
//Localization
|
||||
@@ -338,9 +338,7 @@ namespace FRG {
|
||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
||||
}
|
||||
|
||||
void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
|
||||
inventory = &inv;
|
||||
|
||||
void MenuManager::setup(const std::string& zipFile) {
|
||||
loadUiRoots(zipFile);
|
||||
|
||||
texObjectiveCompleted_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/quest_objective_completed.png", zipFile, true);
|
||||
@@ -360,19 +358,6 @@ namespace FRG {
|
||||
languageLoadingRu = renderer.textureManager.LoadFromPng("resources/loading_land003_ru.png", zipFile);
|
||||
languageLoadingEn = renderer.textureManager.LoadFromPng("resources/loading_land003_en.png", zipFile);
|
||||
|
||||
gameState_.questJournal.onQuestUnlocked = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestNew, q->definition.title);
|
||||
};
|
||||
gameState_.questJournal.onQuestCompleted = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestCompleted, q->definition.title);
|
||||
};
|
||||
gameState_.questJournal.onQuestFailed = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestFailed, q->definition.title);
|
||||
};
|
||||
|
||||
const std::string imgDir = "resources/w/ui/img/phone/";
|
||||
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
|
||||
texBubbleInLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_top.png", zipFile, true);
|
||||
@@ -419,9 +404,10 @@ namespace FRG {
|
||||
}
|
||||
|
||||
std::shared_ptr<UiNode> MenuManager::gameplayHudRoot() const {
|
||||
const std::string& loc = gameState_.currentLocationName;
|
||||
if (loc == "uni_exterior") return gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||
if (loc == "uni_interior") return gameState_.isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||
const auto snapshot = server_.snapshot();
|
||||
const std::string& loc = snapshot->currentLocationId;
|
||||
if (loc == "uni_exterior") return snapshot->isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||
if (loc == "uni_interior") return snapshot->isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||
return hudDormRoot; // dorm, or a new game before the location is set
|
||||
}
|
||||
|
||||
@@ -434,21 +420,37 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::refreshHudVisibility() {
|
||||
if (!inventory) return; // update() already ticks during loading, before setup()
|
||||
const auto snapshot = server_.snapshot();
|
||||
// The initial state can be loaded after the gameplay HUD was first
|
||||
// constructed (notably for the remote campaign). Keep the root derived
|
||||
// from the authoritative snapshot instead of relying on the transition
|
||||
// callback to switch between day and Darklands HUDs.
|
||||
// Pushed in-game screens (inventory, phone, journal, and pause menu)
|
||||
// temporarily own the UI root while keeping the logical UI state as
|
||||
// Gameplay. Do not replace those roots from the per-frame refresh.
|
||||
if (uiState_ == GameUiState::Gameplay && uiManager.menuStackSize() == 0) {
|
||||
const auto desiredRoot = gameplayHudRoot();
|
||||
if (uiManager.getRoot() != desiredRoot) {
|
||||
uiManager.replaceRoot(desiredRoot);
|
||||
applyCurrentHealthBar();
|
||||
setupGameplayHudCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
const TutorialStep step = gameState_.tutorialStep;
|
||||
const bool tutorial = gameState_.isTutorialActive();
|
||||
const bool hasPhone = inventory->hasItem("phone");
|
||||
const bool hasJournal = inventory->hasItem("journal");
|
||||
const bool hasNote = inventory->hasItem("note_spell");
|
||||
const bool dark = gameState_.isDarklands;
|
||||
const auto itemIds = snapshot->inventoryItemIds;
|
||||
const TutorialStep step = snapshot->tutorialStep;
|
||||
const bool tutorial = snapshot->isTutorialActive();
|
||||
const bool hasPhone = std::find(itemIds.begin(), itemIds.end(), "phone") != itemIds.end();
|
||||
const bool hasJournal = std::find(itemIds.begin(), itemIds.end(), "journal") != itemIds.end();
|
||||
const bool hasNote = std::find(itemIds.begin(), itemIds.end(), "note_spell") != itemIds.end();
|
||||
const bool dark = snapshot->isDarklands;
|
||||
|
||||
auto show = [this](std::initializer_list<const char*> names, bool visible) {
|
||||
for (const char* name : names) uiManager.setNodeVisible(name, visible);
|
||||
};
|
||||
|
||||
// --- HUD buttons: present while the matching item is in the inventory ---
|
||||
show({ "inventoryButton" }, inventory->getCount() > 0);
|
||||
show({ "inventoryButton" }, !itemIds.empty());
|
||||
show({ "phoneButton" }, hasPhone && !dark);
|
||||
show({ "journalButton" }, hasJournal);
|
||||
show({ "darklandsButton" }, hasNote && !dark);
|
||||
@@ -466,9 +468,9 @@ namespace FRG {
|
||||
show({ "hint4" }, step == TutorialStep::Zoom);
|
||||
show({ "hint5" }, step == TutorialStep::PickUpItem);
|
||||
// ...then the hints for the picked-up items, which can be done in any order.
|
||||
const bool chatHint = tutorial && !gameState_.tutorialPhoneChatScreenOpened;
|
||||
const bool journalHint = tutorial && !gameState_.tutorialJournalScreenOpened;
|
||||
const bool taxiHint = tutorial && gameState_.tutorialNeedOpenTaxiScreen;
|
||||
const bool chatHint = tutorial && !snapshot->tutorialPhoneChatScreenOpened;
|
||||
const bool journalHint = tutorial && !snapshot->tutorialJournalScreenOpened;
|
||||
const bool taxiHint = tutorial && snapshot->tutorialNeedOpenTaxiScreen;
|
||||
show({ "hint6a", "hint6aarrow" }, hasPhone && chatHint && !taxiHint);
|
||||
show({ "hint6b", "hint6barrow" }, hasJournal && journalHint);
|
||||
show({ "hint7", "hint7arrow" }, taxiHint);
|
||||
@@ -485,7 +487,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::showMainMenu() {
|
||||
gameState_.currentLocationName.clear();
|
||||
server_.submit(SetCurrentLocationCommand{""});
|
||||
|
||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||
lastMusicTrack = "music/main menu final.ogg";
|
||||
@@ -535,7 +537,7 @@ namespace FRG {
|
||||
|
||||
|
||||
|
||||
#if !defined(EMSCRIPTEN) && !defined(__ANDROID__)
|
||||
#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__)
|
||||
uiManager.setTextButtonCallback("menuExitButton", [this](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
Environment::exitGameLoop = true;
|
||||
@@ -546,7 +548,6 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::showMenuAfterGameCompleted() {
|
||||
//gameState_.currentLocationName.clear();
|
||||
|
||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||
lastMusicTrack = "music/main menu final.ogg";
|
||||
@@ -586,7 +587,7 @@ namespace FRG {
|
||||
|
||||
|
||||
|
||||
#ifndef EMSCRIPTEN
|
||||
#ifndef __EMSCRIPTEN__
|
||||
uiManager.setTextButtonCallback("menuExitButton", [this](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
Environment::exitGameLoop = true;
|
||||
@@ -1060,8 +1061,8 @@ namespace FRG {
|
||||
|
||||
steps.push_back([this]() {
|
||||
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState_.campaignId))
|
||||
gameState_.questJournal.reloadDefinitionsPreservingState(campaign->questsPath, CONST_ZIP_FILE);
|
||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(server_.snapshot()->campaignId))
|
||||
server_.submit(ReloadQuestDefinitionsCommand{campaign->questsPath, CONST_ZIP_FILE});
|
||||
});
|
||||
|
||||
steps.push_back([this]() {
|
||||
@@ -1197,7 +1198,7 @@ namespace FRG {
|
||||
|
||||
uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {});
|
||||
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
/*logger() << "Item count: " << items.size() << std::endl;
|
||||
for (int x = 0; x < items.size(); x++)
|
||||
{
|
||||
@@ -1236,7 +1237,7 @@ namespace FRG {
|
||||
|
||||
void MenuManager::selectInventoryItem(int index) {
|
||||
logger() << "MenuManager::selectInventoryItem: " << index << std::endl;
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
if (index < 0 || index >= static_cast<int>(items.size())) return;
|
||||
|
||||
if (isPortraitMode() && inventoryMenuStackDepth_ == 1) {
|
||||
@@ -1288,7 +1289,10 @@ namespace FRG {
|
||||
|
||||
void MenuManager::openQuestJournal() {
|
||||
uiState_ = GameUiState::QuestJournal;
|
||||
gameState_.tutorialJournalScreenOpened = true;
|
||||
auto tutorial = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
tutorial->tutorialPhoneChatScreenOpened, true,
|
||||
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||
journalMenuStackDepth_ = 1;
|
||||
|
||||
const bool portrait = isPortraitMode();
|
||||
@@ -1380,9 +1384,10 @@ namespace FRG {
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
|
||||
if (gameState_.isNight)
|
||||
const auto timeState = server_.snapshot();
|
||||
if (timeState->isNight)
|
||||
{
|
||||
if (gameState_.isDawn)
|
||||
if (timeState->isDawn)
|
||||
{
|
||||
uiManager.setNodeVisible("phoneTimeDay", false);
|
||||
uiManager.setNodeVisible("phoneTimeNight", false);
|
||||
@@ -1418,51 +1423,67 @@ namespace FRG {
|
||||
});
|
||||
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
||||
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
||||
gameState_.chatUnread[0] = false;
|
||||
setChatUnread(0, false);
|
||||
openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
||||
gameState_.chatUnread[1] = false;
|
||||
setChatUnread(1, false);
|
||||
openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
||||
gameState_.chatUnread[2] = false;
|
||||
setChatUnread(2, false);
|
||||
openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
}
|
||||
|
||||
void MenuManager::refreshChatUnreadIndicators() {
|
||||
const auto state = server_.snapshot();
|
||||
static const char* kUnreadNodes[3] = { "chat1Unread", "chat2Unread", "chat3Unread" };
|
||||
static const char* kMsgNodes[3] = { "chat1msg", "chat2msg", "chat3msg" };
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
uiManager.setNodeVisible(kUnreadNodes[i], gameState_.chatUnread[i]);
|
||||
if (!gameState_.chatPreviewMsg[i].empty())
|
||||
uiManager.setText(kMsgNodes[i], gameState_.chatPreviewMsg[i]);
|
||||
uiManager.setNodeVisible(kUnreadNodes[i], state->chatUnread[i]);
|
||||
if (!state->chatPreview[i].empty())
|
||||
uiManager.setText(kMsgNodes[i], state->chatPreview[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuManager::setChatUnread(int chatIndex, bool unread, const std::string& previewMsg) {
|
||||
if (chatIndex < 0 || chatIndex > 2) return;
|
||||
gameState_.chatUnread[chatIndex] = unread;
|
||||
const auto state = server_.snapshot();
|
||||
std::string preview = state->chatPreview[chatIndex];
|
||||
if (!previewMsg.empty()) {
|
||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(previewMsg);
|
||||
} else if (!unread && !gameState_.chatHistory[chatIndex].empty()) {
|
||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(gameState_.chatHistory[chatIndex].back().text);
|
||||
preview = trimChatPreview(previewMsg);
|
||||
} else if (!unread && !state->chatHistory[chatIndex].empty()) {
|
||||
preview = trimChatPreview(state->chatHistory[chatIndex].back().text);
|
||||
}
|
||||
server_.submit(SetChatCommand{chatIndex, unread, std::move(preview)});
|
||||
}
|
||||
|
||||
void MenuManager::spendMoney(int amount) {
|
||||
gameState_.money -= amount;
|
||||
server_.submit(SpendMoneyCommand{amount});
|
||||
}
|
||||
|
||||
std::vector<Item> MenuManager::inventoryItems() const {
|
||||
std::vector<Item> items;
|
||||
for (const std::string& id : server_.snapshot()->inventoryItemIds) {
|
||||
if (const Item* item = ItemRegistry::instance().findById(id)) items.push_back(*item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
bool MenuManager::hasInventoryItem(const std::string& id) const {
|
||||
const auto& ids = server_.snapshot()->inventoryItemIds;
|
||||
return std::find(ids.begin(), ids.end(), id) != ids.end();
|
||||
}
|
||||
|
||||
|
||||
void MenuManager::openPhoneBank() {
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::Bank;
|
||||
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneBankRoot : phoneBankRoot);
|
||||
uiManager.setText("balanceText", formatMoney(gameState_.money));
|
||||
uiManager.setText("balanceText", formatMoney(server_.snapshot()->money));
|
||||
|
||||
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
||||
closePhoneEntirely();
|
||||
@@ -1492,13 +1513,15 @@ namespace FRG {
|
||||
});
|
||||
uiManager.setButtonCallback("videoSkip", [this](const std::string&) {
|
||||
closePhoneEntirely();
|
||||
if (gameState_.isNight)
|
||||
const auto state = server_.snapshot();
|
||||
if (state->isNight)
|
||||
{
|
||||
startDialogueFunc("dialog_video002");
|
||||
}
|
||||
else
|
||||
{
|
||||
auto day = gameState_.globalInts["day"];
|
||||
auto dayIt = state->globalInts.find("day");
|
||||
const int day = dayIt == state->globalInts.end() ? 0 : dayIt->second;
|
||||
if (day == 0)
|
||||
{
|
||||
startDialogueFunc("dialog_video003");
|
||||
@@ -1513,10 +1536,11 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::openPhoneTaxi() {
|
||||
if (gameState_.currentLocationName == "uni_interior") {
|
||||
const std::string location = server_.snapshot()->currentLocationId;
|
||||
if (location == "uni_interior") {
|
||||
closePhoneEntirely();
|
||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi003");
|
||||
} else if (gameState_.currentLocationName == "uni_exterior") {
|
||||
} else if (location == "uni_exterior") {
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
|
||||
openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot);
|
||||
} else {
|
||||
@@ -1539,17 +1563,18 @@ namespace FRG {
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
||||
gameState_.tutorialNeedOpenTaxiScreen = false;
|
||||
gameState_.tutorialTaxiHintDone = true;
|
||||
auto state = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||
false, true});
|
||||
|
||||
if (gameState_.taxiIsCalled == false)
|
||||
if (!state->taxiIsCalled)
|
||||
{
|
||||
gameState_.taxiIsCalled = true;
|
||||
if (callTaxiFunc)
|
||||
{
|
||||
callTaxiFunc();
|
||||
}
|
||||
gameState_.money -= 500;
|
||||
server_.submit(SetTaxiCalledCommand{true});
|
||||
server_.submit(StartTaxiCommand{state->currentLocationId});
|
||||
server_.submit(SetTriggerZoneEnabledCommand{
|
||||
state->currentLocationId, "taxi_zone001", false});
|
||||
server_.submit(SpendMoneyCommand{500});
|
||||
closePhoneEntirely();
|
||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi002");
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
@@ -1569,7 +1594,10 @@ namespace FRG {
|
||||
activeChatIndex_ = chatIndex;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::Chat;
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
gameState_.tutorialPhoneChatScreenOpened = true;
|
||||
auto tutorial = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
true, tutorial->tutorialJournalScreenOpened,
|
||||
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||
uiManager.pushMenuFromSavedRoot(chatRoot);
|
||||
|
||||
// Build a dedicated chatUiManager root sized to the FBO chat area
|
||||
@@ -1616,11 +1644,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::returnToPhoneChatList() {
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
||||
}
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||
activeChatIndex_ = -1;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
@@ -1634,11 +1658,7 @@ namespace FRG {
|
||||
|
||||
|
||||
void MenuManager::closePhoneEntirely() {
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
||||
}
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||
activeChatIndex_ = -1;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::None;
|
||||
uiState_ = GameUiState::Gameplay;
|
||||
@@ -1699,8 +1719,11 @@ namespace FRG {
|
||||
void MenuManager::tutorialShowTaxiHint()
|
||||
{
|
||||
logger() << "tutorialShowTaxiHint" << std::endl;
|
||||
if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return;
|
||||
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
|
||||
auto state = server_.snapshot();
|
||||
if (!state->isTutorialActive() || state->tutorialTaxiHintDone) return;
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||
true, state->tutorialTaxiHintDone});
|
||||
refreshHudVisibility();
|
||||
}
|
||||
|
||||
@@ -1723,16 +1746,17 @@ namespace FRG {
|
||||
|
||||
void MenuManager::onLocationChanged(const std::string& locationName) {
|
||||
if (uiState_ != GameUiState::Gameplay) return;
|
||||
gameState_.currentLocationName = locationName;
|
||||
server_.submit(SetCurrentLocationCommand{locationName});
|
||||
const auto state = server_.snapshot();
|
||||
showGameplayHud();
|
||||
|
||||
if (locationName == "uni_exterior" || locationName == "uni_interior") {
|
||||
if (gameState_.isDarklands)
|
||||
if (state->isDarklands)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||
lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
}
|
||||
else if (gameState_.isNight)
|
||||
else if (state->isNight)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||
lastMusicTrack = "music/bishkek night.ogg";
|
||||
@@ -1743,7 +1767,7 @@ namespace FRG {
|
||||
lastMusicTrack = "music/bishkek univer day.ogg";
|
||||
}
|
||||
} else {
|
||||
if (gameState_.isNight)
|
||||
if (state->isNight)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||
lastMusicTrack = "music/bishkek night.ogg";
|
||||
@@ -1759,13 +1783,16 @@ namespace FRG {
|
||||
void MenuManager::onItemPickedUp(const std::string& itemId) {
|
||||
// The new item's HUD button appears through refreshHudVisibility().
|
||||
if (itemId == "phone" || itemId == "journal")
|
||||
gameState_.completeTutorialStep(TutorialStep::PickUpItem);
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::PickUpItem});
|
||||
refreshHudVisibility();
|
||||
}
|
||||
|
||||
void MenuManager::refreshQuestJournalUi() {
|
||||
visibleQuestIds.clear();
|
||||
auto quests = gameState_.questJournal.getVisibleQuests();
|
||||
const auto state = server_.snapshot();
|
||||
std::vector<const Quest::QuestState*> quests;
|
||||
quests.reserve(state->quests.size());
|
||||
for (const auto& quest : state->quests) quests.push_back(&quest);
|
||||
|
||||
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
|
||||
const int pa = questStatusPriority(a->status);
|
||||
@@ -1840,7 +1867,10 @@ namespace FRG {
|
||||
}
|
||||
|
||||
selectedQuestIndex = index;
|
||||
Quest::QuestState* quest = gameState_.questJournal.findQuest(visibleQuestIds[index]);
|
||||
const auto state = server_.snapshot();
|
||||
const Quest::QuestState* quest = nullptr;
|
||||
for (const auto& candidate : state->quests)
|
||||
if (candidate.definition.id == visibleQuestIds[index]) quest = &candidate;
|
||||
if (!quest) {
|
||||
return;
|
||||
}
|
||||
@@ -1888,7 +1918,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::openPortraitInventoryItem(int index) {
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
inventorySelectedIndex_ = index;
|
||||
inventoryMenuStackDepth_ = 2;
|
||||
uiManager.pushMenuFromSavedRoot(portraitInventoryItemRoot);
|
||||
@@ -1943,7 +1973,10 @@ namespace FRG {
|
||||
uiManager.setButtonCallback("journalMain", [this](const std::string&) {});
|
||||
|
||||
|
||||
Quest::QuestState* quest = gameState_.questJournal.findQuest(visibleQuestIds[index]);
|
||||
const auto state = server_.snapshot();
|
||||
const Quest::QuestState* quest = nullptr;
|
||||
for (const auto& candidate : state->quests)
|
||||
if (candidate.definition.id == visibleQuestIds[index]) quest = &candidate;
|
||||
if (!quest) return;
|
||||
const auto& def = quest->definition;
|
||||
|
||||
@@ -2070,7 +2103,8 @@ namespace FRG {
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
|
||||
if (chatIndex < 0 || chatIndex > 2) return;
|
||||
for (const auto& msg : gameState_.chatHistory[chatIndex]) {
|
||||
const auto state = server_.snapshot();
|
||||
for (const auto& msg : state->chatHistory[chatIndex]) {
|
||||
const bool inc = msg.incoming;
|
||||
const std::string nodeName = chatUiManager.addChatBubble(
|
||||
"chatMessagesContainer", msg.text, inc,
|
||||
@@ -2100,11 +2134,7 @@ namespace FRG {
|
||||
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
|
||||
if (activeChatIndex_ < 0) return;
|
||||
|
||||
auto& history = gameState_.chatHistory[activeChatIndex_];
|
||||
if (static_cast<int>(history.size()) >= 5) {
|
||||
history.erase(history.begin());
|
||||
}
|
||||
history.push_back({ text, incoming });
|
||||
server_.submit(AppendChatMessageCommand{activeChatIndex_, text, incoming});
|
||||
|
||||
if (uiState_ != GameUiState::PhoneScreen) return;
|
||||
|
||||
@@ -2143,8 +2173,8 @@ namespace FRG {
|
||||
{
|
||||
logger() << "MenuManager::setDarklandsMode called" << std::endl;
|
||||
|
||||
if (enabled && gameState_.currentLocationName == "uni_interior" && inventory->hasItem("note_spell"))
|
||||
gameState_.completeTutorialStep(TutorialStep::EnterDarklands);
|
||||
if (enabled && server_.snapshot()->currentLocationId == "uni_interior" && hasInventoryItem("note_spell"))
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::EnterDarklands});
|
||||
|
||||
if (uiState_ == GameUiState::Gameplay)
|
||||
showGameplayHud(); // darklands has its own HUD layouts
|
||||
@@ -2152,30 +2182,31 @@ namespace FRG {
|
||||
|
||||
void MenuManager::onPlayerStartedWalking()
|
||||
{
|
||||
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands)
|
||||
gameState_.completeTutorialStep(TutorialStep::DarklandsHealth);
|
||||
const auto state = server_.snapshot();
|
||||
if (state->currentLocationId == "uni_interior" && state->isDarklands)
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::DarklandsHealth});
|
||||
}
|
||||
|
||||
void MenuManager::advanceUniIntDarklandsHud()
|
||||
{
|
||||
// The ghost zone can be reached without a floor tap (DarklandsHealth never completed).
|
||||
if (gameState_.tutorialStep == TutorialStep::DarklandsHealth
|
||||
|| gameState_.tutorialStep == TutorialStep::DarklandsExplore)
|
||||
gameState_.tutorialStep = TutorialStep::DarklandsAttack;
|
||||
const TutorialStep step = server_.snapshot()->tutorialStep;
|
||||
if (step == TutorialStep::DarklandsHealth || step == TutorialStep::DarklandsExplore)
|
||||
server_.submit(SetTutorialStepCommand{TutorialStep::DarklandsAttack});
|
||||
}
|
||||
|
||||
void MenuManager::onEnemyKilledInUniInterior()
|
||||
{
|
||||
// Every location's enemies report here; only the university interior drives the tutorial.
|
||||
if (gameState_.currentLocationName != "uni_interior") return;
|
||||
const auto state = server_.snapshot();
|
||||
if (state->currentLocationId != "uni_interior") return;
|
||||
// The last tutorial step; like the attack hint, also ends the earlier darklands hints.
|
||||
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
||||
gameState_.tutorialStep = TutorialStep::Completed;
|
||||
if (state->tutorialStep >= TutorialStep::DarklandsHealth)
|
||||
server_.submit(SetTutorialStepCommand{TutorialStep::Completed});
|
||||
}
|
||||
|
||||
void MenuManager::updateHealthBar(float hp, float maxHp) {
|
||||
gameState_.playerHp = hp;
|
||||
gameState_.playerMaxHp = maxHp;
|
||||
//server_.submit(SetPlayerHealthCommand{hp, maxHp});
|
||||
applyCurrentHealthBar();
|
||||
}
|
||||
|
||||
@@ -2191,21 +2222,30 @@ namespace FRG {
|
||||
cutsceneHudActive_ = false;
|
||||
topUiManager.replaceRoot(nullptr);
|
||||
if (uiState_ == GameUiState::Gameplay)
|
||||
onLocationChanged(gameState_.currentLocationName);
|
||||
onLocationChanged(server_.snapshot()->currentLocationId);
|
||||
}
|
||||
|
||||
void MenuManager::applyCurrentHealthBar() {
|
||||
logger() << "MenuManager::applyCurrentHealthBar called step 1" << std::endl;
|
||||
logger() << "currentPlayerMaxHp_ is " << gameState_.playerMaxHp << std::endl;
|
||||
if (gameState_.playerMaxHp <= 0.f) return;
|
||||
logger() << "MenuManager::applyCurrentHealthBar called step 2" << std::endl;
|
||||
logger() << "currentPlayerHp_ is " << gameState_.playerHp << std::endl;
|
||||
const auto snapshot = server_.snapshot();
|
||||
if (!snapshot) return;
|
||||
|
||||
const float fraction = std::clamp(gameState_.playerHp / gameState_.playerMaxHp, 0.f, 1.f);
|
||||
const auto playerIt = snapshot->players.find(server_.localPlayerId());
|
||||
if (playerIt == snapshot->players.end()) return;
|
||||
|
||||
const CharacterState& playerState = playerIt->second.state;
|
||||
const float hp = playerState.hp;
|
||||
const float maxHp = playerState.initialHp;
|
||||
logger() << "currentPlayerMaxHp_ is " << maxHp << std::endl;
|
||||
if (maxHp <= 0.f) return;
|
||||
logger() << "MenuManager::applyCurrentHealthBar called step 2" << std::endl;
|
||||
logger() << "currentPlayerHp_ is " << hp << std::endl;
|
||||
|
||||
const float fraction = std::clamp(hp / maxHp, 0.f, 1.f);
|
||||
|
||||
uiManager.setSliderValue("healthBarFill", fraction);
|
||||
std::string hpText = std::to_string(static_cast<int>(gameState_.playerHp)) + "/" +
|
||||
std::to_string(static_cast<int>(gameState_.playerMaxHp));
|
||||
std::string hpText = std::to_string(static_cast<int>(hp)) + "/" +
|
||||
std::to_string(static_cast<int>(maxHp));
|
||||
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
|
||||
uiManager.setText("healthValue", hpText);
|
||||
}
|
||||
@@ -2401,7 +2441,7 @@ namespace FRG {
|
||||
if (root.contains("soundEnabled"))
|
||||
audioPlayer_.setSoundEnabled(root["soundEnabled"].get<bool>());
|
||||
if (root.contains("fullscreen")) {
|
||||
#ifndef EMSCRIPTEN
|
||||
#ifndef __EMSCRIPTEN__
|
||||
Environment::setFullscreen(root["fullscreen"].get<bool>());
|
||||
#endif
|
||||
}
|
||||
@@ -2447,7 +2487,7 @@ namespace FRG {
|
||||
|
||||
openInventory();
|
||||
|
||||
const int itemCount = static_cast<int>(inventory->getItems().size());
|
||||
const int itemCount = static_cast<int>(inventoryItems().size());
|
||||
if (isPortraitMode() && wasOnItemDetail &&
|
||||
savedIndex >= 0 && savedIndex < itemCount) {
|
||||
selectInventoryItem(savedIndex);
|
||||
|
||||
+12
-11
@@ -3,8 +3,9 @@
|
||||
#include "Environment.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include "UiManager.h"
|
||||
#include "Localization.h"
|
||||
#include "items/Item.h"
|
||||
#include "GameState.h"
|
||||
#include "game_server/IGameServer.h"
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
@@ -39,9 +40,9 @@ namespace FRG {
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<Language, std::string>> localizedLocationName;
|
||||
|
||||
MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer);
|
||||
MenuManager(Renderer& iRenderer, IGameServer& server, AudioPlayerAsync& audioPlayer);
|
||||
|
||||
void setup(Inventory& inv, const std::string& zipFile);
|
||||
void setup(const std::string& zipFile);
|
||||
|
||||
void openInventory();
|
||||
void selectInventoryItem(int index);
|
||||
@@ -56,6 +57,8 @@ namespace FRG {
|
||||
|
||||
void showMainMenu();
|
||||
void showMenuAfterGameCompleted();
|
||||
// Returns to the main menu with the campaign list already open.
|
||||
void showCampaignScreen();
|
||||
bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; }
|
||||
|
||||
void openPhoneScreen();
|
||||
@@ -80,7 +83,7 @@ namespace FRG {
|
||||
void setChatUnread(int chatIndex, bool unread, const std::string& previewMsg = "");
|
||||
|
||||
void spendMoney(int amount);
|
||||
int getMoney() const { return gameState_.money; }
|
||||
int getMoney() const { return server_.snapshot()->money; }
|
||||
|
||||
// Starts a new game of the given campaign (see CampaignRegistry).
|
||||
std::function<void(const std::string& campaignId)> startGameFunc;
|
||||
@@ -89,7 +92,6 @@ namespace FRG {
|
||||
std::function<void()> startNightTransitionFunc;
|
||||
std::function<void(int)> chatOpenCallback;
|
||||
std::function<void()> skipCutsceneFunc;
|
||||
std::function<void()> callTaxiFunc;
|
||||
std::function<void(int)> onSaveGame;
|
||||
std::function<void(int)> onLoadGame;
|
||||
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
||||
@@ -172,14 +174,14 @@ namespace FRG {
|
||||
Renderer& renderer;
|
||||
|
||||
private:
|
||||
GameState& gameState_;
|
||||
IGameServer& server_;
|
||||
AudioPlayerAsync& audioPlayer_;
|
||||
|
||||
void startNewGame(const std::string& campaignId);
|
||||
// HUD layout for the current location and darklands mode.
|
||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||
// Single place that decides which HUD buttons and tutorial hints are visible:
|
||||
// buttons follow the inventory, hints follow gameState_.tutorialStep and the
|
||||
// buttons follow the inventory snapshot, hints follow the server tutorial state and
|
||||
// tutorial flags. Runs every frame; names absent from the shown screen are skipped.
|
||||
void refreshHudVisibility();
|
||||
void refreshQuestJournalUi();
|
||||
@@ -223,8 +225,6 @@ namespace FRG {
|
||||
void showLanguageScreen();
|
||||
void showLoadGameScreen();
|
||||
void showSaveGameScreen();
|
||||
// Campaign list opened by "Start game".
|
||||
void showCampaignScreen();
|
||||
// Text of a load/save slot button.
|
||||
std::string slotLabel(const SaveSlotInfo& info);
|
||||
|
||||
@@ -249,7 +249,8 @@ namespace FRG {
|
||||
static constexpr float TOAST_VISIBLE_MS = 2000.0f;
|
||||
|
||||
GameUiState uiState_ = GameUiState::Gameplay;
|
||||
Inventory* inventory = nullptr;
|
||||
std::vector<Item> inventoryItems() const;
|
||||
bool hasInventoryItem(const std::string& id) const;
|
||||
bool isPlayerInCreditsMenu = false;
|
||||
int inventorySelectedIndex_ = -1;
|
||||
|
||||
@@ -336,7 +337,7 @@ namespace FRG {
|
||||
enum class PhoneSubScreen { None, Main, Bank, Video, MapDorm, MapUni, ChatList, Chat };
|
||||
PhoneSubScreen currentPhoneSubScreen_ = PhoneSubScreen::None;
|
||||
|
||||
// Phone chat UI state (transient — rebuilt from gameState_.chatHistory on open)
|
||||
// Phone chat UI state (transient — rebuilt from the immutable server snapshot)
|
||||
struct PhoneChatBubbleInfo {
|
||||
std::string nodeName;
|
||||
float height;
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
#include <Eigen/Core>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
struct NpcCar
|
||||
|
||||
+307
-350
File diff suppressed because it is too large.
Load diff
+9
-24
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
@@ -11,37 +8,33 @@
|
||||
namespace FRG {
|
||||
|
||||
class Location;
|
||||
class Inventory;
|
||||
|
||||
class ScriptEngine : public ISaveable {
|
||||
class ScriptEngine {
|
||||
public:
|
||||
ScriptEngine();
|
||||
~ScriptEngine();
|
||||
|
||||
// Must be called once, after the Game's NPCs are ready.
|
||||
void init(Location* game, Inventory* inventory, AudioPlayerAsync* audioPlayer, const std::string& scriptPath);
|
||||
void init(Location* game, AudioPlayerAsync* audioPlayer, const std::string& scriptPath);
|
||||
|
||||
// Execute a Lua script file. Logs errors to stderr.
|
||||
//void runScript(const std::string& path);
|
||||
|
||||
void callItemPickupCallback(const std::string& objectName);
|
||||
|
||||
void callFunction(const std::string& functionName);
|
||||
void callActivateFunction(const std::string& functionName);
|
||||
|
||||
void callNpcInteractCallback(int npcIndex);
|
||||
void callNpcInteractCallback(const std::string& npcId);
|
||||
|
||||
void callTriggerEnterCallback(const std::string& zoneId);
|
||||
void callTriggerExitCallback(const std::string& zoneId);
|
||||
|
||||
// Fired when the player (moving) physically bumps into a standing NPC.
|
||||
void callNpcBumpedByPlayerCallback(int npcIndex);
|
||||
void callNpcBumpedByPlayerCallback(const std::string& npcId);
|
||||
// Fired when a moving NPC physically bumps into the standing player.
|
||||
void callNpcBumpsPlayerCallback(int npcIndex);
|
||||
|
||||
void setGlobalStore(std::unordered_map<std::string, int>* store);
|
||||
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
|
||||
void setQuestJournal(Quest::QuestJournal* journal);
|
||||
void callNpcBumpsPlayerCallback(const std::string& npcId);
|
||||
|
||||
void callLocationInitCallback();
|
||||
void callLocationEnterCallback();
|
||||
void callLocationExitCallback();
|
||||
|
||||
@@ -63,16 +56,8 @@ public:
|
||||
// has already run and registered all callbacks.
|
||||
void loadScriptGlobals(const nlohmann::json& in);
|
||||
|
||||
// Re-wires an in-flight, one-shot callback (NPC/player walk-arrival, object
|
||||
// animation-complete) that was serialized by name rather than by value.
|
||||
// Looks up `name` as a global Lua function and assigns a wrapped invoker to
|
||||
// outCallback; leaves outCallback empty and logs an error if not found.
|
||||
// Call after init() (the script must already have run) and after the owning
|
||||
// state (CharacterState / InteractiveObjectState) has been loaded.
|
||||
void rehydrateNamedCallback(const std::string& name, std::function<void()>& outCallback) const;
|
||||
|
||||
void save(nlohmann::json& out) const override { saveScriptGlobals(out); }
|
||||
void load(const nlohmann::json& in) override { loadScriptGlobals(in); }
|
||||
void save(nlohmann::json& out) const { saveScriptGlobals(out); }
|
||||
void load(const nlohmann::json& in) { loadScriptGlobals(in); }
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
|
||||
+3
-3
@@ -51,13 +51,13 @@ namespace FRG {
|
||||
std::string shaderProgramName;
|
||||
|
||||
// Sparks spawn within this radius of each emission point. 0 means
|
||||
// "spawn exactly at the point" (legacy behavior).
|
||||
// "spawn exactly at the point" (old behavior).
|
||||
float emissionRadius;
|
||||
|
||||
bool configured;
|
||||
// Rebuilds the CPU-side quad/UV arrays. If cameraViewMatrix is identity,
|
||||
// particles are rendered as axis-aligned quads in world XY and sorted by
|
||||
// world Z (legacy behavior). Otherwise quads are billboarded toward the
|
||||
// world Z (old behavior). Otherwise quads are billboarded toward the
|
||||
// camera using its world-space right/up axes, and particles are sorted
|
||||
// back-to-front in eye space for correct alpha blending.
|
||||
void prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix);
|
||||
@@ -106,7 +106,7 @@ namespace FRG {
|
||||
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
|
||||
// Pass the current camera view matrix (world → eye) to billboard sparks
|
||||
// toward the camera and sort them back-to-front. Identity (default)
|
||||
// keeps the legacy non-billboarded behavior.
|
||||
// keeps the old non-billboarded behavior.
|
||||
void prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix = Eigen::Matrix4f::Identity());
|
||||
|
||||
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
|
||||
|
||||
+2
-2
@@ -287,10 +287,10 @@ namespace FRG {
|
||||
float x1, u1;
|
||||
if (fillMode) {
|
||||
const float clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
if (clamped <= 0.0f) {
|
||||
/*if (clamped <= 0.0f) {
|
||||
trackMesh.RefreshVBO();
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
x1 = rect.x + rect.w * clamped;
|
||||
u1 = clamped;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
#include "LocationDefinition.h"
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include "../utils/Utils.h"
|
||||
#include <Eigen/Geometry>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
json loadJson(const std::string& path, const std::string& zipFile)
|
||||
{
|
||||
if (path.empty()) return json::object();
|
||||
std::string content;
|
||||
if (zipFile.empty()) {
|
||||
content = readTextFile(path);
|
||||
} else {
|
||||
const auto bytes = readFileFromZIP(path, zipFile);
|
||||
content.assign(bytes.begin(), bytes.end());
|
||||
}
|
||||
if (content.empty()) throw std::runtime_error("Failed to read " + path);
|
||||
try {
|
||||
return json::parse(content);
|
||||
} catch (const std::exception& error) {
|
||||
throw std::runtime_error(
|
||||
"JSON parse error in " + path + ": " + error.what());
|
||||
}
|
||||
}
|
||||
|
||||
GameObjectData parseGameObject(const json& item)
|
||||
{
|
||||
GameObjectData data;
|
||||
data.name = item.value("name", "Unknown");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
data.textureDarkandsPath = item.value("textureDarkandsPath", "");
|
||||
data.meshPath = item.value("meshPath", "");
|
||||
data.rotationX = item.value("rotationX", 0.f);
|
||||
data.rotationY = item.value("rotationY", 0.f);
|
||||
data.rotationZ = item.value("rotationZ", 0.f);
|
||||
data.positionX = item.value("positionX", 0.f);
|
||||
data.positionY = item.value("positionY", 0.f);
|
||||
data.positionZ = item.value("positionZ", 0.f);
|
||||
data.scale = item.value("scale", 1.f);
|
||||
data.instanced = item.value("instanced", true);
|
||||
return data;
|
||||
}
|
||||
|
||||
Eigen::Vector3f vector(
|
||||
const json& item, const char* x, const char* y, const char* z,
|
||||
const Eigen::Vector3f& fallback = Eigen::Vector3f::Zero())
|
||||
{
|
||||
return {item.value(x, fallback.x()), item.value(y, fallback.y()),
|
||||
item.value(z, fallback.z())};
|
||||
}
|
||||
|
||||
std::pair<int, int> readBinaryAnimationMetadata(
|
||||
const std::string& path, const std::string& zipFile)
|
||||
{
|
||||
const std::vector<char> bytes = zipFile.empty()
|
||||
? readFile(path) : readFileFromZIP(path, zipFile);
|
||||
if (bytes.size() < 8)
|
||||
throw std::runtime_error("Invalid animation file " + path);
|
||||
const char* cursor = bytes.data();
|
||||
const char* end = cursor + bytes.size();
|
||||
auto skip = [&](std::size_t size) {
|
||||
if (size > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
cursor += size;
|
||||
};
|
||||
auto readU32 = [&]() {
|
||||
std::uint32_t value = 0;
|
||||
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
std::memcpy(&value, cursor, sizeof(value));
|
||||
cursor += sizeof(value);
|
||||
return value;
|
||||
};
|
||||
auto readI32 = [&]() {
|
||||
std::int32_t value = 0;
|
||||
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
std::memcpy(&value, cursor, sizeof(value));
|
||||
cursor += sizeof(value);
|
||||
return value;
|
||||
};
|
||||
auto skipString = [&]() { skip(readU32()); };
|
||||
|
||||
if (std::memcmp(cursor, "BSMF", 4) != 0)
|
||||
throw std::runtime_error("Invalid animation file " + path);
|
||||
skip(4);
|
||||
if (readU32() != 2)
|
||||
throw std::runtime_error("Unsupported animation version in " + path);
|
||||
skip(16 * sizeof(float));
|
||||
const std::uint32_t boneCount = readU32();
|
||||
for (std::uint32_t bone = 0; bone < boneCount; ++bone) {
|
||||
skip(3 * sizeof(float) + sizeof(float) + 9 * sizeof(float));
|
||||
skip(sizeof(std::int32_t));
|
||||
skip(static_cast<std::size_t>(readU32()) * sizeof(std::int32_t));
|
||||
}
|
||||
for (std::uint32_t bone = 0; bone < boneCount; ++bone) skipString();
|
||||
|
||||
const std::uint32_t meshCount = readU32();
|
||||
for (std::uint32_t mesh = 0; mesh < meshCount; ++mesh) {
|
||||
skipString();
|
||||
const std::uint32_t vertexCount = readU32();
|
||||
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
|
||||
const std::uint32_t faceCount = readU32();
|
||||
skip(static_cast<std::size_t>(faceCount) * 3 * 2 * sizeof(float));
|
||||
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
|
||||
const std::uint32_t triangleCount = readU32();
|
||||
skip(static_cast<std::size_t>(triangleCount) * 3 * sizeof(std::int32_t));
|
||||
for (std::uint32_t vertex = 0; vertex < vertexCount; ++vertex)
|
||||
skip(static_cast<std::size_t>(readU32()) *
|
||||
(sizeof(std::int32_t) + sizeof(float)));
|
||||
}
|
||||
|
||||
const std::uint32_t keyframeCount = readU32();
|
||||
int lastFrame = 0;
|
||||
for (std::uint32_t frame = 0; frame < keyframeCount; ++frame) {
|
||||
lastFrame = readI32();
|
||||
skip(static_cast<std::size_t>(boneCount) *
|
||||
(3 * sizeof(float) + 16 * sizeof(float)));
|
||||
}
|
||||
return {0, keyframeCount == 0 ? 1 : lastFrame + 1};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const
|
||||
{
|
||||
LoadedGameObjectState state;
|
||||
state.texturePath = texturePath;
|
||||
state.textureDarkandsPath = textureDarkandsPath;
|
||||
state.meshPath = meshPath;
|
||||
state.meshRotationX = rotationX;
|
||||
state.meshRotationY = rotationY;
|
||||
state.meshRotationZ = rotationZ;
|
||||
state.meshScale = scale;
|
||||
return state;
|
||||
}
|
||||
|
||||
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const
|
||||
{
|
||||
InteractiveObjectState state;
|
||||
state.objectInfo = base.toLoadedGameObjectState();
|
||||
state.interactionRadius = interactionRadius;
|
||||
state.approachRadius = approachRadius;
|
||||
state.castShadow = castShadow;
|
||||
state.castShadowNight = castShadowNight;
|
||||
state.isActive = isActive;
|
||||
state.activateFunctionName = activateFunctionName;
|
||||
state.pivot = pivot;
|
||||
state.boundsMin = boundsMin;
|
||||
state.boundsMax = boundsMax;
|
||||
state.hasInteractionPosition = hasInteractionPosition;
|
||||
state.interactionPosition = interactionPosition;
|
||||
state.jsonPositionX = base.positionX;
|
||||
state.jsonPositionY = base.positionY;
|
||||
state.jsonPositionZ = base.positionZ;
|
||||
state.position =
|
||||
Eigen::Vector3f(base.positionX, base.positionY, base.positionZ);
|
||||
return state;
|
||||
}
|
||||
|
||||
CharacterState NpcData::toCharacterState() const
|
||||
{
|
||||
CharacterState state;
|
||||
state.npcName = name;
|
||||
state.position = {positionX, positionY, positionZ};
|
||||
state.homePosition = {positionX, 0.f, positionZ};
|
||||
state.stopInPlace();
|
||||
state.facingAngle = facingAngle;
|
||||
state.walkSpeed = walkSpeed;
|
||||
state.rotationSpeed = rotationSpeed;
|
||||
state.modelScale = modelScale;
|
||||
state.setHp(hp);
|
||||
state.initialHp = hp;
|
||||
state.visibleRange = visibleRange;
|
||||
state.canAttack = canAttack;
|
||||
state.enabled = enabled;
|
||||
state.collisionRadius = navigationAvoidanceRadius;
|
||||
state.movementContactRadius =
|
||||
(std::min)(movementContactRadius, navigationAvoidanceRadius);
|
||||
state.interactionRadius = interactionRadius;
|
||||
state.configureNavigationRetryCooldowns(
|
||||
id, navigationRetryCooldownTicks);
|
||||
|
||||
Eigen::Quaternionf correction = Eigen::Quaternionf::Identity();
|
||||
if (modelCorrectionRotX != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotX, Eigen::Vector3f::UnitX())) * correction;
|
||||
if (modelCorrectionRotY != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotY, Eigen::Vector3f::UnitY())) * correction;
|
||||
if (modelCorrectionRotZ != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * correction;
|
||||
state.modelCorrectionRotation = correction;
|
||||
|
||||
auto addAnimation = [&](AnimationState animation, const std::string& path) {
|
||||
if (!path.empty())
|
||||
state.creationInfo.animationPaths.emplace_back(animation, path);
|
||||
};
|
||||
addAnimation(AnimationState::STAND, animationIdlePath);
|
||||
addAnimation(AnimationState::WALK, animationWalkPath);
|
||||
addAnimation(AnimationState::ACTION_IDLE, animationActionIdlePath);
|
||||
addAnimation(AnimationState::ACTION_ATTACK, animationActionAttackPath);
|
||||
addAnimation(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
|
||||
addAnimation(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
|
||||
addAnimation(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
|
||||
addAnimation(AnimationState::DEATH_IDLE, animationDeathIdlePath);
|
||||
state.creationInfo.meshTexturePaths = {
|
||||
meshTextures.begin(), meshTextures.end()};
|
||||
state.creationInfo.singleTexturePath = texturePath;
|
||||
return state;
|
||||
}
|
||||
|
||||
void loadCharacterAnimationMetadata(
|
||||
CharacterState& state, const std::string& zipFile)
|
||||
{
|
||||
for (const auto& [animation, path] : state.creationInfo.animationPaths) {
|
||||
if (path.size() < 5 || path.substr(path.size() - 5) != ".anim")
|
||||
continue;
|
||||
try {
|
||||
const auto [startFrame, frameCount] =
|
||||
readBinaryAnimationMetadata(path, zipFile);
|
||||
state.setAnimationMetadata(animation, startFrame, frameCount);
|
||||
} catch (const std::exception& error) {
|
||||
logger() << "[content] Animation metadata failed for " << path
|
||||
<< ": " << error.what() << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocationDefinition LocationContentLoader::load(
|
||||
const LocationConfig& config, const std::string& zipFile)
|
||||
{
|
||||
LocationDefinition result;
|
||||
result.config = config;
|
||||
|
||||
if (!config.gameObjectsJsonPath.empty()) {
|
||||
const json root = loadJson(config.gameObjectsJsonPath, zipFile);
|
||||
for (const auto& item : root.value("objects", json::array())) {
|
||||
GameObjectData data = parseGameObject(item);
|
||||
if (!data.meshPath.empty())
|
||||
result.staticObjects.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.interactiveObjectsJsonPath.empty()) {
|
||||
const json root = loadJson(config.interactiveObjectsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("objects", json::array())) {
|
||||
InteractiveObjectData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate interactive-object id in " +
|
||||
config.interactiveObjectsJsonPath);
|
||||
data.base = parseGameObject(item);
|
||||
data.interactionRadius = item.value("interactionRadius", 2.f);
|
||||
data.approachRadius =
|
||||
item.value("approachRadius", data.interactionRadius);
|
||||
data.activateFunctionName =
|
||||
item.value("activateFunction", "");
|
||||
data.pivot = vector(item, "pivotX", "pivotY", "pivotZ");
|
||||
data.boundsMin =
|
||||
vector(item, "boundsMinX", "boundsMinY", "boundsMinZ");
|
||||
data.boundsMax =
|
||||
vector(item, "boundsMaxX", "boundsMaxY", "boundsMaxZ");
|
||||
data.hasInteractionPosition =
|
||||
item.contains("interactionPositionX");
|
||||
if (data.hasInteractionPosition) {
|
||||
data.interactionPosition = vector(
|
||||
item, "interactionPositionX", "interactionPositionY",
|
||||
"interactionPositionZ");
|
||||
}
|
||||
data.castShadow = item.value("castShadow", true);
|
||||
data.castShadowNight = item.value("castShadowNight", true);
|
||||
data.isActive = item.value("isActive", true);
|
||||
result.interactiveObjects.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.npcsJsonPath.empty()) {
|
||||
const json root = loadJson(config.npcsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("npcs", json::array())) {
|
||||
NpcData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || data.id == "player" ||
|
||||
!ids.insert(data.id).second) {
|
||||
throw std::runtime_error(
|
||||
"Missing, duplicate, or reserved NPC id in " +
|
||||
config.npcsJsonPath);
|
||||
}
|
||||
data.name = item.value("name", "Unknown NPC");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
if (item.contains("meshTextures") &&
|
||||
item["meshTextures"].is_object()) {
|
||||
for (const auto& [name, path] :
|
||||
item["meshTextures"].items()) {
|
||||
if (path.is_string())
|
||||
data.meshTextures[name] = path.get<std::string>();
|
||||
}
|
||||
}
|
||||
data.interactionRadius =
|
||||
item.value("interactionRadius", 0.f);
|
||||
data.animationIdlePath =
|
||||
item.value("animationIdlePath", "");
|
||||
data.animationWalkPath =
|
||||
item.value("animationWalkPath", "");
|
||||
data.animationActionIdlePath =
|
||||
item.value("animationActionIdlePath", "");
|
||||
data.animationActionAttackPath =
|
||||
item.value("animationActionAttackPath", "");
|
||||
data.animationStandToActionPath =
|
||||
item.value("animationStandToActionPath", "");
|
||||
data.animationActionToStandPath =
|
||||
item.value("animationActionToStandPath", "");
|
||||
data.animationActionToDeathPath =
|
||||
item.value("animationActionToDeathPath", "");
|
||||
data.animationDeathIdlePath =
|
||||
item.value("animationDeathIdlePath", "");
|
||||
data.positionX = item.value("positionX", 0.f);
|
||||
data.positionY = item.value("positionY", 0.f);
|
||||
data.positionZ = item.value("positionZ", 0.f);
|
||||
data.walkSpeed = item.value("walkSpeed", 1.5f);
|
||||
data.rotationSpeed = item.value("rotationSpeed", 8.f);
|
||||
data.modelScale = item.value("modelScale", 0.01f);
|
||||
const float degrees = kPi / 180.f;
|
||||
data.modelCorrectionRotX =
|
||||
item.value("modelCorrectionRotX", 0.f) * degrees;
|
||||
data.modelCorrectionRotY =
|
||||
item.value("modelCorrectionRotY", 0.f) * degrees;
|
||||
data.modelCorrectionRotZ =
|
||||
item.value("modelCorrectionRotZ", 0.f) * degrees;
|
||||
data.facingAngle =
|
||||
item.value("facingAngle", 0.f) * degrees;
|
||||
data.hp = item.value("hp", 100.f);
|
||||
data.visibleRange =
|
||||
item.value("visibleRange", VISIBLE_RANGE);
|
||||
data.canAttack = item.value("canAttack", false);
|
||||
data.enabled = item.value("enabled", true);
|
||||
data.navigationAvoidanceRadius = item.value(
|
||||
"navigationAvoidanceRadius",
|
||||
item.value("collisionRadius", 0.45f));
|
||||
data.navigationAvoidanceRadius =
|
||||
(std::max)(0.05f, data.navigationAvoidanceRadius);
|
||||
data.movementContactRadius = item.value(
|
||||
"movementContactRadius",
|
||||
data.navigationAvoidanceRadius * 0.72f);
|
||||
data.movementContactRadius = (std::max)(
|
||||
0.05f, (std::min)(
|
||||
data.movementContactRadius,
|
||||
data.navigationAvoidanceRadius));
|
||||
const char* cooldownKey =
|
||||
item.contains("navigationRetryCooldownTicks")
|
||||
? "navigationRetryCooldownTicks"
|
||||
: "navigationRetryCooldownMs";
|
||||
if (item.contains(cooldownKey) &&
|
||||
item.at(cooldownKey).is_array()) {
|
||||
for (const auto& value : item.at(cooldownKey)) {
|
||||
if (!value.is_number_integer()) continue;
|
||||
int ticks = value.get<int>();
|
||||
if (std::string(cooldownKey) ==
|
||||
"navigationRetryCooldownMs") {
|
||||
ticks = (std::max)(1, (ticks + 99) / 100);
|
||||
}
|
||||
data.navigationRetryCooldownTicks.push_back(ticks);
|
||||
}
|
||||
}
|
||||
result.npcs.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.teleportsJsonPath.empty()) {
|
||||
const json root = loadJson(config.teleportsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("teleports", json::array())) {
|
||||
TeleportDefinition data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate teleport id in " +
|
||||
config.teleportsJsonPath);
|
||||
data.active = item.value("active", false);
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
data.radius = item.value("radius", 0.f);
|
||||
data.automatic = item.value("automatic", false);
|
||||
data.automaticRadius =
|
||||
item.value("automaticRadius", data.radius);
|
||||
data.destinationLocation =
|
||||
item.value("destinationLocation", "");
|
||||
data.destinationPosition = vector(
|
||||
item, "destinationPositionX", "destinationPositionY",
|
||||
"destinationPositionZ");
|
||||
data.destinationRotationY =
|
||||
item.value("destinationRotationY", 0.f);
|
||||
result.teleports.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.triggerZonesJsonPath.empty()) {
|
||||
const json root = loadJson(config.triggerZonesJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item :
|
||||
root.value("trigger_zones", json::array())) {
|
||||
TriggerZoneDefinition data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate trigger-zone id in " +
|
||||
config.triggerZonesJsonPath);
|
||||
data.enabled = item.value("enabled", true);
|
||||
data.radius = item.value("radius", 1.5f);
|
||||
data.hysteresis = item.value("hysteresis", 0.3f);
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
result.triggerZones.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.lightsJsonPath.empty()) {
|
||||
const json root = loadJson(config.lightsJsonPath, zipFile);
|
||||
for (const auto& item : root.value("lights", json::array())) {
|
||||
PointLightDefinition data;
|
||||
data.id = item.value("id", "");
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
data.direction = vector(
|
||||
item, "directionX", "directionY", "directionZ",
|
||||
Eigen::Vector3f(0.f, -1.f, 0.f));
|
||||
data.color = vector(
|
||||
item, "colorR", "colorG", "colorB",
|
||||
Eigen::Vector3f::Ones());
|
||||
data.autoLight = item.value("autoLight", false);
|
||||
data.autoLightDistance =
|
||||
item.value("autoLightDistance", -1.f);
|
||||
data.limitX = item.value("limitX", 99999.f);
|
||||
data.limitY = item.value("limitY", 99999.f);
|
||||
data.autoLightLimitX =
|
||||
item.value("autoLightLimitX", -1.f);
|
||||
data.autoLightLimitY =
|
||||
item.value("autoLightLimitY", -1.f);
|
||||
result.pointLights.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Campaign.h"
|
||||
#include "../CharacterState.h"
|
||||
#include "../items/InteractiveObjectState.h"
|
||||
#include <Eigen/Core>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct GameObjectData {
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::string textureDarkandsPath;
|
||||
std::string meshPath;
|
||||
float rotationX = 0.f;
|
||||
float rotationY = 0.f;
|
||||
float rotationZ = 0.f;
|
||||
float positionX = 0.f;
|
||||
float positionY = 0.f;
|
||||
float positionZ = 0.f;
|
||||
float scale = 1.f;
|
||||
bool instanced = true;
|
||||
|
||||
LoadedGameObjectState toLoadedGameObjectState() const;
|
||||
};
|
||||
|
||||
struct InteractiveObjectData {
|
||||
std::string id;
|
||||
GameObjectData base;
|
||||
float interactionRadius = 2.f;
|
||||
float approachRadius = 2.f;
|
||||
std::string activateFunctionName;
|
||||
Eigen::Vector3f pivot = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f boundsMin = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f boundsMax = Eigen::Vector3f::Zero();
|
||||
bool hasInteractionPosition = false;
|
||||
Eigen::Vector3f interactionPosition = Eigen::Vector3f::Zero();
|
||||
bool castShadow = true;
|
||||
bool castShadowNight = true;
|
||||
bool isActive = true;
|
||||
|
||||
InteractiveObjectState toInteractiveObjectState() const;
|
||||
};
|
||||
|
||||
struct NpcData {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::map<std::string, std::string> meshTextures;
|
||||
std::string animationIdlePath;
|
||||
std::string animationWalkPath;
|
||||
std::string animationActionIdlePath;
|
||||
std::string animationActionAttackPath;
|
||||
std::string animationStandToActionPath;
|
||||
std::string animationActionToStandPath;
|
||||
std::string animationActionToDeathPath;
|
||||
std::string animationDeathIdlePath;
|
||||
float positionX = 0.f;
|
||||
float positionY = 0.f;
|
||||
float positionZ = 0.f;
|
||||
float walkSpeed = 1.5f;
|
||||
float rotationSpeed = 8.f;
|
||||
float modelScale = 0.01f;
|
||||
float modelCorrectionRotX = 0.f;
|
||||
float modelCorrectionRotY = 0.f;
|
||||
float modelCorrectionRotZ = 0.f;
|
||||
float facingAngle = 0.f;
|
||||
float hp = 100.f;
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
bool canAttack = false;
|
||||
bool enabled = true;
|
||||
float navigationAvoidanceRadius = 0.45f;
|
||||
float movementContactRadius = 0.32f;
|
||||
float interactionRadius = 0.f;
|
||||
std::vector<int> navigationRetryCooldownTicks;
|
||||
|
||||
CharacterState toCharacterState() const;
|
||||
};
|
||||
|
||||
struct TeleportDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 0.f;
|
||||
bool active = false;
|
||||
std::string destinationLocation;
|
||||
Eigen::Vector3f destinationPosition = Eigen::Vector3f::Zero();
|
||||
float destinationRotationY = 0.f;
|
||||
bool automatic = false;
|
||||
float automaticRadius = 0.f;
|
||||
};
|
||||
|
||||
struct TriggerZoneDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 1.5f;
|
||||
float hysteresis = 0.3f;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
struct PointLightDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f direction = Eigen::Vector3f(0.f, -1.f, 0.f);
|
||||
Eigen::Vector3f color = Eigen::Vector3f::Ones();
|
||||
bool autoLight = false;
|
||||
float autoLightDistance = -1.f;
|
||||
float limitX = 99999.f;
|
||||
float limitY = 99999.f;
|
||||
float autoLightLimitX = -1.f;
|
||||
float autoLightLimitY = -1.f;
|
||||
};
|
||||
|
||||
struct LocationDefinition {
|
||||
LocationConfig config;
|
||||
std::vector<GameObjectData> staticObjects;
|
||||
std::vector<InteractiveObjectData> interactiveObjects;
|
||||
std::vector<NpcData> npcs;
|
||||
std::vector<TeleportDefinition> teleports;
|
||||
std::vector<TriggerZoneDefinition> triggerZones;
|
||||
std::vector<PointLightDefinition> pointLights;
|
||||
};
|
||||
|
||||
class LocationContentLoader {
|
||||
public:
|
||||
static LocationDefinition load(
|
||||
const LocationConfig& config, const std::string& zipFile = {});
|
||||
};
|
||||
|
||||
void loadCharacterAnimationMetadata(
|
||||
CharacterState& state, const std::string& zipFile = {});
|
||||
|
||||
} // namespace FRG
|
||||
@@ -162,21 +162,24 @@ void DialogueRuntime::selectChoice(int index) {
|
||||
}
|
||||
|
||||
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
||||
if (flagStore) (*flagStore)[name] = value;
|
||||
if (setGlobalFlag) setGlobalFlag(name, value);
|
||||
}
|
||||
|
||||
int DialogueRuntime::getFlag(const std::string& name) const {
|
||||
if (!flagStore) return 0;
|
||||
auto it = flagStore->find(name);
|
||||
return (it != flagStore->end()) ? it->second : 0;
|
||||
return getGlobalFlag ? getGlobalFlag(name) : 0;
|
||||
}
|
||||
|
||||
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
||||
flagStore = store;
|
||||
void DialogueRuntime::setGlobalFlagAccessors(
|
||||
std::function<int(const std::string&)> getter,
|
||||
std::function<void(const std::string&, int)> setter)
|
||||
{
|
||||
getGlobalFlag = std::move(getter);
|
||||
setGlobalFlag = std::move(setter);
|
||||
}
|
||||
|
||||
void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) {
|
||||
questJournal = journal;
|
||||
void DialogueRuntime::setQuestActionHandler(
|
||||
std::function<void(const QuestActionRequest&)> handler) {
|
||||
onQuestAction = std::move(handler);
|
||||
}
|
||||
|
||||
void DialogueRuntime::applyQuestActions(
|
||||
@@ -184,17 +187,20 @@ void DialogueRuntime::applyQuestActions(
|
||||
const std::string& questFail, const std::string& objectiveComplete,
|
||||
const std::string& objectiveVisible)
|
||||
{
|
||||
if (!questJournal) return;
|
||||
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
|
||||
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
|
||||
if (!questFail.empty()) questJournal->failQuest(questFail);
|
||||
if (!onQuestAction) return;
|
||||
if (!questUnlock.empty())
|
||||
onQuestAction({QuestActionType::Unlock, questUnlock, {}});
|
||||
if (!questComplete.empty())
|
||||
onQuestAction({QuestActionType::Complete, questComplete, {}});
|
||||
if (!questFail.empty())
|
||||
onQuestAction({QuestActionType::Fail, questFail, {}});
|
||||
if (!objectiveComplete.empty()) {
|
||||
auto [qId, oId] = splitDot(objectiveComplete);
|
||||
questJournal->setObjectiveCompleted(qId, oId);
|
||||
onQuestAction({QuestActionType::CompleteObjective, qId, oId});
|
||||
}
|
||||
if (!objectiveVisible.empty()) {
|
||||
auto [qId, oId] = splitDot(objectiveVisible);
|
||||
questJournal->setObjectiveVisible(qId, oId);
|
||||
onQuestAction({QuestActionType::ShowObjective, qId, oId});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,29 @@
|
||||
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG::Dialogue {
|
||||
|
||||
enum class QuestActionType {
|
||||
Unlock,
|
||||
Complete,
|
||||
Fail,
|
||||
CompleteObjective,
|
||||
ShowObjective
|
||||
};
|
||||
|
||||
struct QuestActionRequest {
|
||||
QuestActionType type;
|
||||
std::string questId;
|
||||
std::string objectiveId;
|
||||
};
|
||||
|
||||
class DialogueRuntime : public FRG::ISaveable {
|
||||
public:
|
||||
void setDatabase(const DialogueDatabase* value);
|
||||
@@ -41,8 +53,11 @@ public:
|
||||
|
||||
void setFlag(const std::string& name, int value);
|
||||
int getFlag(const std::string& name) const;
|
||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
|
||||
void setQuestJournal(Quest::QuestJournal* journal);
|
||||
void setGlobalFlagAccessors(
|
||||
std::function<int(const std::string&)> getter,
|
||||
std::function<void(const std::string&, int)> setter);
|
||||
void setQuestActionHandler(
|
||||
std::function<void(const QuestActionRequest&)> handler);
|
||||
|
||||
// Saves whether a dialogue is active, its id, and current node.
|
||||
// Cutscene state is not saved.
|
||||
@@ -62,13 +77,14 @@ private:
|
||||
std::function<void(const std::string&, const std::string&)> onCutsceneStartNeeded;
|
||||
std::function<void(const std::string&)> onDialogueLineStarted;
|
||||
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
||||
std::function<void(const QuestActionRequest&)> onQuestAction;
|
||||
|
||||
const DialogueDatabase* database = nullptr;
|
||||
const TranslationDatabase* translations = nullptr;
|
||||
Quest::QuestJournal* questJournal = nullptr;
|
||||
const DialogueDefinition* activeDialogue = nullptr;
|
||||
|
||||
std::unordered_map<std::string, int>* flagStore = nullptr;
|
||||
std::function<int(const std::string&)> getGlobalFlag;
|
||||
std::function<void(const std::string&, int)> setGlobalFlag;
|
||||
std::unordered_set<std::string> consumedChoices;
|
||||
|
||||
std::string currentNodeId;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "cutscene/CutsceneDatabase.h"
|
||||
#include "cutscene/CutsceneOverlay.h"
|
||||
#include "cutscene/CutsceneRuntime.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "ISaveable.h"
|
||||
#include <SDL.h>
|
||||
#include <functional>
|
||||
@@ -14,7 +13,7 @@
|
||||
|
||||
namespace FRG::Dialogue {
|
||||
|
||||
class DialogueSystem : public FRG::ISaveable {
|
||||
class DialogueSystem {
|
||||
public:
|
||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||
|
||||
@@ -48,8 +47,8 @@ public:
|
||||
void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); }
|
||||
void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); }
|
||||
|
||||
void save(nlohmann::json& out) const override { saveDialogueState(out); }
|
||||
void load(const nlohmann::json& in) override { loadDialogueState(in); }
|
||||
void save(nlohmann::json& out) const { saveDialogueState(out); }
|
||||
void load(const nlohmann::json& in) { loadDialogueState(in); }
|
||||
|
||||
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
|
||||
bool isDialogueActive() const { return dialogueRuntime.isActive(); }
|
||||
@@ -58,11 +57,15 @@ public:
|
||||
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
|
||||
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
|
||||
|
||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
||||
dialogueRuntime.setGlobalFlagStore(store);
|
||||
void setGlobalFlagAccessors(
|
||||
std::function<int(const std::string&)> getter,
|
||||
std::function<void(const std::string&, int)> setter) {
|
||||
dialogueRuntime.setGlobalFlagAccessors(
|
||||
std::move(getter), std::move(setter));
|
||||
}
|
||||
void setQuestJournal(Quest::QuestJournal* journal) {
|
||||
dialogueRuntime.setQuestJournal(journal);
|
||||
void setQuestActionHandler(
|
||||
std::function<void(const QuestActionRequest&)> handler) {
|
||||
dialogueRuntime.setQuestActionHandler(std::move(handler));
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include "../Location.h"
|
||||
#include <utility>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class GameServerHostRouter final : public IGameServerHost {
|
||||
public:
|
||||
explicit GameServerHostRouter(IGameServerHost& initial)
|
||||
: active_(&initial)
|
||||
{
|
||||
}
|
||||
|
||||
void use(IGameServerHost& host) { active_ = &host; }
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override {
|
||||
active_->initializeCampaign(campaignId, questsPath, zipFile);
|
||||
}
|
||||
void clearCampaign() override { active_->clearCampaign(); }
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override {
|
||||
active_->resetCampaign(questsPath, zipFile);
|
||||
}
|
||||
bool prepareLocation(const LocationConfig& config) override {
|
||||
return active_->prepareLocation(config);
|
||||
}
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override {
|
||||
active_->setupRuntimeLocation(
|
||||
id, location, std::move(params), audioPlayer);
|
||||
}
|
||||
Location* runtimeLocation(const std::string& id) const override {
|
||||
return active_->runtimeLocation(id);
|
||||
}
|
||||
bool hasRuntimeLocation(const std::string& id) const override {
|
||||
return active_->hasRuntimeLocation(id);
|
||||
}
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override {
|
||||
return active_->loadedRuntimeLocationIds();
|
||||
}
|
||||
void clearRuntimeLocations(bool retainState) override {
|
||||
active_->clearRuntimeLocations(retainState);
|
||||
}
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override {
|
||||
active_->bindRuntimeLocation(id, std::move(location));
|
||||
}
|
||||
void update(std::int64_t deltaMs) override { active_->update(deltaMs); }
|
||||
void save(nlohmann::json& out) override { active_->save(out); }
|
||||
void load(const nlohmann::json& in) override { active_->load(in); }
|
||||
|
||||
private:
|
||||
IGameServerHost* active_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Keeps references held by UI and runtime objects stable while campaigns switch
|
||||
// between the local and remote implementations.
|
||||
class GameServerRouter final : public IGameServer {
|
||||
public:
|
||||
explicit GameServerRouter(IGameServer& initial)
|
||||
: active_(&initial)
|
||||
{
|
||||
}
|
||||
|
||||
void use(IGameServer& server) { active_ = &server; }
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override {
|
||||
return active_->snapshot();
|
||||
}
|
||||
const std::string& localPlayerId() const override {
|
||||
return active_->localPlayerId();
|
||||
}
|
||||
void submit(const GameCommand& command) override { active_->submit(command); }
|
||||
std::vector<GameServerEvent> drainEvents() override {
|
||||
return active_->drainEvents();
|
||||
}
|
||||
void update(std::int64_t deltaMs) override { active_->update(deltaMs); }
|
||||
void save(nlohmann::json& out) const override { active_->save(out); }
|
||||
void load(const nlohmann::json& in) override { active_->load(in); }
|
||||
|
||||
private:
|
||||
IGameServer* active_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
|
||||
#include "../GameState.h"
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Value-only view of authoritative state. It deliberately contains no runtime
|
||||
// pointers, callbacks, renderer objects, or mutable references, so the same
|
||||
// contract can later be transported over a network.
|
||||
struct ChatMessageSnapshot {
|
||||
std::string text;
|
||||
bool incoming = false;
|
||||
};
|
||||
|
||||
struct CharacterSnapshot {
|
||||
std::string id;
|
||||
CharacterState state;
|
||||
};
|
||||
|
||||
struct InteractiveObjectSnapshot {
|
||||
std::string id;
|
||||
InteractiveObjectState state;
|
||||
};
|
||||
|
||||
struct LocationSnapshot {
|
||||
std::string id;
|
||||
LocationNetworkState state;
|
||||
std::unordered_map<std::string, CharacterSnapshot> npcs;
|
||||
std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects;
|
||||
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue;
|
||||
nlohmann::json scriptGlobals;
|
||||
};
|
||||
|
||||
struct GameSnapshot {
|
||||
std::uint64_t revision = 0;
|
||||
std::string campaignId;
|
||||
std::string currentLocationId;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
int money = 0;
|
||||
TutorialStep tutorialStep = TutorialStep::Completed;
|
||||
bool tutorialPhoneChatScreenOpened = false;
|
||||
bool tutorialJournalScreenOpened = false;
|
||||
bool tutorialNeedOpenTaxiScreen = false;
|
||||
bool tutorialTaxiHintDone = false;
|
||||
bool taxiIsCalled = false;
|
||||
bool chatUnread[3] = {};
|
||||
std::string chatPreview[3];
|
||||
std::vector<ChatMessageSnapshot> chatHistory[3];
|
||||
std::vector<std::string> inventoryItemIds;
|
||||
nlohmann::json questJournal;
|
||||
std::unordered_map<std::string, int> globalInts;
|
||||
std::unordered_map<std::string, float> globalFloats;
|
||||
std::unordered_map<std::string, CharacterSnapshot> players;
|
||||
std::unordered_map<std::string, LocationSnapshot> locations;
|
||||
std::vector<Quest::QuestState> quests;
|
||||
|
||||
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
||||
};
|
||||
|
||||
struct SetWorldModeCommand { bool darklands; bool night; bool dawn; };
|
||||
struct SetCurrentLocationCommand { std::string locationId; };
|
||||
struct SetMoneyCommand { int amount; };
|
||||
struct SpendMoneyCommand { int amount; };
|
||||
struct CompleteTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialFlagsCommand {
|
||||
bool phoneChatOpened;
|
||||
bool journalOpened;
|
||||
bool needTaxiHint;
|
||||
bool taxiHintDone;
|
||||
};
|
||||
struct SetChatCommand {
|
||||
int index;
|
||||
bool unread;
|
||||
std::string preview;
|
||||
};
|
||||
struct AppendChatMessageCommand {
|
||||
int index;
|
||||
std::string text;
|
||||
bool incoming;
|
||||
};
|
||||
struct SetTaxiCalledCommand { bool called; };
|
||||
struct StartTaxiCommand { std::string locationId; };
|
||||
struct ResetTaxiCommand { std::string locationId; };
|
||||
struct SetLocationTargetsCommand {
|
||||
std::string locationId;
|
||||
std::string interactiveObjectId;
|
||||
std::string npcId;
|
||||
std::string teleportZoneId;
|
||||
};
|
||||
struct SwitchLocationNavigationCommand {
|
||||
std::string locationId;
|
||||
int index;
|
||||
};
|
||||
struct SetNavigationAreaAvailableCommand {
|
||||
std::string locationId;
|
||||
std::string areaName;
|
||||
bool available;
|
||||
};
|
||||
struct AddNavigationObstacleCommand {
|
||||
std::string locationId;
|
||||
std::string name;
|
||||
std::vector<Eigen::Vector2f> polygon;
|
||||
};
|
||||
struct AddInventoryItemCommand { std::string itemId; };
|
||||
struct RemoveInventoryItemCommand { std::string itemId; };
|
||||
struct UnlockQuestCommand { std::string questId; };
|
||||
struct CompleteQuestCommand { std::string questId; };
|
||||
struct FailQuestCommand { std::string questId; };
|
||||
struct SetQuestObjectiveCompletedCommand {
|
||||
std::string questId;
|
||||
std::string objectiveId;
|
||||
bool completed;
|
||||
};
|
||||
struct SetQuestObjectiveVisibleCommand {
|
||||
std::string questId;
|
||||
std::string objectiveId;
|
||||
bool visible;
|
||||
};
|
||||
struct SetQuestActiveObjectiveCommand {
|
||||
std::string questId;
|
||||
int objectiveIndex;
|
||||
};
|
||||
struct SetGlobalIntCommand { std::string key; int value; };
|
||||
struct SetGlobalFloatCommand { std::string key; float value; };
|
||||
struct SetCharacterPositionCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
bool updateHomePosition = false;
|
||||
};
|
||||
struct SetCharacterFacingAngleCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float angle;
|
||||
bool setCurrentAngle = false;
|
||||
};
|
||||
struct SetCharacterHomePositionCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
};
|
||||
struct SetCharacterTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct SetCharacterFaceTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct SetCharacterEnabledCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
bool enabled;
|
||||
};
|
||||
struct ClearCharacterAttackTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterDestinationCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f target;
|
||||
std::string callbackName;
|
||||
};
|
||||
struct StopCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct FaceCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct ReplanCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct ResetCharacterAfterDeathCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct TryCharacterAttackComboCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterHealthCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float hp;
|
||||
};
|
||||
struct SetCharacterAnimationMetadataCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
AnimationState animationState;
|
||||
int startFrame;
|
||||
int frameCount;
|
||||
};
|
||||
struct DamageCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float amount;
|
||||
std::string attackerId;
|
||||
};
|
||||
struct SetInteractiveObjectActiveCommand {
|
||||
std::string locationId;
|
||||
std::string objectId;
|
||||
bool active;
|
||||
};
|
||||
struct SetTriggerZoneEnabledCommand {
|
||||
std::string locationId;
|
||||
std::string zoneId;
|
||||
bool enabled;
|
||||
};
|
||||
struct ReloadQuestDefinitionsCommand { std::string path; std::string zipFile; };
|
||||
|
||||
using GameCommand = std::variant<
|
||||
SetWorldModeCommand,
|
||||
SetCurrentLocationCommand,
|
||||
SetMoneyCommand,
|
||||
SpendMoneyCommand,
|
||||
CompleteTutorialStepCommand,
|
||||
SetTutorialStepCommand,
|
||||
SetTutorialFlagsCommand,
|
||||
SetChatCommand,
|
||||
AppendChatMessageCommand,
|
||||
SetTaxiCalledCommand,
|
||||
StartTaxiCommand,
|
||||
ResetTaxiCommand,
|
||||
SetLocationTargetsCommand,
|
||||
SwitchLocationNavigationCommand,
|
||||
SetNavigationAreaAvailableCommand,
|
||||
AddNavigationObstacleCommand,
|
||||
AddInventoryItemCommand,
|
||||
RemoveInventoryItemCommand,
|
||||
UnlockQuestCommand,
|
||||
CompleteQuestCommand,
|
||||
FailQuestCommand,
|
||||
SetQuestObjectiveCompletedCommand,
|
||||
SetQuestObjectiveVisibleCommand,
|
||||
SetQuestActiveObjectiveCommand,
|
||||
SetGlobalIntCommand,
|
||||
SetGlobalFloatCommand,
|
||||
SetCharacterPositionCommand,
|
||||
SetCharacterFacingAngleCommand,
|
||||
SetCharacterHomePositionCommand,
|
||||
SetCharacterTargetCommand,
|
||||
SetCharacterFaceTargetCommand,
|
||||
SetCharacterEnabledCommand,
|
||||
ClearCharacterAttackTargetCommand,
|
||||
SetCharacterDestinationCommand,
|
||||
StopCharacterCommand,
|
||||
FaceCharacterCommand,
|
||||
ReplanCharacterCommand,
|
||||
ResetCharacterAfterDeathCommand,
|
||||
TryCharacterAttackComboCommand,
|
||||
SetCharacterHealthCommand,
|
||||
SetCharacterAnimationMetadataCommand,
|
||||
DamageCharacterCommand,
|
||||
SetInteractiveObjectActiveCommand,
|
||||
SetTriggerZoneEnabledCommand,
|
||||
ReloadQuestDefinitionsCommand>;
|
||||
|
||||
struct InventoryItemAddedEvent { std::string itemId; };
|
||||
struct InventoryItemRemovedEvent { std::string itemId; };
|
||||
|
||||
using GameServerEvent = std::variant<
|
||||
InventoryItemAddedEvent,
|
||||
InventoryItemRemovedEvent,
|
||||
Quest::QuestEvent>;
|
||||
|
||||
class IGameServer {
|
||||
public:
|
||||
virtual ~IGameServer() = default;
|
||||
|
||||
// Each returned object is immutable and remains valid after later commands.
|
||||
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
|
||||
// Client-relative identity assigned by this server connection.
|
||||
virtual const std::string& localPlayerId() const = 0;
|
||||
virtual void submit(const GameCommand& command) = 0;
|
||||
virtual std::vector<GameServerEvent> drainEvents() = 0;
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
|
||||
// Persistence stays server-side. The JSON shape is GameState's established
|
||||
// save schema and is intentionally not the wire snapshot shape.
|
||||
virtual void save(nlohmann::json& out) const = 0;
|
||||
virtual void load(const nlohmann::json& in) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class AudioPlayerAsync;
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Location;
|
||||
struct LocationConfig;
|
||||
struct LocationSetup;
|
||||
|
||||
// Client-only lifecycle and presentation integration for the in-process server.
|
||||
// The public gameplay contract remains IGameServer and is graphics-free.
|
||||
class IGameServerHost {
|
||||
public:
|
||||
virtual ~IGameServerHost() = default;
|
||||
|
||||
virtual void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) = 0;
|
||||
virtual void clearCampaign() = 0;
|
||||
virtual void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) = 0;
|
||||
virtual bool prepareLocation(const LocationConfig& config) = 0;
|
||||
|
||||
virtual void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) = 0;
|
||||
virtual Location* runtimeLocation(const std::string& id) const = 0;
|
||||
virtual bool hasRuntimeLocation(const std::string& id) const = 0;
|
||||
virtual std::vector<std::string> loadedRuntimeLocationIds() const = 0;
|
||||
virtual void clearRuntimeLocations(bool retainState) = 0;
|
||||
virtual void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) = 0;
|
||||
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
virtual void save(nlohmann::json& out) = 0;
|
||||
virtual void load(const nlohmann::json& in) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct CampaignConfig;
|
||||
struct LocationConfig;
|
||||
|
||||
// Graphics-free administrative lifecycle used by local and standalone hosts.
|
||||
class IGameServerLifecycle {
|
||||
public:
|
||||
virtual ~IGameServerLifecycle() = default;
|
||||
|
||||
virtual void startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot) = 0;
|
||||
virtual bool prepareLocation(const LocationConfig& config) = 0;
|
||||
virtual void activateLocation(const std::string& locationId) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,560 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../content/LocationDefinition.h"
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <type_traits>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
constexpr const char* kBattleZoneCampaignId = "battle_zone";
|
||||
constexpr const char* kBattleZonePlayer2Id = "player2";
|
||||
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
|
||||
|
||||
void configurePlayerAnimations(
|
||||
CharacterState& player, const std::string& zipFile)
|
||||
{
|
||||
player.creationInfo.animationPaths = {
|
||||
{AnimationState::STAND,
|
||||
"resources/w/gg/new2/gg_stand_idle001_small.anim"},
|
||||
{AnimationState::WALK,
|
||||
"resources/w/gg/new2/gg_walk001_small.anim"},
|
||||
{AnimationState::STAND_TO_ACTION,
|
||||
"resources/w/gg/new2/gg_stand_to_action001_small.anim"},
|
||||
{AnimationState::ACTION_ATTACK,
|
||||
"resources/w/gg/new2/gg_action_chop001_small.anim"},
|
||||
{AnimationState::ACTION_ATTACK_2,
|
||||
"resources/w/gg/new2/gg_action_stab001_small.anim"},
|
||||
{AnimationState::ACTION_IDLE,
|
||||
"resources/w/gg/new2/gg_action_idle002_small.anim"},
|
||||
{AnimationState::ACTION_TO_STAND,
|
||||
"resources/w/gg/new2/gg_action_to_stand001_small.anim"},
|
||||
{AnimationState::ACTION_TO_DEATH,
|
||||
"resources/w/gg/new2/gg_die001_small.anim"},
|
||||
{AnimationState::DEATH_IDLE,
|
||||
"resources/w/gg/new2/gg_die_idle001_small.anim"}};
|
||||
loadCharacterAnimationMetadata(player, zipFile);
|
||||
}
|
||||
}
|
||||
|
||||
InternalGameServer::InternalGameServer() = default;
|
||||
|
||||
void InternalGameServer::ensureBattleZoneTestBot()
|
||||
{
|
||||
if (state_.campaignId != kBattleZoneCampaignId) return;
|
||||
if (!battleZoneTestBotEnabled_) {
|
||||
state_.players.erase(kBattleZonePlayer2Id);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& bot = state_.players[kBattleZonePlayer2Id];
|
||||
if (!bot) bot = std::make_shared<CharacterState>();
|
||||
bot->configureAsPlayer();
|
||||
bot->enabled = true;
|
||||
configurePlayerAnimations(*bot, resourceZipFile_);
|
||||
bot->npcName = "Test Bot";
|
||||
}
|
||||
|
||||
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
||||
{
|
||||
if (!battleZoneTestBotEnabled_ ||
|
||||
state_.campaignId != kBattleZoneCampaignId ||
|
||||
state_.currentLocationName.empty() ||
|
||||
state_.players.find(kBattleZonePlayer2Id) == state_.players.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
battleZoneTestBotElapsedMs_ += deltaMs;
|
||||
if (battleZoneTestBotElapsedMs_ < kBattleZoneTestBotIntervalMs) return;
|
||||
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
|
||||
|
||||
static std::mt19937 randomEngine(std::random_device{}());
|
||||
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f);
|
||||
submit(SetCharacterDestinationCommand{
|
||||
state_.currentLocationName,
|
||||
kBattleZonePlayer2Id,
|
||||
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)),
|
||||
std::string()});
|
||||
}
|
||||
|
||||
void InternalGameServer::initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
CampaignConfig campaign;
|
||||
campaign.id = campaignId;
|
||||
campaign.questsPath = questsPath;
|
||||
startCampaign(campaign, zipFile, true, true);
|
||||
//startCampaign(campaign, zipFile, true, false);
|
||||
}
|
||||
|
||||
void InternalGameServer::startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot)
|
||||
{
|
||||
resourceZipFile_ = zipFile;
|
||||
networkPlayerSlots_ = campaign.networkPlayerSlots;
|
||||
battleZoneTestBotEnabled_ = enableTestBot;
|
||||
state_.campaignId = campaign.id;
|
||||
state_.currentLocationName.clear();
|
||||
state_.locations.clear();
|
||||
state_.players.clear();
|
||||
state_.questJournal.loadFromFile(campaign.questsPath, zipFile);
|
||||
//state_.globalFloats["player_hp"] = 200.f;
|
||||
|
||||
if (createLocalPlayer) {
|
||||
auto player = std::make_shared<CharacterState>();
|
||||
player->configureAsPlayer();
|
||||
player->enabled = campaign.networkPlayerSlots.empty();
|
||||
configurePlayerAnimations(*player, zipFile);
|
||||
state_.players.emplace(localPlayerId_, std::move(player));
|
||||
}
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::activateLocation(const std::string& locationId)
|
||||
{
|
||||
if (state_.locations.count(locationId) == 0)
|
||||
throw std::runtime_error(
|
||||
"Cannot activate unprepared location '" + locationId + "'");
|
||||
state_.currentLocationName = locationId;
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::clearCampaign()
|
||||
{
|
||||
state_.currentLocationName.clear();
|
||||
state_.players.clear();
|
||||
state_.locations.clear();
|
||||
networkPlayerSlots_.clear();
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile)
|
||||
{
|
||||
pendingEvents_.clear();
|
||||
state_.currentLocationName.clear();
|
||||
state_.isDarklands = false;
|
||||
state_.isNight = false;
|
||||
state_.isDawn = false;
|
||||
//state_.playerHp = 200.f;
|
||||
state_.playerMaxHp = 200.f;
|
||||
state_.money = 5500;
|
||||
state_.globalInts.clear();
|
||||
state_.globalFloats.clear();
|
||||
//state_.globalFloats["player_hp"] = 200.f;
|
||||
state_.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
state_.tutorialPhoneChatScreenOpened = false;
|
||||
state_.tutorialJournalScreenOpened = false;
|
||||
state_.tutorialNeedOpenTaxiScreen = false;
|
||||
state_.tutorialTaxiHintDone = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
state_.chatUnread[i] = true;
|
||||
state_.chatPreviewMsg[i].clear();
|
||||
state_.chatHistory[i].clear();
|
||||
}
|
||||
state_.inventory.clear();
|
||||
state_.questJournal.loadFromFile(questsPath, zipFile);
|
||||
state_.locations.clear();
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
bool InternalGameServer::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
LocationGameState& authoritative = state_.locations[config.id];
|
||||
const bool initializeLocation = authoritative.setup(
|
||||
LocationContentLoader::load(config, resourceZipFile_),
|
||||
resourceZipFile_);
|
||||
auto player = state_.players.find(localPlayerId_);
|
||||
if (player != state_.players.end() && player->second) {
|
||||
player->second->position = config.playerPosition;
|
||||
player->second->setTarget(
|
||||
config.playerPosition, std::string(), *authoritative.location,
|
||||
localPlayerId_);
|
||||
}
|
||||
publishMutation();
|
||||
return initializeLocation;
|
||||
}
|
||||
|
||||
GameSnapshot InternalGameServer::buildSnapshot() const
|
||||
{
|
||||
GameSnapshot result;
|
||||
result.revision = revision_;
|
||||
result.campaignId = state_.campaignId;
|
||||
result.currentLocationId = state_.currentLocationName;
|
||||
result.isDarklands = state_.isDarklands;
|
||||
result.isNight = state_.isNight;
|
||||
result.isDawn = state_.isDawn;
|
||||
result.money = state_.money;
|
||||
result.tutorialStep = state_.tutorialStep;
|
||||
result.tutorialPhoneChatScreenOpened = state_.tutorialPhoneChatScreenOpened;
|
||||
result.tutorialJournalScreenOpened = state_.tutorialJournalScreenOpened;
|
||||
result.tutorialNeedOpenTaxiScreen = state_.tutorialNeedOpenTaxiScreen;
|
||||
result.tutorialTaxiHintDone = state_.tutorialTaxiHintDone;
|
||||
result.taxiIsCalled = state_.taxiIsCalled;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
result.chatUnread[i] = state_.chatUnread[i];
|
||||
result.chatPreview[i] = state_.chatPreviewMsg[i];
|
||||
result.chatHistory[i].reserve(state_.chatHistory[i].size());
|
||||
for (const StoredChatMessage& message : state_.chatHistory[i])
|
||||
result.chatHistory[i].push_back({message.text, message.incoming});
|
||||
}
|
||||
|
||||
nlohmann::json inventoryJson;
|
||||
state_.inventory.save(inventoryJson);
|
||||
if (inventoryJson.contains("items") && inventoryJson["items"].is_array()) {
|
||||
for (const auto& item : inventoryJson["items"])
|
||||
if (item.is_string()) result.inventoryItemIds.push_back(item.get<std::string>());
|
||||
}
|
||||
state_.questJournal.save(result.questJournal);
|
||||
result.globalInts = state_.globalInts;
|
||||
result.globalFloats = state_.globalFloats;
|
||||
for (const Quest::QuestState* quest : state_.questJournal.getVisibleQuests())
|
||||
if (quest) result.quests.push_back(*quest);
|
||||
for (const auto& [playerId, source] : state_.players) {
|
||||
if (!source) continue;
|
||||
CharacterState clean = *source;
|
||||
result.players.emplace(playerId, CharacterSnapshot{playerId, std::move(clean)});
|
||||
}
|
||||
for (const auto& [locationId, source] : state_.locations) {
|
||||
LocationSnapshot location;
|
||||
location.id = locationId;
|
||||
location.state = *source.location;
|
||||
for (const auto& [id, sourceNpc] : source.npcs) {
|
||||
if (!sourceNpc) continue;
|
||||
CharacterState clean = *sourceNpc;
|
||||
location.npcs.emplace(
|
||||
id, CharacterSnapshot{id, std::move(clean)});
|
||||
}
|
||||
for (const auto& [id, sourceObject] : source.interactiveObjects) {
|
||||
if (!sourceObject) continue;
|
||||
InteractiveObjectState clean = *sourceObject;
|
||||
location.interactiveObjects.emplace(
|
||||
id, InteractiveObjectSnapshot{id, std::move(clean)});
|
||||
}
|
||||
location.triggerZones = source.triggerZones;
|
||||
location.teleportZones = source.teleportZones;
|
||||
location.dialogue = source.dialogue;
|
||||
location.scriptGlobals = source.scriptGlobals;
|
||||
result.locations.emplace(locationId, std::move(location));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
|
||||
{
|
||||
if (!cachedSnapshot_ || cachedRevision_ != revision_) {
|
||||
cachedSnapshot_ = std::make_shared<const GameSnapshot>(buildSnapshot());
|
||||
cachedRevision_ = revision_;
|
||||
}
|
||||
return cachedSnapshot_;
|
||||
}
|
||||
|
||||
void InternalGameServer::submit(const GameCommand& command)
|
||||
{
|
||||
auto enqueueQuestEvents = [this](Quest::QuestMutationResult result) {
|
||||
for (Quest::QuestEvent& event : result.events)
|
||||
pendingEvents_.emplace_back(std::move(event));
|
||||
};
|
||||
std::visit([this, &enqueueQuestEvents](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
|
||||
state_.isDarklands = value.darklands;
|
||||
state_.isNight = value.night;
|
||||
state_.isDawn = value.dawn;
|
||||
} else if constexpr (std::is_same_v<T, SetCurrentLocationCommand>) {
|
||||
state_.currentLocationName = value.locationId;
|
||||
} else if constexpr (std::is_same_v<T, SetMoneyCommand>) {
|
||||
state_.money = value.amount;
|
||||
} else if constexpr (std::is_same_v<T, SpendMoneyCommand>) {
|
||||
state_.money -= value.amount;
|
||||
} else if constexpr (std::is_same_v<T, CompleteTutorialStepCommand>) {
|
||||
state_.completeTutorialStep(value.step);
|
||||
} else if constexpr (std::is_same_v<T, SetTutorialStepCommand>) {
|
||||
state_.tutorialStep = value.step;
|
||||
} else if constexpr (std::is_same_v<T, SetTutorialFlagsCommand>) {
|
||||
state_.tutorialPhoneChatScreenOpened = value.phoneChatOpened;
|
||||
state_.tutorialJournalScreenOpened = value.journalOpened;
|
||||
state_.tutorialNeedOpenTaxiScreen = value.needTaxiHint;
|
||||
state_.tutorialTaxiHintDone = value.taxiHintDone;
|
||||
} else if constexpr (std::is_same_v<T, SetChatCommand>) {
|
||||
if (value.index >= 0 && value.index < 3) {
|
||||
state_.chatUnread[value.index] = value.unread;
|
||||
state_.chatPreviewMsg[value.index] = value.preview;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AppendChatMessageCommand>) {
|
||||
if (value.index >= 0 && value.index < 3) {
|
||||
if (state_.chatHistory[value.index].size() >= 5)
|
||||
state_.chatHistory[value.index].erase(state_.chatHistory[value.index].begin());
|
||||
state_.chatHistory[value.index].push_back({value.text, value.incoming});
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetTaxiCalledCommand>) {
|
||||
state_.taxiIsCalled = value.called;
|
||||
} else if constexpr (std::is_same_v<T, StartTaxiCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.startTaxi();
|
||||
} else if constexpr (std::is_same_v<T, ResetTaxiCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.resetTaxi();
|
||||
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
location->second.location->targetInteractiveObjectId =
|
||||
value.interactiveObjectId;
|
||||
location->second.location->targetInteractNpcId = value.npcId;
|
||||
location->second.location->targetTeleportZoneId =
|
||||
value.teleportZoneId;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SwitchLocationNavigationCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location &&
|
||||
value.index >= 0 &&
|
||||
value.index < static_cast<int>(
|
||||
location->second.location->navigationMaps.size())) {
|
||||
LocationNetworkState& locationState = *location->second.location;
|
||||
locationState.activeNavigationIndex = value.index;
|
||||
for (const auto& [id, player] : state_.players)
|
||||
if (player) location->second.replanCharacter(id, state_.players);
|
||||
for (const auto& [id, npc] : location->second.npcs)
|
||||
if (npc) location->second.replanCharacter(id, state_.players);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
LocationNetworkState& locationState = *location->second.location;
|
||||
const int index = locationState.activeNavigationIndex;
|
||||
if (index >= 0 &&
|
||||
index < static_cast<int>(locationState.navigationMaps.size())) {
|
||||
locationState.navigationMaps[index].setAreaAvailable(
|
||||
value.areaName, value.available);
|
||||
for (const auto& [id, player] : state_.players)
|
||||
if (player) location->second.replanCharacter(id, state_.players);
|
||||
for (const auto& [id, npc] : location->second.npcs)
|
||||
if (npc) location->second.replanCharacter(id, state_.players);
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
LocationNetworkState& locationState = *location->second.location;
|
||||
const int index = locationState.activeNavigationIndex;
|
||||
if (index >= 0 &&
|
||||
index < static_cast<int>(locationState.navigationMaps.size())) {
|
||||
PathFinder::ObstaclePolygon polygon;
|
||||
polygon.name = value.name;
|
||||
polygon.polygon = value.polygon;
|
||||
locationState.navigationMaps[index].addObstaclePolygon(polygon);
|
||||
for (const auto& [id, player] : state_.players)
|
||||
if (player) location->second.replanCharacter(id, state_.players);
|
||||
for (const auto& [id, npc] : location->second.npcs)
|
||||
if (npc) location->second.replanCharacter(id, state_.players);
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) {
|
||||
if (state_.inventory.addItem(value.itemId))
|
||||
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
|
||||
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
|
||||
if (state_.inventory.removeItem(value.itemId))
|
||||
pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId});
|
||||
} else if constexpr (std::is_same_v<T, UnlockQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, CompleteQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.completeQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, FailQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.failQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestObjectiveCompletedCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setObjectiveCompleted(
|
||||
value.questId, value.objectiveId, value.completed));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestObjectiveVisibleCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setObjectiveVisible(
|
||||
value.questId, value.objectiveId, value.visible));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestActiveObjectiveCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setActiveObjective(
|
||||
value.questId, value.objectiveIndex));
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
|
||||
state_.globalInts[value.key] = value.value;
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
|
||||
state_.globalFloats[value.key] = value.value;
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterPositionCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->position = value.position;
|
||||
if (value.updateHomePosition)
|
||||
character->homePosition = value.position;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterFacingAngleCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->targetFacingAngle = value.angle;
|
||||
if (value.setCurrentAngle)
|
||||
character->facingAngle = value.angle;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterHomePositionCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character)
|
||||
character->homePosition = value.position;
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterTargetCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->attackTargetId = value.targetId;
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterFaceTargetCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->faceTargetId = value.targetId;
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterEnabledCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->enabled = value.enabled;
|
||||
character->attack = 0;
|
||||
character->attack_cooldown = 0;
|
||||
character->currentState = AnimationState::STAND;
|
||||
character->stopInPlace();
|
||||
character->faceTargetId.clear();
|
||||
character->attackTargetId.clear();
|
||||
character->battle_state = 0;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ClearCharacterAttackTargetCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->attackTargetId.clear();
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterDestinationCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.setCharacterDestination(
|
||||
value.characterId, value.target, value.callbackName,
|
||||
state_.players);
|
||||
} else if constexpr (std::is_same_v<T, StopCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->stopInPlace();
|
||||
} else if constexpr (std::is_same_v<T, FaceCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character)
|
||||
character->stopAndRotateToTarget(value.targetId);
|
||||
} else if constexpr (std::is_same_v<T, ReplanCharacterCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.replanCharacter(
|
||||
value.characterId, state_.players);
|
||||
} else if constexpr (std::is_same_v<T, ResetCharacterAfterDeathCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->resetPlayerAfterDeath();
|
||||
} else if constexpr (std::is_same_v<T, TryCharacterAttackComboCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->tryAttackCombo();
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterHealthCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->setHp(value.hp);
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterAnimationMetadataCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->setAnimationMetadata(
|
||||
value.animationState, value.startFrame, value.frameCount);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, DamageCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->hp = (std::max)(0.f, character->hp - (std::max)(0.f, value.amount));
|
||||
} else if constexpr (std::is_same_v<T, SetInteractiveObjectActiveCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location == state_.locations.end()) return;
|
||||
auto object = location->second.interactiveObjects.find(value.objectId);
|
||||
if (object != location->second.interactiveObjects.end() && object->second)
|
||||
object->second->isActive = value.active;
|
||||
} else if constexpr (std::is_same_v<T, SetTriggerZoneEnabledCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end()) {
|
||||
location->second.triggerZones[value.zoneId].enabled = value.enabled;
|
||||
pendingTriggerZoneUpdates_.emplace_back(
|
||||
value.locationId, value.zoneId);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||
}
|
||||
}, command);
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
||||
{
|
||||
std::vector<GameServerEvent> result;
|
||||
result.swap(pendingEvents_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InternalGameServer::update(std::int64_t deltaMs)
|
||||
{
|
||||
pendingSimulationLocationId_.clear();
|
||||
pendingSimulationEvents_ = {};
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
updateBattleZoneTestBot(deltaMs);
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
pendingSimulationLocationId_ = state_.currentLocationName;
|
||||
pendingSimulationEvents_ =
|
||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
||||
// Navigation recovery is a simulation consequence, not presentation input.
|
||||
// Applying it here makes every lockstep replica produce the same follow-up.
|
||||
for (const CharacterSimulationEvent& event :
|
||||
pendingSimulationEvents_.characters) {
|
||||
if (!event.events.navigationRequested) continue;
|
||||
if (event.events.forceNavigationReplan)
|
||||
stored->second.replanCharacter(
|
||||
event.characterId, state_.players,
|
||||
event.events.blockingCharacterId);
|
||||
else
|
||||
stored->second.setCharacterDestination(
|
||||
event.characterId, event.events.navigationTarget, {},
|
||||
state_.players);
|
||||
}
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::save(nlohmann::json& out) const
|
||||
{
|
||||
state_.save(out);
|
||||
}
|
||||
|
||||
void InternalGameServer::load(const nlohmann::json& in)
|
||||
{
|
||||
pendingEvents_.clear();
|
||||
state_.load(in);
|
||||
for (const std::string& playerId : networkPlayerSlots_) {
|
||||
auto player = state_.players.find(playerId);
|
||||
if (player == state_.players.end() || !player->second) continue;
|
||||
player->second->configureAsPlayer();
|
||||
configurePlayerAnimations(*player->second, resourceZipFile_);
|
||||
//player->second->enabled = false;
|
||||
}
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::publishMutation()
|
||||
{
|
||||
++revision_;
|
||||
cachedSnapshot_.reset();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include "IGameServerLifecycle.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class LocalGameServerHost;
|
||||
class RemoteGameServerHost;
|
||||
struct LocationConfig;
|
||||
|
||||
// In-process implementation used by single-player. GameState exists exactly
|
||||
// once, here; clients see immutable snapshots and mutate it with typed commands.
|
||||
class InternalGameServer final
|
||||
: public IGameServer, public IGameServerLifecycle {
|
||||
public:
|
||||
InternalGameServer();
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
const std::string& localPlayerId() const override { return localPlayerId_; }
|
||||
void submit(const GameCommand& command) override;
|
||||
std::vector<GameServerEvent> drainEvents() override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
void setLocalPlayerId(std::string playerId) {
|
||||
localPlayerId_ = std::move(playerId);
|
||||
}
|
||||
void startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
void activateLocation(const std::string& locationId) override;
|
||||
|
||||
private:
|
||||
friend class LocalGameServerHost;
|
||||
friend class RemoteGameServerHost;
|
||||
friend class WebsocketGameClient;
|
||||
|
||||
// Host-only lifecycle operations. They never expose authoritative state and
|
||||
// are intentionally absent from IGameServer.
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile);
|
||||
void clearCampaign();
|
||||
void resetCampaign(const std::string& questsPath, const std::string& zipFile);
|
||||
void publishMutation();
|
||||
void ensureBattleZoneTestBot();
|
||||
void updateBattleZoneTestBot(std::int64_t deltaMs);
|
||||
|
||||
GameSnapshot buildSnapshot() const;
|
||||
|
||||
GameState state_;
|
||||
std::string localPlayerId_ = "player";
|
||||
std::string resourceZipFile_;
|
||||
std::vector<std::string> networkPlayerSlots_;
|
||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
||||
bool battleZoneTestBotEnabled_ = false;
|
||||
std::string pendingSimulationLocationId_;
|
||||
LocationSimulationEvents pendingSimulationEvents_;
|
||||
std::vector<std::pair<std::string, std::string>> pendingTriggerZoneUpdates_;
|
||||
std::uint64_t revision_ = 1;
|
||||
mutable std::uint64_t cachedRevision_ = 0;
|
||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
||||
std::vector<GameServerEvent> pendingEvents_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "LocalGameServerHost.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
LocalGameServerHost::LocalGameServerHost(InternalGameServer& server)
|
||||
: server_(server)
|
||||
{
|
||||
}
|
||||
|
||||
void LocalGameServerHost::initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
localState_.clear();
|
||||
server_.initializeCampaign(campaignId, questsPath, zipFile);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
localState_.clear();
|
||||
server_.clearCampaign();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile)
|
||||
{
|
||||
localState_.clear();
|
||||
server_.resetCampaign(questsPath, zipFile);
|
||||
}
|
||||
|
||||
bool LocalGameServerHost::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
return server_.prepareLocation(config);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = server_.state_.locations.find(id);
|
||||
if (stored == server_.state_.locations.end())
|
||||
throw std::runtime_error("Location state was not prepared");
|
||||
params.definition = stored->second.definition;
|
||||
params.gameState = &stored->second;
|
||||
params.localState = localState_.getOrCreate(id);
|
||||
location->setup(params, audioPlayer);
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
Location* LocalGameServerHost::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto it = runtimeLocations_.find(id);
|
||||
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
bool LocalGameServerHost::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> LocalGameServerHost::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void LocalGameServerHost::clearRuntimeLocations(bool retainState)
|
||||
{
|
||||
if (retainState) {
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
} else {
|
||||
for (const auto& [id, location] : runtimeLocations_)
|
||||
server_.state_.locations.erase(id);
|
||||
server_.publishMutation();
|
||||
}
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = server_.state_.locations.find(id);
|
||||
if (stored != server_.state_.locations.end() &&
|
||||
stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
} else {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(server_.state_.locations[id]);
|
||||
}
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::update(std::int64_t deltaMs)
|
||||
{
|
||||
server_.update(deltaMs);
|
||||
|
||||
for (const auto& [locationId, zoneId] : server_.pendingTriggerZoneUpdates_) {
|
||||
auto runtime = runtimeLocations_.find(locationId);
|
||||
auto stored = server_.state_.locations.find(locationId);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second ||
|
||||
stored == server_.state_.locations.end()) {
|
||||
continue;
|
||||
}
|
||||
auto zoneState = stored->second.triggerZones.find(zoneId);
|
||||
if (zoneState == stored->second.triggerZones.end()) continue;
|
||||
for (auto& zone : runtime->second->triggerZones)
|
||||
if (zone.id == zoneId) zone.enabled = zoneState->second.enabled;
|
||||
}
|
||||
server_.pendingTriggerZoneUpdates_.clear();
|
||||
|
||||
if (server_.pendingSimulationLocationId_.empty()) return;
|
||||
auto runtime =
|
||||
runtimeLocations_.find(server_.pendingSimulationLocationId_);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
runtime->second->update(deltaMs, server_.pendingSimulationEvents_);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::save(nlohmann::json& out)
|
||||
{
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
nlohmann::json networkState;
|
||||
server_.save(networkState);
|
||||
saveSinglePlayerPersistence(networkState, localState_, out);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::load(const nlohmann::json& in)
|
||||
{
|
||||
nlohmann::json networkState;
|
||||
ClientLocationState loadedLocalState;
|
||||
loadSinglePlayerPersistence(in, networkState, loadedLocalState);
|
||||
server_.load(networkState);
|
||||
nlohmann::json localStateJson;
|
||||
loadedLocalState.save(localStateJson);
|
||||
localState_.load(localStateJson);
|
||||
applyStateToRuntime();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::synchronizeRuntimeAuxiliaryState()
|
||||
{
|
||||
for (const auto& [id, location] : runtimeLocations_) {
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(server_.state_.locations[id]);
|
||||
}
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::applyStateToRuntime()
|
||||
{
|
||||
for (const auto& [id, stored] : server_.state_.locations) {
|
||||
auto runtime = runtimeLocations_.find(id);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
|
||||
runtime->second->applyAuxiliaryState(stored);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include "../LocationState.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class InternalGameServer;
|
||||
|
||||
// Adapts the headless authoritative server to the graphical in-process client.
|
||||
class LocalGameServerHost final : public IGameServerHost {
|
||||
public:
|
||||
explicit LocalGameServerHost(InternalGameServer& server);
|
||||
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override;
|
||||
void clearCampaign() override;
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override;
|
||||
Location* runtimeLocation(const std::string& id) const override;
|
||||
bool hasRuntimeLocation(const std::string& id) const override;
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override;
|
||||
void clearRuntimeLocations(bool retainState) override;
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override;
|
||||
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
void synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
|
||||
InternalGameServer& server_;
|
||||
ClientLocationState localState_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,420 @@
|
||||
#include "NetworkProtocol.h"
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
bool parseEnvelope(
|
||||
const std::string& text, const char* expectedType,
|
||||
nlohmann::json& payload, std::string& error)
|
||||
{
|
||||
try {
|
||||
const nlohmann::json envelope = nlohmann::json::parse(text);
|
||||
if (!envelope.is_object()) {
|
||||
error = "message must be a JSON object";
|
||||
return false;
|
||||
}
|
||||
if (envelope.value("protocolVersion", -1) != kGameProtocolVersion) {
|
||||
error = "unsupported protocol version";
|
||||
return false;
|
||||
}
|
||||
if (envelope.value("type", std::string()) != expectedType) {
|
||||
error = "unsupported message type";
|
||||
return false;
|
||||
}
|
||||
if (!envelope.contains("payload") || !envelope["payload"].is_object()) {
|
||||
error = "message payload must be an object";
|
||||
return false;
|
||||
}
|
||||
payload = envelope["payload"];
|
||||
error.clear();
|
||||
return true;
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string envelope(const char* type, nlohmann::json payload)
|
||||
{
|
||||
return nlohmann::json{
|
||||
{"protocolVersion", kGameProtocolVersion},
|
||||
{"type", type},
|
||||
{"payload", std::move(payload)}
|
||||
}.dump();
|
||||
}
|
||||
|
||||
bool readTick(
|
||||
const nlohmann::json& value, const char* name,
|
||||
std::uint64_t& out, std::string& error)
|
||||
{
|
||||
if (!value.contains(name) ||
|
||||
(!value[name].is_number_unsigned() && !value[name].is_number_integer())) {
|
||||
error = std::string(name) + " must be a non-negative integer";
|
||||
return false;
|
||||
}
|
||||
const auto signedValue = value[name].get<std::int64_t>();
|
||||
if (signedValue < 0) {
|
||||
error = std::string(name) + " must be non-negative";
|
||||
return false;
|
||||
}
|
||||
out = static_cast<std::uint64_t>(signedValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readTarget(
|
||||
const nlohmann::json& value, Eigen::Vector3f& target, std::string& error)
|
||||
{
|
||||
if (!value.is_array() || value.size() != 3) {
|
||||
error = "target must contain three coordinates";
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < 3; ++index) {
|
||||
if (!value[index].is_number()) {
|
||||
error = "target coordinates must be numbers";
|
||||
return false;
|
||||
}
|
||||
const float coordinate = value[index].get<float>();
|
||||
if (!std::isfinite(coordinate) || std::abs(coordinate) > 100000.f) {
|
||||
error = "target coordinate is not finite or is out of range";
|
||||
return false;
|
||||
}
|
||||
target[static_cast<Eigen::Index>(index)] =
|
||||
std::round(coordinate * 100.f) / 100.f;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
nlohmann::json targetJson(const Eigen::Vector3f& target)
|
||||
{
|
||||
return nlohmann::json::array({target.x(), target.y(), target.z()});
|
||||
}
|
||||
|
||||
bool validCharacterAction(
|
||||
const std::string& action, const std::string& targetId,
|
||||
std::string& error)
|
||||
{
|
||||
if (action != "set_target" &&
|
||||
action != "clear_attack_target" &&
|
||||
action != "try_attack_combo") {
|
||||
error = "unsupported character action";
|
||||
return false;
|
||||
}
|
||||
if (action == "set_target" && targetId.empty()) {
|
||||
error = "set_target requires a targetId";
|
||||
return false;
|
||||
}
|
||||
if (action != "set_target" && !targetId.empty()) {
|
||||
error = "targetId is only valid for set_target";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string makeInitialStateMessage(const InitialStateMessage& message)
|
||||
{
|
||||
return envelope("initial_state", {
|
||||
{"state", message.state},
|
||||
{"assignedPlayerId", message.assignedPlayerId},
|
||||
{"snapshotTick", message.snapshotTick},
|
||||
{"tickMs", message.tickMs},
|
||||
{"inputDelayTicks", message.inputDelayTicks},
|
||||
{"compatibilityFingerprint", message.compatibilityFingerprint}
|
||||
});
|
||||
}
|
||||
|
||||
bool parseInitialStateMessage(
|
||||
const std::string& text, InitialStateMessage& message, std::string& error)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
if (!parseEnvelope(text, "initial_state", payload, error)) return false;
|
||||
if (!payload.contains("state") || !payload["state"].is_object() ||
|
||||
!payload.contains("assignedPlayerId") ||
|
||||
!payload["assignedPlayerId"].is_string() ||
|
||||
!payload.contains("compatibilityFingerprint") ||
|
||||
!payload["compatibilityFingerprint"].is_string()) {
|
||||
error = "invalid initial-state payload";
|
||||
return false;
|
||||
}
|
||||
if (!readTick(payload, "snapshotTick", message.snapshotTick, error))
|
||||
return false;
|
||||
message.tickMs = payload.value("tickMs", std::int64_t{0});
|
||||
message.inputDelayTicks =
|
||||
payload.value("inputDelayTicks", std::uint32_t{0});
|
||||
if (message.tickMs != kSimulationTickMs || message.inputDelayTicks == 0) {
|
||||
error = "unsupported lockstep timing";
|
||||
return false;
|
||||
}
|
||||
message.state = payload["state"];
|
||||
message.assignedPlayerId = payload["assignedPlayerId"].get<std::string>();
|
||||
message.compatibilityFingerprint =
|
||||
payload["compatibilityFingerprint"].get<std::string>();
|
||||
return !message.assignedPlayerId.empty();
|
||||
}
|
||||
|
||||
std::uint64_t LockstepTimeline::schedule(
|
||||
const std::string& playerId, const Eigen::Vector3f& target)
|
||||
{
|
||||
const std::uint64_t sequence = nextCommandSequence_++;
|
||||
pending_[currentTick_ + inputDelayTicks_].push_back(
|
||||
ScheduledDestinationCommand{sequence, playerId, target});
|
||||
return sequence;
|
||||
}
|
||||
|
||||
std::uint64_t LockstepTimeline::scheduleCharacterEnabled(
|
||||
const std::string& locationId, const std::string& characterId,
|
||||
bool enabled)
|
||||
{
|
||||
const std::uint64_t sequence = nextCommandSequence_++;
|
||||
pending_[currentTick_ + 1].push_back(
|
||||
ScheduledCharacterEnabledCommand{
|
||||
sequence, locationId, characterId, enabled});
|
||||
return sequence;
|
||||
}
|
||||
|
||||
std::uint64_t LockstepTimeline::scheduleCharacterAction(
|
||||
const std::string& playerId, std::string action, std::string targetId)
|
||||
{
|
||||
const std::uint64_t sequence = nextCommandSequence_++;
|
||||
pending_[currentTick_ + inputDelayTicks_].push_back(
|
||||
ScheduledCharacterActionCommand{
|
||||
sequence, playerId, std::move(action), std::move(targetId)});
|
||||
return sequence;
|
||||
}
|
||||
|
||||
TickMessage LockstepTimeline::advance()
|
||||
{
|
||||
TickMessage result;
|
||||
result.tickId = ++currentTick_;
|
||||
auto commands = pending_.find(result.tickId);
|
||||
if (commands != pending_.end()) {
|
||||
result.commands = std::move(commands->second);
|
||||
pending_.erase(commands);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string makeDestinationInputMessage(const DestinationInputMessage& message)
|
||||
{
|
||||
return envelope("destination_input", {
|
||||
{"clientSequence", message.clientSequence},
|
||||
{"target", targetJson(message.target)}
|
||||
});
|
||||
}
|
||||
|
||||
bool parseDestinationInputMessage(
|
||||
const std::string& text, DestinationInputMessage& message, std::string& error)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
if (!parseEnvelope(text, "destination_input", payload, error)) return false;
|
||||
if (!readTick(payload, "clientSequence", message.clientSequence, error))
|
||||
return false;
|
||||
if (!payload.contains("target"))
|
||||
return error = "target is required", false;
|
||||
return readTarget(payload["target"], message.target, error);
|
||||
}
|
||||
|
||||
std::string makeCharacterActionInputMessage(
|
||||
const CharacterActionInputMessage& message)
|
||||
{
|
||||
return envelope("character_action", {
|
||||
{"clientSequence", message.clientSequence},
|
||||
{"action", message.action},
|
||||
{"targetId", message.targetId}
|
||||
});
|
||||
}
|
||||
|
||||
bool parseCharacterActionInputMessage(
|
||||
const std::string& text, CharacterActionInputMessage& message,
|
||||
std::string& error)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
if (!parseEnvelope(text, "character_action", payload, error))
|
||||
return false;
|
||||
if (!readTick(payload, "clientSequence", message.clientSequence, error))
|
||||
return false;
|
||||
if (!payload.contains("action") || !payload["action"].is_string()) {
|
||||
error = "action is required";
|
||||
return false;
|
||||
}
|
||||
message.action = payload["action"].get<std::string>();
|
||||
message.targetId = payload.value("targetId", std::string());
|
||||
if (!payload.contains("targetId") || !payload["targetId"].is_string()) {
|
||||
error = "targetId must be a string";
|
||||
return false;
|
||||
}
|
||||
return validCharacterAction(message.action, message.targetId, error);
|
||||
}
|
||||
|
||||
std::string makeTickMessage(const TickMessage& message)
|
||||
{
|
||||
nlohmann::json commands = nlohmann::json::array();
|
||||
for (const ScheduledCommand& scheduled : message.commands) {
|
||||
std::visit([&commands](const auto& command) {
|
||||
using T = std::decay_t<decltype(command)>;
|
||||
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||
commands.push_back({
|
||||
{"type", "destination"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"playerId", command.playerId},
|
||||
{"target", targetJson(command.target)}
|
||||
});
|
||||
} else {
|
||||
if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
commands.push_back({
|
||||
{"type", "character_enabled"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"locationId", command.locationId},
|
||||
{"characterId", command.characterId},
|
||||
{"enabled", command.enabled}
|
||||
});
|
||||
} else {
|
||||
commands.push_back({
|
||||
{"type", "character_action"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"playerId", command.playerId},
|
||||
{"action", command.action},
|
||||
{"targetId", command.targetId}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
return envelope("tick", {
|
||||
{"tickId", message.tickId},
|
||||
{"commands", std::move(commands)},
|
||||
{"stateHash", message.stateHash}
|
||||
});
|
||||
}
|
||||
|
||||
bool parseTickMessage(
|
||||
const std::string& text, TickMessage& message, std::string& error)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
if (!parseEnvelope(text, "tick", payload, error)) return false;
|
||||
if (!readTick(payload, "tickId", message.tickId, error)) return false;
|
||||
if (!payload.contains("commands") || !payload["commands"].is_array()) {
|
||||
error = "commands must be an array";
|
||||
return false;
|
||||
}
|
||||
message.commands.clear();
|
||||
std::uint64_t previousSequence = 0;
|
||||
for (const nlohmann::json& value : payload["commands"]) {
|
||||
if (!value.is_object() ||
|
||||
!readTick(value, "commandSequence", previousSequence, error)) {
|
||||
if (error.empty()) error = "invalid scheduled command";
|
||||
return false;
|
||||
}
|
||||
const std::string type = value.value("type", "destination");
|
||||
ScheduledCommand command;
|
||||
if (type == "destination") {
|
||||
ScheduledDestinationCommand destination;
|
||||
destination.commandSequence = previousSequence;
|
||||
if (!value.contains("playerId") || !value["playerId"].is_string() ||
|
||||
!value.contains("target") ||
|
||||
!readTarget(value["target"], destination.target, error)) {
|
||||
if (error.empty()) error = "invalid destination command";
|
||||
return false;
|
||||
}
|
||||
destination.playerId = value["playerId"].get<std::string>();
|
||||
if (destination.playerId.empty()) {
|
||||
error = "destination command playerId must not be empty";
|
||||
return false;
|
||||
}
|
||||
command = std::move(destination);
|
||||
} else if (type == "character_enabled") {
|
||||
ScheduledCharacterEnabledCommand enabled;
|
||||
enabled.commandSequence = previousSequence;
|
||||
if (!value.contains("locationId") || !value["locationId"].is_string() ||
|
||||
!value.contains("characterId") || !value["characterId"].is_string() ||
|
||||
!value.contains("enabled") || !value["enabled"].is_boolean()) {
|
||||
error = "invalid character enabled command";
|
||||
return false;
|
||||
}
|
||||
enabled.locationId = value["locationId"].get<std::string>();
|
||||
enabled.characterId = value["characterId"].get<std::string>();
|
||||
enabled.enabled = value["enabled"].get<bool>();
|
||||
if (enabled.locationId.empty() || enabled.characterId.empty()) {
|
||||
error = "character enabled command IDs must not be empty";
|
||||
return false;
|
||||
}
|
||||
command = std::move(enabled);
|
||||
} else if (type == "character_action") {
|
||||
ScheduledCharacterActionCommand action;
|
||||
action.commandSequence = previousSequence;
|
||||
if (!value.contains("playerId") || !value["playerId"].is_string() ||
|
||||
!value.contains("action") || !value["action"].is_string() ||
|
||||
!value.contains("targetId") || !value["targetId"].is_string()) {
|
||||
error = "invalid character action command";
|
||||
return false;
|
||||
}
|
||||
action.playerId = value["playerId"].get<std::string>();
|
||||
action.action = value["action"].get<std::string>();
|
||||
action.targetId = value["targetId"].get<std::string>();
|
||||
if (action.playerId.empty() ||
|
||||
!validCharacterAction(action.action, action.targetId, error)) {
|
||||
if (error.empty()) error = "invalid character action command";
|
||||
return false;
|
||||
}
|
||||
command = std::move(action);
|
||||
} else {
|
||||
error = "unsupported scheduled command type";
|
||||
return false;
|
||||
}
|
||||
if (previousSequence == 0 ||
|
||||
(!message.commands.empty() &&
|
||||
previousSequence <= std::visit(
|
||||
[](const auto& item) { return item.commandSequence; },
|
||||
message.commands.back()))) {
|
||||
error = "commands must have unique increasing sequences";
|
||||
return false;
|
||||
}
|
||||
message.commands.push_back(std::move(command));
|
||||
}
|
||||
message.stateHash = payload.value("stateHash", std::string());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string makeErrorMessage(const std::string& code, const std::string& detail)
|
||||
{
|
||||
return envelope("error", {{"code", code}, {"detail", detail}});
|
||||
}
|
||||
|
||||
bool parseErrorMessage(
|
||||
const std::string& text, std::string& code, std::string& detail)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
std::string error;
|
||||
if (!parseEnvelope(text, "error", payload, error) ||
|
||||
!payload.contains("code") || !payload["code"].is_string() ||
|
||||
!payload.contains("detail") || !payload["detail"].is_string()) {
|
||||
return false;
|
||||
}
|
||||
code = payload["code"].get<std::string>();
|
||||
detail = payload["detail"].get<std::string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string canonicalStateHash(nlohmann::json state)
|
||||
{
|
||||
state.erase("savedAt");
|
||||
const std::string bytes = state.dump();
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
for (const unsigned char value : bytes) {
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
std::ostringstream output;
|
||||
output << std::hex << std::setfill('0') << std::setw(16) << hash;
|
||||
return output.str();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
inline constexpr int kGameProtocolVersion = 2;
|
||||
inline constexpr std::int64_t kSimulationTickMs = 100;
|
||||
inline constexpr std::uint32_t kDefaultInputDelayTicks = 2;
|
||||
|
||||
struct InitialStateMessage {
|
||||
nlohmann::json state;
|
||||
std::string assignedPlayerId;
|
||||
std::uint64_t snapshotTick = 0;
|
||||
std::int64_t tickMs = kSimulationTickMs;
|
||||
std::uint32_t inputDelayTicks = kDefaultInputDelayTicks;
|
||||
std::string compatibilityFingerprint;
|
||||
};
|
||||
|
||||
struct DestinationInputMessage {
|
||||
std::uint64_t clientSequence = 0;
|
||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct CharacterActionInputMessage {
|
||||
std::uint64_t clientSequence = 0;
|
||||
std::string action;
|
||||
std::string targetId;
|
||||
};
|
||||
|
||||
struct ScheduledDestinationCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string playerId;
|
||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct ScheduledCharacterEnabledCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
struct ScheduledCharacterActionCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string playerId;
|
||||
std::string action;
|
||||
std::string targetId;
|
||||
};
|
||||
|
||||
using ScheduledCommand = std::variant<
|
||||
ScheduledDestinationCommand,
|
||||
ScheduledCharacterEnabledCommand,
|
||||
ScheduledCharacterActionCommand>;
|
||||
|
||||
struct TickMessage {
|
||||
std::uint64_t tickId = 0;
|
||||
std::vector<ScheduledCommand> commands;
|
||||
std::string stateHash;
|
||||
};
|
||||
|
||||
class LockstepTimeline {
|
||||
public:
|
||||
explicit LockstepTimeline(
|
||||
std::uint32_t inputDelayTicks = kDefaultInputDelayTicks)
|
||||
: inputDelayTicks_(inputDelayTicks) {}
|
||||
|
||||
std::uint64_t currentTick() const { return currentTick_; }
|
||||
std::uint64_t schedule(
|
||||
const std::string& playerId, const Eigen::Vector3f& target);
|
||||
std::uint64_t scheduleCharacterEnabled(
|
||||
const std::string& locationId, const std::string& characterId,
|
||||
bool enabled);
|
||||
std::uint64_t scheduleCharacterAction(
|
||||
const std::string& playerId, std::string action,
|
||||
std::string targetId = {});
|
||||
TickMessage advance();
|
||||
|
||||
private:
|
||||
std::uint32_t inputDelayTicks_;
|
||||
std::uint64_t currentTick_ = 0;
|
||||
std::uint64_t nextCommandSequence_ = 1;
|
||||
std::map<std::uint64_t, std::vector<ScheduledCommand>> pending_;
|
||||
};
|
||||
|
||||
std::string makeInitialStateMessage(const InitialStateMessage& message);
|
||||
bool parseInitialStateMessage(
|
||||
const std::string& text, InitialStateMessage& message, std::string& error);
|
||||
std::string makeDestinationInputMessage(const DestinationInputMessage& message);
|
||||
bool parseDestinationInputMessage(
|
||||
const std::string& text, DestinationInputMessage& message, std::string& error);
|
||||
std::string makeCharacterActionInputMessage(
|
||||
const CharacterActionInputMessage& message);
|
||||
bool parseCharacterActionInputMessage(
|
||||
const std::string& text, CharacterActionInputMessage& message,
|
||||
std::string& error);
|
||||
std::string makeTickMessage(const TickMessage& message);
|
||||
bool parseTickMessage(
|
||||
const std::string& text, TickMessage& message, std::string& error);
|
||||
std::string makeErrorMessage(const std::string& code, const std::string& detail);
|
||||
bool parseErrorMessage(
|
||||
const std::string& text, std::string& code, std::string& detail);
|
||||
|
||||
// Stable for the same executable/content build. Persistence-only wall-clock
|
||||
// fields are removed before hashing.
|
||||
std::string canonicalStateHash(nlohmann::json state);
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "RemoteGameServerHost.h"
|
||||
#include "WebsocketGameClient.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
RemoteGameServerHost::RemoteGameServerHost(WebsocketGameClient& client)
|
||||
: client_(client)
|
||||
{
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::initializeCampaign(
|
||||
const std::string& campaignId, const std::string&,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
localState_.clear();
|
||||
const CampaignConfig* campaign =
|
||||
CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign)
|
||||
throw std::runtime_error("Unknown remote campaign '" + campaignId + "'");
|
||||
client_.expectCompatibility(
|
||||
campaign->networkCompatibility.empty()
|
||||
? "lockstep-v2:" + campaignId
|
||||
: campaign->networkCompatibility);
|
||||
client_.replica_.startCampaign(*campaign, zipFile, true, false);
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
localState_.clear();
|
||||
client_.replica_.clearCampaign();
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::resetCampaign(
|
||||
const std::string&, const std::string&)
|
||||
{
|
||||
// The authoritative remote campaign cannot be reset by this client.
|
||||
}
|
||||
|
||||
bool RemoteGameServerHost::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
auto existing = client_.replica_.state_.locations.find(config.id);
|
||||
if (existing != client_.replica_.state_.locations.end() &&
|
||||
existing->second.definition) {
|
||||
// The location was prepared before initial_state. Loading the
|
||||
// authoritative snapshot overlays mutable state while retaining this
|
||||
// definition; preparing it again would reset the assigned player's
|
||||
// position to LocationConfig::playerPosition.
|
||||
return false;
|
||||
}
|
||||
return client_.replica_.prepareLocation(config);
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = client_.replica_.state_.locations.find(id);
|
||||
if (stored == client_.replica_.state_.locations.end())
|
||||
throw std::runtime_error("Remote location state was not prepared");
|
||||
params.definition = stored->second.definition;
|
||||
params.gameState = &stored->second;
|
||||
params.localState = localState_.getOrCreate(id);
|
||||
location->setup(params, audioPlayer);
|
||||
}
|
||||
|
||||
Location* RemoteGameServerHost::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto found = runtimeLocations_.find(id);
|
||||
return found == runtimeLocations_.end() ? nullptr : found->second.get();
|
||||
}
|
||||
|
||||
bool RemoteGameServerHost::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> RemoteGameServerHost::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::clearRuntimeLocations(bool)
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = client_.replica_.state_.locations.find(id);
|
||||
if (stored != client_.replica_.state_.locations.end() &&
|
||||
stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::update(std::int64_t deltaMs)
|
||||
{
|
||||
client_.update(deltaMs);
|
||||
for (auto& completed : client_.takeCompletedSimulationTicks()) {
|
||||
if (completed.locationId.empty()) continue;
|
||||
auto runtime = runtimeLocations_.find(completed.locationId);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
|
||||
runtime->second->update(kSimulationTickMs, completed.events);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::save(nlohmann::json&)
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::load(const nlohmann::json& in)
|
||||
{
|
||||
// The authoritative half is loaded by the standalone server. Each client
|
||||
// restores only its local presentation state from the shared start state.
|
||||
nlohmann::json ignoredNetworkState;
|
||||
ClientLocationState loadedLocalState;
|
||||
loadSinglePlayerPersistence(in, ignoredNetworkState, loadedLocalState);
|
||||
nlohmann::json localStateJson;
|
||||
loadedLocalState.save(localStateJson);
|
||||
localState_.load(localStateJson);
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include "../LocationState.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class WebsocketGameClient;
|
||||
|
||||
class RemoteGameServerHost final : public IGameServerHost {
|
||||
public:
|
||||
explicit RemoteGameServerHost(WebsocketGameClient& client);
|
||||
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override;
|
||||
void clearCampaign() override;
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override;
|
||||
Location* runtimeLocation(const std::string& id) const override;
|
||||
bool hasRuntimeLocation(const std::string& id) const override;
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override;
|
||||
void clearRuntimeLocations(bool retainState) override;
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
WebsocketGameClient& client_;
|
||||
ClientLocationState localState_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,647 @@
|
||||
#include "WebsocketGameClient.h"
|
||||
#include "NetworkProtocol.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/websocket.h>
|
||||
#else
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#ifdef NETWORK_TLS
|
||||
#include <wincrypt.h>
|
||||
// wincrypt.h defines macros that collide with OpenSSL identifiers.
|
||||
#undef X509_NAME
|
||||
#undef X509_EXTENSIONS
|
||||
#undef X509_CERT_PAIR
|
||||
#undef PKCS7_ISSUER_AND_SERIAL
|
||||
#undef OCSP_REQUEST
|
||||
#undef OCSP_RESPONSE
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509_vfy.h>
|
||||
#endif
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
#ifdef NETWORK_TLS
|
||||
// Loads the Windows "ROOT" certificate store into the OpenSSL context.
|
||||
void loadWindowsRootCertificates(asio::ssl::context& context)
|
||||
{
|
||||
HCERTSTORE store = CertOpenSystemStoreA(0, "ROOT");
|
||||
if (!store) {
|
||||
std::cerr << "[network] Could not open the Windows root certificate store\n";
|
||||
return;
|
||||
}
|
||||
X509_STORE* x509Store = SSL_CTX_get_cert_store(context.native_handle());
|
||||
PCCERT_CONTEXT cert = nullptr;
|
||||
while ((cert = CertEnumCertificatesInStore(store, cert)) != nullptr) {
|
||||
const unsigned char* data = cert->pbCertEncoded;
|
||||
X509* x509 = d2i_X509(nullptr, &data, static_cast<long>(cert->cbCertEncoded));
|
||||
if (x509) {
|
||||
X509_STORE_add_cert(x509Store, x509);
|
||||
X509_free(x509);
|
||||
}
|
||||
}
|
||||
CertCloseStore(store, 0);
|
||||
}
|
||||
|
||||
// Certificate problems are logged but deliberately do not abort the connection.
|
||||
bool logAndAcceptCertificate(bool preverified, asio::ssl::verify_context& context)
|
||||
{
|
||||
if (!preverified) {
|
||||
X509_STORE_CTX* storeContext = context.native_handle();
|
||||
const int error = X509_STORE_CTX_get_error(storeContext);
|
||||
const int depth = X509_STORE_CTX_get_error_depth(storeContext);
|
||||
char subject[256] = "?";
|
||||
if (X509* cert = X509_STORE_CTX_get_current_cert(storeContext))
|
||||
X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
|
||||
std::cerr << "[network] TLS certificate warning (depth " << depth
|
||||
<< ", subject " << subject << "): "
|
||||
<< X509_verify_cert_error_string(error)
|
||||
<< " - continuing anyway\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WebsocketEndpoint::parse(const std::string& url)
|
||||
{
|
||||
bool tls = false;
|
||||
std::string rest;
|
||||
if (url.rfind("wss://", 0) == 0) {
|
||||
tls = true;
|
||||
rest = url.substr(6);
|
||||
} else if (url.rfind("ws://", 0) == 0) {
|
||||
rest = url.substr(5);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
std::string authority = rest;
|
||||
std::string newPath = "/";
|
||||
const std::size_t slash = rest.find('/');
|
||||
if (slash != std::string::npos) {
|
||||
authority = rest.substr(0, slash);
|
||||
newPath = rest.substr(slash);
|
||||
}
|
||||
std::string newHost = authority;
|
||||
std::string newPort = tls ? "443" : "80";
|
||||
if (!authority.empty() && authority.front() == '[') { // [ipv6]:port
|
||||
const std::size_t close = authority.find(']');
|
||||
if (close == std::string::npos) return false;
|
||||
newHost = authority.substr(1, close - 1);
|
||||
if (close + 1 < authority.size()) {
|
||||
if (authority[close + 1] != ':') return false;
|
||||
newPort = authority.substr(close + 2);
|
||||
}
|
||||
} else {
|
||||
const std::size_t colon = authority.rfind(':');
|
||||
if (colon != std::string::npos) {
|
||||
newHost = authority.substr(0, colon);
|
||||
newPort = authority.substr(colon + 1);
|
||||
}
|
||||
}
|
||||
if (newHost.empty() || newPort.empty()) return false;
|
||||
host = std::move(newHost);
|
||||
port = std::move(newPort);
|
||||
path = std::move(newPath);
|
||||
useTls = tls;
|
||||
return true;
|
||||
}
|
||||
|
||||
class WebsocketGameClient::Impl {
|
||||
public:
|
||||
#ifdef __EMSCRIPTEN__
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
: host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
{
|
||||
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||
std::cout << "[network] Opening " << url_ << std::endl;
|
||||
|
||||
EmscriptenWebSocketCreateAttributes attributes = {};
|
||||
attributes.url = url_.c_str();
|
||||
attributes.protocols = nullptr;
|
||||
attributes.createOnMainThread = EM_TRUE;
|
||||
socket_ = emscripten_websocket_new(&attributes);
|
||||
if (socket_ <= 0) {
|
||||
fail("create WebSocket");
|
||||
return;
|
||||
}
|
||||
|
||||
emscripten_websocket_set_onopen_callback(
|
||||
socket_, this, &Impl::onOpen);
|
||||
emscripten_websocket_set_onerror_callback(
|
||||
socket_, this, &Impl::onError);
|
||||
emscripten_websocket_set_onclose_callback(
|
||||
socket_, this, &Impl::onClose);
|
||||
emscripten_websocket_set_onmessage_callback(
|
||||
socket_, this, &Impl::onMessage);
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
if (socket_ > 0) {
|
||||
closing_ = true;
|
||||
emscripten_websocket_close(socket_, 1000, "client shutdown");
|
||||
emscripten_websocket_delete(socket_);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> takeMessages()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.swap(messages_);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool connected() const { return connected_; }
|
||||
bool connectionFailed() const { return connectionFailed_; }
|
||||
|
||||
void send(std::string message)
|
||||
{
|
||||
if (!connected_ || socket_ <= 0) return;
|
||||
const EMSCRIPTEN_RESULT result =
|
||||
emscripten_websocket_send_utf8_text(socket_, message.c_str());
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
fail("send WebSocket message");
|
||||
}
|
||||
|
||||
private:
|
||||
static EM_BOOL onOpen(
|
||||
int, const EmscriptenWebSocketOpenEvent*, void* userData)
|
||||
{
|
||||
auto* self = static_cast<Impl*>(userData);
|
||||
self->connected_ = true;
|
||||
std::cout << "[network] Connected to " << self->url_ << std::endl;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
static EM_BOOL onError(
|
||||
int, const EmscriptenWebSocketErrorEvent*, void* userData)
|
||||
{
|
||||
static_cast<Impl*>(userData)->fail("WebSocket");
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
static EM_BOOL onClose(
|
||||
int, const EmscriptenWebSocketCloseEvent*, void* userData)
|
||||
{
|
||||
auto* self = static_cast<Impl*>(userData);
|
||||
self->connected_ = false;
|
||||
if (!self->closing_) self->fail("WebSocket close");
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
static EM_BOOL onMessage(
|
||||
int, const EmscriptenWebSocketMessageEvent* event, void* userData)
|
||||
{
|
||||
auto* self = static_cast<Impl*>(userData);
|
||||
if (!event || event->isText != EM_TRUE) {
|
||||
self->fail("binary WebSocket message");
|
||||
return EM_TRUE;
|
||||
}
|
||||
self->messages_.emplace_back(
|
||||
reinterpret_cast<const char*>(event->data), event->numBytes);
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
void fail(const char* operation)
|
||||
{
|
||||
connected_ = false;
|
||||
connectionFailed_ = true;
|
||||
std::cerr << "[network] " << operation << " failed" << std::endl;
|
||||
}
|
||||
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::string path_;
|
||||
std::string url_;
|
||||
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
|
||||
bool connected_ = false;
|
||||
bool connectionFailed_ = false;
|
||||
bool closing_ = false;
|
||||
std::vector<std::string> messages_;
|
||||
#else
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
:
|
||||
#ifdef NETWORK_TLS
|
||||
sslContext_(asio::ssl::context::tls_client)
|
||||
, resolver_(io_)
|
||||
, secureSocket_(io_, sslContext_)
|
||||
#else
|
||||
resolver_(io_)
|
||||
#endif
|
||||
, socket_(io_)
|
||||
, host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
, useTls_(useTls)
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
sslContext_.set_options(
|
||||
asio::ssl::context::default_workarounds |
|
||||
asio::ssl::context::no_sslv2 | asio::ssl::context::no_sslv3 |
|
||||
asio::ssl::context::no_tlsv1 | asio::ssl::context::no_tlsv1_1);
|
||||
loadWindowsRootCertificates(sslContext_);
|
||||
sslContext_.set_verify_mode(asio::ssl::verify_peer);
|
||||
sslContext_.set_verify_callback(&logAndAcceptCertificate);
|
||||
#else
|
||||
if (useTls_) {
|
||||
std::cerr << "[network] wss:// requested but TLS support was not "
|
||||
"compiled in (NETWORK_TLS)" << std::endl;
|
||||
connectionFailed_.store(true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
std::cout << "[network] Resolving " << scheme() << host_ << ':' << port_
|
||||
<< path_ << std::endl;
|
||||
resolve();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
io_.stop();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
std::vector<std::string> takeMessages()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
std::vector<std::string> result;
|
||||
result.swap(messages_);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool connected() const { return connected_.load(); }
|
||||
bool connectionFailed() const { return connectionFailed_.load(); }
|
||||
|
||||
void send(std::string message)
|
||||
{
|
||||
asio::post(io_, [this, message = std::move(message)]() mutable {
|
||||
if (!connected_.load()) return;
|
||||
const bool writeInProgress = !outbound_.empty();
|
||||
outbound_.push_back(std::move(message));
|
||||
if (!writeInProgress) write();
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
const char* scheme() const { return useTls_ ? "wss://" : "ws://"; }
|
||||
|
||||
template <class F>
|
||||
void withStream(F&& f)
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
if (useTls_) {
|
||||
f(secureSocket_);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
f(socket_);
|
||||
}
|
||||
|
||||
void resolve()
|
||||
{
|
||||
resolver_.async_resolve(
|
||||
host_, port_,
|
||||
[this](beast::error_code error, tcp::resolver::results_type results) {
|
||||
if (error) return fail("resolve", error);
|
||||
std::cout << "[network] WebSocket host resolved; connecting"
|
||||
<< std::endl;
|
||||
withStream([&](auto& stream) {
|
||||
asio::async_connect(
|
||||
beast::get_lowest_layer(stream), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
std::cout << "[network] TCP connected" << std::endl;
|
||||
startTlsOrWebsocketHandshake();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void startTlsOrWebsocketHandshake()
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
if (useTls_) {
|
||||
SSL* ssl = secureSocket_.next_layer().native_handle();
|
||||
// SNI, and hostname verification (failures are only logged).
|
||||
SSL_set_tlsext_host_name(ssl, const_cast<char*>(host_.c_str()));
|
||||
SSL_set1_host(ssl, host_.c_str());
|
||||
std::cout << "[network] Starting TLS handshake" << std::endl;
|
||||
secureSocket_.next_layer().async_handshake(
|
||||
asio::ssl::stream_base::client,
|
||||
[this](beast::error_code tlsError) {
|
||||
if (tlsError) return fail("TLS handshake", tlsError);
|
||||
std::cout << "[network] TLS established" << std::endl;
|
||||
startWebsocketHandshake();
|
||||
});
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
startWebsocketHandshake();
|
||||
}
|
||||
|
||||
void startWebsocketHandshake()
|
||||
{
|
||||
const bool defaultPort = port_ == (useTls_ ? "443" : "80");
|
||||
const std::string hostHeader = defaultPort ? host_ : host_ + ':' + port_;
|
||||
std::cout << "[network] Starting WebSocket handshake" << std::endl;
|
||||
withStream([&](auto& stream) {
|
||||
stream.set_option(
|
||||
websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
stream.async_handshake(
|
||||
hostHeader, path_,
|
||||
[this](beast::error_code handshakeError) {
|
||||
if (handshakeError)
|
||||
return fail("handshake", handshakeError);
|
||||
connected_.store(true);
|
||||
std::cout
|
||||
<< "[network] Connected to " << scheme()
|
||||
<< host_ << ':' << port_ << path_ << std::endl;
|
||||
read();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void read()
|
||||
{
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
messages_.push_back(std::move(message));
|
||||
}
|
||||
read();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) return fail("write", error);
|
||||
outbound_.pop_front();
|
||||
if (!outbound_.empty()) write();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void fail(const char* operation, beast::error_code error)
|
||||
{
|
||||
connected_.store(false);
|
||||
connectionFailed_.store(true);
|
||||
std::cerr << "[network] WebSocket " << operation << " failed: "
|
||||
<< error.message() << '\n';
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
#ifdef NETWORK_TLS
|
||||
asio::ssl::context sslContext_;
|
||||
#endif
|
||||
tcp::resolver resolver_;
|
||||
#ifdef NETWORK_TLS
|
||||
websocket::stream<beast::ssl_stream<tcp::socket>> secureSocket_;
|
||||
#endif
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::thread thread_;
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::string path_;
|
||||
bool useTls_ = false;
|
||||
std::atomic_bool connected_{false};
|
||||
std::atomic_bool connectionFailed_{false};
|
||||
std::mutex messagesMutex_;
|
||||
std::vector<std::string> messages_;
|
||||
std::deque<std::string> outbound_;
|
||||
#endif
|
||||
};
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(
|
||||
std::string host, std::string port, std::string path, bool useTls)
|
||||
: impl_(std::make_unique<Impl>(
|
||||
std::move(host), std::move(port), std::move(path), useTls))
|
||||
{
|
||||
}
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(const WebsocketEndpoint& endpoint)
|
||||
: WebsocketGameClient(
|
||||
endpoint.host, endpoint.port, endpoint.path, endpoint.useTls)
|
||||
{
|
||||
}
|
||||
|
||||
WebsocketGameClient::~WebsocketGameClient() = default;
|
||||
|
||||
std::shared_ptr<const GameSnapshot> WebsocketGameClient::snapshot() const
|
||||
{
|
||||
return replica_.snapshot();
|
||||
}
|
||||
|
||||
const std::string& WebsocketGameClient::localPlayerId() const
|
||||
{
|
||||
return replica_.localPlayerId();
|
||||
}
|
||||
|
||||
void WebsocketGameClient::submit(const GameCommand& command)
|
||||
{
|
||||
if (!initialized_) return;
|
||||
if (const auto* destination =
|
||||
std::get_if<SetCharacterDestinationCommand>(&command)) {
|
||||
impl_->send(makeDestinationInputMessage(
|
||||
{nextClientSequence_++, destination->target}));
|
||||
} else if (const auto* target =
|
||||
std::get_if<SetCharacterTargetCommand>(&command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "set_target", target->targetId}));
|
||||
} else if (std::holds_alternative<ClearCharacterAttackTargetCommand>(command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "clear_attack_target", {}}));
|
||||
} else if (std::holds_alternative<TryCharacterAttackComboCommand>(command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "try_attack_combo", {}}));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GameServerEvent> WebsocketGameClient::drainEvents()
|
||||
{
|
||||
std::vector<GameServerEvent> result;
|
||||
result.swap(pendingEvents_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void WebsocketGameClient::update(std::int64_t)
|
||||
{
|
||||
std::vector<std::string> messages = impl_->takeMessages();
|
||||
for (const std::string& text : messages) {
|
||||
std::string error;
|
||||
InitialStateMessage initial;
|
||||
if (parseInitialStateMessage(text, initial, error)) {
|
||||
if (!expectedCompatibilityFingerprint_.empty() &&
|
||||
initial.compatibilityFingerprint !=
|
||||
expectedCompatibilityFingerprint_) {
|
||||
std::cerr << "[network] Incompatible server content: expected "
|
||||
<< expectedCompatibilityFingerprint_ << ", received "
|
||||
<< initial.compatibilityFingerprint << '\n';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
replica_.load(initial.state);
|
||||
replica_.setLocalPlayerId(initial.assignedPlayerId);
|
||||
nextExpectedTick_ = initial.snapshotTick + 1;
|
||||
compatibilityFingerprint_ =
|
||||
std::move(initial.compatibilityFingerprint);
|
||||
pendingTicks_.clear();
|
||||
initialized_ = true;
|
||||
std::cout << "[network] Applied initial state at tick "
|
||||
<< initial.snapshotTick << " as player '"
|
||||
<< initial.assignedPlayerId << "' (delay "
|
||||
<< initial.inputDelayTicks << " ticks)" << std::endl;
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "[network] Invalid initial state: "
|
||||
<< exception.what() << '\n';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
TickMessage tick;
|
||||
if (parseTickMessage(text, tick, error)) {
|
||||
if (initialized_ && tick.tickId >= nextExpectedTick_) {
|
||||
if (!loggedFirstTick_) {
|
||||
std::cout << "[network] Received first lockstep tick "
|
||||
<< tick.tickId << std::endl;
|
||||
loggedFirstTick_ = true;
|
||||
}
|
||||
pendingTicks_.emplace(tick.tickId, std::move(tick));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::string errorCode;
|
||||
std::string errorDetail;
|
||||
if (parseErrorMessage(text, errorCode, errorDetail)) {
|
||||
std::cerr << "[network] Server rejected request (" << errorCode
|
||||
<< "): " << errorDetail << '\n';
|
||||
if (errorCode == "server_full" ||
|
||||
errorCode == "incompatible")
|
||||
rejected_ = true;
|
||||
continue;
|
||||
}
|
||||
std::cerr << "[network] Ignoring WebSocket message: "
|
||||
<< error << '\n';
|
||||
}
|
||||
|
||||
while (initialized_) {
|
||||
auto next = pendingTicks_.find(nextExpectedTick_);
|
||||
if (next == pendingTicks_.end()) break;
|
||||
TickMessage tick = std::move(next->second);
|
||||
pendingTicks_.erase(next);
|
||||
const std::string locationId =
|
||||
replica_.snapshot()->currentLocationId;
|
||||
for (const ScheduledCommand& scheduled : tick.commands) {
|
||||
std::visit([&](const auto& command) {
|
||||
using T = std::decay_t<decltype(command)>;
|
||||
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||
replica_.submit(SetCharacterDestinationCommand{
|
||||
locationId, command.playerId, command.target, {}});
|
||||
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
replica_.submit(SetCharacterEnabledCommand{
|
||||
command.locationId, command.characterId, command.enabled});
|
||||
} else if (command.action == "set_target") {
|
||||
replica_.submit(SetCharacterTargetCommand{
|
||||
locationId, command.playerId, command.targetId});
|
||||
} else if (command.action == "clear_attack_target") {
|
||||
replica_.submit(ClearCharacterAttackTargetCommand{
|
||||
locationId, command.playerId});
|
||||
} else if (command.action == "try_attack_combo") {
|
||||
replica_.submit(TryCharacterAttackComboCommand{
|
||||
locationId, command.playerId});
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
replica_.update(kSimulationTickMs);
|
||||
completedSimulationTicks_.push_back({
|
||||
replica_.pendingSimulationLocationId_,
|
||||
replica_.pendingSimulationEvents_});
|
||||
std::vector<GameServerEvent> events = replica_.drainEvents();
|
||||
pendingEvents_.insert(
|
||||
pendingEvents_.end(),
|
||||
std::make_move_iterator(events.begin()),
|
||||
std::make_move_iterator(events.end()));
|
||||
if (!tick.stateHash.empty()) {
|
||||
nlohmann::json state;
|
||||
replica_.save(state);
|
||||
/*const std::string localHash = canonicalStateHash(std::move(state));
|
||||
if (localHash != tick.stateHash) {
|
||||
std::cerr << "[network] Lockstep divergence at tick "
|
||||
<< tick.tickId << ": server=" << tick.stateHash
|
||||
<< ", client=" << localHash << '\n';
|
||||
}*/
|
||||
}
|
||||
++nextExpectedTick_;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WebsocketGameClient::CompletedSimulationTick>
|
||||
WebsocketGameClient::takeCompletedSimulationTicks()
|
||||
{
|
||||
std::vector<CompletedSimulationTick> result;
|
||||
result.swap(completedSimulationTicks_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void WebsocketGameClient::save(nlohmann::json&) const
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
void WebsocketGameClient::load(const nlohmann::json&)
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
bool WebsocketGameClient::connected() const
|
||||
{
|
||||
return impl_->connected();
|
||||
}
|
||||
|
||||
bool WebsocketGameClient::connectionFailed() const
|
||||
{
|
||||
return impl_->connectionFailed();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include "NetworkProtocol.h"
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class RemoteGameServerHost;
|
||||
|
||||
// Parsed "ws://host[:port][/path]" or "wss://host[:port][/path]".
|
||||
struct WebsocketEndpoint {
|
||||
std::string host = "127.0.0.1";
|
||||
std::string port = "8080";
|
||||
std::string path = "/";
|
||||
bool useTls = false;
|
||||
|
||||
// Returns false (and leaves *this untouched) for an unsupported/malformed URL.
|
||||
// Default ports: 80 for ws://, 443 for wss://.
|
||||
bool parse(const std::string& url);
|
||||
};
|
||||
|
||||
class WebsocketGameClient final : public IGameServer {
|
||||
public:
|
||||
WebsocketGameClient(
|
||||
std::string host = "127.0.0.1",
|
||||
std::string port = "8080",
|
||||
std::string path = "/",
|
||||
bool useTls = false);
|
||||
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
||||
~WebsocketGameClient();
|
||||
|
||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||
WebsocketGameClient& operator=(const WebsocketGameClient&) = delete;
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
const std::string& localPlayerId() const override;
|
||||
void submit(const GameCommand& command) override;
|
||||
std::vector<GameServerEvent> drainEvents() override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
bool connected() const;
|
||||
bool connectionFailed() const;
|
||||
bool initialized() const { return initialized_; }
|
||||
bool rejected() const { return rejected_; }
|
||||
|
||||
private:
|
||||
friend class RemoteGameServerHost;
|
||||
struct CompletedSimulationTick {
|
||||
std::string locationId;
|
||||
LocationSimulationEvents events;
|
||||
};
|
||||
std::vector<CompletedSimulationTick> takeCompletedSimulationTicks();
|
||||
void expectCompatibility(std::string fingerprint) {
|
||||
expectedCompatibilityFingerprint_ = std::move(fingerprint);
|
||||
}
|
||||
class Impl;
|
||||
InternalGameServer replica_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
std::map<std::uint64_t, TickMessage> pendingTicks_;
|
||||
std::vector<CompletedSimulationTick> completedSimulationTicks_;
|
||||
std::vector<GameServerEvent> pendingEvents_;
|
||||
std::uint64_t nextExpectedTick_ = 0;
|
||||
std::uint64_t nextClientSequence_ = 1;
|
||||
std::string compatibilityFingerprint_;
|
||||
std::string expectedCompatibilityFingerprint_;
|
||||
bool initialized_ = false;
|
||||
bool rejected_ = false;
|
||||
bool loggedFirstTick_ = false;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,398 @@
|
||||
#include "WebsocketGameServer.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include "NetworkProtocol.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
class ServerSession : public std::enable_shared_from_this<ServerSession> {
|
||||
public:
|
||||
using InputHandler = std::function<void(
|
||||
const std::shared_ptr<ServerSession>&, const std::string&)>;
|
||||
using CloseHandler =
|
||||
std::function<void(const std::shared_ptr<ServerSession>&)>;
|
||||
|
||||
ServerSession(
|
||||
tcp::socket socket, std::string playerId,
|
||||
InputHandler inputHandler, CloseHandler closeHandler)
|
||||
: socket_(std::move(socket))
|
||||
, playerId_(std::move(playerId))
|
||||
, inputHandler_(std::move(inputHandler))
|
||||
, closeHandler_(std::move(closeHandler))
|
||||
{
|
||||
}
|
||||
|
||||
const std::string& playerId() const { return playerId_; }
|
||||
|
||||
void start(std::string initialState, bool closeAfterWrite = false)
|
||||
{
|
||||
closeAfterWrite_ = closeAfterWrite;
|
||||
socket_.set_option(
|
||||
websocket::stream_base::timeout::suggested(beast::role_type::server));
|
||||
socket_.async_accept(
|
||||
[self = shared_from_this(), initialState = std::move(initialState)](
|
||||
beast::error_code error) mutable {
|
||||
if (error) {
|
||||
std::cerr << "[network] WebSocket accept failed: "
|
||||
<< error.message() << '\n';
|
||||
return;
|
||||
}
|
||||
self->socket_.text(true);
|
||||
self->send(std::move(initialState));
|
||||
if (!self->closeAfterWrite_) self->read();
|
||||
});
|
||||
}
|
||||
|
||||
void send(std::string message)
|
||||
{
|
||||
const bool writeInProgress = !outbound_.empty();
|
||||
outbound_.push_back(std::move(message));
|
||||
if (!writeInProgress) write();
|
||||
}
|
||||
|
||||
private:
|
||||
void read()
|
||||
{
|
||||
socket_.async_read(
|
||||
inbound_,
|
||||
[self = shared_from_this()](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
if (error != websocket::error::closed)
|
||||
std::cerr << "[network] WebSocket read failed: "
|
||||
<< error.message() << '\n';
|
||||
self->notifyClosed();
|
||||
return;
|
||||
}
|
||||
const std::string message =
|
||||
beast::buffers_to_string(self->inbound_.data());
|
||||
self->inbound_.consume(self->inbound_.size());
|
||||
self->inputHandler_(self, message);
|
||||
self->read();
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
socket_.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[self = shared_from_this()](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
std::cerr << "[network] WebSocket write failed: "
|
||||
<< error.message() << '\n';
|
||||
return;
|
||||
}
|
||||
self->outbound_.pop_front();
|
||||
if (!self->outbound_.empty()) {
|
||||
self->write();
|
||||
} else if (self->closeAfterWrite_) {
|
||||
self->socket_.async_close(
|
||||
websocket::close_code::policy_error,
|
||||
[self](beast::error_code) { self->notifyClosed(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void notifyClosed()
|
||||
{
|
||||
if (closed_) return;
|
||||
closed_ = true;
|
||||
if (closeHandler_) closeHandler_(shared_from_this());
|
||||
}
|
||||
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer inbound_;
|
||||
std::deque<std::string> outbound_;
|
||||
std::string playerId_;
|
||||
InputHandler inputHandler_;
|
||||
CloseHandler closeHandler_;
|
||||
bool closeAfterWrite_ = false;
|
||||
bool closed_ = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class WebsocketGameServer::Impl {
|
||||
public:
|
||||
struct PendingInput {
|
||||
std::string playerId;
|
||||
std::variant<
|
||||
DestinationInputMessage,
|
||||
CharacterActionInputMessage> input;
|
||||
};
|
||||
|
||||
Impl(
|
||||
const std::string& address, std::uint16_t port,
|
||||
std::vector<std::string> playerSlots, nlohmann::json initialState,
|
||||
std::string compatibilityFingerprint)
|
||||
: acceptor_(io_)
|
||||
, playerSlots_(std::move(playerSlots))
|
||||
, latestState_(std::move(initialState))
|
||||
, compatibilityFingerprint_(std::move(compatibilityFingerprint))
|
||||
{
|
||||
const tcp::endpoint endpoint(asio::ip::make_address(address), port);
|
||||
acceptor_.open(endpoint.protocol());
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true));
|
||||
acceptor_.bind(endpoint);
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections);
|
||||
port_ = acceptor_.local_endpoint().port();
|
||||
accept();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
asio::post(io_, [this]() {
|
||||
beast::error_code ignored;
|
||||
acceptor_.close(ignored);
|
||||
});
|
||||
io_.stop();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
std::vector<PendingInput> takeInputs()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
std::vector<PendingInput> result;
|
||||
result.swap(pendingInputs_);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> takeReleasedPlayers()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
std::vector<std::string> result;
|
||||
result.swap(releasedPlayers_);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> takeConnectedPlayers()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
std::vector<std::string> result;
|
||||
result.swap(connectedPlayers_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void publishTick(
|
||||
std::string tick, nlohmann::json latestState, std::uint64_t tickId)
|
||||
{
|
||||
asio::post(io_, [this, tick = std::move(tick),
|
||||
latestState = std::move(latestState), tickId]() mutable {
|
||||
latestState_ = std::move(latestState);
|
||||
latestTick_ = tickId;
|
||||
auto output = sessions_.begin();
|
||||
for (auto input = sessions_.begin(); input != sessions_.end(); ++input) {
|
||||
if (auto session = input->lock()) {
|
||||
session->send(tick);
|
||||
*output++ = *input;
|
||||
}
|
||||
}
|
||||
sessions_.erase(output, sessions_.end());
|
||||
});
|
||||
}
|
||||
|
||||
std::uint16_t port() const { return port_; }
|
||||
|
||||
private:
|
||||
void accept()
|
||||
{
|
||||
acceptor_.async_accept(
|
||||
[this](beast::error_code error, tcp::socket socket) {
|
||||
if (!error) {
|
||||
std::string playerId;
|
||||
for (const std::string& candidate : playerSlots_) {
|
||||
if (assignedSlots_.count(candidate) == 0) {
|
||||
playerId = candidate;
|
||||
assignedSlots_[candidate] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto session =
|
||||
std::make_shared<ServerSession>(
|
||||
std::move(socket), playerId,
|
||||
[this](const std::shared_ptr<ServerSession>& source,
|
||||
const std::string& message) {
|
||||
DestinationInputMessage destination;
|
||||
std::string destinationError;
|
||||
if (parseDestinationInputMessage(
|
||||
message, destination, destinationError)) {
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
pendingInputs_.push_back(
|
||||
{source->playerId(), std::move(destination)});
|
||||
return;
|
||||
}
|
||||
CharacterActionInputMessage action;
|
||||
std::string actionError;
|
||||
if (!parseCharacterActionInputMessage(
|
||||
message, action, actionError)) {
|
||||
source->send(makeErrorMessage(
|
||||
"invalid_input", actionError.empty()
|
||||
? destinationError : actionError));
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
pendingInputs_.push_back(
|
||||
{source->playerId(), std::move(action)});
|
||||
},
|
||||
[this](const std::shared_ptr<ServerSession>& source) {
|
||||
if (!source->playerId().empty())
|
||||
std::cout << "[network] Player slot '"
|
||||
<< source->playerId()
|
||||
<< "' disconnected" << std::endl;
|
||||
assignedSlots_.erase(source->playerId());
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
if (!source->playerId().empty())
|
||||
releasedPlayers_.push_back(source->playerId());
|
||||
});
|
||||
if (playerId.empty()) {
|
||||
std::cout << "[network] Rejecting connection: no player slot"
|
||||
<< std::endl;
|
||||
session->start(
|
||||
makeErrorMessage(
|
||||
"server_full", "No multiplayer player slot is available"),
|
||||
true);
|
||||
} else {
|
||||
std::cout << "[network] Accepted connection as player slot '"
|
||||
<< playerId << "' at snapshot tick "
|
||||
<< latestTick_ << std::endl;
|
||||
sessions_.push_back(session);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
connectedPlayers_.push_back(playerId);
|
||||
}
|
||||
InitialStateMessage initial;
|
||||
initial.state = latestState_;
|
||||
initial.assignedPlayerId = playerId;
|
||||
initial.snapshotTick = latestTick_;
|
||||
initial.compatibilityFingerprint =
|
||||
compatibilityFingerprint_;
|
||||
session->start(makeInitialStateMessage(initial));
|
||||
std::cout << "[network] Initial state queued for player '"
|
||||
<< playerId << "'" << std::endl;
|
||||
}
|
||||
} else if (error != asio::error::operation_aborted) {
|
||||
std::cerr << "[network] TCP accept failed: "
|
||||
<< error.message() << '\n';
|
||||
}
|
||||
if (acceptor_.is_open()) accept();
|
||||
});
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::thread thread_;
|
||||
std::vector<std::weak_ptr<ServerSession>> sessions_;
|
||||
std::vector<std::string> playerSlots_;
|
||||
std::unordered_map<std::string, bool> assignedSlots_;
|
||||
nlohmann::json latestState_;
|
||||
std::string compatibilityFingerprint_;
|
||||
std::uint64_t latestTick_ = 0;
|
||||
std::mutex inputsMutex_;
|
||||
std::vector<PendingInput> pendingInputs_;
|
||||
std::vector<std::string> connectedPlayers_;
|
||||
std::vector<std::string> releasedPlayers_;
|
||||
std::uint16_t port_ = 0;
|
||||
};
|
||||
|
||||
WebsocketGameServer::WebsocketGameServer(
|
||||
InternalGameServer& gameServer,
|
||||
std::vector<std::string> playerSlots,
|
||||
std::string compatibilityFingerprint,
|
||||
const std::string& address,
|
||||
std::uint16_t port)
|
||||
: gameServer_(gameServer)
|
||||
{
|
||||
nlohmann::json state;
|
||||
gameServer_.save(state);
|
||||
impl_ = std::make_unique<Impl>(
|
||||
address, port, std::move(playerSlots), std::move(state),
|
||||
std::move(compatibilityFingerprint));
|
||||
}
|
||||
|
||||
WebsocketGameServer::~WebsocketGameServer() = default;
|
||||
|
||||
void WebsocketGameServer::update()
|
||||
{
|
||||
const std::string locationId = gameServer_.snapshot()->currentLocationId;
|
||||
for (const std::string& playerId : impl_->takeConnectedPlayers())
|
||||
timeline_.scheduleCharacterEnabled(locationId, playerId, true);
|
||||
const std::vector<std::string> releasedPlayers =
|
||||
impl_->takeReleasedPlayers();
|
||||
for (const std::string& playerId : releasedPlayers)
|
||||
timeline_.scheduleCharacterEnabled(locationId, playerId, false);
|
||||
for (const std::string& playerId : releasedPlayers)
|
||||
lastClientSequences_.erase(playerId);
|
||||
for (const Impl::PendingInput& pending : impl_->takeInputs()) {
|
||||
std::uint64_t& previous = lastClientSequences_[pending.playerId];
|
||||
const std::uint64_t sequence = std::visit(
|
||||
[](const auto& input) { return input.clientSequence; },
|
||||
pending.input);
|
||||
if (sequence <= previous) continue;
|
||||
previous = sequence;
|
||||
std::visit([&](const auto& input) {
|
||||
using T = std::decay_t<decltype(input)>;
|
||||
if constexpr (std::is_same_v<T, DestinationInputMessage>) {
|
||||
timeline_.schedule(pending.playerId, input.target);
|
||||
} else {
|
||||
timeline_.scheduleCharacterAction(
|
||||
pending.playerId, input.action, input.targetId);
|
||||
}
|
||||
}, pending.input);
|
||||
}
|
||||
|
||||
TickMessage tick = timeline_.advance();
|
||||
for (const ScheduledCommand& scheduled : tick.commands) {
|
||||
std::visit([this](const auto& command) {
|
||||
using T = std::decay_t<decltype(command)>;
|
||||
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||
gameServer_.submit(SetCharacterDestinationCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId, command.target, {}});
|
||||
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
gameServer_.submit(SetCharacterEnabledCommand{
|
||||
command.locationId, command.characterId, command.enabled});
|
||||
} else if (command.action == "set_target") {
|
||||
gameServer_.submit(SetCharacterTargetCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId, command.targetId});
|
||||
} else if (command.action == "clear_attack_target") {
|
||||
gameServer_.submit(ClearCharacterAttackTargetCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId});
|
||||
} else if (command.action == "try_attack_combo") {
|
||||
gameServer_.submit(TryCharacterAttackComboCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId});
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
gameServer_.update(kSimulationTickMs);
|
||||
nlohmann::json state;
|
||||
gameServer_.save(state);
|
||||
tick.stateHash = canonicalStateHash(state);
|
||||
impl_->publishTick(makeTickMessage(tick), std::move(state), tick.tickId);
|
||||
}
|
||||
|
||||
std::uint16_t WebsocketGameServer::port() const
|
||||
{
|
||||
return impl_->port();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "NetworkProtocol.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class InternalGameServer;
|
||||
|
||||
class WebsocketGameServer {
|
||||
public:
|
||||
WebsocketGameServer(
|
||||
InternalGameServer& gameServer,
|
||||
std::vector<std::string> playerSlots,
|
||||
std::string compatibilityFingerprint,
|
||||
const std::string& address = "0.0.0.0",
|
||||
std::uint16_t port = 8080);
|
||||
~WebsocketGameServer();
|
||||
|
||||
WebsocketGameServer(const WebsocketGameServer&) = delete;
|
||||
WebsocketGameServer& operator=(const WebsocketGameServer&) = delete;
|
||||
|
||||
void update();
|
||||
std::uint16_t port() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
InternalGameServer& gameServer_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
LockstepTimeline timeline_;
|
||||
std::unordered_map<std::string, std::uint64_t> lastClientSequences_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+84
-119
@@ -8,6 +8,7 @@
|
||||
#include <Eigen/Geometry>
|
||||
#include "../Character.h"
|
||||
#include <SDL.h>
|
||||
#include <unordered_set>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
@@ -91,6 +92,9 @@ namespace FRG {
|
||||
|
||||
for (const auto& item : j["objects"]) {
|
||||
InteractiveObjectData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty())
|
||||
throw std::runtime_error("Interactive object id must not be empty in " + jsonPath);
|
||||
data.base = parseGameObjectData(item);
|
||||
data.interactionRadius = item.value("interactionRadius", 2.0f);
|
||||
data.approachRadius = item.value("approachRadius", data.interactionRadius);
|
||||
@@ -98,20 +102,21 @@ namespace FRG {
|
||||
data.castShadowNight = item.value("castShadowNight", true);
|
||||
data.isActive = item.value("isActive", true);
|
||||
data.activateFunctionName = item.value("activateFunction", "");
|
||||
data.pivotX = item.value("pivotX", 0.0f);
|
||||
data.pivotY = item.value("pivotY", 0.0f);
|
||||
data.pivotZ = item.value("pivotZ", 0.0f);
|
||||
data.boundsMinX = item.value("boundsMinX", 0.0f);
|
||||
data.boundsMinY = item.value("boundsMinY", 0.0f);
|
||||
data.boundsMinZ = item.value("boundsMinZ", 0.0f);
|
||||
data.boundsMaxX = item.value("boundsMaxX", 0.0f);
|
||||
data.boundsMaxY = item.value("boundsMaxY", 0.0f);
|
||||
data.boundsMaxZ = item.value("boundsMaxZ", 0.0f);
|
||||
data.pivot = {
|
||||
item.value("pivotX", 0.0f), item.value("pivotY", 0.0f),
|
||||
item.value("pivotZ", 0.0f)};
|
||||
data.boundsMin = {
|
||||
item.value("boundsMinX", 0.0f), item.value("boundsMinY", 0.0f),
|
||||
item.value("boundsMinZ", 0.0f)};
|
||||
data.boundsMax = {
|
||||
item.value("boundsMaxX", 0.0f), item.value("boundsMaxY", 0.0f),
|
||||
item.value("boundsMaxZ", 0.0f)};
|
||||
if (item.contains("interactionPositionX")) {
|
||||
data.hasInteractionPosition = true;
|
||||
data.interactionPositionX = item.value("interactionPositionX", 0.0f);
|
||||
data.interactionPositionY = item.value("interactionPositionY", 0.0f);
|
||||
data.interactionPositionZ = item.value("interactionPositionZ", 0.0f);
|
||||
data.interactionPosition = {
|
||||
item.value("interactionPositionX", 0.0f),
|
||||
item.value("interactionPositionY", 0.0f),
|
||||
item.value("interactionPositionZ", 0.0f)};
|
||||
}
|
||||
if (!data.base.meshPath.empty())
|
||||
objects.push_back(std::move(data));
|
||||
@@ -121,40 +126,6 @@ namespace FRG {
|
||||
return objects;
|
||||
}
|
||||
|
||||
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const {
|
||||
LoadedGameObjectState st;
|
||||
st.texturePath = texturePath;
|
||||
st.textureDarkandsPath = textureDarkandsPath;
|
||||
st.meshPath = meshPath;
|
||||
st.meshRotationX = rotationX;
|
||||
st.meshRotationY = rotationY;
|
||||
st.meshRotationZ = rotationZ;
|
||||
st.meshScale = scale;
|
||||
return st;
|
||||
}
|
||||
|
||||
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const {
|
||||
InteractiveObjectState st;
|
||||
st.objectInfo = base.toLoadedGameObjectState();
|
||||
st.interactionRadius = interactionRadius;
|
||||
st.approachRadius = approachRadius;
|
||||
st.castShadow = castShadow;
|
||||
st.castShadowNight = castShadowNight;
|
||||
st.isActive = isActive;
|
||||
st.activateFunctionName = activateFunctionName;
|
||||
st.pivot = Eigen::Vector3f(pivotX, pivotY, pivotZ);
|
||||
st.boundsMin = Eigen::Vector3f(boundsMinX, boundsMinY, boundsMinZ);
|
||||
st.boundsMax = Eigen::Vector3f(boundsMaxX, boundsMaxY, boundsMaxZ);
|
||||
st.hasInteractionPosition = hasInteractionPosition;
|
||||
if (hasInteractionPosition)
|
||||
st.interactionPosition = Eigen::Vector3f(interactionPositionX, interactionPositionY, interactionPositionZ);
|
||||
st.jsonPositionX = base.positionX;
|
||||
st.jsonPositionY = base.positionY;
|
||||
st.jsonPositionZ = base.positionZ;
|
||||
// state.position is set by InteractiveObject::createFromState() after mesh-center computation
|
||||
return st;
|
||||
}
|
||||
|
||||
static VertexDataStruct loadMeshData(const std::string& meshPath, const std::string& zipPath)
|
||||
{
|
||||
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
||||
@@ -209,20 +180,18 @@ namespace FRG {
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||
const std::string& jsonPath,
|
||||
StaticObjectSet GameObjectLoader::createGameObjects(
|
||||
const std::vector<GameObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
StaticObjectSet result;
|
||||
|
||||
std::vector<GameObjectData> allData = loadFromJson(jsonPath, zipPath);
|
||||
|
||||
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
||||
std::vector<std::vector<const GameObjectData*>> groups;
|
||||
std::unordered_map<std::string, size_t> groupIndexByKey;
|
||||
|
||||
for (const auto& data : allData) {
|
||||
for (const auto& data : definitions) {
|
||||
if (!data.instanced) {
|
||||
groups.push_back({ &data });
|
||||
continue;
|
||||
@@ -290,22 +259,33 @@ namespace FRG {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath,
|
||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createGameObjects(
|
||||
loadFromJson(jsonPath, zipPath), renderer, zipPath);
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject> GameObjectLoader::createInteractiveObjects(
|
||||
const std::vector<InteractiveObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<InteractiveObject> interactiveObjects;
|
||||
|
||||
if (jsonPath.empty())
|
||||
return interactiveObjects;
|
||||
|
||||
for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) {
|
||||
for (const auto& data : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
||||
try {
|
||||
InteractiveObject intObj = InteractiveObject::createFromState(
|
||||
data.toInteractiveObjectState(), renderer, zipPath);
|
||||
auto state = states.find(data.id);
|
||||
if (state == states.end() || !state->second)
|
||||
throw std::runtime_error(
|
||||
"Authoritative state missing for '" + data.id + "'");
|
||||
InteractiveObject intObj =
|
||||
InteractiveObject::createFromState(
|
||||
*state->second, renderer, zipPath);
|
||||
intObj.id = data.id;
|
||||
intObj.loadedObject.name = data.base.name;
|
||||
|
||||
if (!data.activateFunctionName.empty())
|
||||
@@ -324,6 +304,18 @@ namespace FRG {
|
||||
return interactiveObjects;
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject>
|
||||
GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createInteractiveObjects(
|
||||
loadInteractiveFromJson(jsonPath, zipPath), renderer, states,
|
||||
zipPath);
|
||||
}
|
||||
|
||||
std::vector<NpcData> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
|
||||
{
|
||||
std::vector<NpcData> npcs;
|
||||
@@ -338,7 +330,9 @@ namespace FRG {
|
||||
for (const auto& item : j["npcs"]) {
|
||||
NpcData data;
|
||||
|
||||
data.id = item.value("id", "npc_unknown");
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty())
|
||||
throw std::runtime_error("NPC id must not be empty in " + jsonPath);
|
||||
data.name = item.value("name", "Unknown NPC");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
if (item.contains("meshTextures") && item["meshTextures"].is_object()) {
|
||||
@@ -376,88 +370,59 @@ namespace FRG {
|
||||
logger() << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl;
|
||||
throw;
|
||||
}
|
||||
|
||||
return npcs;
|
||||
}
|
||||
|
||||
CharacterState NpcData::toCharacterState() const {
|
||||
CharacterState st;
|
||||
|
||||
// Identity + game state
|
||||
st.npcId = id;
|
||||
st.npcName = name;
|
||||
st.position = Eigen::Vector3f(positionX, positionY, positionZ);
|
||||
st.facingAngle = facingAngle;
|
||||
st.walkSpeed = walkSpeed;
|
||||
st.rotationSpeed = rotationSpeed;
|
||||
st.modelScale = modelScale;
|
||||
st.setHp(hp);
|
||||
st.initialHp = hp;
|
||||
st.visibleRange = visibleRange;
|
||||
st.canAttack = canAttack;
|
||||
st.enabled = enabled;
|
||||
st.interactionRadius = interactionRadius;
|
||||
|
||||
Eigen::Quaternionf corrRot = Eigen::Quaternionf::Identity();
|
||||
if (modelCorrectionRotX != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotX, Eigen::Vector3f::UnitX())) * corrRot;
|
||||
if (modelCorrectionRotY != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRot;
|
||||
if (modelCorrectionRotZ != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRot;
|
||||
st.modelCorrectionRotation = corrRot;
|
||||
|
||||
// Creation info: animation paths
|
||||
CharacterCreationInfo& info = st.creationInfo;
|
||||
auto addAnim = [&](AnimationState animState, const std::string& path) {
|
||||
if (!path.empty())
|
||||
info.animationPaths.emplace_back(animState, path);
|
||||
};
|
||||
addAnim(AnimationState::STAND, animationIdlePath);
|
||||
addAnim(AnimationState::WALK, animationWalkPath);
|
||||
addAnim(AnimationState::ACTION_IDLE, animationActionIdlePath);
|
||||
addAnim(AnimationState::ACTION_ATTACK, animationActionAttackPath);
|
||||
addAnim(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
|
||||
addAnim(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
|
||||
addAnim(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
|
||||
addAnim(AnimationState::DEATH_IDLE, animationDeathIdlePath);
|
||||
|
||||
// Creation info: textures
|
||||
info.meshTexturePaths = std::unordered_map<std::string, std::string>(meshTextures.begin(), meshTextures.end());
|
||||
info.singleTexturePath = texturePath;
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Character>> GameObjectLoader::loadAndCreate_Npcs(
|
||||
const std::string& jsonPath,
|
||||
std::vector<std::unique_ptr<Character>> GameObjectLoader::createNpcs(
|
||||
const std::vector<NpcData>& definitions,
|
||||
Renderer& renderer,
|
||||
const std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server,
|
||||
const std::string& locationId,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<std::unique_ptr<Character>> npcs;
|
||||
|
||||
int index = 0;
|
||||
|
||||
for (const auto& npcData : loadNpcsFromJson(jsonPath, zipPath)) {
|
||||
for (const auto& npcData : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading NPC: " << npcData.name << std::endl;
|
||||
try {
|
||||
auto npc = Character::createFromState(npcData.toCharacterState(), renderer, zipPath.c_str());
|
||||
auto state = states.find(npcData.id);
|
||||
if (state == states.end() || !state->second)
|
||||
throw std::runtime_error(
|
||||
"Authoritative state missing for '" +
|
||||
npcData.id + "'");
|
||||
auto npc = Character::createFromState(
|
||||
*state->second, server, locationId, npcData.id,
|
||||
renderer, zipPath.c_str());
|
||||
if (npc) {
|
||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||
npc->index = index;
|
||||
npcs.push_back(std::move(npc));
|
||||
index = index + 1;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GameObjectLoader: Failed to load NPC '" << npcData.name << "': " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
logger() << "Total NPCs loaded: " << npcs.size() << std::endl;
|
||||
return npcs;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Character>>
|
||||
GameObjectLoader::loadAndCreate_Npcs(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server, const std::string& locationId,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createNpcs(
|
||||
loadNpcsFromJson(jsonPath, zipPath), renderer, states, server,
|
||||
locationId, zipPath);
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -9,30 +9,13 @@
|
||||
#include "InteractiveObject.h"
|
||||
#include "InteractiveObjectState.h"
|
||||
#include "../CharacterState.h"
|
||||
#include "../content/LocationDefinition.h"
|
||||
|
||||
namespace FRG {
|
||||
class IGameServer;
|
||||
|
||||
class Character;
|
||||
|
||||
struct GameObjectData {
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::string textureDarkandsPath;
|
||||
std::string meshPath;
|
||||
float rotationX = 0.0f;
|
||||
float rotationY = 0.0f;
|
||||
float rotationZ = 0.0f;
|
||||
float positionX = 0.0f;
|
||||
float positionY = 0.0f;
|
||||
float positionZ = 0.0f;
|
||||
float scale = 1.0f;
|
||||
// Objects sharing mesh and textures are drawn with one instanced call.
|
||||
// Set "instanced": false in the JSON to keep an object as a standalone mesh.
|
||||
bool instanced = true;
|
||||
|
||||
LoadedGameObjectState toLoadedGameObjectState() const;
|
||||
};
|
||||
|
||||
// Result of loading a static object list: unique objects keep their own mesh,
|
||||
// repeated ones are merged into instanced batches.
|
||||
struct StaticObjectSet {
|
||||
@@ -40,62 +23,25 @@ namespace FRG {
|
||||
std::vector<InstancedBatch> batches;
|
||||
};
|
||||
|
||||
struct InteractiveObjectData {
|
||||
GameObjectData base;
|
||||
float interactionRadius = 2.0f;
|
||||
float approachRadius = 2.0f;
|
||||
std::string activateFunctionName;
|
||||
float pivotX = 0.0f;
|
||||
float pivotY = 0.0f;
|
||||
float pivotZ = 0.0f;
|
||||
float boundsMinX = 0.0f, boundsMinY = 0.0f, boundsMinZ = 0.0f;
|
||||
float boundsMaxX = 0.0f, boundsMaxY = 0.0f, boundsMaxZ = 0.0f;
|
||||
bool hasInteractionPosition = false;
|
||||
float interactionPositionX = 0.0f, interactionPositionY = 0.0f, interactionPositionZ = 0.0f;
|
||||
bool castShadow = true;
|
||||
bool castShadowNight = true;
|
||||
bool isActive = true;
|
||||
|
||||
InteractiveObjectState toInteractiveObjectState() const;
|
||||
};
|
||||
|
||||
struct NpcData {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::map<std::string, std::string> meshTextures;
|
||||
std::string animationIdlePath;
|
||||
std::string animationWalkPath;
|
||||
// Optional combat / death animations. Empty path = slot not loaded.
|
||||
std::string animationActionIdlePath;
|
||||
std::string animationActionAttackPath;
|
||||
std::string animationStandToActionPath;
|
||||
std::string animationActionToStandPath;
|
||||
std::string animationActionToDeathPath;
|
||||
std::string animationDeathIdlePath;
|
||||
float positionX = 0.0f;
|
||||
float positionY = 0.0f;
|
||||
float positionZ = 0.0f;
|
||||
float walkSpeed = 1.5f;
|
||||
float rotationSpeed = 8.0f;
|
||||
float modelScale = 0.01f;
|
||||
float modelCorrectionRotX = 0.0f;
|
||||
float modelCorrectionRotY = 0.0f;
|
||||
float modelCorrectionRotZ = 0.0f;
|
||||
float facingAngle = 0.0f;
|
||||
float hp = 100.0f;
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
bool canAttack = false;
|
||||
bool enabled = true;
|
||||
float interactionRadius = 0.0f;
|
||||
|
||||
// Converts this JSON-parsed descriptor into a fully-populated CharacterState
|
||||
// (including creationInfo) ready for Character::createFromState().
|
||||
CharacterState toCharacterState() const;
|
||||
};
|
||||
|
||||
class GameObjectLoader {
|
||||
public:
|
||||
static StaticObjectSet createGameObjects(
|
||||
const std::vector<GameObjectData>& definitions,
|
||||
Renderer& renderer, const std::string& zipPath = "");
|
||||
static std::vector<InteractiveObject> createInteractiveObjects(
|
||||
const std::vector<InteractiveObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath = "");
|
||||
static std::vector<std::unique_ptr<FRG::Character>> createNpcs(
|
||||
const std::vector<NpcData>& definitions,
|
||||
Renderer& renderer,
|
||||
const std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server, const std::string& locationId,
|
||||
const std::string& zipPath);
|
||||
|
||||
static StaticObjectSet loadAndCreateGameObjects(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
@@ -105,13 +51,17 @@ namespace FRG {
|
||||
static std::vector<InteractiveObject> loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath = ""
|
||||
);
|
||||
|
||||
static std::vector<std::unique_ptr<FRG::Character>> loadAndCreate_Npcs(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath = ""
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server,
|
||||
const std::string& locationId,
|
||||
const std::string& zipPath
|
||||
);
|
||||
|
||||
static std::vector<NpcData> loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath = "");
|
||||
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user