Compare commits
13
Commits
main
...
networking
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,510 @@
|
|||||||
|
# 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 uses one strict, versioned schema. `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.
|
||||||
|
- 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`; 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 priority queues use cell index as a stable secondary key.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## 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, collisions, 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.
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# 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 and presence
|
||||||
|
commands (or an empty command list).
|
||||||
|
|
||||||
|
Client intent is intentionally limited to `SetCharacterDestinationCommand`.
|
||||||
|
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 inputs cross from the Asio thread through a queue. The simulation
|
||||||
|
thread validates client sequence numbers, replaces client identity with the
|
||||||
|
assigned slot, and schedules commands two ticks in the future.
|
||||||
|
- Every tick is broadcast, including ticks with no commands. No periodic state
|
||||||
|
snapshots are sent.
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
`WebsocketGameClient` implements `IGameServer`.
|
||||||
|
|
||||||
|
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
||||||
|
- Received messages are queued by the network thread and applied on the game
|
||||||
|
thread from `update()`.
|
||||||
|
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
||||||
|
consumers and transitional runtime-state bindings continue to work.
|
||||||
|
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
|
||||||
|
authoritative snapshots cannot overwrite camera movement.
|
||||||
|
- `submit()` serializes destination intent only. 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 and the start-state path,
|
||||||
|
so the state can be regenerated through the single-player entry. At startup, the
|
||||||
|
standalone server loads the authoritative half of that shared save and rewrites
|
||||||
|
its campaign ID to `battle_zone_multiplayer`; each remote client restores only
|
||||||
|
the local presentation half. 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; 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;
|
||||||
|
- `tick` (server to client): tick ID, globally sequenced destination 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, and unsupported commands cannot enter
|
||||||
|
the authoritative command stream.
|
||||||
|
|
||||||
|
WebSocket ordering is not used as simulation time. Integer tick IDs are the
|
||||||
|
only authoritative clock. The server uses `steady_clock` only to pace its loop,
|
||||||
|
and clients may have different wall clocks without affecting the result. A
|
||||||
|
client stalls on a missing tick instead of predicting or rolling back.
|
||||||
|
|
||||||
|
Single-player persistence remains a separate envelope combining authoritative
|
||||||
|
and client-local state. `savedAt`, camera state, and presentation objects are
|
||||||
|
excluded from the diagnostic deterministic hash.
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
Networking is enabled only for Windows:
|
||||||
|
|
||||||
|
- `proj-windows/CMakeLists.txt` defines `NETWORK`, compiles the WebSocket client
|
||||||
|
and remote host, and links `ws2_32` and `mswsock`.
|
||||||
|
- `server/CMakeLists.txt` defines `NETWORK` for the Windows `GameServer`, compiles
|
||||||
|
the WebSocket server, and links the Windows socket libraries.
|
||||||
|
- Linux, Android, and Web builds do not define `NETWORK` and do not compile the
|
||||||
|
network transport.
|
||||||
|
|
||||||
|
The previous Web-build `NETWORK` definition was removed.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Build the server:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake -S server -B server/build
|
||||||
|
cmake --build server/build --config Debug --target GameServer
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it from the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
||||||
|
```
|
||||||
|
|
||||||
|
The listener defaults to `0.0.0.0:8080`. It can be changed with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--address IP
|
||||||
|
--port N
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
|
||||||
|
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
CTest covers protocol-v2 round trips and validation, input-delay ordering,
|
||||||
|
fixed-tick replica behavior, campaign slot metadata, initial connection and
|
||||||
|
slot assignment, connection/disconnection presence propagation, destination
|
||||||
|
submission, and contiguous tick consumption.
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
- Only preconfigured player slots can connect; dynamic join/spawn commands are
|
||||||
|
not implemented.
|
||||||
|
- There is no authentication or reconnect/replay history.
|
||||||
|
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
||||||
|
- The endpoint is hardcoded in the graphical client.
|
||||||
|
- Multiplayer Lua and dialogue behavior remains unsupported.
|
||||||
|
- Determinism is supported for identical executable/content builds. The
|
||||||
|
floating-point simulation is not guaranteed bit-identical across platforms
|
||||||
|
or compilers.
|
||||||
@@ -96,6 +96,8 @@ add_library(main SHARED
|
|||||||
../../../../src/Location.cpp
|
../../../../src/Location.cpp
|
||||||
../../../../src/LocationState.h
|
../../../../src/LocationState.h
|
||||||
../../../../src/LocationState.cpp
|
../../../../src/LocationState.cpp
|
||||||
|
../../../../src/LocationGameState.h
|
||||||
|
../../../../src/LocationGameState.cpp
|
||||||
../../../../src/LocationEditor.h
|
../../../../src/LocationEditor.h
|
||||||
../../../../src/LocationEditor.cpp
|
../../../../src/LocationEditor.cpp
|
||||||
../../../../src/NpcCar.h
|
../../../../src/NpcCar.h
|
||||||
@@ -104,6 +106,11 @@ add_library(main SHARED
|
|||||||
../../../../src/GameConstants.cpp
|
../../../../src/GameConstants.cpp
|
||||||
../../../../src/GameState.h
|
../../../../src/GameState.h
|
||||||
../../../../src/GameState.cpp
|
../../../../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.h
|
||||||
../../../../src/ScriptEngine.cpp
|
../../../../src/ScriptEngine.cpp
|
||||||
../../../../src/navigation/PathFinder.h
|
../../../../src/navigation/PathFinder.h
|
||||||
@@ -112,11 +119,13 @@ add_library(main SHARED
|
|||||||
../../../../src/items/GameObjectLoader.cpp
|
../../../../src/items/GameObjectLoader.cpp
|
||||||
../../../../src/items/Item.h
|
../../../../src/items/Item.h
|
||||||
../../../../src/items/Item.cpp
|
../../../../src/items/Item.cpp
|
||||||
|
../../../../src/items/Inventory.cpp
|
||||||
../../../../src/items/ItemRegistry.h
|
../../../../src/items/ItemRegistry.h
|
||||||
../../../../src/items/ItemRegistry.cpp
|
../../../../src/items/ItemRegistry.cpp
|
||||||
../../../../src/Campaign.cpp
|
../../../../src/Campaign.cpp
|
||||||
../../../../src/items/InteractiveObject.h
|
../../../../src/items/InteractiveObject.h
|
||||||
../../../../src/items/InteractiveObject.cpp
|
../../../../src/items/InteractiveObject.cpp
|
||||||
|
../../../../src/items/InteractiveObjectState.cpp
|
||||||
../../../../src/items/InteractiveObjectState.h
|
../../../../src/items/InteractiveObjectState.h
|
||||||
../../../../src/dialogue/DialogueTypes.h
|
../../../../src/dialogue/DialogueTypes.h
|
||||||
../../../../src/dialogue/DialogueDatabase.h
|
../../../../src/dialogue/DialogueDatabase.h
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/Location.cpp
|
../src/Location.cpp
|
||||||
../src/LocationState.h
|
../src/LocationState.h
|
||||||
../src/LocationState.cpp
|
../src/LocationState.cpp
|
||||||
|
../src/LocationGameState.h
|
||||||
|
../src/LocationGameState.cpp
|
||||||
../src/LocationEditor.h
|
../src/LocationEditor.h
|
||||||
../src/LocationEditor.cpp
|
../src/LocationEditor.cpp
|
||||||
../src/NpcCar.h
|
../src/NpcCar.h
|
||||||
@@ -129,6 +131,15 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../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.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -137,12 +148,14 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
../src/dialogue/DialogueDatabase.cpp
|
../src/dialogue/DialogueDatabase.cpp
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ set(SOURCES
|
|||||||
../src/Location.cpp
|
../src/Location.cpp
|
||||||
../src/LocationState.h
|
../src/LocationState.h
|
||||||
../src/LocationState.cpp
|
../src/LocationState.cpp
|
||||||
|
../src/LocationGameState.h
|
||||||
|
../src/LocationGameState.cpp
|
||||||
../src/LocationEditor.h
|
../src/LocationEditor.h
|
||||||
../src/LocationEditor.cpp
|
../src/LocationEditor.cpp
|
||||||
../src/NpcCar.h
|
../src/NpcCar.h
|
||||||
@@ -123,6 +125,11 @@ set(SOURCES
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../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.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -131,11 +138,13 @@ set(SOURCES
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
@@ -193,7 +202,6 @@ set(EMSCRIPTEN_FLAGS
|
|||||||
#"-pthread"
|
#"-pthread"
|
||||||
#"-sUSE_PTHREADS=1"
|
#"-sUSE_PTHREADS=1"
|
||||||
"-fexceptions"
|
"-fexceptions"
|
||||||
"-DNETWORK"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_options(ShadowOverBishkekDemo PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
target_compile_options(ShadowOverBishkekDemo PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/Location.cpp
|
../src/Location.cpp
|
||||||
../src/LocationState.h
|
../src/LocationState.h
|
||||||
../src/LocationState.cpp
|
../src/LocationState.cpp
|
||||||
|
../src/LocationGameState.h
|
||||||
|
../src/LocationGameState.cpp
|
||||||
../src/LocationEditor.h
|
../src/LocationEditor.h
|
||||||
../src/LocationEditor.cpp
|
../src/LocationEditor.cpp
|
||||||
../src/NpcCar.h
|
../src/NpcCar.h
|
||||||
@@ -71,6 +73,17 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../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.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -79,12 +92,14 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
../src/dialogue/DialogueDatabase.cpp
|
../src/dialogue/DialogueDatabase.cpp
|
||||||
@@ -125,7 +140,9 @@ set_target_properties(ShadowOverBishkekDemo PROPERTIES
|
|||||||
# Определения препроцессора
|
# Определения препроцессора
|
||||||
target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
||||||
WIN32_LEAN_AND_MEAN
|
WIN32_LEAN_AND_MEAN
|
||||||
|
_WIN32_WINNT=0x0A00
|
||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
|
NETWORK
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем с SDL2main, если он вообще установлен
|
# Линкуем с SDL2main, если он вообще установлен
|
||||||
@@ -147,7 +164,16 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
|||||||
|
|
||||||
# Линкуем OpenGL (Windows)
|
# Линкуем OpenGL (Windows)
|
||||||
if(WIN32)
|
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()
|
endif()
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -17,7 +17,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "battle_zone",
|
"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": [
|
"locations": [
|
||||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
"resources/config/locations/battle_zone_uni_exterior.json"
|
||||||
],
|
],
|
||||||
@@ -26,6 +39,8 @@
|
|||||||
"startState": "resources/config/start_state_battle_zone.json",
|
"startState": "resources/config/start_state_battle_zone.json",
|
||||||
"quests": "resources/config/quests_battle_zone.json",
|
"quests": "resources/config/quests_battle_zone.json",
|
||||||
"tutorial": false,
|
"tutorial": false,
|
||||||
|
"networkPlayerSlots": ["player", "player2"],
|
||||||
|
"networkCompatibility": "battle-zone-lockstep-v2",
|
||||||
"loadingScreen": "resources/w/ui/screen_loading.json"
|
"loadingScreen": "resources/w/ui/screen_loading.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"objects": [
|
"objects": [
|
||||||
{
|
{
|
||||||
|
"id": "WashingMachine002",
|
||||||
"name": "WashingMachine002",
|
"name": "WashingMachine002",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"activateFunction": "on_washing_machine1_click"
|
"activateFunction": "on_washing_machine1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Dryer002",
|
||||||
"name": "Dryer002",
|
"name": "Dryer002",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"activateFunction": "on_dryer1_click"
|
"activateFunction": "on_dryer1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Toilet001",
|
||||||
"name": "Toilet001",
|
"name": "Toilet001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
"activateFunction": "on_toilet_click"
|
"activateFunction": "on_toilet_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Sink002",
|
||||||
"name": "Sink002",
|
"name": "Sink002",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -105,6 +109,7 @@
|
|||||||
"activateFunction": "on_sink1_click"
|
"activateFunction": "on_sink1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Sink003",
|
||||||
"name": "Sink003",
|
"name": "Sink003",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -131,6 +136,7 @@
|
|||||||
"activateFunction": "on_sink1_click"
|
"activateFunction": "on_sink1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Sink004",
|
||||||
"name": "Sink004",
|
"name": "Sink004",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -157,6 +163,7 @@
|
|||||||
"activateFunction": "on_sink1_click"
|
"activateFunction": "on_sink1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Sink005",
|
||||||
"name": "Sink005",
|
"name": "Sink005",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -183,6 +190,7 @@
|
|||||||
"activateFunction": "on_sink1_click"
|
"activateFunction": "on_sink1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Shower001",
|
||||||
"name": "Shower001",
|
"name": "Shower001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"activateFunction": "on_shower_click"
|
"activateFunction": "on_shower_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Freezer001",
|
||||||
"name": "Freezer001",
|
"name": "Freezer001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -235,6 +244,7 @@
|
|||||||
"activateFunction": "on_freezer_click"
|
"activateFunction": "on_freezer_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Boombox001",
|
||||||
"name": "Boombox001",
|
"name": "Boombox001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -261,6 +271,7 @@
|
|||||||
"activateFunction": "on_boombox_click"
|
"activateFunction": "on_boombox_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "WashingMachine001",
|
||||||
"name": "WashingMachine001",
|
"name": "WashingMachine001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -287,6 +298,7 @@
|
|||||||
"activateFunction": "on_washing_machine1_click"
|
"activateFunction": "on_washing_machine1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Shelf001",
|
||||||
"name": "Shelf001",
|
"name": "Shelf001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -313,6 +325,7 @@
|
|||||||
"activateFunction": "on_shelf_click"
|
"activateFunction": "on_shelf_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Stove001",
|
||||||
"name": "Stove001",
|
"name": "Stove001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -339,6 +352,7 @@
|
|||||||
"activateFunction": "on_stove1_click"
|
"activateFunction": "on_stove1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Dryer001",
|
||||||
"name": "Dryer001",
|
"name": "Dryer001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -365,6 +379,7 @@
|
|||||||
"activateFunction": "on_dryer1_click"
|
"activateFunction": "on_dryer1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Sink001",
|
||||||
"name": "Sink001",
|
"name": "Sink001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -391,6 +406,7 @@
|
|||||||
"activateFunction": "on_sink1_click"
|
"activateFunction": "on_sink1_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_1_1_0_Leaf_001",
|
||||||
"name": "Door_Room_1_1_0_Leaf_001",
|
"name": "Door_Room_1_1_0_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_1_1_0_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_1_1_0_Leaf_001.txt.bin",
|
||||||
@@ -398,7 +414,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 10.0,
|
"positionZ": 10.0,
|
||||||
"approachRadius": 0.89,
|
"approachRadius": 0.89,
|
||||||
"boundsMaxX": 0.01552116870880127,
|
"boundsMaxX": 0.01552116870880127,
|
||||||
@@ -415,6 +431,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_1_-1_0_Leaf_001",
|
||||||
"name": "Door_Room_1_-1_0_Leaf_001",
|
"name": "Door_Room_1_-1_0_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_1_-1_0_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_1_-1_0_Leaf_001.txt.bin",
|
||||||
@@ -422,7 +439,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 10.0,
|
"positionZ": 10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10440409183502197,
|
"boundsMaxX": 0.10440409183502197,
|
||||||
@@ -439,6 +456,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_-1_1_0_Leaf_001",
|
||||||
"name": "Door_Room_-1_1_0_Leaf_001",
|
"name": "Door_Room_-1_1_0_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_-1_1_0_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_-1_1_0_Leaf_001.txt.bin",
|
||||||
@@ -446,7 +464,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -10.0,
|
"positionZ": -10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.05766594409942627,
|
"boundsMaxX": 0.05766594409942627,
|
||||||
@@ -463,6 +481,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_-1_-1_0_Leaf_001",
|
||||||
"name": "Door_Room_-1_-1_0_Leaf_001",
|
"name": "Door_Room_-1_-1_0_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_0_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_0_Leaf_001.txt.bin",
|
||||||
@@ -470,7 +489,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -10.0,
|
"positionZ": -10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10906755924224854,
|
"boundsMaxX": 0.10906755924224854,
|
||||||
@@ -487,6 +506,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_1_1_1_Leaf_001",
|
||||||
"name": "Door_Room_1_1_1_Leaf_001",
|
"name": "Door_Room_1_1_1_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_1_1_1_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_1_1_1_Leaf_001.txt.bin",
|
||||||
@@ -494,7 +514,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 15.7,
|
"positionZ": 15.7,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.046350955963134766,
|
"boundsMaxX": 0.046350955963134766,
|
||||||
@@ -511,6 +531,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_1_-1_1_Leaf_001",
|
||||||
"name": "Door_Room_1_-1_1_Leaf_001",
|
"name": "Door_Room_1_-1_1_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_1_-1_1_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_1_-1_1_Leaf_001.txt.bin",
|
||||||
@@ -518,7 +539,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 15.7,
|
"positionZ": 15.7,
|
||||||
"approachRadius": 0.800000011920929,
|
"approachRadius": 0.800000011920929,
|
||||||
"boundsMaxX": 0.12420153617858887,
|
"boundsMaxX": 0.12420153617858887,
|
||||||
@@ -535,6 +556,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Utility_1_1_2_Leaf_001",
|
||||||
"name": "Door_Utility_1_1_2_Leaf_001",
|
"name": "Door_Utility_1_1_2_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Utility_1_1_2_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Utility_1_1_2_Leaf_001.txt.bin",
|
||||||
@@ -542,7 +564,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 19.95,
|
"positionZ": 19.95,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": -0.050987482070922852,
|
"boundsMaxX": -0.050987482070922852,
|
||||||
@@ -559,6 +581,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Utility_1_-1_2_Leaf_001",
|
||||||
"name": "Door_Utility_1_-1_2_Leaf_001",
|
"name": "Door_Utility_1_-1_2_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Utility_1_-1_2_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Utility_1_-1_2_Leaf_001.txt.bin",
|
||||||
@@ -566,7 +589,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 19.95,
|
"positionZ": 19.95,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.073349356651306152,
|
"boundsMaxX": 0.073349356651306152,
|
||||||
@@ -583,6 +606,7 @@
|
|||||||
"activateFunction": "on_locked_door_click"
|
"activateFunction": "on_locked_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Utility_-1_1_2_Leaf_001",
|
||||||
"name": "Door_Utility_-1_1_2_Leaf_001",
|
"name": "Door_Utility_-1_1_2_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Utility_-1_1_2_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Utility_-1_1_2_Leaf_001.txt.bin",
|
||||||
@@ -590,7 +614,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -19.95,
|
"positionZ": -19.95,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -599,6 +623,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Utility_-1_-1_2_Leaf_001",
|
||||||
"name": "Door_Utility_-1_-1_2_Leaf_001",
|
"name": "Door_Utility_-1_-1_2_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Utility_-1_-1_2_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Utility_-1_-1_2_Leaf_001.txt.bin",
|
||||||
@@ -606,7 +631,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -19.95,
|
"positionZ": -19.95,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -615,13 +640,14 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Phone001",
|
||||||
"name": "Phone001",
|
"name": "Phone001",
|
||||||
"texturePath": "resources/w/dorm2/phone001_tex001.png",
|
"texturePath": "resources/w/dorm2/phone001_tex001.png",
|
||||||
"meshPath": "resources/w/dorm2/phone001_001.txt.bin",
|
"meshPath": "resources/w/dorm2/phone001_001.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 6.6644,
|
"positionX": 6.6644004991889,
|
||||||
"positionY": 0.9,
|
"positionY": 0.9,
|
||||||
"positionZ": -12.5262,
|
"positionZ": -12.5262,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
@@ -640,6 +666,7 @@
|
|||||||
"activateFunction": "on_phone_pickup"
|
"activateFunction": "on_phone_pickup"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Journal001",
|
||||||
"name": "Journal001",
|
"name": "Journal001",
|
||||||
"texturePath": "resources/w/dorm2/Book_Cover_tex002.png",
|
"texturePath": "resources/w/dorm2/Book_Cover_tex002.png",
|
||||||
"meshPath": "resources/w/dorm2/journal002_002.txt.bin",
|
"meshPath": "resources/w/dorm2/journal002_002.txt.bin",
|
||||||
@@ -647,7 +674,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.84296,
|
"positionX": 5.84296,
|
||||||
"positionY": 0.9,
|
"positionY": 0.9284919999539852,
|
||||||
"positionZ": -12.4661,
|
"positionZ": -12.4661,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.17081403732299805,
|
"boundsMaxX": 0.17081403732299805,
|
||||||
@@ -665,6 +692,7 @@
|
|||||||
"activateFunction": "on_journal_pickup"
|
"activateFunction": "on_journal_pickup"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Bed001",
|
||||||
"name": "Bed001",
|
"name": "Bed001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -691,21 +719,23 @@
|
|||||||
"activateFunction": "on_bed_sleep"
|
"activateFunction": "on_bed_sleep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Music001",
|
||||||
"name": "Music001",
|
"name": "Music001",
|
||||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||||
"meshPath": "resources/w/dorm2/D_Music_004.txt.bin",
|
"meshPath": "resources/w/dorm2/D_Music_004.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.10067,
|
"positionX": 5.1006695008111,
|
||||||
"positionY": 0.91385,
|
"positionY": 1.0181855017900467,
|
||||||
"positionZ": -11.9566,
|
"positionZ": -11.958324999397993,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.3,
|
"interactionRadius": 0.3,
|
||||||
"activateFunction": ""
|
"activateFunction": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Bath_-1_1_1_Leaf_002",
|
||||||
"name": "Door_Bath_-1_1_1_Leaf_002",
|
"name": "Door_Bath_-1_1_1_Leaf_002",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Bath_-1_1_1_Leaf_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Bath_-1_1_1_Leaf_002.txt.bin",
|
||||||
@@ -713,7 +743,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 2.825,
|
"positionX": 2.825,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -14.3,
|
"positionZ": -14.3,
|
||||||
"pivotX": -0.6,
|
"pivotX": -0.6,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -733,6 +763,7 @@
|
|||||||
"activateFunction": "on_player_bathroom_click"
|
"activateFunction": "on_player_bathroom_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Bath_-1_-1_1_Leaf_002",
|
||||||
"name": "Door_Bath_-1_-1_1_Leaf_002",
|
"name": "Door_Bath_-1_-1_1_Leaf_002",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Bath_-1_-1_1_Leaf_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Bath_-1_-1_1_Leaf_002.txt.bin",
|
||||||
@@ -740,7 +771,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -2.825,
|
"positionX": -2.825,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -14.3,
|
"positionZ": -14.3,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.55439233779907227,
|
"boundsMaxX": 0.55439233779907227,
|
||||||
@@ -757,6 +788,7 @@
|
|||||||
"activateFunction": "on_alik_bathroom_click"
|
"activateFunction": "on_alik_bathroom_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_-1_1_1_Leaf_001",
|
||||||
"name": "Door_Room_-1_1_1_Leaf_001",
|
"name": "Door_Room_-1_1_1_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_-1_1_1_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_-1_1_1_Leaf_001.txt.bin",
|
||||||
@@ -764,7 +796,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -15.7,
|
"positionZ": -15.7,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -784,6 +816,7 @@
|
|||||||
"activateFunction": "on_player_door_click"
|
"activateFunction": "on_player_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Door_Room_-1_-1_1_Leaf_001",
|
||||||
"name": "Door_Room_-1_-1_1_Leaf_001",
|
"name": "Door_Room_-1_-1_1_Leaf_001",
|
||||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||||
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_1_Leaf_001.txt.bin",
|
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_1_Leaf_001.txt.bin",
|
||||||
@@ -791,7 +824,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -15.7,
|
"positionZ": -15.7,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.13942182064056396,
|
"boundsMaxX": 0.13942182064056396,
|
||||||
@@ -811,6 +844,7 @@
|
|||||||
"activateFunction": "on_alik_door_click"
|
"activateFunction": "on_alik_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Bath_W_N_2_001",
|
||||||
"name": "Room_Cover_Bath_W_N_2_001",
|
"name": "Room_Cover_Bath_W_N_2_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_Bath_W_N_2_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_Bath_W_N_2_002.txt.bin",
|
||||||
@@ -818,13 +852,14 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 2.8249999284744263,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -12.849999904632568,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_LivingRoom_W_N_2_001",
|
||||||
"name": "Room_Cover_LivingRoom_W_N_2_001",
|
"name": "Room_Cover_LivingRoom_W_N_2_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_N_2_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_N_2_002.txt.bin",
|
||||||
@@ -832,13 +867,14 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -14.25,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_LivingRoom_W_S_2_001",
|
||||||
"name": "Room_Cover_LivingRoom_W_S_2_001",
|
"name": "Room_Cover_LivingRoom_W_S_2_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_S_2_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_S_2_002.txt.bin",
|
||||||
@@ -846,13 +882,14 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -14.25,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Main_Hall_And_Corridors_002",
|
||||||
"name": "Room_Cover_Main_Hall_And_Corridors_002",
|
"name": "Room_Cover_Main_Hall_And_Corridors_002",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_Main_Hall_And_Corridors_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_Main_Hall_And_Corridors_002.txt.bin",
|
||||||
@@ -861,12 +898,13 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Others_001",
|
||||||
"name": "Room_Cover_Others_001",
|
"name": "Room_Cover_Others_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_All_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_All_002.txt.bin",
|
||||||
@@ -875,12 +913,13 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.250000476837158,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Utility_W_N_3_001",
|
||||||
"name": "Room_Cover_Utility_W_N_3_001",
|
"name": "Room_Cover_Utility_W_N_3_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_N_3_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_N_3_002.txt.bin",
|
||||||
@@ -888,13 +927,14 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -19.950000762939453,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Utility_W_S_3_001",
|
||||||
"name": "Room_Cover_Utility_W_S_3_001",
|
"name": "Room_Cover_Utility_W_S_3_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_S_3_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_S_3_002.txt.bin",
|
||||||
@@ -902,13 +942,14 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -19.950000762939453,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Roof",
|
||||||
"name": "Roof",
|
"name": "Roof",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/dorm2/Shadow_Roof001_002.txt.bin",
|
"meshPath": "resources/w/dorm2/Shadow_Roof001_002.txt.bin",
|
||||||
@@ -919,7 +960,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.1,
|
"positionY": 3.2050000190734864,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"objects": [
|
"objects": [
|
||||||
{
|
{
|
||||||
|
"id": "Container001",
|
||||||
"name": "Container001",
|
"name": "Container001",
|
||||||
"texturePath": "resources/w/exterior/derbis001_tex001.png",
|
"texturePath": "resources/w/exterior/derbis001_tex001.png",
|
||||||
"meshPath": "resources/w/exterior/Derbis_001.txt.bin",
|
"meshPath": "resources/w/exterior/Derbis_001.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -26.8049,
|
"positionX": -27.003390023612976,
|
||||||
"positionY": 0.0,
|
"positionY": 0.32889999728649855,
|
||||||
"positionZ": 9.11524,
|
"positionZ": 8.820399544944763,
|
||||||
"approachRadius": 1.5,
|
"approachRadius": 1.5,
|
||||||
"boundsMaxX": 2.9518299102783203,
|
"boundsMaxX": 2.9518299102783203,
|
||||||
"boundsMaxY": 0.5,
|
"boundsMaxY": 0.5,
|
||||||
@@ -25,15 +26,16 @@
|
|||||||
"activateFunction": "on_container_clicked"
|
"activateFunction": "on_container_clicked"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "car001",
|
||||||
"name": "car001",
|
"name": "car001",
|
||||||
"texturePath": "resources/e/car4096blue_v002.png",
|
"texturePath": "resources/e/car4096blue_v002.png",
|
||||||
"meshPath": "resources/e/car004.txt.bin",
|
"meshPath": "resources/e/car004.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.503284412622452,
|
||||||
"positionY": 0.55,
|
"positionY": 0.9012498408555984,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.861633367061614,
|
||||||
"approachRadius": 0.5,
|
"approachRadius": 0.5,
|
||||||
"boundsMaxX": 0.8858480453491211,
|
"boundsMaxX": 0.8858480453491211,
|
||||||
"boundsMaxY": 0.7,
|
"boundsMaxY": 0.7,
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
"scale": 0.7
|
"scale": 0.7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "car001wheels",
|
||||||
"name": "car001wheels",
|
"name": "car001wheels",
|
||||||
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
||||||
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
||||||
@@ -56,8 +59,8 @@
|
|||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.5,
|
||||||
"positionY": 0.0,
|
"positionY": 0.3359999924898147,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.839499933242797,
|
||||||
"approachRadius": 0.5,
|
"approachRadius": 0.5,
|
||||||
"boundsMaxX": 0,
|
"boundsMaxX": 0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"objects": [
|
"objects": [
|
||||||
{
|
{
|
||||||
|
"id": "Bookshelf002",
|
||||||
"name": "Bookshelf002",
|
"name": "Bookshelf002",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Bookshelf003",
|
||||||
"name": "Bookshelf003",
|
"name": "Bookshelf003",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Bookshelf004",
|
||||||
"name": "Bookshelf004",
|
"name": "Bookshelf004",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Bookshelf005",
|
||||||
"name": "Bookshelf005",
|
"name": "Bookshelf005",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -105,15 +109,16 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Cake001",
|
||||||
"name": "Cake001",
|
"name": "Cake001",
|
||||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||||
"meshPath": "resources/w/interior/D_Cake_005.txt.bin",
|
"meshPath": "resources/w/interior/D_Cake_005.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0,
|
"positionX": -5.365539073944092,
|
||||||
"positionY": -5.0,
|
"positionY": 0.9888315200805664,
|
||||||
"positionZ": 0,
|
"positionZ": 2.468366503715515,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.19605731964111328,
|
"boundsMaxX": 0.19605731964111328,
|
||||||
"boundsMaxY": 0.4,
|
"boundsMaxY": 0.4,
|
||||||
@@ -131,6 +136,7 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Computer001",
|
||||||
"name": "Computer001",
|
"name": "Computer001",
|
||||||
"texturePath": "resources/w/interior/computer_texture002.png",
|
"texturePath": "resources/w/interior/computer_texture002.png",
|
||||||
"textureDarkandsPath": "resources/w/interior/computer_texture002_dark.png",
|
"textureDarkandsPath": "resources/w/interior/computer_texture002_dark.png",
|
||||||
@@ -138,9 +144,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.0916,
|
"positionX": 5.01753252658844,
|
||||||
"positionY": 1.1534,
|
"positionY": 1.353411484324932,
|
||||||
"positionZ": 0.751629,
|
"positionZ": 0.6829135021486282,
|
||||||
"approachRadius": 1.2,
|
"approachRadius": 1.2,
|
||||||
"boundsMaxX": 0.6253643035888672,
|
"boundsMaxX": 0.6253643035888672,
|
||||||
"boundsMaxY": 0.5,
|
"boundsMaxY": 0.5,
|
||||||
@@ -155,15 +161,16 @@
|
|||||||
"activateFunction": "on_computer_clicked"
|
"activateFunction": "on_computer_clicked"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Knife001",
|
||||||
"name": "Knife001",
|
"name": "Knife001",
|
||||||
"texturePath": "resources/w/gg/Props.png",
|
"texturePath": "resources/w/gg/Props.png",
|
||||||
"meshPath": "resources/w/interior/Knife_lay001_004.txt.bin",
|
"meshPath": "resources/w/interior/Knife_lay001_004.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -4.8327,
|
"positionX": -4.959765496635623,
|
||||||
"positionY": 0.93551,
|
"positionY": 0.93551,
|
||||||
"positionZ": 2.2418,
|
"positionZ": 2.369899506953731,
|
||||||
"approachRadius": 1.4,
|
"approachRadius": 1.4,
|
||||||
"boundsMaxX": 0.30867958068847656,
|
"boundsMaxX": 0.30867958068847656,
|
||||||
"boundsMaxY": 0.4,
|
"boundsMaxY": 0.4,
|
||||||
@@ -177,6 +184,7 @@
|
|||||||
"castShadowNight" : false
|
"castShadowNight" : false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Book001",
|
||||||
"name": "Book001",
|
"name": "Book001",
|
||||||
"texturePath": "resources/w/interior/Book_Cover_tex003.png",
|
"texturePath": "resources/w/interior/Book_Cover_tex003.png",
|
||||||
"meshPath": "resources/w/interior/book002_002.txt.bin",
|
"meshPath": "resources/w/interior/book002_002.txt.bin",
|
||||||
@@ -184,7 +192,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.84795,
|
"positionX": 1.84795,
|
||||||
"positionY": 0.6595,
|
"positionY": 0.6809705001562834,
|
||||||
"positionZ": 2.90552,
|
"positionZ": 2.90552,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.2350165843963623,
|
"boundsMaxX": 0.2350165843963623,
|
||||||
@@ -202,6 +210,7 @@
|
|||||||
"castShadowNight" : false
|
"castShadowNight" : false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Bookshelf001",
|
||||||
"name": "Bookshelf001",
|
"name": "Bookshelf001",
|
||||||
"texturePath": "resources/transparent.png",
|
"texturePath": "resources/transparent.png",
|
||||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||||
@@ -228,6 +237,7 @@
|
|||||||
"castShadow": false
|
"castShadow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "ReportCard001",
|
||||||
"name": "ReportCard001",
|
"name": "ReportCard001",
|
||||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||||
"meshPath": "resources/w/interior/D_report_cardGG_005.txt.bin",
|
"meshPath": "resources/w/interior/D_report_cardGG_005.txt.bin",
|
||||||
@@ -252,6 +262,7 @@
|
|||||||
"activateFunction": "on_report_card_pickup"
|
"activateFunction": "on_report_card_pickup"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Note001",
|
||||||
"name": "Note001",
|
"name": "Note001",
|
||||||
"texturePath": "resources/w/interior/note001_tex003.png",
|
"texturePath": "resources/w/interior/note001_tex003.png",
|
||||||
"meshPath": "resources/w/interior/note001_002.txt.bin",
|
"meshPath": "resources/w/interior/note001_002.txt.bin",
|
||||||
@@ -276,15 +287,16 @@
|
|||||||
"activateFunction": "on_note_pickup"
|
"activateFunction": "on_note_pickup"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "car001",
|
||||||
"name": "car001",
|
"name": "car001",
|
||||||
"texturePath": "resources/e/car4096blue_v002.png",
|
"texturePath": "resources/e/car4096blue_v002.png",
|
||||||
"meshPath": "resources/e/car004.txt.bin",
|
"meshPath": "resources/e/car004.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.503284412622452,
|
||||||
"positionY": -5.0,
|
"positionY": -4.648750159144401,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.861633367061614,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.0,
|
"boundsMaxX": 0.0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
@@ -299,6 +311,7 @@
|
|||||||
"scale": 0.7
|
"scale": 0.7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "car001wheels",
|
||||||
"name": "car001wheels",
|
"name": "car001wheels",
|
||||||
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
||||||
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
||||||
@@ -306,8 +319,8 @@
|
|||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.5,
|
||||||
"positionY": -5.0,
|
"positionY": -4.664000007510185,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.839499933242797,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.0,
|
"boundsMaxX": 0.0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
@@ -322,6 +335,7 @@
|
|||||||
"scale": 0.7
|
"scale": 0.7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_N_2_Leaf001",
|
||||||
"name": "Room_N_2_Leaf001",
|
"name": "Room_N_2_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputRoom_N_2_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputRoom_N_2_Leaf001.txt.bin",
|
||||||
@@ -330,7 +344,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.5,
|
"positionX": 1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 6.965,
|
"positionZ": 6.400000002384186,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.06785714626312256,
|
"boundsMaxX": 0.06785714626312256,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -349,6 +363,7 @@
|
|||||||
"activateFunction": "on_library_door_click"
|
"activateFunction": "on_library_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_N_1_Leaf001",
|
||||||
"name": "Room_N_1_Leaf001",
|
"name": "Room_N_1_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputRoom_N_1_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputRoom_N_1_Leaf001.txt.bin",
|
||||||
@@ -357,7 +372,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.5,
|
"positionX": 1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -1.035,
|
"positionZ": -1.5999999976158141,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10772454738616943,
|
"boundsMaxX": 0.10772454738616943,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -376,6 +391,7 @@
|
|||||||
"activateFunction": "on_n2_door_click"
|
"activateFunction": "on_n2_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_S_1_Leaf001",
|
||||||
"name": "Room_S_1_Leaf001",
|
"name": "Room_S_1_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputRoom_S_1_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputRoom_S_1_Leaf001.txt.bin",
|
||||||
@@ -384,7 +400,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -1.035,
|
"positionZ": -1.5999999976158141,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.12115073204040527,
|
"boundsMaxX": 0.12115073204040527,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -403,6 +419,7 @@
|
|||||||
"activateFunction": "on_s2_door_click"
|
"activateFunction": "on_s2_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_S_2_Leaf001",
|
||||||
"name": "Room_S_2_Leaf001",
|
"name": "Room_S_2_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputRoom_S_2_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputRoom_S_2_Leaf001.txt.bin",
|
||||||
@@ -411,7 +428,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 6.965,
|
"positionZ": 6.400000002384186,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.09747374057769775,
|
"boundsMaxX": 0.09747374057769775,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -430,15 +447,16 @@
|
|||||||
"activateFunction": "on_teachers_door_click"
|
"activateFunction": "on_teachers_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Hall_Leaf001",
|
||||||
"name": "Hall_Leaf001",
|
"name": "Hall_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputHall_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputHall_Leaf001.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.565,
|
"positionX": 0.0,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 8,
|
"positionZ": 8.000001000240445,
|
||||||
"approachRadius": 0.9,
|
"approachRadius": 0.9,
|
||||||
"boundsMaxX": 0.6243956089019775,
|
"boundsMaxX": 0.6243956089019775,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -457,6 +475,7 @@
|
|||||||
"activateFunction": "on_hall_door_click"
|
"activateFunction": "on_hall_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_S_0_Leaf001",
|
||||||
"name": "Room_S_0_Leaf001",
|
"name": "Room_S_0_Leaf001",
|
||||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||||
"meshPath": "resources/w/interior/outputRoom_S_0_Leaf001.txt.bin",
|
"meshPath": "resources/w/interior/outputRoom_S_0_Leaf001.txt.bin",
|
||||||
@@ -465,7 +484,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -9.035,
|
"positionZ": -9.599999020778657,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10914039611816406,
|
"boundsMaxX": 0.10914039611816406,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -484,6 +503,7 @@
|
|||||||
"activateFunction": "on_s1_door_click"
|
"activateFunction": "on_s1_door_click"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Corridor_001",
|
||||||
"name": "Room_Cover_Corridor_001",
|
"name": "Room_Cover_Corridor_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_Corridor_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_Corridor_003.txt.bin",
|
||||||
@@ -491,9 +511,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 2.2500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -3.999999761581421,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -501,6 +521,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_Main_Hall_001",
|
||||||
"name": "Room_Cover_Main_Hall_001",
|
"name": "Room_Cover_Main_Hall_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_Main_Hall_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_Main_Hall_003.txt.bin",
|
||||||
@@ -509,8 +530,8 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 12.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -518,6 +539,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_South_1_001",
|
||||||
"name": "Room_Cover_South_1_001",
|
"name": "Room_Cover_South_1_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_South_1_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_South_1_003.txt.bin",
|
||||||
@@ -525,9 +547,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -12.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -535,6 +557,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_North_2_001",
|
||||||
"name": "Room_Cover_North_2_001",
|
"name": "Room_Cover_North_2_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_North_2_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_North_2_003.txt.bin",
|
||||||
@@ -542,9 +565,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -552,6 +575,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_South_2_001",
|
||||||
"name": "Room_Cover_South_2_001",
|
"name": "Room_Cover_South_2_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_South_2_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_South_2_003.txt.bin",
|
||||||
@@ -559,9 +583,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -569,6 +593,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_North_3_001",
|
||||||
"name": "Room_Cover_North_3_001",
|
"name": "Room_Cover_North_3_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_North_3_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_North_3_003.txt.bin",
|
||||||
@@ -576,9 +601,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -586,6 +611,7 @@
|
|||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "Room_Cover_South_3_001",
|
||||||
"name": "Room_Cover_South_3_001",
|
"name": "Room_Cover_South_3_001",
|
||||||
"texturePath": "resources/black.png",
|
"texturePath": "resources/black.png",
|
||||||
"meshPath": "resources/w/interior/Room_Cover_South_3_003.txt.bin",
|
"meshPath": "resources/w/interior/Room_Cover_South_3_003.txt.bin",
|
||||||
@@ -593,9 +619,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
{
|
{
|
||||||
"npcs": [
|
"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": -6.0,
|
||||||
|
"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
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"canAttack": true
|
"canAttack": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "npc_02_woman",
|
"id": "npc_altynai",
|
||||||
"name": "Алтынай",
|
"name": "Алтынай",
|
||||||
"animationIdlePath": "resources/w/girl/girl_idle001_small.anim",
|
"animationIdlePath": "resources/w/girl/girl_idle001_small.anim",
|
||||||
"animationWalkPath": "resources/w/girl/girl_walk010_small.anim",
|
"animationWalkPath": "resources/w/girl/girl_walk010_small.anim",
|
||||||
|
|||||||
+319
-1200
File diff suppressed because it is too large.
Load diff
@@ -1,88 +1,169 @@
|
|||||||
{
|
{
|
||||||
"campaignId": "battle_zone",
|
"localState": {
|
||||||
"chatHistory": [
|
"locations": {
|
||||||
[],
|
"uni_exterior": {
|
||||||
[],
|
|
||||||
[]
|
|
||||||
],
|
|
||||||
"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,
|
|
||||||
"cameraAzimuth": -2.3499999046325684,
|
"cameraAzimuth": -2.3499999046325684,
|
||||||
"cameraInclination": 1.103600025177002,
|
"cameraInclination": 1.103600025177002
|
||||||
"isDarklands": false,
|
}
|
||||||
"isDawn": false,
|
}
|
||||||
"isNight": false,
|
},
|
||||||
"targetInteractNpcIndex": -1,
|
"networkState": {
|
||||||
"targetInteractiveObjectIndex": -1,
|
"campaignId": "battle_zone",
|
||||||
"targetTeleportZoneIndex": -1,
|
"chatHistory": [
|
||||||
"taxi": {
|
[],
|
||||||
"taxiArrivedCallbackCalled": false,
|
[],
|
||||||
"taxiCar": {
|
[]
|
||||||
"currentWaypoint": 0,
|
],
|
||||||
"mode": 1,
|
"chatPreviewMsg": [
|
||||||
"positionX": -100.0,
|
"",
|
||||||
"positionY": -100.0,
|
"",
|
||||||
"positionZ": -100.0,
|
""
|
||||||
"rotation": 0.0,
|
],
|
||||||
"steeringAngle": 0.0,
|
"chatUnread": [
|
||||||
"velocity": 0.0,
|
true,
|
||||||
"waypointReachRadius": 3.0,
|
true,
|
||||||
"waypoints": []
|
true
|
||||||
|
],
|
||||||
|
"currentLocationId": "",
|
||||||
|
"globalFloats": {
|
||||||
|
"player_hp": 200.0
|
||||||
|
},
|
||||||
|
"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": {
|
"intBin_Plastic": {
|
||||||
"currentWaypoint": 0,
|
"alpha": 1.0,
|
||||||
"mode": 1,
|
"isActive": true,
|
||||||
"positionX": -100.0,
|
"isAnimating": false,
|
||||||
"positionY": -100.0,
|
"positionX": 1.399999976158142,
|
||||||
"positionZ": -100.0,
|
"positionY": 0.45736101269721985,
|
||||||
"rotation": 0.0,
|
"positionZ": 3.8499999046325684,
|
||||||
"steeringAngle": 0.0,
|
"rotationY": 0.0,
|
||||||
"velocity": 0.0,
|
"scale": 1.0
|
||||||
"waypointReachRadius": 3.0,
|
|
||||||
"waypoints": []
|
|
||||||
},
|
},
|
||||||
"taxiCarTriggerPositionX": 0.0,
|
"intBin_Waste_A": {
|
||||||
"taxiCarTriggerPositionY": 0.0,
|
"alpha": 1.0,
|
||||||
"taxiCarTriggerPositionZ": 0.0,
|
"isActive": true,
|
||||||
"taxiDefaultWaypoints": []
|
"isAnimating": false,
|
||||||
}
|
"positionX": 2.200000047683716,
|
||||||
},
|
"positionY": 0.45736101269721985,
|
||||||
"npcStates": [],
|
"positionZ": 3.8499999046325684,
|
||||||
"playerState": {
|
"rotationY": 0.0,
|
||||||
"attackTargetIndex": -1,
|
"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": {},
|
||||||
|
"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,
|
"attack_cooldown": 0.0,
|
||||||
"battle_state": 0,
|
"battle_state": 0,
|
||||||
"currentState": 0,
|
"currentState": 0,
|
||||||
"currentWaypointIndex": 0,
|
"currentWaypointIndex": 0,
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"faceTargetIndex": -1,
|
"faceTargetId": "",
|
||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
@@ -93,33 +174,79 @@
|
|||||||
"hp": 200.0,
|
"hp": 200.0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [],
|
||||||
"position": [
|
"position": [
|
||||||
-6, 0, 12.8
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6, 0, 12.8
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
],
|
],
|
||||||
"showWeapon": true,
|
"showWeapon": true,
|
||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.0,
|
"visibleRange": 6.0,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6, 0, 12.8
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scriptGlobals": null,
|
"player": {
|
||||||
"teleportZoneState": {},
|
"animationFrame": 0.0,
|
||||||
"triggerZoneState": {}
|
"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.025001525878906,
|
||||||
|
0.0,
|
||||||
|
12.775001525878906
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
-6.0,
|
||||||
|
0.0,
|
||||||
|
12.800000190734863
|
||||||
|
],
|
||||||
|
"requestedWalkTarget": [
|
||||||
|
-6.0,
|
||||||
|
0.0,
|
||||||
|
12.800000190734863
|
||||||
|
],
|
||||||
|
"showWeapon": true,
|
||||||
|
"targetFacingAngle": 0.0,
|
||||||
|
"visibleRange": 6.0,
|
||||||
|
"walkTarget": [
|
||||||
|
-6.025001525878906,
|
||||||
|
0.0,
|
||||||
|
12.775001525878906
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"questJournal": {
|
||||||
|
"quests": []
|
||||||
|
},
|
||||||
|
"savedAt": "2026-11-05 17:43",
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"taxiIsCalled": false,
|
||||||
|
"tutorialJournalScreenOpened": false,
|
||||||
|
"tutorialNeedOpenTaxiScreen": false,
|
||||||
|
"tutorialPhoneChatScreenOpened": false,
|
||||||
|
"tutorialStep": 0,
|
||||||
|
"tutorialTaxiHintDone": false
|
||||||
},
|
},
|
||||||
"money": 5500,
|
"persistenceVersion": 1
|
||||||
"playerHp": 200.0,
|
|
||||||
"playerMaxHp": 200.0,
|
|
||||||
"questJournal": {
|
|
||||||
"quests": []
|
|
||||||
},
|
|
||||||
"savedAt": "2026-07-24 23:40",
|
|
||||||
"taxiIsCalled": false,
|
|
||||||
"tutorialJournalScreenOpened": false,
|
|
||||||
"tutorialNeedOpenTaxiScreen": false,
|
|
||||||
"tutorialPhoneChatScreenOpened": false,
|
|
||||||
"tutorialStep": 9
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
{
|
||||||
|
"localState": {
|
||||||
|
"locations": {
|
||||||
|
"uni_exterior": {
|
||||||
|
"cameraAzimuth": -2.3499999046325684,
|
||||||
|
"cameraInclination": 1.103600025177002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networkState": {
|
||||||
|
"campaignId": "battle_zone",
|
||||||
|
"chatHistory": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"chatPreviewMsg": [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"chatUnread": [
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"currentLocationId": "",
|
||||||
|
"globalFloats": {
|
||||||
|
"player_hp": 200.0
|
||||||
|
},
|
||||||
|
"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": {},
|
||||||
|
"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": [],
|
||||||
|
"position": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"requestedWalkTarget": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"showWeapon": true,
|
||||||
|
"targetFacingAngle": 0.0,
|
||||||
|
"visibleRange": 6.0,
|
||||||
|
"walkTarget": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.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": [
|
||||||
|
[
|
||||||
|
-6.025001525878906,
|
||||||
|
0.0,
|
||||||
|
12.775001525878906
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
-6.0,
|
||||||
|
0.0,
|
||||||
|
12.800000190734863
|
||||||
|
],
|
||||||
|
"requestedWalkTarget": [
|
||||||
|
-6.0,
|
||||||
|
0.0,
|
||||||
|
12.800000190734863
|
||||||
|
],
|
||||||
|
"showWeapon": true,
|
||||||
|
"targetFacingAngle": 0.0,
|
||||||
|
"visibleRange": 6.0,
|
||||||
|
"walkTarget": [
|
||||||
|
-6.025001525878906,
|
||||||
|
0.0,
|
||||||
|
12.775001525878906
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"questJournal": {
|
||||||
|
"quests": []
|
||||||
|
},
|
||||||
|
"savedAt": "2026-11-05 17:43",
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"taxiIsCalled": false,
|
||||||
|
"tutorialJournalScreenOpened": false,
|
||||||
|
"tutorialNeedOpenTaxiScreen": false,
|
||||||
|
"tutorialPhoneChatScreenOpened": false,
|
||||||
|
"tutorialStep": 0,
|
||||||
|
"tutorialTaxiHintDone": false
|
||||||
|
},
|
||||||
|
"persistenceVersion": 1
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
-- uni_exterior level with static objects and navigation only.
|
-- uni_exterior level with static objects and navigation only.
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
|
function callback_init_location()
|
||||||
|
end
|
||||||
|
|
||||||
function callback_enter_location()
|
function callback_enter_location()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@ function callback_exit_location()
|
|||||||
end
|
end
|
||||||
|
|
||||||
game_api.set_location_callbacks(
|
game_api.set_location_callbacks(
|
||||||
|
callback_init_location,
|
||||||
callback_enter_location,
|
callback_enter_location,
|
||||||
callback_exit_location
|
callback_exit_location
|
||||||
)
|
)
|
||||||
+26
-27
@@ -2,18 +2,8 @@
|
|||||||
-- NPC PATROL WAYPOINTS
|
-- NPC PATROL WAYPOINTS
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
bathroom_door_opened = false
|
function on_npc_interact(npc_id)
|
||||||
player_door_opened = false
|
if npc_id == "npc_alik" then
|
||||||
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
|
|
||||||
--[[
|
--[[
|
||||||
local player_alik_aware = game_api.getIntValue("alik_aware")
|
local player_alik_aware = game_api.getIntValue("alik_aware")
|
||||||
local player_container_aware = game_api.getIntValue("player_container_aware")
|
local player_container_aware = game_api.getIntValue("player_container_aware")
|
||||||
@@ -25,7 +15,7 @@ function on_npc_interact(npc_index)
|
|||||||
else
|
else
|
||||||
game_api.start_dialogue("dialog_alik001")
|
game_api.start_dialogue("dialog_alik001")
|
||||||
end]]
|
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")
|
game_api.start_dialogue("dialog_alik002")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -119,8 +109,8 @@ end
|
|||||||
|
|
||||||
function callback_deactivate_cover_alik_room()
|
function callback_deactivate_cover_alik_room()
|
||||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
||||||
game_api.set_npc_enabled(0, true)
|
game_api.set_npc_enabled("npc_alik", true)
|
||||||
game_api.npc_rotate_to(0, 90)
|
game_api.npc_rotate_to("npc_alik", 90)
|
||||||
end
|
end
|
||||||
|
|
||||||
function callback_deactivate_cover_bathroom()
|
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.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_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_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
|
if (bathroom_door_opened) then
|
||||||
game_api.switch_navigation(3) -- closed only alik (a)
|
game_api.switch_navigation(3) -- closed only alik (a)
|
||||||
else
|
else
|
||||||
@@ -330,11 +320,27 @@ function on_toilet_click()
|
|||||||
game_api.player_stop()
|
game_api.player_stop()
|
||||||
end
|
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()
|
function callback_enter_location()
|
||||||
print("Enter location dorm")
|
print("Enter location dorm")
|
||||||
local hp = game_api.getFloatValue("player_hp")
|
local hp = game_api.getFloatValue("player_hp")
|
||||||
game_api.set_player_hp(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
|
if (game_api.is_night()) then
|
||||||
closeAlikDoor()
|
closeAlikDoor()
|
||||||
end
|
end
|
||||||
@@ -347,25 +353,18 @@ function callback_exit_location()
|
|||||||
end
|
end
|
||||||
|
|
||||||
game_api.set_location_callbacks(
|
game_api.set_location_callbacks(
|
||||||
|
callback_init_location,
|
||||||
callback_enter_location,
|
callback_enter_location,
|
||||||
callback_exit_location
|
callback_exit_location
|
||||||
)
|
)
|
||||||
|
|
||||||
function callback_call_taxi()
|
function callback_call_taxi()
|
||||||
game_api.set_teleport_active(0, true)
|
game_api.set_teleport_active("tp_dorm_to_loc2", true)
|
||||||
game_api.set_trigger_zone_enabled(0, false)
|
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||||
end
|
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(
|
game_api.set_call_taxi_callback(
|
||||||
callback_call_taxi
|
callback_call_taxi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
game_api.set_npc_enabled(0, false)
|
|
||||||
game_api.switch_navigation(0)
|
|
||||||
print("Lua script loaded successfully--!")
|
print("Lua script loaded successfully--!")
|
||||||
@@ -1,16 +1,5 @@
|
|||||||
print("Lua script loaded successfully!")
|
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()
|
function darklands_exit001_enter_callback()
|
||||||
if game_api.is_darklands() then
|
if game_api.is_darklands() then
|
||||||
print("Lua script start_darklands_transition")
|
print("Lua script start_darklands_transition")
|
||||||
@@ -77,11 +66,14 @@ game_api.set_enter_night_callback(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
function callback_init_location()
|
||||||
|
end
|
||||||
|
|
||||||
function callback_enter_location()
|
function callback_enter_location()
|
||||||
print("Enter location uni exterior")
|
print("Enter location uni exterior")
|
||||||
local hp = game_api.getFloatValue("player_hp")
|
local hp = game_api.getFloatValue("player_hp")
|
||||||
game_api.set_player_hp(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
|
if (game_api.is_night()) then
|
||||||
game_api.deactivate_interactive_object("car001")
|
game_api.deactivate_interactive_object("car001")
|
||||||
game_api.deactivate_interactive_object("car001wheels")
|
game_api.deactivate_interactive_object("car001wheels")
|
||||||
@@ -100,6 +92,7 @@ function callback_exit_location()
|
|||||||
end
|
end
|
||||||
|
|
||||||
game_api.set_location_callbacks(
|
game_api.set_location_callbacks(
|
||||||
|
callback_init_location,
|
||||||
callback_enter_location,
|
callback_enter_location,
|
||||||
callback_exit_location
|
callback_exit_location
|
||||||
)
|
)
|
||||||
@@ -108,10 +101,6 @@ game_api.set_trigger_zone_callbacks("darklands_exit001",
|
|||||||
darklands_exit001_enter_callback,
|
darklands_exit001_enter_callback,
|
||||||
nil
|
nil
|
||||||
)
|
)
|
||||||
game_api.set_trigger_zone_callbacks("darklands_exit002",
|
|
||||||
darklands_exit001_enter_callback,
|
|
||||||
nil
|
|
||||||
)
|
|
||||||
game_api.set_trigger_zone_callbacks("darklands_exit003",
|
game_api.set_trigger_zone_callbacks("darklands_exit003",
|
||||||
darklands_exit001_enter_callback,
|
darklands_exit001_enter_callback,
|
||||||
nil
|
nil
|
||||||
@@ -129,12 +118,12 @@ game_api.set_trigger_zone_callbacks("darklands_exit005",
|
|||||||
|
|
||||||
function taxi_zone001_enter_callback()
|
function taxi_zone001_enter_callback()
|
||||||
game_api.start_dialogue("dialog_taxi001")
|
game_api.start_dialogue("dialog_taxi001")
|
||||||
game_api.set_trigger_zone_enabled(0, false)
|
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
function callback_call_taxi()
|
function callback_call_taxi()
|
||||||
game_api.set_teleport_active(0, true)
|
game_api.set_teleport_active("tp_loc2_to_dorm", true)
|
||||||
game_api.set_trigger_zone_enabled(0, false)
|
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
game_api.set_call_taxi_callback(
|
game_api.set_call_taxi_callback(
|
||||||
|
|||||||
+129
-136
@@ -1,14 +1,6 @@
|
|||||||
CONST_MSG_EMPTY = "(пусто)"
|
CONST_MSG_EMPTY = "(пусто)"
|
||||||
CONST_MSG_WHERE = "Бекзат, ты где? Я жду..."
|
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
|
0 - day0
|
||||||
1 - darklands
|
1 - darklands
|
||||||
@@ -17,14 +9,6 @@ teacher_told_about_book = false
|
|||||||
4 - day
|
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()
|
function dialog_aiperi_give_key()
|
||||||
local player_hold_key = game_api.getIntValue("player_hold_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
|
if (player_hold_knife == 0) then
|
||||||
local lection_is_over = game_api.getIntValue("lection_is_over")
|
local lection_is_over = game_api.getIntValue("lection_is_over")
|
||||||
if lection_is_over == 1 then
|
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
|
if (player_hold_key == 1) then
|
||||||
game_api.start_dialogue("knife_dialog002")
|
game_api.start_dialogue("knife_dialog002")
|
||||||
else
|
else
|
||||||
@@ -79,7 +63,7 @@ function knife_dialog_zone001_enter_callback()
|
|||||||
if (aiperi_talked_after_knife == 0) then
|
if (aiperi_talked_after_knife == 0) then
|
||||||
game_api.start_dialogue("knife_dialog_second001")
|
game_api.start_dialogue("knife_dialog_second001")
|
||||||
end
|
end
|
||||||
game_api.set_trigger_zone_enabled(1, false)
|
game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -110,11 +94,11 @@ game_api.set_trigger_zone_callbacks("knife_dialog_zone001",
|
|||||||
function on_knife_pickup()
|
function on_knife_pickup()
|
||||||
game_api.pickup_item("knife")
|
game_api.pickup_item("knife")
|
||||||
game_api.deactivate_interactive_object("Knife001")
|
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.setIntValue("player_hold_knife", 1)
|
||||||
|
|
||||||
game_api.set_trigger_zone_enabled(2, true)
|
game_api.set_trigger_zone_enabled("teacher_dialog_zone001", true)
|
||||||
game_api.npc_walk_to(0, -4.57412, 0, 6.78495, on_teacher_arrived2)
|
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.quest_set_objective_completed("aiperi_knife", "aiperi_knife_take")
|
||||||
game_api.player_stop()
|
game_api.player_stop()
|
||||||
end
|
end
|
||||||
@@ -133,14 +117,14 @@ function on_book_pickup()
|
|||||||
player_hold_book = true
|
player_hold_book = true
|
||||||
game_api.quest_set_objective_completed("study_project", "study_project_book")
|
game_api.quest_set_objective_completed("study_project", "study_project_book")
|
||||||
game_api.start_dialogue("book_dialog003")
|
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
|
end
|
||||||
else
|
else
|
||||||
game_api.remove_item("book")
|
game_api.remove_item("book")
|
||||||
game_api.activate_interactive_object("Book001")
|
game_api.activate_interactive_object("Book001")
|
||||||
player_hold_book = false
|
player_hold_book = false
|
||||||
game_api.start_dialogue("book_dialog004")
|
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
|
end
|
||||||
else
|
else
|
||||||
print("on_book_pickup step2")
|
print("on_book_pickup step2")
|
||||||
@@ -218,14 +202,14 @@ function on_cake_clicked()
|
|||||||
game_api.player_stop()
|
game_api.player_stop()
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_npc_interact(npc_index)
|
function on_npc_interact(npc_id)
|
||||||
local player_ghost_aware = game_api.getIntValue("ghost_aware")
|
local player_ghost_aware = game_api.getIntValue("ghost_aware")
|
||||||
local player_hold_key = game_api.getIntValue("player_hold_key")
|
local player_hold_key = game_api.getIntValue("player_hold_key")
|
||||||
local day = game_api.getIntValue("day")
|
local day = game_api.getIntValue("day")
|
||||||
|
|
||||||
print("[Lua] NPC interaction! Index: " .. tostring(npc_index))
|
print("[Lua] NPC interaction! ID: " .. tostring(npc_id))
|
||||||
if npc_index == 1 then
|
if npc_id == "npc_02_woman" then
|
||||||
game_api.player_stop_and_rotate_to_npc(1)
|
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||||
if (game_api.is_dawn()) or (day > 0) then
|
if (game_api.is_dawn()) or (day > 0) then
|
||||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||||
else
|
else
|
||||||
@@ -236,8 +220,8 @@ function on_npc_interact(npc_index)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if npc_index == 0 then
|
if npc_id == "npc_teacher01" then
|
||||||
game_api.player_stop_and_rotate_to_npc(0)
|
game_api.player_stop_and_rotate_to_npc("npc_teacher01")
|
||||||
if (day == 0) then
|
if (day == 0) then
|
||||||
if (teacher_arrived_to_library) then
|
if (teacher_arrived_to_library) then
|
||||||
if (teacher_told_about_book) then
|
if (teacher_told_about_book) then
|
||||||
@@ -264,8 +248,8 @@ function on_npc_interact(npc_index)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if npc_index == 2 then
|
if npc_id == "npc_04_ghost" then
|
||||||
game_api.player_stop_and_rotate_to_npc(2)
|
game_api.player_stop_and_rotate_to_npc("npc_04_ghost")
|
||||||
local report_card_signed = game_api.getIntValue("report_card_signed")
|
local report_card_signed = game_api.getIntValue("report_card_signed")
|
||||||
if (report_card_signed == 1) then
|
if (report_card_signed == 1) then
|
||||||
game_api.start_dialogue("dialog_with_ghost003")
|
game_api.start_dialogue("dialog_with_ghost003")
|
||||||
@@ -273,18 +257,18 @@ function on_npc_interact(npc_index)
|
|||||||
game_api.start_dialogue("dialog_with_ghost001")
|
game_api.start_dialogue("dialog_with_ghost001")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if npc_index == 6 then
|
if npc_id == "npc_altynai" then
|
||||||
game_api.player_stop_and_rotate_to_npc(6)
|
game_api.player_stop_and_rotate_to_npc("npc_altynai")
|
||||||
game_api.start_dialogue("dialog_female_student002")
|
game_api.start_dialogue("dialog_female_student002")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_quest_over()
|
function on_quest_over()
|
||||||
ghost_gone = true
|
ghost_gone = true
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.quest_set_objective_completed("ghost_release", "ghost_release_show")
|
game_api.quest_set_objective_completed("ghost_release", "ghost_release_show")
|
||||||
|
|
||||||
game_api.start_cutscene("game_complete_cutscene001")
|
game_api.start_cutscene("game_complete_cutscene001")
|
||||||
@@ -297,7 +281,7 @@ end
|
|||||||
|
|
||||||
function teacher_zone001_enter_callback()
|
function teacher_zone001_enter_callback()
|
||||||
game_api.start_dialogue("teacher_dialog002")
|
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
|
teacher_told_about_book = true
|
||||||
game_api.player_stop()
|
game_api.player_stop()
|
||||||
end
|
end
|
||||||
@@ -474,9 +458,9 @@ function on_hall_door_click()
|
|||||||
game_api.switch_navigation(1)
|
game_api.switch_navigation(1)
|
||||||
game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
|
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.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("npc_teacher01", true)
|
||||||
game_api.set_npc_enabled(6, true)
|
game_api.set_npc_enabled("npc_altynai", true)
|
||||||
game_api.npc_rotate_to(6, 180)
|
game_api.npc_rotate_to("npc_altynai", 180)
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
game_api.player_stop()
|
game_api.player_stop()
|
||||||
end
|
end
|
||||||
@@ -558,22 +542,22 @@ function on_n2_door_click()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function on_teacher_arrived()
|
function on_teacher_arrived()
|
||||||
game_api.npc_rotate_to(0, 180)
|
game_api.npc_rotate_to("npc_teacher01", 180)
|
||||||
teacher_arrived_to_library = true
|
teacher_arrived_to_library = true
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_student_arrived()
|
function on_student_arrived()
|
||||||
game_api.npc_rotate_to(6, 270)
|
game_api.npc_rotate_to("npc_altynai", 270)
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_teacher_arrived2()
|
function on_teacher_arrived2()
|
||||||
game_api.npc_rotate_to(0, 0)
|
game_api.npc_rotate_to("npc_teacher01", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function book_dialog_zone001_enter_callback()
|
function book_dialog_zone001_enter_callback()
|
||||||
if (game_api.is_darklands()) then
|
if (game_api.is_darklands()) then
|
||||||
game_api.start_dialogue("ghost_dialog002")
|
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()
|
game_api.advance_darklands_hud()
|
||||||
else
|
else
|
||||||
if (player_hold_book) and (game_api.is_night() == false) then
|
if (player_hold_book) and (game_api.is_night() == false) then
|
||||||
@@ -621,9 +605,9 @@ function on_sleep_cutscene()
|
|||||||
print("Cutscene 2 done!")
|
print("Cutscene 2 done!")
|
||||||
|
|
||||||
game_api.set_night()
|
game_api.set_night()
|
||||||
--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("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(6, false)
|
game_api.set_npc_enabled("npc_altynai", false)
|
||||||
game_api.switch_navigation(16)
|
game_api.switch_navigation(16)
|
||||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||||
game_api.set_object_rotation("Hall_Leaf001", 0)
|
game_api.set_object_rotation("Hall_Leaf001", 0)
|
||||||
@@ -649,14 +633,14 @@ function on_teacher_arrived_intermediate()
|
|||||||
game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil)
|
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.fade_object("Room_Cover_North_3_001", 0, 0.5, callback_deactivate_cover_n3)
|
||||||
game_api.switch_navigation(2)
|
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")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_lection_cutscene_fadein_callback()
|
function on_lection_cutscene_fadein_callback()
|
||||||
print("Lection cutscene in progress!")
|
print("Lection cutscene in progress!")
|
||||||
-- -0.0234149 0 10.1139
|
-- -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
|
--4.91002 0 10.375
|
||||||
--game_api.player_set_position(-2.41002, 0, 10.375)
|
--game_api.player_set_position(-2.41002, 0, 10.375)
|
||||||
@@ -670,10 +654,10 @@ function callback_on_lection_cutscene_over()
|
|||||||
print("Cutscene done!")
|
print("Cutscene done!")
|
||||||
game_api.setIntValue("lection_is_over", 1)
|
game_api.setIntValue("lection_is_over", 1)
|
||||||
|
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_rotate_to(1, 180)
|
game_api.npc_rotate_to("npc_02_woman", 180)
|
||||||
game_api.npc_walk_to(0, 0.758123, 0, 6.50063, on_teacher_arrived_intermediate)
|
game_api.npc_walk_to("npc_teacher01", 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.npc_walk_to("npc_altynai", 5.28998, 0, -5.525, on_student_arrived)
|
||||||
|
|
||||||
local aiperi_knife_aware = game_api.getIntValue("aiperi_knife_aware")
|
local aiperi_knife_aware = game_api.getIntValue("aiperi_knife_aware")
|
||||||
|
|
||||||
@@ -715,38 +699,36 @@ function on_bookshelf_teacher_clicked()
|
|||||||
on_report_card_pickup()
|
on_report_card_pickup()
|
||||||
end
|
end
|
||||||
|
|
||||||
first_time_darklands = true
|
|
||||||
|
|
||||||
function callback_enter_darklands()
|
function callback_enter_darklands()
|
||||||
if (first_time_darklands) then
|
if (first_time_darklands) then
|
||||||
game_api.start_dialogue("ghost_dialog001")
|
game_api.start_dialogue("ghost_dialog001")
|
||||||
first_time_darklands = false
|
first_time_darklands = false
|
||||||
end
|
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("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(1, false)
|
game_api.set_npc_enabled("npc_02_woman", false)
|
||||||
game_api.set_npc_enabled(6, false)
|
game_api.set_npc_enabled("npc_altynai", false)
|
||||||
|
|
||||||
if (ghost_gone) then
|
if (ghost_gone) then
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
else
|
else
|
||||||
game_api.set_npc_enabled(2, true)
|
game_api.set_npc_enabled("npc_04_ghost", true)
|
||||||
game_api.set_npc_enabled(3, true)
|
game_api.set_npc_enabled("ghost_01x", true)
|
||||||
game_api.set_npc_enabled(4, true)
|
game_api.set_npc_enabled("ghost_02x", true)
|
||||||
game_api.set_npc_enabled(5, true)
|
game_api.set_npc_enabled("ghost_03x", true)
|
||||||
|
|
||||||
game_api.npc_set_hp(3, 35)
|
game_api.npc_set_hp("ghost_01x", 35)
|
||||||
game_api.npc_set_hp(4, 35)
|
game_api.npc_set_hp("ghost_02x", 35)
|
||||||
game_api.npc_set_hp(5, 35)
|
game_api.npc_set_hp("ghost_03x", 35)
|
||||||
|
|
||||||
game_api.npc_set_position(3, 0, 0, -6.4)
|
game_api.npc_set_position("ghost_01x", 0, 0, -6.4)
|
||||||
game_api.npc_set_position(4, -5.0, 0, 12)
|
game_api.npc_set_position("ghost_02x", -5.0, 0, 12)
|
||||||
game_api.npc_set_position(5, 5.0, 0, 12)
|
game_api.npc_set_position("ghost_03x", 5.0, 0, 12)
|
||||||
end
|
end
|
||||||
|
|
||||||
game_api.switch_navigation(5)
|
game_api.switch_navigation(5)
|
||||||
@@ -755,13 +737,13 @@ end
|
|||||||
|
|
||||||
function callback_exit_darklands()
|
function callback_exit_darklands()
|
||||||
game_api.start_cutscene("darklands_exit001")
|
game_api.start_cutscene("darklands_exit001")
|
||||||
game_api.set_npc_enabled(0, false)
|
game_api.set_npc_enabled("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(1, false)
|
game_api.set_npc_enabled("npc_02_woman", false)
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.set_npc_enabled(6, false)
|
game_api.set_npc_enabled("npc_altynai", false)
|
||||||
|
|
||||||
morning_did_open_door_index = 0
|
morning_did_open_door_index = 0
|
||||||
setDay1MorningSetup()
|
setDay1MorningSetup()
|
||||||
@@ -873,13 +855,13 @@ function setDay0setup()
|
|||||||
|
|
||||||
game_api.deactivate_interactive_object("Note001")
|
game_api.deactivate_interactive_object("Note001")
|
||||||
|
|
||||||
game_api.set_npc_enabled(0, false)
|
game_api.set_npc_enabled("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(1, false)
|
game_api.set_npc_enabled("npc_02_woman", false)
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.set_npc_enabled(6, false)
|
game_api.set_npc_enabled("npc_altynai", false)
|
||||||
|
|
||||||
game_api.activate_interactive_object("car001")
|
game_api.activate_interactive_object("car001")
|
||||||
game_api.activate_interactive_object("car001wheels")
|
game_api.activate_interactive_object("car001wheels")
|
||||||
@@ -903,7 +885,6 @@ function setDay1setup()
|
|||||||
game_api.activate_interactive_object("Room_Cover_South_2_001")
|
game_api.activate_interactive_object("Room_Cover_South_2_001")
|
||||||
game_api.deactivate_interactive_object("Room_Cover_South_3_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_1_Leaf001", -90)
|
||||||
game_api.set_object_rotation("Room_N_2_Leaf001", -90)
|
game_api.set_object_rotation("Room_N_2_Leaf001", -90)
|
||||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||||
@@ -911,15 +892,15 @@ function setDay1setup()
|
|||||||
game_api.set_object_rotation("Room_S_2_Leaf001", 90)
|
game_api.set_object_rotation("Room_S_2_Leaf001", 90)
|
||||||
game_api.set_object_rotation("Hall_Leaf001", 90)
|
game_api.set_object_rotation("Hall_Leaf001", 90)
|
||||||
|
|
||||||
game_api.set_npc_enabled(0, true)
|
game_api.set_npc_enabled("npc_teacher01", true)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.set_npc_enabled(6, true)
|
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")
|
game_api.deactivate_interactive_object("Cake001")
|
||||||
|
|
||||||
@@ -939,7 +920,6 @@ function setDay1MorningSetup()
|
|||||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_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_1_Leaf001", 0)
|
||||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||||
@@ -964,15 +944,15 @@ function setDay1MorningSetup()
|
|||||||
|
|
||||||
-- TODO: Make check for teacher_door_opened false or true
|
-- TODO: Make check for teacher_door_opened false or true
|
||||||
|
|
||||||
game_api.set_npc_enabled(0, false)
|
game_api.set_npc_enabled("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(1, false)
|
game_api.set_npc_enabled("npc_02_woman", false)
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.set_npc_enabled(6, 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
|
if morning_did_open_door_index == 1 then
|
||||||
game_api.deactivate_interactive_object("Room_Cover_South_1_001")
|
game_api.deactivate_interactive_object("Room_Cover_South_1_001")
|
||||||
@@ -1043,7 +1023,6 @@ function setDay1NightSetup()
|
|||||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_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_1_Leaf001", 0)
|
||||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||||
@@ -1059,13 +1038,13 @@ function setDay1NightSetup()
|
|||||||
game_api.set_object_alpha("Room_Cover_Corridor_001", 1)
|
game_api.set_object_alpha("Room_Cover_Corridor_001", 1)
|
||||||
game_api.set_object_alpha("Room_Cover_Main_Hall_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("npc_teacher01", false)
|
||||||
game_api.set_npc_enabled(1, false)
|
game_api.set_npc_enabled("npc_02_woman", false)
|
||||||
game_api.set_npc_enabled(2, false)
|
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||||
game_api.set_npc_enabled(3, false)
|
game_api.set_npc_enabled("ghost_01x", false)
|
||||||
game_api.set_npc_enabled(4, false)
|
game_api.set_npc_enabled("ghost_02x", false)
|
||||||
game_api.set_npc_enabled(5, false)
|
game_api.set_npc_enabled("ghost_03x", false)
|
||||||
game_api.set_npc_enabled(6, false)
|
game_api.set_npc_enabled("npc_altynai", false)
|
||||||
|
|
||||||
game_api.deactivate_interactive_object("car001")
|
game_api.deactivate_interactive_object("car001")
|
||||||
game_api.deactivate_interactive_object("car001wheels")
|
game_api.deactivate_interactive_object("car001wheels")
|
||||||
@@ -1222,8 +1201,8 @@ game_api.set_chat_callbacks(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function callback_aiperi_dialog_morning()
|
function callback_aiperi_dialog_morning()
|
||||||
game_api.player_stop_and_rotate_to_npc(1)
|
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||||
game_api.npc_stop_and_rotate_to_player(1)
|
game_api.npc_stop_and_rotate_to_player("npc_02_woman")
|
||||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1246,9 +1225,9 @@ function on_aiperi_opens_door()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--Aiperi incoming
|
--Aiperi incoming
|
||||||
game_api.npc_set_position(1, 0, 0, 6.5)
|
game_api.npc_set_position("npc_02_woman", 0, 0, 6.5)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_walk_to(1, 0.0, 0, 9.1, callback_aiperi_dialog_morning)
|
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.player_walk_to(0.0, 0.0, 10.0, nil)
|
||||||
|
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
@@ -1268,9 +1247,9 @@ function on_aiperi_opens_door()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--Aiperi incoming
|
--Aiperi incoming
|
||||||
game_api.npc_set_position(1, 0.0, 0, 6.43818)
|
game_api.npc_set_position("npc_02_woman", 0.0, 0, 6.43818)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_walk_to(1, 2.34349, 0, 6.31229, callback_aiperi_dialog_morning)
|
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.player_walk_to(3.52174, 0, 6.28993, nil)
|
||||||
|
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
@@ -1290,9 +1269,9 @@ function on_aiperi_opens_door()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--Aiperi incoming
|
--Aiperi incoming
|
||||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_walk_to(1, 2.11898, 0, -1.7074, callback_aiperi_dialog_morning)
|
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.player_walk_to(3.36055, 0, -2.0345, nil)
|
||||||
|
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
@@ -1312,9 +1291,9 @@ function on_aiperi_opens_door()
|
|||||||
game_api.switch_navigation(9)
|
game_api.switch_navigation(9)
|
||||||
end
|
end
|
||||||
--Aiperi incoming
|
--Aiperi incoming
|
||||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_walk_to(1, -2.454, 0, -1.61871, callback_aiperi_dialog_morning)
|
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.player_walk_to(-3.63363, 0, -1.50727, nil)
|
||||||
|
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
@@ -1334,9 +1313,9 @@ function on_aiperi_opens_door()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--Aiperi incoming
|
--Aiperi incoming
|
||||||
game_api.npc_set_position(1, 0, 0, -9.77948)
|
game_api.npc_set_position("npc_02_woman", 0, 0, -9.77948)
|
||||||
game_api.set_npc_enabled(1, true)
|
game_api.set_npc_enabled("npc_02_woman", true)
|
||||||
game_api.npc_walk_to(1, -2.53977, 0, -9.77852, callback_aiperi_dialog_morning)
|
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.player_walk_to(-3.64494, 0, -9.66711, nil)
|
||||||
|
|
||||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||||
@@ -1345,7 +1324,24 @@ function on_aiperi_opens_door()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function on_aiperi_morning_dialog_finished()
|
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
|
end
|
||||||
|
|
||||||
function callback_enter_location()
|
function callback_enter_location()
|
||||||
@@ -1437,6 +1433,7 @@ function callback_exit_location()
|
|||||||
end
|
end
|
||||||
|
|
||||||
game_api.set_location_callbacks(
|
game_api.set_location_callbacks(
|
||||||
|
callback_init_location,
|
||||||
callback_enter_location,
|
callback_enter_location,
|
||||||
callback_exit_location
|
callback_exit_location
|
||||||
)
|
)
|
||||||
@@ -1467,9 +1464,5 @@ end
|
|||||||
|
|
||||||
game_api.set_cutscene_callback("game_complete_cutscene001", callback_game_complete_cutscene)
|
game_api.set_cutscene_callback("game_complete_cutscene001", callback_game_complete_cutscene)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setDay0setup()
|
|
||||||
game_api.switch_navigation(0)
|
|
||||||
--debugAllOpen()
|
--debugAllOpen()
|
||||||
print("Lua script loaded successfully!---!")
|
print("Lua script loaded successfully!---!")
|
||||||
+2845
-1419
File diff suppressed because it is too large.
Load diff
@@ -1,108 +1,124 @@
|
|||||||
{
|
{
|
||||||
"objects": [
|
"objects": [
|
||||||
{
|
{
|
||||||
|
"id": "intBin_Cans",
|
||||||
"name": "intBin_Cans",
|
"name": "intBin_Cans",
|
||||||
"texturePath": "resources/w/shelter/models/intBin_Cans_atlas.png",
|
"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",
|
"meshPath": "resources/w/shelter/models/intBin_Cans.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.6,
|
"positionX": 0.6,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intBin_Plastic",
|
||||||
"name": "intBin_Plastic",
|
"name": "intBin_Plastic",
|
||||||
"texturePath": "resources/w/shelter/models/intBin_Plastic_atlas.png",
|
"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",
|
"meshPath": "resources/w/shelter/models/intBin_Plastic.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.4,
|
"positionX": 1.4,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intBin_Waste_A",
|
||||||
"name": "intBin_Waste_A",
|
"name": "intBin_Waste_A",
|
||||||
"texturePath": "resources/w/shelter/models/intBin_Waste_A_atlas.png",
|
"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",
|
"meshPath": "resources/w/shelter/models/intBin_Waste_A.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 2.2,
|
"positionX": 2.2,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intBin_Waste_B",
|
||||||
"name": "intBin_Waste_B",
|
"name": "intBin_Waste_B",
|
||||||
"texturePath": "resources/w/shelter/models/intBin_Waste_B_atlas.png",
|
"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",
|
"meshPath": "resources/w/shelter/models/intBin_Waste_B.txt.bin",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 3.0,
|
"positionX": 3.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intRoof",
|
||||||
"name": "intRoof",
|
"name": "intRoof",
|
||||||
"texturePath": "resources/w/shelter/models/intRoof_atlas.png",
|
"texturePath": "resources/w/shelter/models/intRoof_atlas.png",
|
||||||
|
"textureDarkandsPath": "resources/w/shelter/models/intRoof_atlas.png",
|
||||||
"meshPath": "resources/w/shelter/models/intRoof.txt",
|
"meshPath": "resources/w/shelter/models/intRoof.txt",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.0,
|
"positionY": 3.2,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intVending.00",
|
||||||
"name": "intVending.00",
|
"name": "intVending.00",
|
||||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
|
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.0,
|
"positionX": -1.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intVending.01",
|
||||||
"name": "intVending.01",
|
"name": "intVending.01",
|
||||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
|
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "intVending.02",
|
||||||
"name": "intVending.02",
|
"name": "intVending.02",
|
||||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
|
"textureDarkandsPath": "resources/w/shelter/models/vending_atlas.png",
|
||||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.0,
|
"positionX": 1.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"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 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 1: Pos(27.0, -4.68, -0.01) Norm(0.0, 0.0, 1.0) UV(54.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 2: Pos(27.0, -7.68, -0.01) Norm(0.0, 0.0, 1.0) UV(54.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 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 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,175 @@
|
|||||||
|
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(
|
||||||
|
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,280 @@
|
|||||||
|
#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");
|
||||||
|
}
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#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::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.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() == 2 &&
|
||||||
|
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
|
parsedTick.commands[1]) &&
|
||||||
|
std::get<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
|
parsedTick.commands[1]).enabled,
|
||||||
|
"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);
|
||||||
|
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");
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#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"];
|
||||||
|
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"});
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
server.update();
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
return client->snapshot()->revision > 1;
|
||||||
|
}),
|
||||||
|
"client did not consume finalized lockstep ticks");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -722,9 +722,10 @@ namespace FRG
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoneSystemNew::Interpolate(int frame)
|
void BoneSystemNew::Interpolate(float frame)
|
||||||
{
|
{
|
||||||
int startingKeyFrame = -1;
|
int startingKeyFrame = -1;
|
||||||
|
int nextKeyFrame = -1;
|
||||||
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
||||||
{
|
{
|
||||||
int oldFrame = animations[0].keyFrames[i].frame;
|
int oldFrame = animations[0].keyFrames[i].frame;
|
||||||
@@ -732,22 +733,29 @@ namespace FRG
|
|||||||
if (frame >= oldFrame && frame < nextFrame)
|
if (frame >= oldFrame && frame < nextFrame)
|
||||||
{
|
{
|
||||||
startingKeyFrame = i;
|
startingKeyFrame = i;
|
||||||
|
nextKeyFrame = i + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startingKeyFrame == -1)
|
if (startingKeyFrame == -1)
|
||||||
{
|
{
|
||||||
logger() << "Exception here: frame number is out of range of keyframes. Frame: " << frame << std::endl;
|
if (animations[0].keyFrames.size() < 2)
|
||||||
throw std::runtime_error("Exception here");
|
return;
|
||||||
|
startingKeyFrame = static_cast<int>(animations[0].keyFrames.size()) - 1;
|
||||||
|
nextKeyFrame = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
float modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||||
int diffFrames = animations[0].keyFrames[startingKeyFrame + 1].frame - animations[0].keyFrames[startingKeyFrame].frame;
|
int diffFrames = nextKeyFrame == 0
|
||||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
? 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>& 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());
|
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 startingKeyFrame = -1;
|
||||||
|
int nextKeyFrame = -1;
|
||||||
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
||||||
{
|
{
|
||||||
int oldFrame = keyFrames[i].frame;
|
int oldFrame = keyFrames[i].frame;
|
||||||
@@ -916,6 +925,7 @@ namespace FRG
|
|||||||
if (frame >= oldFrame && frame < nextFrame)
|
if (frame >= oldFrame && frame < nextFrame)
|
||||||
{
|
{
|
||||||
startingKeyFrame = static_cast<int>(i);
|
startingKeyFrame = static_cast<int>(i);
|
||||||
|
nextKeyFrame = static_cast<int>(i + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,16 +934,23 @@ namespace FRG
|
|||||||
|
|
||||||
if (startingKeyFrame == -1)
|
if (startingKeyFrame == -1)
|
||||||
{
|
{
|
||||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
if (keyFrames.size() < 2)
|
||||||
return;
|
{
|
||||||
|
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startingKeyFrame = static_cast<int>(keyFrames.size()) - 1;
|
||||||
|
nextKeyFrame = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
float modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||||
int diffFrames = keyFrames[startingKeyFrame + 1].frame - keyFrames[startingKeyFrame].frame;
|
int diffFrames = nextKeyFrame == 0
|
||||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
? 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>& 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++)
|
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 LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||||
void LoadFromBinaryFile(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;
|
int findBoneIndex(const std::string& name) const;
|
||||||
};
|
};
|
||||||
@@ -94,14 +94,14 @@ namespace FRG
|
|||||||
|
|
||||||
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
||||||
void RenderVBO(Renderer& renderer, const std::vector<std::string>& meshNamesOrdered);
|
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
|
struct BoneAnimationDataNew
|
||||||
{
|
{
|
||||||
BoneSystemNew model;
|
BoneSystemNew model;
|
||||||
float currentFrame = 0.f;
|
float currentFrame = 0.f;
|
||||||
int lastFrame = -1;
|
float lastFrame = -1.0f;
|
||||||
int totalFrames = 1;
|
int totalFrames = 1;
|
||||||
|
|
||||||
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
@@ -109,6 +110,10 @@ namespace FRG {
|
|||||||
c.startStatePath = entry.value("startState", "");
|
c.startStatePath = entry.value("startState", "");
|
||||||
c.questsPath = entry.value("quests", "");
|
c.questsPath = entry.value("quests", "");
|
||||||
c.tutorial = entry.value("tutorial", false);
|
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));
|
c.loadingScreenPath = entry.value("loadingScreen", std::string(CampaignConfig::kDefaultLoadingScreen));
|
||||||
for (const std::string& path : c.locationConfigPaths) {
|
for (const std::string& path : c.locationConfigPaths) {
|
||||||
LocationConfig loc;
|
LocationConfig loc;
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ namespace FRG {
|
|||||||
std::string startStatePath;
|
std::string startStatePath;
|
||||||
std::string questsPath;
|
std::string questsPath;
|
||||||
bool tutorial = false;
|
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).
|
// UI layout shown while the campaign loads (preloaded with the rest of the UI).
|
||||||
std::string loadingScreenPath = kDefaultLoadingScreen;
|
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 <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <chrono>
|
||||||
#include "dialogue/TranslationDatabase.h"
|
#include "dialogue/TranslationDatabase.h"
|
||||||
#include "AudioPlayerAsync.h"
|
#include "AudioPlayerAsync.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
class TextRenderer;
|
class TextRenderer;
|
||||||
|
class IGameServer;
|
||||||
|
|
||||||
class Character {
|
class Character {
|
||||||
public:
|
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;
|
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 ----
|
// ---- Asset loading ----
|
||||||
void loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
void loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||||
void loadBinaryAnimation(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).
|
// 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.
|
// 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 ----
|
// ---- Per-frame update ----
|
||||||
// resolver maps character indices to live pointers so attackTargetIndex / faceTargetIndex
|
// Applies authoritative animation state to runtime resources and consumes
|
||||||
// can be dereferenced inside update(). Pass nullptr only if no targets are set.
|
// presentation-only events emitted by CharacterState::update.
|
||||||
void update(int64_t deltaMs, const CharacterResolver& resolver = nullptr);
|
void update(int64_t deltaMs, const CharacterUpdateEvents* events = 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();
|
|
||||||
|
|
||||||
// ---- Damage ----
|
// ---- 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 ----
|
// ---- Spark setup ----
|
||||||
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
|
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
|
||||||
@@ -80,6 +66,10 @@ public:
|
|||||||
void drawName(TextRenderer& textRenderer,
|
void drawName(TextRenderer& textRenderer,
|
||||||
const Eigen::Matrix4f& cameraViewMatrix,
|
const Eigen::Matrix4f& cameraViewMatrix,
|
||||||
const Eigen::Matrix4f& projectionMatrix);
|
const Eigen::Matrix4f& projectionMatrix);
|
||||||
|
void drawPlayerName(TextRenderer& textRenderer,
|
||||||
|
const Eigen::Matrix4f& cameraViewMatrix,
|
||||||
|
const Eigen::Matrix4f& projectionMatrix,
|
||||||
|
const std::string& displayName);
|
||||||
void drawHealthBar(Renderer& renderer,
|
void drawHealthBar(Renderer& renderer,
|
||||||
const Eigen::Matrix4f& cameraViewMatrix,
|
const Eigen::Matrix4f& cameraViewMatrix,
|
||||||
const Eigen::Matrix4f& projectionMatrix);
|
const Eigen::Matrix4f& projectionMatrix);
|
||||||
@@ -101,14 +91,12 @@ public:
|
|||||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||||
const float* ambientColor, const float* fogColor);
|
const float* ambientColor, const float* fogColor);
|
||||||
|
|
||||||
// ---- Query helpers (delegate to state) ----
|
// ---- Query helpers (read immutable snapshots) ----
|
||||||
bool isMoving() const { return state.isMoving(); }
|
bool isMoving() const;
|
||||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return state.getRequestedWalkTarget(); }
|
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||||
Eigen::Vector3f getCurrentNavigationTarget() const { return state.getCurrentNavigationTarget(); }
|
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||||
float getHp() const { return state.getHp(); }
|
Eigen::Vector3f renderPosition() const;
|
||||||
void setHp(float newHp) { state.setHp(newHp); }
|
float getHp() const;
|
||||||
|
|
||||||
float getSpeedMultiplier(float transitionProgress) const;
|
|
||||||
|
|
||||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||||
VertexRenderStruct weaponMesh;
|
VertexRenderStruct weaponMesh;
|
||||||
@@ -123,38 +111,62 @@ public:
|
|||||||
static Dialogue::TranslationDatabase nameTranslations;
|
static Dialogue::TranslationDatabase nameTranslations;
|
||||||
|
|
||||||
private:
|
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;
|
std::map<AnimationState, BoneAnimationDataNew> animations;
|
||||||
VertexRenderStruct modelMutable;
|
VertexRenderStruct modelMutable;
|
||||||
VertexRenderStruct healthBarMesh;
|
VertexRenderStruct healthBarMesh;
|
||||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
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;
|
PresentationSample previousPresentationSample_;
|
||||||
static constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
PresentationSample currentPresentationSample_;
|
||||||
|
std::chrono::steady_clock::time_point lastPresentationUpdate_;
|
||||||
|
bool hasPresentationSample_ = false;
|
||||||
|
|
||||||
AnimationState resolveActiveState() const;
|
AnimationState resolveActiveState(const CharacterState& state) const;
|
||||||
bool prepareGpuSkinning();
|
CharacterState interpolatedState(const CharacterState& state) const;
|
||||||
void drawAttachedWeapon(Renderer& renderer);
|
void updateAnimationForRender(const CharacterState& state);
|
||||||
|
bool prepareGpuSkinning(const CharacterState& state);
|
||||||
|
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
||||||
|
|
||||||
void drawGpuSkinning(Renderer& renderer);
|
void drawGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||||
void drawShadowDepthGpuSkinning(Renderer& renderer);
|
void drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||||
void drawShadowDepthCpu(Renderer& renderer);
|
void drawShadowDepthCpu(Renderer& renderer, const CharacterState& state);
|
||||||
void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
void drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, 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 drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||||
void drawNightGpuSkinning(Renderer& renderer,
|
void drawNightGpuSkinning(Renderer& renderer,
|
||||||
|
const CharacterState& state,
|
||||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||||
const float* ambientColor, const float* fogColor);
|
const float* ambientColor, const float* fogColor);
|
||||||
void drawNightCpu(Renderer& renderer,
|
void drawNightCpu(Renderer& renderer,
|
||||||
|
const CharacterState& state,
|
||||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||||
const float* ambientColor, const float* fogColor);
|
const float* ambientColor, const float* fogColor);
|
||||||
void drawNightGpuSkinningWithShadow(Renderer& renderer,
|
void drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||||
|
const CharacterState& state,
|
||||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||||
void drawNightCpuWithShadow(Renderer& renderer,
|
void drawNightCpuWithShadow(Renderer& renderer,
|
||||||
|
const CharacterState& state,
|
||||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||||
|
|||||||
+305
-6
@@ -1,7 +1,28 @@
|
|||||||
#include "CharacterState.h"
|
#include "CharacterState.h"
|
||||||
|
#include "LocationState.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
bool CharacterState::isMoving() const {
|
bool CharacterState::isMoving() const {
|
||||||
Eigen::Vector3f toTarget = walkTarget - position;
|
Eigen::Vector3f toTarget = walkTarget - position;
|
||||||
toTarget.y() = 0.f;
|
toTarget.y() = 0.f;
|
||||||
@@ -20,14 +41,290 @@ void CharacterState::stopInPlace() {
|
|||||||
requestedWalkTarget = walkTarget;
|
requestedWalkTarget = walkTarget;
|
||||||
pathWaypoints.clear();
|
pathWaypoints.clear();
|
||||||
currentWaypointIndex = 0;
|
currentWaypointIndex = 0;
|
||||||
onArrivedCallback = nullptr;
|
|
||||||
onArrivedCallbackName.clear();
|
onArrivedCallbackName.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterState::setTarget(
|
||||||
|
const Eigen::Vector3f& target,
|
||||||
|
std::string callbackName,
|
||||||
|
const LocationNetworkState& location,
|
||||||
|
const std::string& entityId)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
pathWaypoints = location.navigationMaps.empty()
|
||||||
|
? std::vector<Eigen::Vector3f>{}
|
||||||
|
: location.planPathExceptMyself(position, normalizedTarget, entityId);
|
||||||
|
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());
|
||||||
|
onArrivedCallbackName.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterState::forceReplan(
|
||||||
|
const LocationNetworkState& location, const std::string& entityId)
|
||||||
|
{
|
||||||
|
const Eigen::Vector3f normalizedTarget(
|
||||||
|
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||||
|
pathWaypoints = location.navigationMaps.empty()
|
||||||
|
? std::vector<Eigen::Vector3f>{}
|
||||||
|
: location.planPathExceptMyself(position, normalizedTarget, entityId);
|
||||||
|
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());
|
||||||
|
onArrivedCallbackName.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
void CharacterState::setHp(float newHp) {
|
||||||
hp = newHp;
|
hp = newHp;
|
||||||
if (hp < 0.f) hp = 0.f;
|
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;
|
||||||
|
if (!enabled) return events;
|
||||||
|
|
||||||
|
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;
|
||||||
|
position = moveAmount >= distance ? activeTarget : position + direction * moveAmount;
|
||||||
|
position.y() = 0.f;
|
||||||
|
targetFacingAngle = std::atan2(direction.x(), -direction.z());
|
||||||
|
currentState = AnimationState::WALK;
|
||||||
|
} else {
|
||||||
|
currentState = AnimationState::STAND;
|
||||||
|
const bool hasNextWaypoint =
|
||||||
|
!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
||||||
|
if (hasNextWaypoint) {
|
||||||
|
++currentWaypointIndex;
|
||||||
|
} else {
|
||||||
|
pathWaypoints.clear();
|
||||||
|
currentWaypointIndex = 0;
|
||||||
|
if (!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 (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
|
void CharacterState::save(nlohmann::json& out) const
|
||||||
@@ -41,8 +338,8 @@ void CharacterState::save(nlohmann::json& out) const
|
|||||||
out["currentState"] = static_cast<int>(currentState);
|
out["currentState"] = static_cast<int>(currentState);
|
||||||
out["attack_cooldown"] = attack_cooldown;
|
out["attack_cooldown"] = attack_cooldown;
|
||||||
out["showWeapon"] = showWeapon;
|
out["showWeapon"] = showWeapon;
|
||||||
out["attackTargetIndex"] = attackTargetIndex;
|
out["attackTargetId"] = attackTargetId;
|
||||||
out["faceTargetIndex"] = faceTargetIndex;
|
out["faceTargetId"] = faceTargetId;
|
||||||
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() };
|
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() };
|
||||||
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() };
|
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() };
|
||||||
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() };
|
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() };
|
||||||
@@ -53,6 +350,7 @@ void CharacterState::save(nlohmann::json& out) const
|
|||||||
out["pathWaypoints"] = std::move(wpArr);
|
out["pathWaypoints"] = std::move(wpArr);
|
||||||
out["currentWaypointIndex"] = currentWaypointIndex;
|
out["currentWaypointIndex"] = currentWaypointIndex;
|
||||||
out["homeDriftCheckTimer"] = homeDriftCheckTimer;
|
out["homeDriftCheckTimer"] = homeDriftCheckTimer;
|
||||||
|
out["animationFrame"] = animationFrame;
|
||||||
out["visibleRange"] = visibleRange;
|
out["visibleRange"] = visibleRange;
|
||||||
if (!onArrivedCallbackName.empty())
|
if (!onArrivedCallbackName.empty())
|
||||||
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
||||||
@@ -76,8 +374,8 @@ void CharacterState::load(const nlohmann::json& in)
|
|||||||
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
||||||
attack_cooldown = in.value("attack_cooldown", attack_cooldown);
|
attack_cooldown = in.value("attack_cooldown", attack_cooldown);
|
||||||
showWeapon = in.value("showWeapon", showWeapon);
|
showWeapon = in.value("showWeapon", showWeapon);
|
||||||
attackTargetIndex = in.value("attackTargetIndex", attackTargetIndex);
|
attackTargetId = in.value("attackTargetId", attackTargetId);
|
||||||
faceTargetIndex = in.value("faceTargetIndex", faceTargetIndex);
|
faceTargetId = in.value("faceTargetId", faceTargetId);
|
||||||
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition);
|
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition);
|
||||||
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget);
|
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget);
|
||||||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget);
|
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget);
|
||||||
@@ -89,6 +387,7 @@ void CharacterState::load(const nlohmann::json& in)
|
|||||||
}
|
}
|
||||||
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
||||||
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
|
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
|
||||||
|
animationFrame = in.value("animationFrame", animationFrame);
|
||||||
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-25
@@ -2,7 +2,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <functional>
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <Eigen/Geometry>
|
#include <Eigen/Geometry>
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
@@ -12,6 +14,10 @@ namespace FRG {
|
|||||||
|
|
||||||
constexpr float VISIBLE_RANGE = 6.f;
|
constexpr float VISIBLE_RANGE = 6.f;
|
||||||
|
|
||||||
|
struct CharacterUpdateContext;
|
||||||
|
struct CharacterUpdateEvents;
|
||||||
|
struct LocationNetworkState;
|
||||||
|
|
||||||
enum class AnimationState {
|
enum class AnimationState {
|
||||||
STAND = 0,
|
STAND = 0,
|
||||||
WALK = 1,
|
WALK = 1,
|
||||||
@@ -24,6 +30,8 @@ enum class AnimationState {
|
|||||||
DEATH_IDLE = 8,
|
DEATH_IDLE = 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Paths and fixed parameters needed to recreate a Character's visual assets
|
// Paths and fixed parameters needed to recreate a Character's visual assets
|
||||||
// from scratch (e.g. after loading a saved game).
|
// from scratch (e.g. after loading a saved game).
|
||||||
struct CharacterCreationInfo {
|
struct CharacterCreationInfo {
|
||||||
@@ -45,22 +53,17 @@ struct CharacterCreationInfo {
|
|||||||
float weaponMeshScale = 1.0f;
|
float weaponMeshScale = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// All serializable game state for a character.
|
// All serializable authoritative state for a character. GameState or
|
||||||
// This is a member of Character; rendering-only data lives in Character itself.
|
// LocationGameState owns it; rendering-only data lives in Character.
|
||||||
class CharacterState : public ISaveable {
|
class CharacterState : public ISaveable {
|
||||||
public:
|
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.
|
// Distance below which the character is considered to have reached its target.
|
||||||
static constexpr float kWalkThreshold = 0.05f;
|
static constexpr float kWalkThreshold = 0.05f;
|
||||||
|
|
||||||
// --- Identity ---
|
// --- Identity ---
|
||||||
std::string npcId;
|
|
||||||
std::string npcName;
|
std::string npcName;
|
||||||
bool isPlayer = false;
|
bool isPlayer = false;
|
||||||
bool enabled = true;
|
bool enabled = false;
|
||||||
bool canAttack = false;
|
bool canAttack = false;
|
||||||
|
|
||||||
// --- Position / orientation ---
|
// --- Position / orientation ---
|
||||||
@@ -84,18 +87,22 @@ public:
|
|||||||
// Weapon visibility toggle (changes during animation)
|
// Weapon visibility toggle (changes during animation)
|
||||||
bool showWeapon = true;
|
bool showWeapon = true;
|
||||||
|
|
||||||
// --- Index-based character references (replace raw pointers) ---
|
// --- Stable character references ---
|
||||||
int attackTargetIndex = kNoTarget;
|
// Empty means no target. "player" is the campaign player; NPC IDs are
|
||||||
int faceTargetIndex = kNoTarget;
|
// location-local and validated while loading location content.
|
||||||
|
std::string attackTargetId;
|
||||||
|
std::string faceTargetId;
|
||||||
|
|
||||||
// --- Home position for drift recovery ---
|
// --- Home position for drift recovery ---
|
||||||
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
||||||
float maxHomeDrift = 3.0f;
|
float maxHomeDrift = 3.0f;
|
||||||
|
|
||||||
// --- Slow-motion effect ---
|
// --- Authoritative logical animation clock ---
|
||||||
float slowMoTimeRemaining = 0.0f;
|
// Frame counts are immutable metadata copied from loaded clips during
|
||||||
float slowMoActiveTime = 6.0f;
|
// character construction. Simulation advances animationFrame itself.
|
||||||
float slowMoTransitionTime = 0.5f;
|
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 ---
|
// --- Movement / visual parameters ---
|
||||||
float walkSpeed = 3.0f;
|
float walkSpeed = 3.0f;
|
||||||
@@ -115,30 +122,58 @@ public:
|
|||||||
// Internal drift-check timer (serializable so it survives save/load).
|
// Internal drift-check timer (serializable so it survives save/load).
|
||||||
float homeDriftCheckTimer = 5.0f;
|
float homeDriftCheckTimer = 5.0f;
|
||||||
|
|
||||||
// --- Non-serializable callbacks (re-wired after each load) ---
|
// Global Lua function called when the character reaches the final waypoint.
|
||||||
std::function<void()> onArrivedCallback;
|
// The name is serialized, so no runtime callback needs to be rehydrated.
|
||||||
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.
|
|
||||||
std::string onArrivedCallbackName;
|
std::string onArrivedCallbackName;
|
||||||
|
|
||||||
// --- Asset / creation info (used to reload visuals from a saved state) ---
|
// --- Asset / creation info (used to reload visuals from a saved state) ---
|
||||||
CharacterCreationInfo creationInfo;
|
CharacterCreationInfo creationInfo;
|
||||||
|
|
||||||
// --- Pure-state methods ---
|
// --- Pure-state methods ---
|
||||||
|
void configureAsPlayer();
|
||||||
bool isMoving() const;
|
bool isMoving() const;
|
||||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||||
void stopInPlace();
|
void stopInPlace();
|
||||||
|
void setTarget(const Eigen::Vector3f& target,
|
||||||
|
std::string callbackName,
|
||||||
|
const LocationNetworkState& location,
|
||||||
|
const std::string& entityId);
|
||||||
|
void forceReplan(const LocationNetworkState& location,
|
||||||
|
const std::string& entityId);
|
||||||
|
void stopAndRotateToTarget(std::string characterId);
|
||||||
|
void resetPlayerAfterDeath();
|
||||||
|
void tryAttackCombo();
|
||||||
float getHp() const { return hp; }
|
float getHp() const { return hp; }
|
||||||
void setHp(float newHp);
|
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) ---
|
// --- Serialisation (runtime mutable fields only; creation info is reloaded from config) ---
|
||||||
void save(nlohmann::json& out) const override;
|
void save(nlohmann::json& out) const override;
|
||||||
void load(const nlohmann::json& in) override;
|
void load(const nlohmann::json& in) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using PlayerStateMap =
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<CharacterState>>;
|
||||||
|
|
||||||
|
struct CharacterObservation {
|
||||||
|
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||||
|
float hp = 0.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CharacterUpdateContext {
|
||||||
|
const CharacterObservation* attackTarget = nullptr;
|
||||||
|
const CharacterObservation* faceTarget = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CharacterUpdateEvents {
|
||||||
|
bool attackImpact = false;
|
||||||
|
bool deathAnimationComplete = false;
|
||||||
|
bool navigationRequested = false;
|
||||||
|
Eigen::Vector3f navigationTarget = Eigen::Vector3f::Zero();
|
||||||
|
std::string arrivedCallbackName;
|
||||||
|
std::string attackTargetId;
|
||||||
|
Eigen::Vector3f attackDirection = Eigen::Vector3f::Zero();
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace FRG
|
} // namespace FRG
|
||||||
+332
-210
@@ -1,4 +1,5 @@
|
|||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
|
#include "game_server/NetworkProtocol.h"
|
||||||
#include "AnimatedModel.h"
|
#include "AnimatedModel.h"
|
||||||
#include "Localization.h"
|
#include "Localization.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <type_traits>
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -76,16 +78,19 @@ namespace FRG
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
Location* Game::currentLocation() const {
|
Location* Game::currentLocation() const {
|
||||||
if (gameState.currentLocationName.empty()) return nullptr;
|
const auto state = stateSnapshot();
|
||||||
auto it = gameState.locations.find(gameState.currentLocationName);
|
if (!state || state->currentLocationId.empty()) return nullptr;
|
||||||
return (it != gameState.locations.end()) ? it->second.get() : nullptr;
|
return gameServerHost.runtimeLocation(state->currentLocationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Game::Game()
|
Game::Game()
|
||||||
: newTickCount(0)
|
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||||
|
, internalGameServer()
|
||||||
|
, localGameServerHost(internalGameServer)
|
||||||
|
, gameServer(internalGameServer)
|
||||||
|
, gameServerHost(localGameServerHost)
|
||||||
|
, menuManager(renderer, gameServer, *audioPlayer)
|
||||||
, lastTickCount(0)
|
, lastTickCount(0)
|
||||||
, audioPlayer(std::make_unique<AudioPlayerAsync>())
|
|
||||||
, menuManager(renderer, gameState, *audioPlayer)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,19 +362,6 @@ namespace FRG
|
|||||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||||
|
|
||||||
loadSteps.push([this]() {
|
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.
|
// Wire phone dialogue start function so MenuManager can trigger dialogues.
|
||||||
menuManager.startDialogueFunc = [this](const std::string& id) {
|
menuManager.startDialogueFunc = [this](const std::string& id) {
|
||||||
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
|
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
|
||||||
@@ -391,15 +383,11 @@ namespace FRG
|
|||||||
menuManager.skipCutsceneFunc = [this]() {
|
menuManager.skipCutsceneFunc = [this]() {
|
||||||
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
||||||
};
|
};
|
||||||
|
|
||||||
menuManager.callTaxiFunc = [this]() {
|
|
||||||
if (currentLocation()) currentLocation()->onTaxiCalled();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSteps.push([this]() {
|
loadSteps.push([this]() {
|
||||||
try {
|
try {
|
||||||
menuManager.setup(gameState.inventory, CONST_ZIP_FILE);
|
menuManager.setup(CONST_ZIP_FILE);
|
||||||
logger() << "UI loaded successfully" << std::endl;
|
logger() << "UI loaded successfully" << std::endl;
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
@@ -414,7 +402,8 @@ namespace FRG
|
|||||||
performResetToInitialState();
|
performResetToInitialState();
|
||||||
applyTutorialSetting();
|
applyTutorialSetting();
|
||||||
const CampaignConfig& campaign = activeCampaign();
|
const CampaignConfig& campaign = activeCampaign();
|
||||||
gameState.currentLocationName = campaign.startLocation;
|
server().submit(
|
||||||
|
SetCurrentLocationCommand{campaign.startLocation});
|
||||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||||
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||||
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
||||||
@@ -505,7 +494,10 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CampaignConfig& Game::activeCampaign() const {
|
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) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
static const CampaignConfig empty;
|
static const CampaignConfig empty;
|
||||||
@@ -515,7 +507,8 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
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,
|
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||||
@@ -555,48 +548,83 @@ namespace FRG
|
|||||||
|
|
||||||
void Game::loadCampaign(const std::string& campaignId) {
|
void Game::loadCampaign(const std::string& campaignId) {
|
||||||
loadedCampaignId = campaignId;
|
loadedCampaignId = campaignId;
|
||||||
gameState.campaignId = campaignId;
|
const CampaignConfig* campaign =
|
||||||
gameState.storedLocationStates.clear();
|
CampaignRegistry::instance().find(campaignId);
|
||||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
if (!campaign) return;
|
||||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
#if defined(NETWORK)
|
||||||
|
remoteCampaignActive = campaignId == "battle_zone_multiplayer";
|
||||||
|
remoteInitialStateBound = false;
|
||||||
|
if (remoteCampaignActive) {
|
||||||
|
logger() << "[network] Campaign '" << campaignId
|
||||||
|
<< "' selected; creating WebSocket client" << std::endl;
|
||||||
|
websocketGameClient = std::make_unique<WebsocketGameClient>();
|
||||||
|
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.
|
// 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)
|
if (player)
|
||||||
player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::unloadCampaign() {
|
void Game::unloadCampaign() {
|
||||||
unloadLocation(false);
|
unloadLocation(false);
|
||||||
player.reset();
|
player.reset();
|
||||||
loadedCampaignId.clear();
|
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
|
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::loadLocation(const std::string& locationId) {
|
void Game::loadLocation(const std::string& locationId) {
|
||||||
const LocationConfig* config = activeCampaign().findLocation(locationId);
|
const LocationConfig* config = activeCampaign().findLocation(locationId);
|
||||||
if (!config) {
|
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;
|
<< locationId << "'" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
SDL_PumpEvents();
|
||||||
|
const bool initializeLocation = gameServerHost.prepareLocation(*config);
|
||||||
std::shared_ptr<Location> loc = createLocation(*config);
|
std::shared_ptr<Location> loc = createLocation(*config);
|
||||||
wireLocation(loc.get());
|
wireLocation(loc.get());
|
||||||
if (shadowMap) loc->shadowMap = shadowMap;
|
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).
|
// Back to how the player left it (or to the start state's snapshot of it).
|
||||||
auto stored = gameState.storedLocationStates.find(locationId);
|
gameServerHost.bindRuntimeLocation(locationId, loc);
|
||||||
if (stored != gameState.storedLocationStates.end()) loc->load(stored->second);
|
|
||||||
gameState.locations[locationId] = loc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::unloadLocation(bool storeState) {
|
void Game::unloadLocation(bool storeState) {
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
gameServerHost.clearRuntimeLocations(storeState);
|
||||||
if (!storeState) continue;
|
|
||||||
nlohmann::json locJson;
|
|
||||||
loc->save(locJson);
|
|
||||||
gameState.storedLocationStates[name] = std::move(locJson);
|
|
||||||
}
|
|
||||||
gameState.locations.clear();
|
|
||||||
// Textures only the location's models and characters used are freed now.
|
// Textures only the location's models and characters used are freed now.
|
||||||
renderer.textureManager.UnloadUnused();
|
renderer.textureManager.UnloadUnused();
|
||||||
}
|
}
|
||||||
@@ -614,61 +642,38 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
// Runs from inside the current location's update(); the location itself is
|
// Runs from inside the current location's update(); the location itself is
|
||||||
// released by a later loading step.
|
// released by a later loading step.
|
||||||
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() {
|
const auto state = stateSnapshot();
|
||||||
gameState.currentLocationName = destName;
|
if (!state) return;
|
||||||
|
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
|
||||||
|
server().submit(SetCurrentLocationCommand{destName});
|
||||||
Location* loc = currentLocation();
|
Location* loc = currentLocation();
|
||||||
if (!loc) return;
|
if (!loc) return;
|
||||||
if (loc->player) {
|
if (loc->player) {
|
||||||
loc->player->state.position = destPos;
|
server().submit(SetCharacterPositionCommand{
|
||||||
loc->player->setTarget(destPos);
|
destName, loc->player->entityId(), destPos});
|
||||||
loc->player->stopInPlace();
|
server().submit(SetCharacterFacingAngleCommand{
|
||||||
loc->player->state.facingAngle = destRotY;
|
destName, loc->player->entityId(), destRotY, true});
|
||||||
loc->player->state.targetFacingAngle = destRotY;
|
loc->stopCharacter(*loc->player);
|
||||||
}
|
}
|
||||||
loc->state.cameraAzimuth = destRotY;
|
loc->localState().cameraAzimuth = destRotY;
|
||||||
loc->state.isDarklands = gameState.isDarklands;
|
|
||||||
loc->state.isNight = gameState.isNight;
|
|
||||||
loc->state.isDawn = gameState.isDawn;
|
|
||||||
loc->scriptEngine.callLocationEnterCallback();
|
loc->scriptEngine.callLocationEnterCallback();
|
||||||
|
|
||||||
//Reset taxi
|
//Reset taxi
|
||||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
server().submit(ResetTaxiCommand{destName});
|
||||||
loc->state.taxiArrivedCallbackCalled = false;
|
server().submit(SetTaxiCalledCommand{false});
|
||||||
gameState.taxiIsCalled = false;
|
|
||||||
|
|
||||||
menuManager.onLocationChanged(destName);
|
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
|
// 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.
|
// any more, so they are built fresh when the player first goes there.
|
||||||
gameState.storedLocationStates.clear();
|
std::vector<std::string> loadedIds =
|
||||||
std::vector<std::string> loadedIds;
|
gameServerHost.loadedRuntimeLocationIds();
|
||||||
for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name);
|
gameServerHost.resetCampaign(
|
||||||
|
activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||||
unloadLocation(false);
|
unloadLocation(false);
|
||||||
for (const std::string& id : loadedIds) loadLocation(id);
|
for (const std::string& id : loadedIds) loadLocation(id);
|
||||||
|
|
||||||
@@ -676,6 +681,7 @@ namespace FRG
|
|||||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||||
// Re-comment before shipping.
|
// Re-comment before shipping.
|
||||||
//saveInitialState();
|
//saveInitialState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::performResetToInitialState() {
|
void Game::performResetToInitialState() {
|
||||||
@@ -698,15 +704,8 @@ namespace FRG
|
|||||||
if (!content.empty()) {
|
if (!content.empty()) {
|
||||||
try {
|
try {
|
||||||
nlohmann::json root = nlohmann::json::parse(content);
|
nlohmann::json root = nlohmann::json::parse(content);
|
||||||
const std::string campaignId = gameState.campaignId;
|
gameServerHost.load(root); // applies to the loaded location in place and stores
|
||||||
// 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
|
|
||||||
// the other locations' states for when they load.
|
// the other locations' states for when they load.
|
||||||
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
|
||||||
return;
|
return;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||||
@@ -724,12 +723,13 @@ namespace FRG
|
|||||||
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
|
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
|
||||||
void Game::applyTutorialSetting() {
|
void Game::applyTutorialSetting() {
|
||||||
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
||||||
gameState.tutorialStep = TutorialStep::Completed;
|
server().submit(
|
||||||
|
SetTutorialStepCommand{TutorialStep::Completed});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::saveInitialState() {
|
void Game::saveInitialState() {
|
||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
gameState.save(root);
|
gameServerHost.save(root);
|
||||||
// Written next to the executable; copy it over the campaign's start-state file.
|
// Written next to the executable; copy it over the campaign's start-state file.
|
||||||
const std::string startStatePath = activeCampaign().startStatePath;
|
const std::string startStatePath = activeCampaign().startStatePath;
|
||||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||||
@@ -752,7 +752,7 @@ namespace FRG
|
|||||||
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
||||||
params.npcsJsonPath = config.npcsJsonPath;
|
params.npcsJsonPath = config.npcsJsonPath;
|
||||||
params.dialoguesJsonPath = config.dialoguesJsonPath;
|
params.dialoguesJsonPath = config.dialoguesJsonPath;
|
||||||
params.navigationJsonPaths = config.navigationJsonPaths;
|
//params.navigationJsonPaths = config.navigationJsonPaths;
|
||||||
params.scriptPath = config.scriptPath;
|
params.scriptPath = config.scriptPath;
|
||||||
params.teleportsJsonPath = config.teleportsJsonPath;
|
params.teleportsJsonPath = config.teleportsJsonPath;
|
||||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||||
@@ -760,27 +760,30 @@ namespace FRG
|
|||||||
params.playerPosition = config.playerPosition;
|
params.playerPosition = config.playerPosition;
|
||||||
params.player = player;
|
params.player = player;
|
||||||
|
|
||||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
auto loc = std::make_shared<Location>(
|
||||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
renderer, server(), config.id);
|
||||||
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
|
gameServerHost.setupRuntimeLocation(
|
||||||
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
config.id, loc, std::move(params), audioPlayer.get());
|
||||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||||
gameState.isNight = isNight;
|
const auto state = stateSnapshot();
|
||||||
gameState.isDawn = isDawn;
|
if (!state) return;
|
||||||
|
server().submit(SetWorldModeCommand{
|
||||||
|
state->isDarklands, isNight, isDawn});
|
||||||
};
|
};
|
||||||
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||||
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||||
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||||
loc->requestReturnToMainMenu = [this]() {
|
loc->requestReturnToMainMenu = [this]() {
|
||||||
gameState.currentLocationName.clear();
|
server().submit(SetCurrentLocationCommand{std::string()});
|
||||||
menuManager.showMenuAfterGameCompleted();
|
menuManager.showMenuAfterGameCompleted();
|
||||||
};
|
};
|
||||||
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||||
menuManager.setChatUnread(idx, unread, msg);
|
menuManager.setChatUnread(idx, unread, msg);
|
||||||
};
|
};
|
||||||
for (auto& npc : loc->npcs) {
|
for (auto& npc : loc->npcs) {
|
||||||
if (npc && npc->state.canAttack) {
|
auto state = npc ? npc->stateSnapshot() : nullptr;
|
||||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
if (state && state->canAttack) {
|
||||||
|
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loc->requestDarklandsPlayBattleMusic = [this]() {
|
loc->requestDarklandsPlayBattleMusic = [this]() {
|
||||||
@@ -792,26 +795,6 @@ namespace FRG
|
|||||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
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;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,20 +809,16 @@ namespace FRG
|
|||||||
teleportPlayer(destName, destPos, destRotY);
|
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.
|
// Wire tutorial advance callbacks.
|
||||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||||
{
|
{
|
||||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
server().submit(
|
||||||
|
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
|
||||||
});
|
});
|
||||||
loc->onPlayerFloorWalk = [this]() {
|
loc->onPlayerFloorWalk = [this]() {
|
||||||
gameState.completeTutorialStep(TutorialStep::Walk);
|
server().submit(
|
||||||
|
CompleteTutorialStepCommand{TutorialStep::Walk});
|
||||||
menuManager.onPlayerStartedWalking();
|
menuManager.onPlayerStartedWalking();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -866,24 +845,34 @@ namespace FRG
|
|||||||
if (loc->player) {
|
if (loc->player) {
|
||||||
// Raw pointer: the player belongs to the location (and outlives it), so a
|
// 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.
|
// 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);
|
menuManager.updateHealthBar(hp, maxHp);
|
||||||
|
|
||||||
if (attackerIndex != CharacterState::kNoTarget) {
|
if (!attackerId.empty()) {
|
||||||
//Someone attacked, cancel dialogue and agro the player
|
//Someone attacked, cancel dialogue and agro the player
|
||||||
|
|
||||||
if (loc->dialogueSystem.isDialogueActive()) {
|
if (loc->dialogueSystem.isDialogueActive()) {
|
||||||
loc->dialogueSystem.stopDialogue();
|
loc->dialogueSystem.stopDialogue();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((loc->player->state.attackTargetIndex == CharacterState::kNoTarget ||
|
bool currentTargetDead = false;
|
||||||
((loc->player->state.attackTargetIndex >= 0) &&
|
auto playerState = loc->player->stateSnapshot();
|
||||||
(loc->npcs[loc->player->state.attackTargetIndex]->state.hp <= 0)))
|
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->isMoving() == false
|
||||||
&&
|
&&
|
||||||
loc->player->getHp() > 0) {
|
loc->player->getHp() > 0) {
|
||||||
loc->player->state.attackTargetIndex = attackerIndex;
|
server().submit(SetCharacterTargetCommand{
|
||||||
|
loc->locationId(),
|
||||||
|
server().localPlayerId(), attackerId});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -905,7 +894,9 @@ namespace FRG
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
logger() << "Shadow map initialized" << std::endl;
|
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()
|
void Game::destroyShadowMapAndProparateToLocations()
|
||||||
@@ -915,7 +906,9 @@ namespace FRG
|
|||||||
shadowMap.reset();
|
shadowMap.reset();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
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__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -988,16 +981,12 @@ namespace FRG
|
|||||||
Location* loc = currentLocation();
|
Location* loc = currentLocation();
|
||||||
if (loc)
|
if (loc)
|
||||||
{
|
{
|
||||||
// Sync global flags so Location's draw functions see them.
|
const auto state = stateSnapshot();
|
||||||
loc->state.isDarklands = gameState.isDarklands;
|
if (state && state->isDarklands) {
|
||||||
loc->state.isNight = gameState.isNight;
|
|
||||||
loc->state.isDawn = gameState.isDawn;
|
|
||||||
|
|
||||||
if (gameState.isDarklands) {
|
|
||||||
loc->drawGameDarklands();
|
loc->drawGameDarklands();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
}
|
}
|
||||||
else if (gameState.isNight) {
|
else if (state && state->isNight) {
|
||||||
loc->drawGameNight();
|
loc->drawGameNight();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
}
|
}
|
||||||
@@ -1142,7 +1131,7 @@ namespace FRG
|
|||||||
|
|
||||||
int64_t Game::getSyncTimeMs() {
|
int64_t Game::getSyncTimeMs() {
|
||||||
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
|
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;
|
return localNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1151,21 +1140,15 @@ namespace FRG
|
|||||||
|
|
||||||
if (lastTickCount == 0) {
|
if (lastTickCount == 0) {
|
||||||
lastTickCount = getSyncTimeMs();
|
lastTickCount = getSyncTimeMs();
|
||||||
|
|
||||||
lastTickCount = (lastTickCount / 50) * 50;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newTickCount = getSyncTimeMs();
|
const int64_t now = getSyncTimeMs();
|
||||||
|
const int64_t delta =
|
||||||
newTickCount = (newTickCount / 50) * 50;
|
(std::min)(now - lastTickCount,
|
||||||
|
static_cast<int64_t>(CONST_MAX_TIME_INTERVAL));
|
||||||
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
|
lastTickCount = now;
|
||||||
|
if (delta > 0) {
|
||||||
int64_t delta = newTickCount - lastTickCount;
|
|
||||||
|
|
||||||
lastTickCount = newTickCount;
|
|
||||||
|
|
||||||
updateDarklandsFlash(delta);
|
updateDarklandsFlash(delta);
|
||||||
menuManager.uiManager.update(static_cast<float>(delta));
|
menuManager.uiManager.update(static_cast<float>(delta));
|
||||||
@@ -1175,14 +1158,115 @@ namespace FRG
|
|||||||
if (!menuManager.isMainMenuOpen())
|
if (!menuManager.isMainMenuOpen())
|
||||||
menuManager.update(static_cast<float>(delta));
|
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() {
|
void Game::render() {
|
||||||
FRG::CheckGlError(__FILE__, __LINE__);
|
FRG::CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
@@ -1228,6 +1312,7 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Game::update() {
|
void Game::update() {
|
||||||
|
processServerEvents();
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
|
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
@@ -1332,8 +1417,9 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
else if (event.button.button == SDL_BUTTON_RIGHT
|
else if (event.button.button == SDL_BUTTON_RIGHT
|
||||||
&& event.type == SDL_MOUSEBUTTONUP
|
&& event.type == SDL_MOUSEBUTTONUP
|
||||||
&& editorMode == EditorMode::Navigation
|
&& currentLocation()
|
||||||
&& currentLocation()) {
|
&& currentLocation()->editorMode == EditorMode::Navigation
|
||||||
|
) {
|
||||||
currentLocation()->editor.handleRightClick();
|
currentLocation()->editor.handleRightClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1361,7 +1447,8 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
//logger() << "Current zoom: " << Environment::zoom << std::endl;
|
//logger() << "Current zoom: " << Environment::zoom << std::endl;
|
||||||
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
|
// 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) {
|
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
|
||||||
@@ -1378,15 +1465,29 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
if (event.key.keysym.sym == SDLK_1)
|
if (event.key.keysym.sym == SDLK_1)
|
||||||
{
|
{
|
||||||
startNightTransition();
|
currentLocation()->switchNavigation(0);
|
||||||
|
//startNightTransition();
|
||||||
}
|
}
|
||||||
if (event.key.keysym.sym == SDLK_2)
|
if (event.key.keysym.sym == SDLK_2)
|
||||||
{
|
{
|
||||||
startDarklandsTransition();
|
currentLocation()->switchNavigation(1);
|
||||||
|
//startDarklandsTransition();
|
||||||
}
|
}
|
||||||
if (event.key.keysym.sym == SDLK_3)
|
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 +1535,6 @@ namespace FRG
|
|||||||
return n;
|
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)
|
void Game::enterCameraDragMode(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
cameraDragging = true;
|
cameraDragging = true;
|
||||||
@@ -1460,8 +1547,9 @@ namespace FRG
|
|||||||
currentLocation()->lastMouseY = eventY;
|
currentLocation()->lastMouseY = eventY;
|
||||||
|
|
||||||
// Snapshot current angles so we can measure how far the user rotates.
|
// Snapshot current angles so we can measure how far the user rotates.
|
||||||
dragStartAzimuth = currentLocation()->state.cameraAzimuth;
|
const LocationLocalState& state = currentLocation()->localState();
|
||||||
dragStartInclination = currentLocation()->state.cameraInclination;
|
dragStartAzimuth = state.cameraAzimuth;
|
||||||
|
dragStartInclination = state.cameraInclination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1521,7 +1609,8 @@ namespace FRG
|
|||||||
|
|
||||||
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
|
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
|
||||||
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
|
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
|
||||||
gameState.completeTutorialStep(TutorialStep::Zoom);
|
server().submit(
|
||||||
|
CompleteTutorialStepCommand{TutorialStep::Zoom});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1664,12 +1753,16 @@ namespace FRG
|
|||||||
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
|
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
|
||||||
// not just nudged a single axis by accident.
|
// not just nudged a single axis by accident.
|
||||||
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
||||||
|
const auto game = stateSnapshot();
|
||||||
if (cameraDragging
|
if (cameraDragging
|
||||||
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
|
&& game
|
||||||
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
|
&& game->tutorialStep == TutorialStep::RotateCamera) {
|
||||||
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
|
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) {
|
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
|
||||||
gameState.completeTutorialStep(TutorialStep::RotateCamera);
|
server().submit(
|
||||||
|
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1677,13 +1770,14 @@ namespace FRG
|
|||||||
|
|
||||||
bool Game::startDarklandsTransition()
|
bool Game::startDarklandsTransition()
|
||||||
{
|
{
|
||||||
if (!gameState.isNight)
|
const auto state = stateSnapshot();
|
||||||
|
if (!state || !state->isNight)
|
||||||
{
|
{
|
||||||
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
|
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameState.isDawn)
|
if (state->isDawn)
|
||||||
{
|
{
|
||||||
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
|
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
|
||||||
return false;
|
return false;
|
||||||
@@ -1717,11 +1811,13 @@ namespace FRG
|
|||||||
if (darklandsFlashFadingIn) {
|
if (darklandsFlashFadingIn) {
|
||||||
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
|
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
|
||||||
if (darklandsFlashAlpha >= 1.0f) {
|
if (darklandsFlashAlpha >= 1.0f) {
|
||||||
|
const auto before = stateSnapshot();
|
||||||
|
if (!before) return;
|
||||||
if (isNightTransition) {
|
if (isNightTransition) {
|
||||||
gameState.isNight = !gameState.isNight;
|
server().submit(SetWorldModeCommand{
|
||||||
|
before->isDarklands, !before->isNight, before->isDawn});
|
||||||
if (currentLocation())
|
if (currentLocation())
|
||||||
{
|
{
|
||||||
currentLocation()->state.isNight = gameState.isNight;
|
|
||||||
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
|
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1729,12 +1825,15 @@ namespace FRG
|
|||||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
gameState.isDarklands = !gameState.isDarklands;
|
server().submit(SetWorldModeCommand{
|
||||||
|
!before->isDarklands, before->isNight, before->isDawn});
|
||||||
|
const auto after = stateSnapshot();
|
||||||
|
if (!after) return;
|
||||||
|
|
||||||
// Change HUD
|
// Change HUD
|
||||||
menuManager.setDarklandsMode(gameState.isDarklands);
|
menuManager.setDarklandsMode(after->isDarklands);
|
||||||
|
|
||||||
if (gameState.isDarklands)
|
if (after->isDarklands)
|
||||||
{
|
{
|
||||||
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||||
@@ -1746,8 +1845,7 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentLocation()) {
|
if (currentLocation()) {
|
||||||
currentLocation()->state.isDarklands = gameState.isDarklands;
|
if (after->isDarklands)
|
||||||
if (gameState.isDarklands)
|
|
||||||
currentLocation()->scriptEngine.callDarklandsEnterCallback();
|
currentLocation()->scriptEngine.callDarklandsEnterCallback();
|
||||||
else
|
else
|
||||||
currentLocation()->scriptEngine.callDarklandsExitCallback();
|
currentLocation()->scriptEngine.callDarklandsExitCallback();
|
||||||
@@ -1816,11 +1914,18 @@ namespace FRG
|
|||||||
if (content.empty()) return {};
|
if (content.empty()) return {};
|
||||||
try {
|
try {
|
||||||
nlohmann::json root = nlohmann::json::parse(content);
|
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;
|
SaveSlotInfo info;
|
||||||
info.empty = false;
|
info.empty = false;
|
||||||
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
info.campaignId =
|
||||||
info.locationName = root.value("currentLocationName", "");
|
networkState.at("campaignId").get<std::string>();
|
||||||
info.savedAt = root.value("savedAt", "");
|
info.locationName =
|
||||||
|
networkState.at("currentLocationId").get<std::string>();
|
||||||
|
info.savedAt = networkState.value("savedAt", "");
|
||||||
return info;
|
return info;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return {};
|
return {};
|
||||||
@@ -1830,7 +1935,7 @@ namespace FRG
|
|||||||
void Game::saveGame(int slot)
|
void Game::saveGame(int slot)
|
||||||
{
|
{
|
||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
gameState.save(root);
|
gameServerHost.save(root);
|
||||||
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
||||||
|
|
||||||
/*std::ofstream file(path);
|
/*std::ofstream file(path);
|
||||||
@@ -1858,11 +1963,26 @@ namespace FRG
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The save's campaign and location may not be loaded yet; the state is applied once they are.
|
if (root.value("persistenceVersion", -1) !=
|
||||||
const std::string campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
kSinglePlayerPersistenceVersion ||
|
||||||
const std::string locationId = root.value("currentLocationName", std::string());
|
!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]() {
|
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||||
gameState.load(root);
|
gameServerHost.load(root);
|
||||||
playMusicForLoadedGame();
|
playMusicForLoadedGame();
|
||||||
menuManager.enterGameplay(true);
|
menuManager.enterGameplay(true);
|
||||||
});
|
});
|
||||||
@@ -1870,19 +1990,21 @@ namespace FRG
|
|||||||
|
|
||||||
void Game::playMusicForLoadedGame()
|
void Game::playMusicForLoadedGame()
|
||||||
{
|
{
|
||||||
if (gameState.isDarklands)
|
const auto state = stateSnapshot();
|
||||||
|
if (!state) return;
|
||||||
|
if (state->isDarklands)
|
||||||
{
|
{
|
||||||
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||||
menuManager.lastMusicTrack = "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");
|
audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg");
|
||||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (gameState.currentLocationName == "location_dorm") {
|
if (state->currentLocationId == "location_dorm") {
|
||||||
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
||||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-13
@@ -21,7 +21,14 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "Location.h"
|
#include "Location.h"
|
||||||
#include "AudioPlayerAsync.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 {
|
namespace FRG {
|
||||||
|
|
||||||
struct LocationConfig;
|
struct LocationConfig;
|
||||||
@@ -73,9 +80,7 @@ namespace FRG {
|
|||||||
// content reload, don't have a meaningful denominator, so the bar is hidden.
|
// content reload, don't have a meaningful denominator, so the bar is hidden.
|
||||||
bool showLoadingProgressBar = true;
|
bool showLoadingProgressBar = true;
|
||||||
|
|
||||||
GameState gameState;
|
//EditorMode editorMode = EditorMode::None;
|
||||||
|
|
||||||
EditorMode editorMode = EditorMode::None;
|
|
||||||
|
|
||||||
// Returns false if a transition is already in progress.
|
// Returns false if a transition is already in progress.
|
||||||
bool startDarklandsTransition();
|
bool startDarklandsTransition();
|
||||||
@@ -83,9 +88,23 @@ namespace FRG {
|
|||||||
|
|
||||||
std::unique_ptr<AudioPlayerAsync> audioPlayer;
|
std::unique_ptr<AudioPlayerAsync> audioPlayer;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public:
|
||||||
|
IGameServer& server() { return gameServer; }
|
||||||
|
const IGameServer& server() const { return gameServer; }
|
||||||
|
|
||||||
MenuManager menuManager;
|
MenuManager menuManager;
|
||||||
|
|
||||||
void activateSlowMoEffect();
|
|
||||||
|
|
||||||
#ifdef EMSCRIPTEN
|
#ifdef EMSCRIPTEN
|
||||||
static Game* s_instance;
|
static Game* s_instance;
|
||||||
@@ -123,15 +142,16 @@ namespace FRG {
|
|||||||
|
|
||||||
|
|
||||||
Location* currentLocation() const;
|
Location* currentLocation() const;
|
||||||
|
std::shared_ptr<const GameSnapshot> stateSnapshot() const {
|
||||||
|
return gameServer.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
void saveGame(int slot);
|
void saveGame(int slot);
|
||||||
void loadGame(int slot);
|
void loadGame(int slot);
|
||||||
void playMusicForLoadedGame();
|
void playMusicForLoadedGame();
|
||||||
SaveSlotInfo readSlotInfo(int slot) const;
|
SaveSlotInfo readSlotInfo(int slot) const;
|
||||||
// Loading model: one campaign at a time, and within it only the location the player
|
// Loading model: one campaign at a time. InternalGameServer privately owns the
|
||||||
// is in (gameState.locations). Leaving a location stores its state in
|
// one live Location while GameState retains pure value state for every location.
|
||||||
// gameState.storedLocationStates and releases it; the player is created once per
|
|
||||||
// campaign and shared by its locations.
|
|
||||||
std::string loadedCampaignId;
|
std::string loadedCampaignId;
|
||||||
std::shared_ptr<Character> player;
|
std::shared_ptr<Character> player;
|
||||||
|
|
||||||
@@ -146,7 +166,7 @@ namespace FRG {
|
|||||||
void unloadCampaign();
|
void unloadCampaign();
|
||||||
// Builds the campaign's location and restores its stored state, if any.
|
// Builds the campaign's location and restores its stored state, if any.
|
||||||
void loadLocation(const std::string& locationId);
|
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);
|
void unloadLocation(bool storeState);
|
||||||
// Teleport zones: moves the player to another location of the campaign.
|
// Teleport zones: moves the player to another location of the campaign.
|
||||||
void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY);
|
void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY);
|
||||||
@@ -174,6 +194,7 @@ namespace FRG {
|
|||||||
|
|
||||||
int64_t getSyncTimeMs();
|
int64_t getSyncTimeMs();
|
||||||
void processTickCount();
|
void processTickCount();
|
||||||
|
void processServerEvents();
|
||||||
void drawScene();
|
void drawScene();
|
||||||
void drawUI();
|
void drawUI();
|
||||||
|
|
||||||
@@ -230,13 +251,12 @@ namespace FRG {
|
|||||||
*/
|
*/
|
||||||
//void drawMobilePortraitOverlay();
|
//void drawMobilePortraitOverlay();
|
||||||
|
|
||||||
int64_t newTickCount;
|
|
||||||
int64_t lastTickCount;
|
int64_t lastTickCount;
|
||||||
|
int64_t simulationAccumulatorMs = 0;
|
||||||
|
|
||||||
int lastDrawableW = -1;
|
int lastDrawableW = -1;
|
||||||
int lastDrawableH = -1;
|
int lastDrawableH = -1;
|
||||||
|
|
||||||
static const size_t CONST_TIMER_INTERVAL = 10;
|
|
||||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+74
-44
@@ -1,24 +1,12 @@
|
|||||||
#include "GameState.h"
|
#include "GameState.h"
|
||||||
|
#include "utils/Utils.h"
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
namespace FRG {
|
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)
|
bool GameState::completeTutorialStep(TutorialStep step)
|
||||||
{
|
{
|
||||||
if (tutorialStep != step || step == TutorialStep::Completed) return false;
|
if (tutorialStep != step || step == TutorialStep::Completed) return false;
|
||||||
@@ -26,8 +14,29 @@ bool GameState::completeTutorialStep(TutorialStep step)
|
|||||||
return true;
|
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
|
void GameState::save(nlohmann::json& out) const
|
||||||
{
|
{
|
||||||
|
out["schemaVersion"] = kSchemaVersion;
|
||||||
// Timestamp
|
// Timestamp
|
||||||
std::time_t now = std::time(nullptr);
|
std::time_t now = std::time(nullptr);
|
||||||
std::tm* tm = std::localtime(&now);
|
std::tm* tm = std::localtime(&now);
|
||||||
@@ -37,7 +46,7 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
|
|
||||||
// --- World ---
|
// --- World ---
|
||||||
out["campaignId"] = campaignId;
|
out["campaignId"] = campaignId;
|
||||||
out["currentLocationName"] = currentLocationName;
|
out["currentLocationId"] = currentLocationName;
|
||||||
out["isDarklands"] = isDarklands;
|
out["isDarklands"] = isDarklands;
|
||||||
out["isNight"] = isNight;
|
out["isNight"] = isNight;
|
||||||
out["isDawn"] = isDawn;
|
out["isDawn"] = isDawn;
|
||||||
@@ -87,16 +96,21 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
questJournal.save(questJson);
|
questJournal.save(questJson);
|
||||||
out["questJournal"] = std::move(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 ---
|
// --- Locations ---
|
||||||
// Unloaded locations keep their last stored state; loaded ones are saved live.
|
|
||||||
nlohmann::json locsJson = nlohmann::json::object();
|
nlohmann::json locsJson = nlohmann::json::object();
|
||||||
for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored;
|
for (const auto& [name, location] : locations) {
|
||||||
for (const auto& [name, loc] : locations) {
|
nlohmann::json locJson;
|
||||||
if (loc) {
|
location.save(locJson);
|
||||||
nlohmann::json locJson;
|
locsJson[name] = std::move(locJson);
|
||||||
loc->save(locJson);
|
|
||||||
locsJson[name] = std::move(locJson);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out["locations"] = std::move(locsJson);
|
out["locations"] = std::move(locsJson);
|
||||||
|
|
||||||
@@ -105,12 +119,16 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
|
|
||||||
void GameState::load(const nlohmann::json& in)
|
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;
|
//logger() << "GameState::load step 1" << std::endl;
|
||||||
|
|
||||||
// --- World ---
|
// --- World ---
|
||||||
// Literal default: saves made before campaigns existed belong to the main game.
|
campaignId = in.at("campaignId").get<std::string>();
|
||||||
campaignId = in.value("campaignId", std::string("main"));
|
currentLocationName = in.at("currentLocationId").get<std::string>();
|
||||||
currentLocationName = in.value("currentLocationName", currentLocationName);
|
|
||||||
isDarklands = in.value("isDarklands", isDarklands);
|
isDarklands = in.value("isDarklands", isDarklands);
|
||||||
isNight = in.value("isNight", isNight);
|
isNight = in.value("isNight", isNight);
|
||||||
isDawn = in.value("isDawn", isDawn);
|
isDawn = in.value("isDawn", isDawn);
|
||||||
@@ -142,11 +160,8 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
//logger() << "GameState::load step 5" << std::endl;
|
//logger() << "GameState::load step 5" << std::endl;
|
||||||
|
|
||||||
// --- Tutorial ---
|
// --- Tutorial ---
|
||||||
if (in.contains("uniIntTutorialState")) {
|
tutorialStep = static_cast<TutorialStep>(
|
||||||
tutorialStep = migrateLegacyTutorialStep(in.value("tutorialStep", 0), in["uniIntTutorialState"].get<int>());
|
in.at("tutorialStep").get<int>());
|
||||||
} else {
|
|
||||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
|
||||||
}
|
|
||||||
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
||||||
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
||||||
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
||||||
@@ -193,20 +208,35 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
|
|
||||||
logger() << "GameState::load step 10" << std::endl;
|
logger() << "GameState::load step 10" << std::endl;
|
||||||
|
|
||||||
// --- Locations ---
|
// --- Players ---
|
||||||
// Every location's state is stored (applied when the location loads); the ones
|
const auto& playerValues = in.at("players");
|
||||||
// already loaded get it right away.
|
if (!playerValues.is_object())
|
||||||
storedLocationStates.clear();
|
throw std::runtime_error("Game state players must be an object");
|
||||||
if (in.contains("locations") && in["locations"].is_object()) {
|
for (const auto& [id, value] : playerValues.items()) {
|
||||||
for (const auto& [name, locJson] : in["locations"].items()) {
|
auto& player = players[id];
|
||||||
//logger() << "GameState::load step 10.1: " << name << std::endl;
|
if (!player) player = std::make_shared<CharacterState>();
|
||||||
storedLocationStates[name] = locJson;
|
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);
|
// --- Locations ---
|
||||||
if (it != locations.end() && it->second) {
|
const auto& locationValues = in.at("locations");
|
||||||
it->second->load(locJson);
|
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;
|
//logger() << "GameState::load step 11" << std::endl;
|
||||||
|
|
||||||
|
|||||||
+13
-8
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "items/Item.h"
|
#include "items/Item.h"
|
||||||
#include "quest/QuestJournal.h"
|
#include "quest/QuestJournal.h"
|
||||||
#include "Location.h"
|
#include "LocationGameState.h"
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -32,17 +32,17 @@ struct StoredChatMessage {
|
|||||||
bool incoming;
|
bool incoming;
|
||||||
};
|
};
|
||||||
|
|
||||||
using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
|
||||||
|
|
||||||
struct GameState : public ISaveable {
|
struct GameState : public ISaveable {
|
||||||
|
static constexpr int kSchemaVersion = 2;
|
||||||
|
|
||||||
// --- World ---
|
// --- World ---
|
||||||
// Campaign being played (see CampaignRegistry).
|
// Campaign being played (see CampaignRegistry).
|
||||||
std::string campaignId = "main";
|
std::string campaignId = "main";
|
||||||
// The campaign's loaded location(s); only the one the player is in is kept loaded.
|
// Pure authoritative location data. Live Location/render/script instances are
|
||||||
LocationMap locations;
|
// private runtime storage in InternalGameServer and never enter GameState.
|
||||||
// Saved state (Location::save) of the campaign's locations that are not loaded,
|
std::unordered_map<std::string, LocationGameState> locations;
|
||||||
// applied when one is loaded again. save()/load() cover both maps.
|
// Players are campaign-level entities and remain stable across locations.
|
||||||
std::unordered_map<std::string, nlohmann::json> storedLocationStates;
|
PlayerStateMap players;
|
||||||
std::string currentLocationName;
|
std::string currentLocationName;
|
||||||
bool isDarklands = false;
|
bool isDarklands = false;
|
||||||
bool isNight = false;
|
bool isNight = false;
|
||||||
@@ -73,6 +73,11 @@ struct GameState : public ISaveable {
|
|||||||
// Moves to the next step if `step` is the current one; returns whether it did.
|
// Moves to the next step if `step` is the current one; returns whether it did.
|
||||||
bool completeTutorialStep(TutorialStep step);
|
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 ---
|
// --- Phone / chat ---
|
||||||
bool chatUnread[3] = { true, true, true };
|
bool chatUnread[3] = { true, true, true };
|
||||||
std::string chatPreviewMsg[3];
|
std::string chatPreviewMsg[3];
|
||||||
|
|||||||
+752
-872
File diff suppressed because it is too large.
Load diff
+83
-47
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render/Renderer.h"
|
#include "render/Renderer.h"
|
||||||
#include "Environment.h"
|
#include "Environment.h"
|
||||||
@@ -16,13 +16,16 @@
|
|||||||
#include "TeleportZone.h"
|
#include "TeleportZone.h"
|
||||||
#include "LocationEditor.h"
|
#include "LocationEditor.h"
|
||||||
#include "LocationState.h"
|
#include "LocationState.h"
|
||||||
#include "ISaveable.h"
|
#include "LocationGameState.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include "Character.h"
|
||||||
|
|
||||||
namespace FRG
|
namespace FRG
|
||||||
{
|
{
|
||||||
|
class IGameServer;
|
||||||
|
|
||||||
struct PointLight
|
struct PointLight
|
||||||
{
|
{
|
||||||
@@ -35,7 +38,7 @@ namespace FRG
|
|||||||
// Rectangular limits — keep light confined to a room-sized AABB centred on the light
|
// 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 limitX = 99999.f; // shader fragment limit: half-width in world X
|
||||||
float limitY = 99999.f; // shader fragment limit: half-width in world Z
|
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
|
float autoLightLimitY = -1.0f; // activation rect half-width Z
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,16 +49,17 @@ namespace FRG
|
|||||||
float radius = 1.5f;
|
float radius = 1.5f;
|
||||||
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
|
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
|
||||||
bool enabled = true;
|
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
|
struct LocationSetup
|
||||||
{
|
{
|
||||||
|
std::shared_ptr<const LocationDefinition> definition;
|
||||||
std::string gameObjectsJsonPath;
|
std::string gameObjectsJsonPath;
|
||||||
std::string interactiveObjectsJsonPath;
|
std::string interactiveObjectsJsonPath;
|
||||||
std::string npcsJsonPath;
|
std::string npcsJsonPath;
|
||||||
std::string dialoguesJsonPath;
|
std::string dialoguesJsonPath;
|
||||||
std::vector<std::string> navigationJsonPaths;
|
//std::vector<std::string> navigationJsonPaths;
|
||||||
std::string scriptPath;
|
std::string scriptPath;
|
||||||
std::string teleportsJsonPath;
|
std::string teleportsJsonPath;
|
||||||
std::string triggerZonesJsonPath;
|
std::string triggerZonesJsonPath;
|
||||||
@@ -64,17 +68,29 @@ namespace FRG
|
|||||||
// Player shared by all locations of a campaign (see Location::createPlayer);
|
// Player shared by all locations of a campaign (see Location::createPlayer);
|
||||||
// setup() creates a private one when this is null.
|
// setup() creates a private one when this is null.
|
||||||
std::shared_ptr<Character> player;
|
std::shared_ptr<Character> player;
|
||||||
|
LocationGameState* gameState = nullptr;
|
||||||
|
std::shared_ptr<LocationLocalState> localState;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Location : public ISaveable
|
class Location
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Location(Renderer& iRenderer, Inventory& iInventory);
|
Location(Renderer& iRenderer, IGameServer& server,
|
||||||
|
std::string locationId);
|
||||||
// Detaches the (shared, longer-lived) player from this location.
|
// Detaches the (shared, longer-lived) player from this location.
|
||||||
~Location();
|
~Location();
|
||||||
|
const std::string& locationId() const { return locationId_; }
|
||||||
// ---- All serialisable runtime state ----
|
IGameServer& gameServer() { return gameServer_; }
|
||||||
LocationState state;
|
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;
|
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
||||||
|
|
||||||
@@ -88,10 +104,8 @@ namespace FRG
|
|||||||
std::vector<std::unique_ptr<Character>> npcs;
|
std::vector<std::unique_ptr<Character>> npcs;
|
||||||
|
|
||||||
// Builds the player character (models, animations, weapon, textures).
|
// Builds the player character (models, animations, weapon, textures).
|
||||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer);
|
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
|
||||||
|
IGameServer& server, const std::string& playerId);
|
||||||
std::vector<PathFinder> navigationMaps;
|
|
||||||
PathFinder* navigation = nullptr;
|
|
||||||
|
|
||||||
std::shared_ptr<ShadowMap> shadowMap;
|
std::shared_ptr<ShadowMap> shadowMap;
|
||||||
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
|
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
|
||||||
@@ -114,10 +128,6 @@ namespace FRG
|
|||||||
|
|
||||||
std::function<void()> onPlayerTaxiRequired;
|
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
|
// Set by Game after setup(). Lua and onDeathAnimComplete call this to
|
||||||
// request the transition without coupling Location to Game directly.
|
// request the transition without coupling Location to Game directly.
|
||||||
std::function<bool()> requestDarklandsTransition;
|
std::function<bool()> requestDarklandsTransition;
|
||||||
@@ -140,7 +150,7 @@ namespace FRG
|
|||||||
EditorMode editorMode = EditorMode::None;
|
EditorMode editorMode = EditorMode::None;
|
||||||
LocationEditor editor;
|
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)
|
// 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.
|
// has crossed the tap-vs-drag threshold and is now rotating the camera.
|
||||||
@@ -161,9 +171,30 @@ namespace FRG
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
void setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer);
|
void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
|
||||||
void setupNavigation(const std::vector<std::string>& paths);
|
//void setupNavigation(const std::vector<std::string>& paths);
|
||||||
bool switchNavigation(int index);
|
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);
|
||||||
|
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);
|
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
||||||
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
||||||
|
|
||||||
@@ -177,8 +208,8 @@ namespace FRG
|
|||||||
|
|
||||||
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||||
|
|
||||||
void update(int64_t deltaMs);
|
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
||||||
void updateNpcCar(int64_t deltaMs);
|
void updatePresentation(int64_t deltaMs);
|
||||||
// Purely visual feedback for click-triggered player routes. Disabling it
|
// Purely visual feedback for click-triggered player routes. Disabling it
|
||||||
// also cancels an animation that is currently playing.
|
// also cancels an animation that is currently playing.
|
||||||
void setPathVisualizationEnabled(bool enabled);
|
void setPathVisualizationEnabled(bool enabled);
|
||||||
@@ -194,26 +225,26 @@ namespace FRG
|
|||||||
void setDialogueFlag(const std::string& flag, int value);
|
void setDialogueFlag(const std::string& flag, int value);
|
||||||
int getDialogueFlag(const std::string& flag) const;
|
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:
|
protected:
|
||||||
friend class LocationEditor;
|
friend class LocationEditor;
|
||||||
|
friend class Game;
|
||||||
|
friend class ScriptEngine;
|
||||||
|
friend class LocalGameServerHost;
|
||||||
|
friend class RemoteGameServerHost;
|
||||||
Renderer& renderer;
|
Renderer& renderer;
|
||||||
Inventory& inventory;
|
|
||||||
AudioPlayerAsync* audioPlayer;
|
AudioPlayerAsync* audioPlayer;
|
||||||
|
|
||||||
private:
|
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 ----
|
// ---- Pointer-to-index helpers ----
|
||||||
InteractiveObject* getTargetInteractiveObject();
|
TeleportZone* getTargetTeleportZone(const LocationNetworkState& state);
|
||||||
TeleportZone* getTargetTeleportZone();
|
|
||||||
int findInteractiveObjectIndex(const InteractiveObject* obj) const;
|
|
||||||
int findTeleportZoneIndex(const TeleportZone* tz) const;
|
|
||||||
|
|
||||||
enum class InstancedTexture { None, Color, Darklands };
|
enum class InstancedTexture { None, Color, Darklands };
|
||||||
|
|
||||||
@@ -223,26 +254,31 @@ namespace FRG
|
|||||||
void drawInstancedStatics(const std::string& instancedShaderName, InstancedTexture textureKind,
|
void drawInstancedStatics(const std::string& instancedShaderName, InstancedTexture textureKind,
|
||||||
const std::function<void()>& setUniforms = {});
|
const std::function<void()>& setUniforms = {});
|
||||||
|
|
||||||
void resolveCharacterCollisions();
|
|
||||||
void updateDynamicReplans(int64_t deltaMs);
|
|
||||||
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
|
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
|
||||||
void loadTriggerZones(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 loadPointLights(const std::string& jsonPath, const char* zipFile);
|
||||||
void updateTriggerZones(const Eigen::Vector3f& playerPos);
|
void updateTriggerZones(const Eigen::Vector3f& playerPos);
|
||||||
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
|
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
|
||||||
int findNpcIndex(const Character* npc) const;
|
void captureAuxiliaryState(LocationGameState& out) const;
|
||||||
void fireBumpCallbacks(Character* mover, Character* standing);
|
void applyAuxiliaryState(const LocationGameState& in);
|
||||||
|
|
||||||
PathVisualization pathVisualization;
|
PathVisualization pathVisualization;
|
||||||
|
|
||||||
std::unordered_map<Character*, Eigen::Vector3f> lastCharacterPositions;
|
struct NpcCarPresentationSample {
|
||||||
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;
|
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||||
std::unordered_map<Character*, int> stuckFrameCounts;
|
float rotation = 0.f;
|
||||||
std::unordered_map<Character*, float> stuckPauseRemainingS;
|
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
|
} // namespace FRG
|
||||||
+45
-15
@@ -1,6 +1,7 @@
|
|||||||
#include "LocationEditor.h"
|
#include "LocationEditor.h"
|
||||||
#include "Location.h"
|
#include "Location.h"
|
||||||
#include "Character.h"
|
#include "Character.h"
|
||||||
|
#include "game_server/IGameServer.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include "render/OpenGlExtensions.h"
|
#include "render/OpenGlExtensions.h"
|
||||||
#include "TextModel.h"
|
#include "TextModel.h"
|
||||||
@@ -24,10 +25,14 @@ namespace FRG
|
|||||||
void LocationEditor::buildNavMeshes()
|
void LocationEditor::buildNavMeshes()
|
||||||
{
|
{
|
||||||
navigationEditorNavMeshes.clear();
|
navigationEditorNavMeshes.clear();
|
||||||
if (!loc.navigation) return;
|
auto state = loc.stateSnapshot();
|
||||||
const float y = loc.navigation->getFloorY() + 0.02f;
|
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);
|
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;
|
if (obs.polygon.size() < 3) continue;
|
||||||
VertexRenderStruct mesh;
|
VertexRenderStruct mesh;
|
||||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
||||||
@@ -51,7 +56,11 @@ namespace FRG
|
|||||||
{
|
{
|
||||||
VertexDataStruct data;
|
VertexDataStruct data;
|
||||||
const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
|
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;
|
const float s = 0.2f;
|
||||||
|
|
||||||
for (const auto& pt : navigationEditorPoints) {
|
for (const auto& pt : navigationEditorPoints) {
|
||||||
@@ -68,12 +77,19 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigationEditorPointsMesh.data = std::move(data);
|
navigationEditorPointsMesh.data = std::move(data);
|
||||||
navigationEditorPointsMesh.RefreshVBO();
|
if (navigationEditorPointsMesh.data.PositionData.size() > 0)
|
||||||
|
{
|
||||||
|
navigationEditorPointsMesh.RefreshVBO();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocationEditor::drawPoints()
|
void LocationEditor::drawPoints()
|
||||||
{
|
{
|
||||||
if (navigationEditorPoints.empty()) return;
|
if (navigationEditorPoints.empty()) return;
|
||||||
|
if (navigationEditorPointsMesh.data.PositionData.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
loc.renderer.shaderManager.PushShader("defaultColor");
|
loc.renderer.shaderManager.PushShader("defaultColor");
|
||||||
loc.renderer.SetMatrix();
|
loc.renderer.SetMatrix();
|
||||||
loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh);
|
loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh);
|
||||||
@@ -120,12 +136,17 @@ namespace FRG
|
|||||||
poly.polygon.emplace_back(pt.x(), pt.z());
|
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 "
|
logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
|
||||||
<< poly.polygon.size() << " vertices\n";
|
<< 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);
|
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||||
VertexRenderStruct mesh;
|
VertexRenderStruct mesh;
|
||||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
||||||
@@ -147,14 +168,21 @@ namespace FRG
|
|||||||
if (!std::filesystem::exists(baseName + ".json")) break;
|
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";
|
logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
|
||||||
else
|
else
|
||||||
std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";
|
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";
|
logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
|
||||||
else
|
else
|
||||||
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
|
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
|
||||||
@@ -162,8 +190,8 @@ namespace FRG
|
|||||||
|
|
||||||
void LocationEditor::reload()
|
void LocationEditor::reload()
|
||||||
{
|
{
|
||||||
loc.setupNavigation(loc.navigationMapPaths);
|
//loc.setupNavigation(loc.navigationMapPaths);
|
||||||
logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
//logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color)
|
static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color)
|
||||||
@@ -454,9 +482,11 @@ namespace FRG
|
|||||||
data.rotationX = 0.0f;
|
data.rotationX = 0.0f;
|
||||||
data.rotationY = rotDist(rng);
|
data.rotationY = rotDist(rng);
|
||||||
data.rotationZ = 0.0f;
|
data.rotationZ = 0.0f;
|
||||||
data.positionX = loc.player->state.position.x();
|
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
|
||||||
data.positionY = loc.player->state.position.y();//-0.1;
|
if (!playerState) return;
|
||||||
data.positionZ = loc.player->state.position.z();
|
data.positionX = playerState->position.x();
|
||||||
|
data.positionY = playerState->position.y();//-0.1;
|
||||||
|
data.positionZ = playerState->position.z();
|
||||||
data.scale = scaleDist(rng);
|
data.scale = scaleDist(rng);
|
||||||
|
|
||||||
GameObjectData data2;
|
GameObjectData data2;
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
npc.setTarget(homeTarget, {}, *location, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto advance = [&](const std::string& id, CharacterState& state) {
|
||||||
|
CharacterUpdateContext context;
|
||||||
|
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);
|
||||||
|
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,78 @@
|
|||||||
|
#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;
|
||||||
|
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
|
||||||
+161
-218
@@ -1,234 +1,177 @@
|
|||||||
#include "LocationState.h"
|
#include "LocationState.h"
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
void LocationState::save(nlohmann::json& out) const
|
std::shared_ptr<LocationLocalState> ClientLocationState::getOrCreate(
|
||||||
|
const std::string& locationId)
|
||||||
{
|
{
|
||||||
out["cameraAzimuth"] = cameraAzimuth;
|
auto& state = locations[locationId];
|
||||||
out["cameraInclination"] = cameraInclination;
|
if (!state) state = std::make_shared<LocationLocalState>();
|
||||||
|
return state;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocationState::load(const nlohmann::json& in)
|
void ClientLocationState::clear()
|
||||||
{
|
{
|
||||||
//logger() << "LocationState::load step 1" << std::endl;
|
locations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
cameraAzimuth = in.value("cameraAzimuth", cameraAzimuth);
|
void LocationLocalState::save(nlohmann::json& out) const
|
||||||
cameraInclination = in.value("cameraInclination", cameraInclination);
|
{
|
||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const
|
||||||
|
{
|
||||||
|
return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex);
|
||||||
|
|
||||||
targetInteractiveObjectIndex = in.value("targetInteractiveObjectIndex", -1);
|
targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string());
|
||||||
targetInteractNpcIndex = in.value("targetInteractNpcIndex", -1);
|
targetInteractNpcId = in.value("targetInteractNpcId", std::string());
|
||||||
targetTeleportZoneIndex = in.value("targetTeleportZoneIndex", -1);
|
targetTeleportZoneId = in.value("targetTeleportZoneId", std::string());
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
|
const auto& taxi = in.at("taxi");
|
||||||
|
loadCar(taxiCar, taxi.at("car"));
|
||||||
|
taxiArrivedCallbackCalled =
|
||||||
|
taxi.at("arrivedCallbackCalled").get<bool>();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace FRG
|
} // namespace FRG
|
||||||
+46
-17
@@ -2,34 +2,63 @@
|
|||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
#include "NpcCar.h"
|
#include "NpcCar.h"
|
||||||
|
#include "navigation/PathFinder.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
struct LocationState : public ISaveable {
|
inline constexpr int kSinglePlayerPersistenceVersion = 1;
|
||||||
// ---- Camera ----
|
|
||||||
float cameraAzimuth = -2.35f;
|
struct LocationLocalState : public ISaveable {
|
||||||
float cameraInclination = 1.1036f;
|
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;
|
||||||
|
void initNavigation(
|
||||||
|
const std::vector<std::string>& paths,
|
||||||
|
const std::string& zipFile = std::string());
|
||||||
|
|
||||||
// ---- Navigation ----
|
// ---- Navigation ----
|
||||||
int activeNavigationIndex = 0;
|
int activeNavigationIndex = 0;
|
||||||
|
|
||||||
// ---- Walk-to targets (-1 = none) ----
|
// ---- Walk-to targets (empty = none) ----
|
||||||
// Mirrors CharacterState's kNoTarget convention; raw pointers are not serialisable.
|
std::string targetInteractiveObjectId;
|
||||||
int targetInteractiveObjectIndex = -1;
|
std::string targetInteractNpcId;
|
||||||
int targetInteractNpcIndex = -1;
|
std::string targetTeleportZoneId;
|
||||||
int targetTeleportZoneIndex = -1;
|
|
||||||
|
|
||||||
// ---- World-mode flags (synced from Game after each transition) ----
|
|
||||||
bool isDarklands = false;
|
|
||||||
bool isNight = false;
|
|
||||||
bool isDawn = false;
|
|
||||||
|
|
||||||
|
|
||||||
NpcCar taxiCarInitialState;
|
NpcCar taxiCarInitialState;
|
||||||
NpcCar taxiCar;
|
NpcCar taxiCar;
|
||||||
bool taxiArrivedCallbackCalled = false;
|
bool taxiArrivedCallbackCalled = false;
|
||||||
Vector3f taxiCarTriggerPosition = Vector3f(0,0,0);
|
Eigen::Vector3f taxiCarTriggerPosition = Eigen::Vector3f(0,0,0);
|
||||||
std::vector<Vector3f> taxiCarWaypoints;
|
std::vector<Eigen::Vector3f> taxiCarWaypoints;
|
||||||
|
|
||||||
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
||||||
|
|
||||||
|
|||||||
+155
-115
@@ -201,9 +201,9 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
MenuManager::MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer) :
|
MenuManager::MenuManager(Renderer& iRenderer, IGameServer& server, AudioPlayerAsync& audioPlayer) :
|
||||||
renderer(iRenderer),
|
renderer(iRenderer),
|
||||||
gameState_(gameState),
|
server_(server),
|
||||||
audioPlayer_(audioPlayer)
|
audioPlayer_(audioPlayer)
|
||||||
{
|
{
|
||||||
//Localization
|
//Localization
|
||||||
@@ -338,9 +338,7 @@ namespace FRG {
|
|||||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
|
void MenuManager::setup(const std::string& zipFile) {
|
||||||
inventory = &inv;
|
|
||||||
|
|
||||||
loadUiRoots(zipFile);
|
loadUiRoots(zipFile);
|
||||||
|
|
||||||
texObjectiveCompleted_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/quest_objective_completed.png", zipFile, true);
|
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);
|
languageLoadingRu = renderer.textureManager.LoadFromPng("resources/loading_land003_ru.png", zipFile);
|
||||||
languageLoadingEn = renderer.textureManager.LoadFromPng("resources/loading_land003_en.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/";
|
const std::string imgDir = "resources/w/ui/img/phone/";
|
||||||
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
|
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
|
||||||
texBubbleInLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_top.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 {
|
std::shared_ptr<UiNode> MenuManager::gameplayHudRoot() const {
|
||||||
const std::string& loc = gameState_.currentLocationName;
|
const auto snapshot = server_.snapshot();
|
||||||
if (loc == "uni_exterior") return gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
const std::string& loc = snapshot->currentLocationId;
|
||||||
if (loc == "uni_interior") return gameState_.isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
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
|
return hudDormRoot; // dorm, or a new game before the location is set
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,21 +420,37 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::refreshHudVisibility() {
|
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 auto itemIds = snapshot->inventoryItemIds;
|
||||||
const bool tutorial = gameState_.isTutorialActive();
|
const TutorialStep step = snapshot->tutorialStep;
|
||||||
const bool hasPhone = inventory->hasItem("phone");
|
const bool tutorial = snapshot->isTutorialActive();
|
||||||
const bool hasJournal = inventory->hasItem("journal");
|
const bool hasPhone = std::find(itemIds.begin(), itemIds.end(), "phone") != itemIds.end();
|
||||||
const bool hasNote = inventory->hasItem("note_spell");
|
const bool hasJournal = std::find(itemIds.begin(), itemIds.end(), "journal") != itemIds.end();
|
||||||
const bool dark = gameState_.isDarklands;
|
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) {
|
auto show = [this](std::initializer_list<const char*> names, bool visible) {
|
||||||
for (const char* name : names) uiManager.setNodeVisible(name, visible);
|
for (const char* name : names) uiManager.setNodeVisible(name, visible);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- HUD buttons: present while the matching item is in the inventory ---
|
// --- 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({ "phoneButton" }, hasPhone && !dark);
|
||||||
show({ "journalButton" }, hasJournal);
|
show({ "journalButton" }, hasJournal);
|
||||||
show({ "darklandsButton" }, hasNote && !dark);
|
show({ "darklandsButton" }, hasNote && !dark);
|
||||||
@@ -466,9 +468,9 @@ namespace FRG {
|
|||||||
show({ "hint4" }, step == TutorialStep::Zoom);
|
show({ "hint4" }, step == TutorialStep::Zoom);
|
||||||
show({ "hint5" }, step == TutorialStep::PickUpItem);
|
show({ "hint5" }, step == TutorialStep::PickUpItem);
|
||||||
// ...then the hints for the picked-up items, which can be done in any order.
|
// ...then the hints for the picked-up items, which can be done in any order.
|
||||||
const bool chatHint = tutorial && !gameState_.tutorialPhoneChatScreenOpened;
|
const bool chatHint = tutorial && !snapshot->tutorialPhoneChatScreenOpened;
|
||||||
const bool journalHint = tutorial && !gameState_.tutorialJournalScreenOpened;
|
const bool journalHint = tutorial && !snapshot->tutorialJournalScreenOpened;
|
||||||
const bool taxiHint = tutorial && gameState_.tutorialNeedOpenTaxiScreen;
|
const bool taxiHint = tutorial && snapshot->tutorialNeedOpenTaxiScreen;
|
||||||
show({ "hint6a", "hint6aarrow" }, hasPhone && chatHint && !taxiHint);
|
show({ "hint6a", "hint6aarrow" }, hasPhone && chatHint && !taxiHint);
|
||||||
show({ "hint6b", "hint6barrow" }, hasJournal && journalHint);
|
show({ "hint6b", "hint6barrow" }, hasJournal && journalHint);
|
||||||
show({ "hint7", "hint7arrow" }, taxiHint);
|
show({ "hint7", "hint7arrow" }, taxiHint);
|
||||||
@@ -485,7 +487,7 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::showMainMenu() {
|
void MenuManager::showMainMenu() {
|
||||||
gameState_.currentLocationName.clear();
|
server_.submit(SetCurrentLocationCommand{""});
|
||||||
|
|
||||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||||
lastMusicTrack = "music/main menu final.ogg";
|
lastMusicTrack = "music/main menu final.ogg";
|
||||||
@@ -546,7 +548,6 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::showMenuAfterGameCompleted() {
|
void MenuManager::showMenuAfterGameCompleted() {
|
||||||
//gameState_.currentLocationName.clear();
|
|
||||||
|
|
||||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||||
lastMusicTrack = "music/main menu final.ogg";
|
lastMusicTrack = "music/main menu final.ogg";
|
||||||
@@ -1060,8 +1061,8 @@ namespace FRG {
|
|||||||
|
|
||||||
steps.push_back([this]() {
|
steps.push_back([this]() {
|
||||||
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
||||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState_.campaignId))
|
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(server_.snapshot()->campaignId))
|
||||||
gameState_.questJournal.reloadDefinitionsPreservingState(campaign->questsPath, CONST_ZIP_FILE);
|
server_.submit(ReloadQuestDefinitionsCommand{campaign->questsPath, CONST_ZIP_FILE});
|
||||||
});
|
});
|
||||||
|
|
||||||
steps.push_back([this]() {
|
steps.push_back([this]() {
|
||||||
@@ -1197,7 +1198,7 @@ namespace FRG {
|
|||||||
|
|
||||||
uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {});
|
uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {});
|
||||||
|
|
||||||
const auto& items = inventory->getItems();
|
const auto items = inventoryItems();
|
||||||
/*logger() << "Item count: " << items.size() << std::endl;
|
/*logger() << "Item count: " << items.size() << std::endl;
|
||||||
for (int x = 0; x < items.size(); x++)
|
for (int x = 0; x < items.size(); x++)
|
||||||
{
|
{
|
||||||
@@ -1236,7 +1237,7 @@ namespace FRG {
|
|||||||
|
|
||||||
void MenuManager::selectInventoryItem(int index) {
|
void MenuManager::selectInventoryItem(int index) {
|
||||||
logger() << "MenuManager::selectInventoryItem: " << index << std::endl;
|
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 (index < 0 || index >= static_cast<int>(items.size())) return;
|
||||||
|
|
||||||
if (isPortraitMode() && inventoryMenuStackDepth_ == 1) {
|
if (isPortraitMode() && inventoryMenuStackDepth_ == 1) {
|
||||||
@@ -1288,7 +1289,10 @@ namespace FRG {
|
|||||||
|
|
||||||
void MenuManager::openQuestJournal() {
|
void MenuManager::openQuestJournal() {
|
||||||
uiState_ = GameUiState::QuestJournal;
|
uiState_ = GameUiState::QuestJournal;
|
||||||
gameState_.tutorialJournalScreenOpened = true;
|
auto tutorial = server_.snapshot();
|
||||||
|
server_.submit(SetTutorialFlagsCommand{
|
||||||
|
tutorial->tutorialPhoneChatScreenOpened, true,
|
||||||
|
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||||
journalMenuStackDepth_ = 1;
|
journalMenuStackDepth_ = 1;
|
||||||
|
|
||||||
const bool portrait = isPortraitMode();
|
const bool portrait = isPortraitMode();
|
||||||
@@ -1380,9 +1384,10 @@ namespace FRG {
|
|||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
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("phoneTimeDay", false);
|
||||||
uiManager.setNodeVisible("phoneTimeNight", false);
|
uiManager.setNodeVisible("phoneTimeNight", false);
|
||||||
@@ -1418,51 +1423,67 @@ namespace FRG {
|
|||||||
});
|
});
|
||||||
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
||||||
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
||||||
gameState_.chatUnread[0] = false;
|
setChatUnread(0, false);
|
||||||
openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
|
openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
|
||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||||
});
|
});
|
||||||
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
||||||
gameState_.chatUnread[1] = false;
|
setChatUnread(1, false);
|
||||||
openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
|
openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
|
||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||||
});
|
});
|
||||||
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
||||||
gameState_.chatUnread[2] = false;
|
setChatUnread(2, false);
|
||||||
openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
|
openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
|
||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::refreshChatUnreadIndicators() {
|
void MenuManager::refreshChatUnreadIndicators() {
|
||||||
|
const auto state = server_.snapshot();
|
||||||
static const char* kUnreadNodes[3] = { "chat1Unread", "chat2Unread", "chat3Unread" };
|
static const char* kUnreadNodes[3] = { "chat1Unread", "chat2Unread", "chat3Unread" };
|
||||||
static const char* kMsgNodes[3] = { "chat1msg", "chat2msg", "chat3msg" };
|
static const char* kMsgNodes[3] = { "chat1msg", "chat2msg", "chat3msg" };
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
uiManager.setNodeVisible(kUnreadNodes[i], gameState_.chatUnread[i]);
|
uiManager.setNodeVisible(kUnreadNodes[i], state->chatUnread[i]);
|
||||||
if (!gameState_.chatPreviewMsg[i].empty())
|
if (!state->chatPreview[i].empty())
|
||||||
uiManager.setText(kMsgNodes[i], gameState_.chatPreviewMsg[i]);
|
uiManager.setText(kMsgNodes[i], state->chatPreview[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::setChatUnread(int chatIndex, bool unread, const std::string& previewMsg) {
|
void MenuManager::setChatUnread(int chatIndex, bool unread, const std::string& previewMsg) {
|
||||||
if (chatIndex < 0 || chatIndex > 2) return;
|
if (chatIndex < 0 || chatIndex > 2) return;
|
||||||
gameState_.chatUnread[chatIndex] = unread;
|
const auto state = server_.snapshot();
|
||||||
|
std::string preview = state->chatPreview[chatIndex];
|
||||||
if (!previewMsg.empty()) {
|
if (!previewMsg.empty()) {
|
||||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(previewMsg);
|
preview = trimChatPreview(previewMsg);
|
||||||
} else if (!unread && !gameState_.chatHistory[chatIndex].empty()) {
|
} else if (!unread && !state->chatHistory[chatIndex].empty()) {
|
||||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(gameState_.chatHistory[chatIndex].back().text);
|
preview = trimChatPreview(state->chatHistory[chatIndex].back().text);
|
||||||
}
|
}
|
||||||
|
server_.submit(SetChatCommand{chatIndex, unread, std::move(preview)});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::spendMoney(int amount) {
|
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() {
|
void MenuManager::openPhoneBank() {
|
||||||
currentPhoneSubScreen_ = PhoneSubScreen::Bank;
|
currentPhoneSubScreen_ = PhoneSubScreen::Bank;
|
||||||
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneBankRoot : phoneBankRoot);
|
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&) {
|
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
||||||
closePhoneEntirely();
|
closePhoneEntirely();
|
||||||
@@ -1492,13 +1513,15 @@ namespace FRG {
|
|||||||
});
|
});
|
||||||
uiManager.setButtonCallback("videoSkip", [this](const std::string&) {
|
uiManager.setButtonCallback("videoSkip", [this](const std::string&) {
|
||||||
closePhoneEntirely();
|
closePhoneEntirely();
|
||||||
if (gameState_.isNight)
|
const auto state = server_.snapshot();
|
||||||
|
if (state->isNight)
|
||||||
{
|
{
|
||||||
startDialogueFunc("dialog_video002");
|
startDialogueFunc("dialog_video002");
|
||||||
}
|
}
|
||||||
else
|
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)
|
if (day == 0)
|
||||||
{
|
{
|
||||||
startDialogueFunc("dialog_video003");
|
startDialogueFunc("dialog_video003");
|
||||||
@@ -1513,10 +1536,11 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::openPhoneTaxi() {
|
void MenuManager::openPhoneTaxi() {
|
||||||
if (gameState_.currentLocationName == "uni_interior") {
|
const std::string location = server_.snapshot()->currentLocationId;
|
||||||
|
if (location == "uni_interior") {
|
||||||
closePhoneEntirely();
|
closePhoneEntirely();
|
||||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi003");
|
if (startDialogueFunc) startDialogueFunc("dialog_taxi003");
|
||||||
} else if (gameState_.currentLocationName == "uni_exterior") {
|
} else if (location == "uni_exterior") {
|
||||||
currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
|
currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
|
||||||
openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot);
|
openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot);
|
||||||
} else {
|
} else {
|
||||||
@@ -1539,17 +1563,18 @@ namespace FRG {
|
|||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||||
});
|
});
|
||||||
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
||||||
gameState_.tutorialNeedOpenTaxiScreen = false;
|
auto state = server_.snapshot();
|
||||||
gameState_.tutorialTaxiHintDone = true;
|
server_.submit(SetTutorialFlagsCommand{
|
||||||
|
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||||
|
false, true});
|
||||||
|
|
||||||
if (gameState_.taxiIsCalled == false)
|
if (!state->taxiIsCalled)
|
||||||
{
|
{
|
||||||
gameState_.taxiIsCalled = true;
|
server_.submit(SetTaxiCalledCommand{true});
|
||||||
if (callTaxiFunc)
|
server_.submit(StartTaxiCommand{state->currentLocationId});
|
||||||
{
|
server_.submit(SetTriggerZoneEnabledCommand{
|
||||||
callTaxiFunc();
|
state->currentLocationId, "taxi_zone001", false});
|
||||||
}
|
server_.submit(SpendMoneyCommand{500});
|
||||||
gameState_.money -= 500;
|
|
||||||
closePhoneEntirely();
|
closePhoneEntirely();
|
||||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi002");
|
if (startDialogueFunc) startDialogueFunc("dialog_taxi002");
|
||||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||||
@@ -1569,7 +1594,10 @@ namespace FRG {
|
|||||||
activeChatIndex_ = chatIndex;
|
activeChatIndex_ = chatIndex;
|
||||||
currentPhoneSubScreen_ = PhoneSubScreen::Chat;
|
currentPhoneSubScreen_ = PhoneSubScreen::Chat;
|
||||||
phoneChatVisibleBubbles_.clear();
|
phoneChatVisibleBubbles_.clear();
|
||||||
gameState_.tutorialPhoneChatScreenOpened = true;
|
auto tutorial = server_.snapshot();
|
||||||
|
server_.submit(SetTutorialFlagsCommand{
|
||||||
|
true, tutorial->tutorialJournalScreenOpened,
|
||||||
|
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||||
uiManager.pushMenuFromSavedRoot(chatRoot);
|
uiManager.pushMenuFromSavedRoot(chatRoot);
|
||||||
|
|
||||||
// Build a dedicated chatUiManager root sized to the FBO chat area
|
// Build a dedicated chatUiManager root sized to the FBO chat area
|
||||||
@@ -1616,11 +1644,7 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::returnToPhoneChatList() {
|
void MenuManager::returnToPhoneChatList() {
|
||||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
|
||||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
|
||||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
|
||||||
}
|
|
||||||
activeChatIndex_ = -1;
|
activeChatIndex_ = -1;
|
||||||
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
|
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
|
||||||
phoneChatVisibleBubbles_.clear();
|
phoneChatVisibleBubbles_.clear();
|
||||||
@@ -1634,11 +1658,7 @@ namespace FRG {
|
|||||||
|
|
||||||
|
|
||||||
void MenuManager::closePhoneEntirely() {
|
void MenuManager::closePhoneEntirely() {
|
||||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
|
||||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
|
||||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
|
||||||
}
|
|
||||||
activeChatIndex_ = -1;
|
activeChatIndex_ = -1;
|
||||||
currentPhoneSubScreen_ = PhoneSubScreen::None;
|
currentPhoneSubScreen_ = PhoneSubScreen::None;
|
||||||
uiState_ = GameUiState::Gameplay;
|
uiState_ = GameUiState::Gameplay;
|
||||||
@@ -1699,8 +1719,11 @@ namespace FRG {
|
|||||||
void MenuManager::tutorialShowTaxiHint()
|
void MenuManager::tutorialShowTaxiHint()
|
||||||
{
|
{
|
||||||
logger() << "tutorialShowTaxiHint" << std::endl;
|
logger() << "tutorialShowTaxiHint" << std::endl;
|
||||||
if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return;
|
auto state = server_.snapshot();
|
||||||
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
|
if (!state->isTutorialActive() || state->tutorialTaxiHintDone) return;
|
||||||
|
server_.submit(SetTutorialFlagsCommand{
|
||||||
|
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||||
|
true, state->tutorialTaxiHintDone});
|
||||||
refreshHudVisibility();
|
refreshHudVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1723,16 +1746,17 @@ namespace FRG {
|
|||||||
|
|
||||||
void MenuManager::onLocationChanged(const std::string& locationName) {
|
void MenuManager::onLocationChanged(const std::string& locationName) {
|
||||||
if (uiState_ != GameUiState::Gameplay) return;
|
if (uiState_ != GameUiState::Gameplay) return;
|
||||||
gameState_.currentLocationName = locationName;
|
server_.submit(SetCurrentLocationCommand{locationName});
|
||||||
|
const auto state = server_.snapshot();
|
||||||
showGameplayHud();
|
showGameplayHud();
|
||||||
|
|
||||||
if (locationName == "uni_exterior" || locationName == "uni_interior") {
|
if (locationName == "uni_exterior" || locationName == "uni_interior") {
|
||||||
if (gameState_.isDarklands)
|
if (state->isDarklands)
|
||||||
{
|
{
|
||||||
audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||||
lastMusicTrack = "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");
|
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||||
lastMusicTrack = "music/bishkek night.ogg";
|
lastMusicTrack = "music/bishkek night.ogg";
|
||||||
@@ -1743,7 +1767,7 @@ namespace FRG {
|
|||||||
lastMusicTrack = "music/bishkek univer day.ogg";
|
lastMusicTrack = "music/bishkek univer day.ogg";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (gameState_.isNight)
|
if (state->isNight)
|
||||||
{
|
{
|
||||||
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||||
lastMusicTrack = "music/bishkek night.ogg";
|
lastMusicTrack = "music/bishkek night.ogg";
|
||||||
@@ -1759,13 +1783,16 @@ namespace FRG {
|
|||||||
void MenuManager::onItemPickedUp(const std::string& itemId) {
|
void MenuManager::onItemPickedUp(const std::string& itemId) {
|
||||||
// The new item's HUD button appears through refreshHudVisibility().
|
// The new item's HUD button appears through refreshHudVisibility().
|
||||||
if (itemId == "phone" || itemId == "journal")
|
if (itemId == "phone" || itemId == "journal")
|
||||||
gameState_.completeTutorialStep(TutorialStep::PickUpItem);
|
server_.submit(CompleteTutorialStepCommand{TutorialStep::PickUpItem});
|
||||||
refreshHudVisibility();
|
refreshHudVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::refreshQuestJournalUi() {
|
void MenuManager::refreshQuestJournalUi() {
|
||||||
visibleQuestIds.clear();
|
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) {
|
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
|
||||||
const int pa = questStatusPriority(a->status);
|
const int pa = questStatusPriority(a->status);
|
||||||
@@ -1840,7 +1867,10 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedQuestIndex = index;
|
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) {
|
if (!quest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1888,7 +1918,7 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::openPortraitInventoryItem(int index) {
|
void MenuManager::openPortraitInventoryItem(int index) {
|
||||||
const auto& items = inventory->getItems();
|
const auto items = inventoryItems();
|
||||||
inventorySelectedIndex_ = index;
|
inventorySelectedIndex_ = index;
|
||||||
inventoryMenuStackDepth_ = 2;
|
inventoryMenuStackDepth_ = 2;
|
||||||
uiManager.pushMenuFromSavedRoot(portraitInventoryItemRoot);
|
uiManager.pushMenuFromSavedRoot(portraitInventoryItemRoot);
|
||||||
@@ -1943,7 +1973,10 @@ namespace FRG {
|
|||||||
uiManager.setButtonCallback("journalMain", [this](const std::string&) {});
|
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;
|
if (!quest) return;
|
||||||
const auto& def = quest->definition;
|
const auto& def = quest->definition;
|
||||||
|
|
||||||
@@ -2070,7 +2103,8 @@ namespace FRG {
|
|||||||
phoneChatVisibleBubbles_.clear();
|
phoneChatVisibleBubbles_.clear();
|
||||||
|
|
||||||
if (chatIndex < 0 || chatIndex > 2) return;
|
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 bool inc = msg.incoming;
|
||||||
const std::string nodeName = chatUiManager.addChatBubble(
|
const std::string nodeName = chatUiManager.addChatBubble(
|
||||||
"chatMessagesContainer", msg.text, inc,
|
"chatMessagesContainer", msg.text, inc,
|
||||||
@@ -2100,11 +2134,7 @@ namespace FRG {
|
|||||||
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
|
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
|
||||||
if (activeChatIndex_ < 0) return;
|
if (activeChatIndex_ < 0) return;
|
||||||
|
|
||||||
auto& history = gameState_.chatHistory[activeChatIndex_];
|
server_.submit(AppendChatMessageCommand{activeChatIndex_, text, incoming});
|
||||||
if (static_cast<int>(history.size()) >= 5) {
|
|
||||||
history.erase(history.begin());
|
|
||||||
}
|
|
||||||
history.push_back({ text, incoming });
|
|
||||||
|
|
||||||
if (uiState_ != GameUiState::PhoneScreen) return;
|
if (uiState_ != GameUiState::PhoneScreen) return;
|
||||||
|
|
||||||
@@ -2143,8 +2173,8 @@ namespace FRG {
|
|||||||
{
|
{
|
||||||
logger() << "MenuManager::setDarklandsMode called" << std::endl;
|
logger() << "MenuManager::setDarklandsMode called" << std::endl;
|
||||||
|
|
||||||
if (enabled && gameState_.currentLocationName == "uni_interior" && inventory->hasItem("note_spell"))
|
if (enabled && server_.snapshot()->currentLocationId == "uni_interior" && hasInventoryItem("note_spell"))
|
||||||
gameState_.completeTutorialStep(TutorialStep::EnterDarklands);
|
server_.submit(CompleteTutorialStepCommand{TutorialStep::EnterDarklands});
|
||||||
|
|
||||||
if (uiState_ == GameUiState::Gameplay)
|
if (uiState_ == GameUiState::Gameplay)
|
||||||
showGameplayHud(); // darklands has its own HUD layouts
|
showGameplayHud(); // darklands has its own HUD layouts
|
||||||
@@ -2152,30 +2182,31 @@ namespace FRG {
|
|||||||
|
|
||||||
void MenuManager::onPlayerStartedWalking()
|
void MenuManager::onPlayerStartedWalking()
|
||||||
{
|
{
|
||||||
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands)
|
const auto state = server_.snapshot();
|
||||||
gameState_.completeTutorialStep(TutorialStep::DarklandsHealth);
|
if (state->currentLocationId == "uni_interior" && state->isDarklands)
|
||||||
|
server_.submit(CompleteTutorialStepCommand{TutorialStep::DarklandsHealth});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::advanceUniIntDarklandsHud()
|
void MenuManager::advanceUniIntDarklandsHud()
|
||||||
{
|
{
|
||||||
// The ghost zone can be reached without a floor tap (DarklandsHealth never completed).
|
// The ghost zone can be reached without a floor tap (DarklandsHealth never completed).
|
||||||
if (gameState_.tutorialStep == TutorialStep::DarklandsHealth
|
const TutorialStep step = server_.snapshot()->tutorialStep;
|
||||||
|| gameState_.tutorialStep == TutorialStep::DarklandsExplore)
|
if (step == TutorialStep::DarklandsHealth || step == TutorialStep::DarklandsExplore)
|
||||||
gameState_.tutorialStep = TutorialStep::DarklandsAttack;
|
server_.submit(SetTutorialStepCommand{TutorialStep::DarklandsAttack});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::onEnemyKilledInUniInterior()
|
void MenuManager::onEnemyKilledInUniInterior()
|
||||||
{
|
{
|
||||||
// Every location's enemies report here; only the university interior drives the tutorial.
|
// 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.
|
// The last tutorial step; like the attack hint, also ends the earlier darklands hints.
|
||||||
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
if (state->tutorialStep >= TutorialStep::DarklandsHealth)
|
||||||
gameState_.tutorialStep = TutorialStep::Completed;
|
server_.submit(SetTutorialStepCommand{TutorialStep::Completed});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::updateHealthBar(float hp, float maxHp) {
|
void MenuManager::updateHealthBar(float hp, float maxHp) {
|
||||||
gameState_.playerHp = hp;
|
server_.submit(SetPlayerHealthCommand{hp, maxHp});
|
||||||
gameState_.playerMaxHp = maxHp;
|
|
||||||
applyCurrentHealthBar();
|
applyCurrentHealthBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2191,21 +2222,30 @@ namespace FRG {
|
|||||||
cutsceneHudActive_ = false;
|
cutsceneHudActive_ = false;
|
||||||
topUiManager.replaceRoot(nullptr);
|
topUiManager.replaceRoot(nullptr);
|
||||||
if (uiState_ == GameUiState::Gameplay)
|
if (uiState_ == GameUiState::Gameplay)
|
||||||
onLocationChanged(gameState_.currentLocationName);
|
onLocationChanged(server_.snapshot()->currentLocationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::applyCurrentHealthBar() {
|
void MenuManager::applyCurrentHealthBar() {
|
||||||
logger() << "MenuManager::applyCurrentHealthBar called step 1" << std::endl;
|
logger() << "MenuManager::applyCurrentHealthBar called step 1" << std::endl;
|
||||||
logger() << "currentPlayerMaxHp_ is " << gameState_.playerMaxHp << std::endl;
|
const auto snapshot = server_.snapshot();
|
||||||
if (gameState_.playerMaxHp <= 0.f) return;
|
if (!snapshot) return;
|
||||||
logger() << "MenuManager::applyCurrentHealthBar called step 2" << std::endl;
|
|
||||||
logger() << "currentPlayerHp_ is " << gameState_.playerHp << std::endl;
|
|
||||||
|
|
||||||
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);
|
uiManager.setSliderValue("healthBarFill", fraction);
|
||||||
std::string hpText = std::to_string(static_cast<int>(gameState_.playerHp)) + "/" +
|
std::string hpText = std::to_string(static_cast<int>(hp)) + "/" +
|
||||||
std::to_string(static_cast<int>(gameState_.playerMaxHp));
|
std::to_string(static_cast<int>(maxHp));
|
||||||
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
|
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
|
||||||
uiManager.setText("healthValue", hpText);
|
uiManager.setText("healthValue", hpText);
|
||||||
}
|
}
|
||||||
@@ -2447,7 +2487,7 @@ namespace FRG {
|
|||||||
|
|
||||||
openInventory();
|
openInventory();
|
||||||
|
|
||||||
const int itemCount = static_cast<int>(inventory->getItems().size());
|
const int itemCount = static_cast<int>(inventoryItems().size());
|
||||||
if (isPortraitMode() && wasOnItemDetail &&
|
if (isPortraitMode() && wasOnItemDetail &&
|
||||||
savedIndex >= 0 && savedIndex < itemCount) {
|
savedIndex >= 0 && savedIndex < itemCount) {
|
||||||
selectInventoryItem(savedIndex);
|
selectInventoryItem(savedIndex);
|
||||||
|
|||||||
+12
-11
@@ -3,8 +3,9 @@
|
|||||||
#include "Environment.h"
|
#include "Environment.h"
|
||||||
#include "render/TextureManager.h"
|
#include "render/TextureManager.h"
|
||||||
#include "UiManager.h"
|
#include "UiManager.h"
|
||||||
|
#include "Localization.h"
|
||||||
#include "items/Item.h"
|
#include "items/Item.h"
|
||||||
#include "GameState.h"
|
#include "game_server/IGameServer.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -39,9 +40,9 @@ namespace FRG {
|
|||||||
std::unordered_map<std::string,
|
std::unordered_map<std::string,
|
||||||
std::unordered_map<Language, std::string>> localizedLocationName;
|
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 openInventory();
|
||||||
void selectInventoryItem(int index);
|
void selectInventoryItem(int index);
|
||||||
@@ -56,6 +57,8 @@ namespace FRG {
|
|||||||
|
|
||||||
void showMainMenu();
|
void showMainMenu();
|
||||||
void showMenuAfterGameCompleted();
|
void showMenuAfterGameCompleted();
|
||||||
|
// Returns to the main menu with the campaign list already open.
|
||||||
|
void showCampaignScreen();
|
||||||
bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; }
|
bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; }
|
||||||
|
|
||||||
void openPhoneScreen();
|
void openPhoneScreen();
|
||||||
@@ -80,7 +83,7 @@ namespace FRG {
|
|||||||
void setChatUnread(int chatIndex, bool unread, const std::string& previewMsg = "");
|
void setChatUnread(int chatIndex, bool unread, const std::string& previewMsg = "");
|
||||||
|
|
||||||
void spendMoney(int amount);
|
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).
|
// Starts a new game of the given campaign (see CampaignRegistry).
|
||||||
std::function<void(const std::string& campaignId)> startGameFunc;
|
std::function<void(const std::string& campaignId)> startGameFunc;
|
||||||
@@ -89,7 +92,6 @@ namespace FRG {
|
|||||||
std::function<void()> startNightTransitionFunc;
|
std::function<void()> startNightTransitionFunc;
|
||||||
std::function<void(int)> chatOpenCallback;
|
std::function<void(int)> chatOpenCallback;
|
||||||
std::function<void()> skipCutsceneFunc;
|
std::function<void()> skipCutsceneFunc;
|
||||||
std::function<void()> callTaxiFunc;
|
|
||||||
std::function<void(int)> onSaveGame;
|
std::function<void(int)> onSaveGame;
|
||||||
std::function<void(int)> onLoadGame;
|
std::function<void(int)> onLoadGame;
|
||||||
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
||||||
@@ -172,14 +174,14 @@ namespace FRG {
|
|||||||
Renderer& renderer;
|
Renderer& renderer;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GameState& gameState_;
|
IGameServer& server_;
|
||||||
AudioPlayerAsync& audioPlayer_;
|
AudioPlayerAsync& audioPlayer_;
|
||||||
|
|
||||||
void startNewGame(const std::string& campaignId);
|
void startNewGame(const std::string& campaignId);
|
||||||
// HUD layout for the current location and darklands mode.
|
// HUD layout for the current location and darklands mode.
|
||||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||||
// Single place that decides which HUD buttons and tutorial hints are visible:
|
// 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.
|
// tutorial flags. Runs every frame; names absent from the shown screen are skipped.
|
||||||
void refreshHudVisibility();
|
void refreshHudVisibility();
|
||||||
void refreshQuestJournalUi();
|
void refreshQuestJournalUi();
|
||||||
@@ -223,8 +225,6 @@ namespace FRG {
|
|||||||
void showLanguageScreen();
|
void showLanguageScreen();
|
||||||
void showLoadGameScreen();
|
void showLoadGameScreen();
|
||||||
void showSaveGameScreen();
|
void showSaveGameScreen();
|
||||||
// Campaign list opened by "Start game".
|
|
||||||
void showCampaignScreen();
|
|
||||||
// Text of a load/save slot button.
|
// Text of a load/save slot button.
|
||||||
std::string slotLabel(const SaveSlotInfo& info);
|
std::string slotLabel(const SaveSlotInfo& info);
|
||||||
|
|
||||||
@@ -249,7 +249,8 @@ namespace FRG {
|
|||||||
static constexpr float TOAST_VISIBLE_MS = 2000.0f;
|
static constexpr float TOAST_VISIBLE_MS = 2000.0f;
|
||||||
|
|
||||||
GameUiState uiState_ = GameUiState::Gameplay;
|
GameUiState uiState_ = GameUiState::Gameplay;
|
||||||
Inventory* inventory = nullptr;
|
std::vector<Item> inventoryItems() const;
|
||||||
|
bool hasInventoryItem(const std::string& id) const;
|
||||||
bool isPlayerInCreditsMenu = false;
|
bool isPlayerInCreditsMenu = false;
|
||||||
int inventorySelectedIndex_ = -1;
|
int inventorySelectedIndex_ = -1;
|
||||||
|
|
||||||
@@ -336,7 +337,7 @@ namespace FRG {
|
|||||||
enum class PhoneSubScreen { None, Main, Bank, Video, MapDorm, MapUni, ChatList, Chat };
|
enum class PhoneSubScreen { None, Main, Bank, Video, MapDorm, MapUni, ChatList, Chat };
|
||||||
PhoneSubScreen currentPhoneSubScreen_ = PhoneSubScreen::None;
|
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 {
|
struct PhoneChatBubbleInfo {
|
||||||
std::string nodeName;
|
std::string nodeName;
|
||||||
float height;
|
float height;
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "render/Renderer.h"
|
#include <Eigen/Core>
|
||||||
#include "Environment.h"
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
struct NpcCar
|
struct NpcCar
|
||||||
|
|||||||
+304
-347
File diff suppressed because it is too large.
Load diff
+9
-24
@@ -1,9 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
|
||||||
#include <functional>
|
|
||||||
#include "quest/QuestJournal.h"
|
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
#include "AudioPlayerAsync.h"
|
#include "AudioPlayerAsync.h"
|
||||||
@@ -11,37 +8,33 @@
|
|||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
class Location;
|
class Location;
|
||||||
class Inventory;
|
|
||||||
|
|
||||||
class ScriptEngine : public ISaveable {
|
class ScriptEngine {
|
||||||
public:
|
public:
|
||||||
ScriptEngine();
|
ScriptEngine();
|
||||||
~ScriptEngine();
|
~ScriptEngine();
|
||||||
|
|
||||||
// Must be called once, after the Game's NPCs are ready.
|
// 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.
|
// Execute a Lua script file. Logs errors to stderr.
|
||||||
//void runScript(const std::string& path);
|
//void runScript(const std::string& path);
|
||||||
|
|
||||||
void callItemPickupCallback(const std::string& objectName);
|
void callItemPickupCallback(const std::string& objectName);
|
||||||
|
void callFunction(const std::string& functionName);
|
||||||
void callActivateFunction(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 callTriggerEnterCallback(const std::string& zoneId);
|
||||||
void callTriggerExitCallback(const std::string& zoneId);
|
void callTriggerExitCallback(const std::string& zoneId);
|
||||||
|
|
||||||
// Fired when the player (moving) physically bumps into a standing NPC.
|
// 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.
|
// Fired when a moving NPC physically bumps into the standing player.
|
||||||
void callNpcBumpsPlayerCallback(int npcIndex);
|
void callNpcBumpsPlayerCallback(const std::string& npcId);
|
||||||
|
|
||||||
void setGlobalStore(std::unordered_map<std::string, int>* store);
|
|
||||||
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
|
|
||||||
void setQuestJournal(Quest::QuestJournal* journal);
|
|
||||||
|
|
||||||
|
void callLocationInitCallback();
|
||||||
void callLocationEnterCallback();
|
void callLocationEnterCallback();
|
||||||
void callLocationExitCallback();
|
void callLocationExitCallback();
|
||||||
|
|
||||||
@@ -63,16 +56,8 @@ public:
|
|||||||
// has already run and registered all callbacks.
|
// has already run and registered all callbacks.
|
||||||
void loadScriptGlobals(const nlohmann::json& in);
|
void loadScriptGlobals(const nlohmann::json& in);
|
||||||
|
|
||||||
// Re-wires an in-flight, one-shot callback (NPC/player walk-arrival, object
|
void save(nlohmann::json& out) const { saveScriptGlobals(out); }
|
||||||
// animation-complete) that was serialized by name rather than by value.
|
void load(const nlohmann::json& in) { loadScriptGlobals(in); }
|
||||||
// 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); }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Impl;
|
struct Impl;
|
||||||
|
|||||||
+3
-3
@@ -51,13 +51,13 @@ namespace FRG {
|
|||||||
std::string shaderProgramName;
|
std::string shaderProgramName;
|
||||||
|
|
||||||
// Sparks spawn within this radius of each emission point. 0 means
|
// 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;
|
float emissionRadius;
|
||||||
|
|
||||||
bool configured;
|
bool configured;
|
||||||
// Rebuilds the CPU-side quad/UV arrays. If cameraViewMatrix is identity,
|
// Rebuilds the CPU-side quad/UV arrays. If cameraViewMatrix is identity,
|
||||||
// particles are rendered as axis-aligned quads in world XY and sorted by
|
// 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
|
// camera using its world-space right/up axes, and particles are sorted
|
||||||
// back-to-front in eye space for correct alpha blending.
|
// back-to-front in eye space for correct alpha blending.
|
||||||
void prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix);
|
void prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix);
|
||||||
@@ -106,7 +106,7 @@ namespace FRG {
|
|||||||
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
|
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
|
||||||
// Pass the current camera view matrix (world → eye) to billboard sparks
|
// Pass the current camera view matrix (world → eye) to billboard sparks
|
||||||
// toward the camera and sort them back-to-front. Identity (default)
|
// 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 prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix = Eigen::Matrix4f::Identity());
|
||||||
|
|
||||||
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
|
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
|
||||||
|
|||||||
+2
-2
@@ -287,10 +287,10 @@ namespace FRG {
|
|||||||
float x1, u1;
|
float x1, u1;
|
||||||
if (fillMode) {
|
if (fillMode) {
|
||||||
const float clamped = std::clamp(value, 0.0f, 1.0f);
|
const float clamped = std::clamp(value, 0.0f, 1.0f);
|
||||||
if (clamped <= 0.0f) {
|
/*if (clamped <= 0.0f) {
|
||||||
trackMesh.RefreshVBO();
|
trackMesh.RefreshVBO();
|
||||||
return;
|
return;
|
||||||
}
|
}*/
|
||||||
x1 = rect.x + rect.w * clamped;
|
x1 = rect.x + rect.w * clamped;
|
||||||
u1 = clamped;
|
u1 = clamped;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
#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.interactionRadius = interactionRadius;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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,132 @@
|
|||||||
|
#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 interactionRadius = 0.f;
|
||||||
|
|
||||||
|
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) {
|
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 {
|
int DialogueRuntime::getFlag(const std::string& name) const {
|
||||||
if (!flagStore) return 0;
|
return getGlobalFlag ? getGlobalFlag(name) : 0;
|
||||||
auto it = flagStore->find(name);
|
|
||||||
return (it != flagStore->end()) ? it->second : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
void DialogueRuntime::setGlobalFlagAccessors(
|
||||||
flagStore = store;
|
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) {
|
void DialogueRuntime::setQuestActionHandler(
|
||||||
questJournal = journal;
|
std::function<void(const QuestActionRequest&)> handler) {
|
||||||
|
onQuestAction = std::move(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::applyQuestActions(
|
void DialogueRuntime::applyQuestActions(
|
||||||
@@ -184,17 +187,20 @@ void DialogueRuntime::applyQuestActions(
|
|||||||
const std::string& questFail, const std::string& objectiveComplete,
|
const std::string& questFail, const std::string& objectiveComplete,
|
||||||
const std::string& objectiveVisible)
|
const std::string& objectiveVisible)
|
||||||
{
|
{
|
||||||
if (!questJournal) return;
|
if (!onQuestAction) return;
|
||||||
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
|
if (!questUnlock.empty())
|
||||||
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
|
onQuestAction({QuestActionType::Unlock, questUnlock, {}});
|
||||||
if (!questFail.empty()) questJournal->failQuest(questFail);
|
if (!questComplete.empty())
|
||||||
|
onQuestAction({QuestActionType::Complete, questComplete, {}});
|
||||||
|
if (!questFail.empty())
|
||||||
|
onQuestAction({QuestActionType::Fail, questFail, {}});
|
||||||
if (!objectiveComplete.empty()) {
|
if (!objectiveComplete.empty()) {
|
||||||
auto [qId, oId] = splitDot(objectiveComplete);
|
auto [qId, oId] = splitDot(objectiveComplete);
|
||||||
questJournal->setObjectiveCompleted(qId, oId);
|
onQuestAction({QuestActionType::CompleteObjective, qId, oId});
|
||||||
}
|
}
|
||||||
if (!objectiveVisible.empty()) {
|
if (!objectiveVisible.empty()) {
|
||||||
auto [qId, oId] = splitDot(objectiveVisible);
|
auto [qId, oId] = splitDot(objectiveVisible);
|
||||||
questJournal->setObjectiveVisible(qId, oId);
|
onQuestAction({QuestActionType::ShowObjective, qId, oId});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
#include "dialogue/DialogueDatabase.h"
|
#include "dialogue/DialogueDatabase.h"
|
||||||
#include "dialogue/TranslationDatabase.h"
|
#include "dialogue/TranslationDatabase.h"
|
||||||
#include "quest/QuestJournal.h"
|
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace FRG::Dialogue {
|
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 {
|
class DialogueRuntime : public FRG::ISaveable {
|
||||||
public:
|
public:
|
||||||
void setDatabase(const DialogueDatabase* value);
|
void setDatabase(const DialogueDatabase* value);
|
||||||
@@ -41,8 +53,11 @@ public:
|
|||||||
|
|
||||||
void setFlag(const std::string& name, int value);
|
void setFlag(const std::string& name, int value);
|
||||||
int getFlag(const std::string& name) const;
|
int getFlag(const std::string& name) const;
|
||||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
|
void setGlobalFlagAccessors(
|
||||||
void setQuestJournal(Quest::QuestJournal* journal);
|
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.
|
// Saves whether a dialogue is active, its id, and current node.
|
||||||
// Cutscene state is not saved.
|
// 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&, const std::string&)> onCutsceneStartNeeded;
|
||||||
std::function<void(const std::string&)> onDialogueLineStarted;
|
std::function<void(const std::string&)> onDialogueLineStarted;
|
||||||
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
||||||
|
std::function<void(const QuestActionRequest&)> onQuestAction;
|
||||||
|
|
||||||
const DialogueDatabase* database = nullptr;
|
const DialogueDatabase* database = nullptr;
|
||||||
const TranslationDatabase* translations = nullptr;
|
const TranslationDatabase* translations = nullptr;
|
||||||
Quest::QuestJournal* questJournal = nullptr;
|
|
||||||
const DialogueDefinition* activeDialogue = 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::unordered_set<std::string> consumedChoices;
|
||||||
|
|
||||||
std::string currentNodeId;
|
std::string currentNodeId;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include "cutscene/CutsceneDatabase.h"
|
#include "cutscene/CutsceneDatabase.h"
|
||||||
#include "cutscene/CutsceneOverlay.h"
|
#include "cutscene/CutsceneOverlay.h"
|
||||||
#include "cutscene/CutsceneRuntime.h"
|
#include "cutscene/CutsceneRuntime.h"
|
||||||
#include "quest/QuestJournal.h"
|
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -14,7 +13,7 @@
|
|||||||
|
|
||||||
namespace FRG::Dialogue {
|
namespace FRG::Dialogue {
|
||||||
|
|
||||||
class DialogueSystem : public FRG::ISaveable {
|
class DialogueSystem {
|
||||||
public:
|
public:
|
||||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||||
|
|
||||||
@@ -48,8 +47,8 @@ public:
|
|||||||
void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); }
|
void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); }
|
||||||
void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); }
|
void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); }
|
||||||
|
|
||||||
void save(nlohmann::json& out) const override { saveDialogueState(out); }
|
void save(nlohmann::json& out) const { saveDialogueState(out); }
|
||||||
void load(const nlohmann::json& in) override { loadDialogueState(in); }
|
void load(const nlohmann::json& in) { loadDialogueState(in); }
|
||||||
|
|
||||||
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
|
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
|
||||||
bool isDialogueActive() const { return dialogueRuntime.isActive(); }
|
bool isDialogueActive() const { return dialogueRuntime.isActive(); }
|
||||||
@@ -58,11 +57,15 @@ public:
|
|||||||
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
|
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
|
||||||
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
|
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
|
||||||
|
|
||||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
void setGlobalFlagAccessors(
|
||||||
dialogueRuntime.setGlobalFlagStore(store);
|
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) {
|
void setQuestActionHandler(
|
||||||
dialogueRuntime.setQuestJournal(journal);
|
std::function<void(const QuestActionRequest&)> handler) {
|
||||||
|
dialogueRuntime.setQuestActionHandler(std::move(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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,304 @@
|
|||||||
|
#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 SetPlayerHealthCommand { float hp; float maxHp; };
|
||||||
|
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,
|
||||||
|
SetPlayerHealthCommand,
|
||||||
|
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,562 @@
|
|||||||
|
#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, SetPlayerHealthCommand>) {
|
||||||
|
state_.playerHp = value.hp;
|
||||||
|
state_.playerMaxHp = value.maxHp;
|
||||||
|
} 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) player->forceReplan(locationState, id);
|
||||||
|
for (const auto& [id, npc] : location->second.npcs)
|
||||||
|
if (npc) npc->forceReplan(locationState, id);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
CharacterState* character =
|
||||||
|
state_.findCharacter(value.locationId, value.characterId);
|
||||||
|
if (character && location != state_.locations.end() &&
|
||||||
|
location->second.location) {
|
||||||
|
character->setTarget(
|
||||||
|
value.target, value.callbackName,
|
||||||
|
*location->second.location, value.characterId);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
CharacterState* character =
|
||||||
|
state_.findCharacter(value.locationId, value.characterId);
|
||||||
|
if (character && location != state_.locations.end() &&
|
||||||
|
location->second.location) {
|
||||||
|
character->forceReplan(
|
||||||
|
*location->second.location, value.characterId);
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
CharacterState* character = state_.findCharacter(
|
||||||
|
state_.currentLocationName, event.characterId);
|
||||||
|
if (character && stored->second.location) {
|
||||||
|
character->setTarget(
|
||||||
|
event.events.navigationTarget, {},
|
||||||
|
*stored->second.location, event.characterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,329 @@
|
|||||||
|
#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()});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
commands.push_back({
|
||||||
|
{"type", "character_enabled"},
|
||||||
|
{"commandSequence", command.commandSequence},
|
||||||
|
{"locationId", command.locationId},
|
||||||
|
{"characterId", command.characterId},
|
||||||
|
{"enabled", command.enabled}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 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 {
|
||||||
|
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,92 @@
|
|||||||
|
#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 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ScheduledCommand = std::variant<
|
||||||
|
ScheduledDestinationCommand,
|
||||||
|
ScheduledCharacterEnabledCommand>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 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,319 @@
|
|||||||
|
#include "WebsocketGameClient.h"
|
||||||
|
#include "NetworkProtocol.h"
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/beast.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <deque>
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace FRG {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace asio = boost::asio;
|
||||||
|
namespace beast = boost::beast;
|
||||||
|
namespace websocket = beast::websocket;
|
||||||
|
using tcp = asio::ip::tcp;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class WebsocketGameClient::Impl {
|
||||||
|
public:
|
||||||
|
Impl(std::string host, std::string port, std::string path)
|
||||||
|
: resolver_(io_)
|
||||||
|
, socket_(io_)
|
||||||
|
, host_(std::move(host))
|
||||||
|
, port_(std::move(port))
|
||||||
|
, path_(std::move(path))
|
||||||
|
{
|
||||||
|
std::cout << "[network] Resolving ws://" << 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:
|
||||||
|
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;
|
||||||
|
asio::async_connect(
|
||||||
|
socket_.next_layer(), results,
|
||||||
|
[this](beast::error_code connectError,
|
||||||
|
const tcp::endpoint&) {
|
||||||
|
if (connectError) return fail("connect", connectError);
|
||||||
|
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
||||||
|
<< std::endl;
|
||||||
|
socket_.set_option(
|
||||||
|
websocket::stream_base::timeout::suggested(
|
||||||
|
beast::role_type::client));
|
||||||
|
socket_.async_handshake(
|
||||||
|
host_, path_,
|
||||||
|
[this](beast::error_code handshakeError) {
|
||||||
|
if (handshakeError)
|
||||||
|
return fail("handshake", handshakeError);
|
||||||
|
connected_.store(true);
|
||||||
|
std::cout
|
||||||
|
<< "[network] Connected to ws://"
|
||||||
|
<< host_ << ':' << port_ << path_ << std::endl;
|
||||||
|
read();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void read()
|
||||||
|
{
|
||||||
|
socket_.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()
|
||||||
|
{
|
||||||
|
socket_.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_;
|
||||||
|
tcp::resolver resolver_;
|
||||||
|
websocket::stream<tcp::socket> socket_;
|
||||||
|
beast::flat_buffer buffer_;
|
||||||
|
std::thread thread_;
|
||||||
|
std::string host_;
|
||||||
|
std::string port_;
|
||||||
|
std::string path_;
|
||||||
|
std::atomic_bool connected_{false};
|
||||||
|
std::atomic_bool connectionFailed_{false};
|
||||||
|
std::mutex messagesMutex_;
|
||||||
|
std::vector<std::string> messages_;
|
||||||
|
std::deque<std::string> outbound_;
|
||||||
|
};
|
||||||
|
|
||||||
|
WebsocketGameClient::WebsocketGameClient(
|
||||||
|
std::string host, std::string port, std::string path)
|
||||||
|
: impl_(std::make_unique<Impl>(
|
||||||
|
std::move(host), std::move(port), std::move(path)))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
replica_.submit(SetCharacterEnabledCommand{
|
||||||
|
command.locationId, command.characterId, command.enabled});
|
||||||
|
}
|
||||||
|
}, 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,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IGameServer.h"
|
||||||
|
#include "InternalGameServer.h"
|
||||||
|
#include "NetworkProtocol.h"
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace FRG {
|
||||||
|
|
||||||
|
class RemoteGameServerHost;
|
||||||
|
|
||||||
|
class WebsocketGameClient final : public IGameServer {
|
||||||
|
public:
|
||||||
|
WebsocketGameClient(
|
||||||
|
std::string host = "127.0.0.1",
|
||||||
|
std::string port = "8080",
|
||||||
|
std::string path = "/");
|
||||||
|
~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,363 @@
|
|||||||
|
#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;
|
||||||
|
DestinationInputMessage 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 parsed;
|
||||||
|
std::string parseError;
|
||||||
|
if (!parseDestinationInputMessage(
|
||||||
|
message, parsed, parseError)) {
|
||||||
|
source->send(makeErrorMessage(
|
||||||
|
"invalid_input", parseError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||||
|
pendingInputs_.push_back(
|
||||||
|
{source->playerId(), std::move(parsed)});
|
||||||
|
},
|
||||||
|
[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];
|
||||||
|
if (pending.input.clientSequence <= previous) continue;
|
||||||
|
previous = pending.input.clientSequence;
|
||||||
|
timeline_.schedule(pending.playerId, pending.input.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
gameServer_.submit(SetCharacterEnabledCommand{
|
||||||
|
command.locationId, command.characterId, command.enabled});
|
||||||
|
}
|
||||||
|
}, 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 <Eigen/Geometry>
|
||||||
#include "../Character.h"
|
#include "../Character.h"
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
@@ -91,6 +92,9 @@ namespace FRG {
|
|||||||
|
|
||||||
for (const auto& item : j["objects"]) {
|
for (const auto& item : j["objects"]) {
|
||||||
InteractiveObjectData data;
|
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.base = parseGameObjectData(item);
|
||||||
data.interactionRadius = item.value("interactionRadius", 2.0f);
|
data.interactionRadius = item.value("interactionRadius", 2.0f);
|
||||||
data.approachRadius = item.value("approachRadius", data.interactionRadius);
|
data.approachRadius = item.value("approachRadius", data.interactionRadius);
|
||||||
@@ -98,20 +102,21 @@ namespace FRG {
|
|||||||
data.castShadowNight = item.value("castShadowNight", true);
|
data.castShadowNight = item.value("castShadowNight", true);
|
||||||
data.isActive = item.value("isActive", true);
|
data.isActive = item.value("isActive", true);
|
||||||
data.activateFunctionName = item.value("activateFunction", "");
|
data.activateFunctionName = item.value("activateFunction", "");
|
||||||
data.pivotX = item.value("pivotX", 0.0f);
|
data.pivot = {
|
||||||
data.pivotY = item.value("pivotY", 0.0f);
|
item.value("pivotX", 0.0f), item.value("pivotY", 0.0f),
|
||||||
data.pivotZ = item.value("pivotZ", 0.0f);
|
item.value("pivotZ", 0.0f)};
|
||||||
data.boundsMinX = item.value("boundsMinX", 0.0f);
|
data.boundsMin = {
|
||||||
data.boundsMinY = item.value("boundsMinY", 0.0f);
|
item.value("boundsMinX", 0.0f), item.value("boundsMinY", 0.0f),
|
||||||
data.boundsMinZ = item.value("boundsMinZ", 0.0f);
|
item.value("boundsMinZ", 0.0f)};
|
||||||
data.boundsMaxX = item.value("boundsMaxX", 0.0f);
|
data.boundsMax = {
|
||||||
data.boundsMaxY = item.value("boundsMaxY", 0.0f);
|
item.value("boundsMaxX", 0.0f), item.value("boundsMaxY", 0.0f),
|
||||||
data.boundsMaxZ = item.value("boundsMaxZ", 0.0f);
|
item.value("boundsMaxZ", 0.0f)};
|
||||||
if (item.contains("interactionPositionX")) {
|
if (item.contains("interactionPositionX")) {
|
||||||
data.hasInteractionPosition = true;
|
data.hasInteractionPosition = true;
|
||||||
data.interactionPositionX = item.value("interactionPositionX", 0.0f);
|
data.interactionPosition = {
|
||||||
data.interactionPositionY = item.value("interactionPositionY", 0.0f);
|
item.value("interactionPositionX", 0.0f),
|
||||||
data.interactionPositionZ = item.value("interactionPositionZ", 0.0f);
|
item.value("interactionPositionY", 0.0f),
|
||||||
|
item.value("interactionPositionZ", 0.0f)};
|
||||||
}
|
}
|
||||||
if (!data.base.meshPath.empty())
|
if (!data.base.meshPath.empty())
|
||||||
objects.push_back(std::move(data));
|
objects.push_back(std::move(data));
|
||||||
@@ -121,40 +126,6 @@ namespace FRG {
|
|||||||
return objects;
|
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)
|
static VertexDataStruct loadMeshData(const std::string& meshPath, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
||||||
@@ -209,20 +180,18 @@ namespace FRG {
|
|||||||
// Public API
|
// Public API
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
StaticObjectSet GameObjectLoader::createGameObjects(
|
||||||
const std::string& jsonPath,
|
const std::vector<GameObjectData>& definitions,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
StaticObjectSet result;
|
StaticObjectSet result;
|
||||||
|
|
||||||
std::vector<GameObjectData> allData = loadFromJson(jsonPath, zipPath);
|
|
||||||
|
|
||||||
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
||||||
std::vector<std::vector<const GameObjectData*>> groups;
|
std::vector<std::vector<const GameObjectData*>> groups;
|
||||||
std::unordered_map<std::string, size_t> groupIndexByKey;
|
std::unordered_map<std::string, size_t> groupIndexByKey;
|
||||||
|
|
||||||
for (const auto& data : allData) {
|
for (const auto& data : definitions) {
|
||||||
if (!data.instanced) {
|
if (!data.instanced) {
|
||||||
groups.push_back({ &data });
|
groups.push_back({ &data });
|
||||||
continue;
|
continue;
|
||||||
@@ -290,22 +259,33 @@ namespace FRG {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||||
const std::string& jsonPath,
|
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,
|
Renderer& renderer,
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<InteractiveObject> interactiveObjects;
|
std::vector<InteractiveObject> interactiveObjects;
|
||||||
|
for (const auto& data : definitions) {
|
||||||
if (jsonPath.empty())
|
|
||||||
return interactiveObjects;
|
|
||||||
|
|
||||||
for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) {
|
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
||||||
try {
|
try {
|
||||||
InteractiveObject intObj = InteractiveObject::createFromState(
|
auto state = states.find(data.id);
|
||||||
data.toInteractiveObjectState(), renderer, zipPath);
|
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;
|
intObj.loadedObject.name = data.base.name;
|
||||||
|
|
||||||
if (!data.activateFunctionName.empty())
|
if (!data.activateFunctionName.empty())
|
||||||
@@ -324,6 +304,18 @@ namespace FRG {
|
|||||||
return interactiveObjects;
|
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> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<NpcData> npcs;
|
std::vector<NpcData> npcs;
|
||||||
@@ -338,7 +330,9 @@ namespace FRG {
|
|||||||
for (const auto& item : j["npcs"]) {
|
for (const auto& item : j["npcs"]) {
|
||||||
NpcData data;
|
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.name = item.value("name", "Unknown NPC");
|
||||||
data.texturePath = item.value("texturePath", "");
|
data.texturePath = item.value("texturePath", "");
|
||||||
if (item.contains("meshTextures") && item["meshTextures"].is_object()) {
|
if (item.contains("meshTextures") && item["meshTextures"].is_object()) {
|
||||||
@@ -376,88 +370,59 @@ namespace FRG {
|
|||||||
logger() << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl;
|
logger() << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl;
|
std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl;
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
return npcs;
|
return npcs;
|
||||||
}
|
}
|
||||||
|
|
||||||
CharacterState NpcData::toCharacterState() const {
|
std::vector<std::unique_ptr<Character>> GameObjectLoader::createNpcs(
|
||||||
CharacterState st;
|
const std::vector<NpcData>& definitions,
|
||||||
|
|
||||||
// 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,
|
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
|
const std::unordered_map<std::string,
|
||||||
|
std::shared_ptr<CharacterState>>& states,
|
||||||
|
IGameServer& server,
|
||||||
|
const std::string& locationId,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<std::unique_ptr<Character>> npcs;
|
std::vector<std::unique_ptr<Character>> npcs;
|
||||||
|
|
||||||
int index = 0;
|
for (const auto& npcData : definitions) {
|
||||||
|
|
||||||
for (const auto& npcData : loadNpcsFromJson(jsonPath, zipPath)) {
|
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
logger() << "Loading NPC: " << npcData.name << std::endl;
|
logger() << "Loading NPC: " << npcData.name << std::endl;
|
||||||
try {
|
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) {
|
if (npc) {
|
||||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||||
npc->index = index;
|
|
||||||
npcs.push_back(std::move(npc));
|
npcs.push_back(std::move(npc));
|
||||||
index = index + 1;
|
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "GameObjectLoader: Failed to load NPC '" << npcData.name << "': " << e.what() << std::endl;
|
std::cerr << "GameObjectLoader: Failed to load NPC '" << npcData.name << "': " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger() << "Total NPCs loaded: " << npcs.size() << std::endl;
|
logger() << "Total NPCs loaded: " << npcs.size() << std::endl;
|
||||||
return npcs;
|
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
|
} // namespace FRG
|
||||||
@@ -9,30 +9,13 @@
|
|||||||
#include "InteractiveObject.h"
|
#include "InteractiveObject.h"
|
||||||
#include "InteractiveObjectState.h"
|
#include "InteractiveObjectState.h"
|
||||||
#include "../CharacterState.h"
|
#include "../CharacterState.h"
|
||||||
|
#include "../content/LocationDefinition.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
class IGameServer;
|
||||||
|
|
||||||
class Character;
|
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,
|
// Result of loading a static object list: unique objects keep their own mesh,
|
||||||
// repeated ones are merged into instanced batches.
|
// repeated ones are merged into instanced batches.
|
||||||
struct StaticObjectSet {
|
struct StaticObjectSet {
|
||||||
@@ -40,62 +23,25 @@ namespace FRG {
|
|||||||
std::vector<InstancedBatch> batches;
|
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 {
|
class GameObjectLoader {
|
||||||
public:
|
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(
|
static StaticObjectSet loadAndCreateGameObjects(
|
||||||
const std::string& jsonPath,
|
const std::string& jsonPath,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
@@ -105,13 +51,17 @@ namespace FRG {
|
|||||||
static std::vector<InteractiveObject> loadAndCreateInteractiveObjects(
|
static std::vector<InteractiveObject> loadAndCreateInteractiveObjects(
|
||||||
const std::string& jsonPath,
|
const std::string& jsonPath,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||||
const std::string& zipPath = ""
|
const std::string& zipPath = ""
|
||||||
);
|
);
|
||||||
|
|
||||||
static std::vector<std::unique_ptr<FRG::Character>> loadAndCreate_Npcs(
|
static std::vector<std::unique_ptr<FRG::Character>> loadAndCreate_Npcs(
|
||||||
const std::string& jsonPath,
|
const std::string& jsonPath,
|
||||||
Renderer& renderer,
|
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 = "");
|
static std::vector<NpcData> loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath = "");
|
||||||
|
|||||||
+90
-184
@@ -5,6 +5,8 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <Eigen/Geometry>
|
#include <Eigen/Geometry>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
@@ -19,12 +21,11 @@ namespace FRG {
|
|||||||
using std::max;
|
using std::max;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
InteractiveObject InteractiveObject::createFromState(InteractiveObjectState st,
|
InteractiveObject InteractiveObject::createFromState(InteractiveObjectState& st,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
InteractiveObject obj;
|
InteractiveObject obj(st);
|
||||||
obj.state = std::move(st);
|
|
||||||
|
|
||||||
const LoadedGameObjectState& info = obj.state.objectInfo;
|
const LoadedGameObjectState& info = obj.state.objectInfo;
|
||||||
|
|
||||||
@@ -52,7 +53,8 @@ namespace FRG {
|
|||||||
|
|
||||||
obj.loadedObject.mesh.RefreshVBO();
|
obj.loadedObject.mesh.RefreshVBO();
|
||||||
|
|
||||||
// Center mesh at origin; adjust world position by the centering offset.
|
// Interactive-object definitions store the centered world position.
|
||||||
|
// Center the presentation mesh at the origin without changing state.
|
||||||
if (!obj.loadedObject.mesh.data.PositionData.empty()) {
|
if (!obj.loadedObject.mesh.data.PositionData.empty()) {
|
||||||
Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0];
|
Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0];
|
||||||
Eigen::Vector3f meshMax = obj.loadedObject.mesh.data.PositionData[0];
|
Eigen::Vector3f meshMax = obj.loadedObject.mesh.data.PositionData[0];
|
||||||
@@ -63,131 +65,77 @@ namespace FRG {
|
|||||||
const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f;
|
const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f;
|
||||||
obj.loadedObject.mesh.data.Move(-meshCenter);
|
obj.loadedObject.mesh.data.Move(-meshCenter);
|
||||||
obj.loadedObject.mesh.RefreshVBO();
|
obj.loadedObject.mesh.RefreshVBO();
|
||||||
obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX,
|
|
||||||
obj.state.jsonPositionY,
|
|
||||||
obj.state.jsonPositionZ) + meshCenter;
|
|
||||||
} else {
|
|
||||||
obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX,
|
|
||||||
obj.state.jsonPositionY,
|
|
||||||
obj.state.jsonPositionZ);
|
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
void InteractiveObject::capturePresentationState()
|
||||||
if (state.isAnimating) return;
|
{
|
||||||
AnimTask task;
|
PresentationSample sample;
|
||||||
task.type = AnimTask::Type::Move;
|
sample.position = state.position;
|
||||||
task.startPos = state.position;
|
sample.rotationY = state.rotationY;
|
||||||
task.startRotY = state.rotationY;
|
sample.scale = state.scale;
|
||||||
task.startScale = state.scale;
|
sample.alpha = state.alpha;
|
||||||
task.targetPos = target;
|
sample.isActive = state.isActive;
|
||||||
task.targetRotY = state.rotationY;
|
|
||||||
task.targetScale = state.scale;
|
|
||||||
task.durationMs = durationSec * 1000.f;
|
|
||||||
task.elapsedMs = 0.f;
|
|
||||||
task.onComplete = std::move(onComplete);
|
|
||||||
task.onCompleteName = std::move(callbackName);
|
|
||||||
state.animTask = std::move(task);
|
|
||||||
state.isAnimating = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (state.isAnimating) return;
|
const bool largeGap = hasPresentationSample_ &&
|
||||||
AnimTask task;
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
task.type = AnimTask::Type::Rotate;
|
now - lastPresentationUpdate_).count() > 250;
|
||||||
task.startPos = state.position;
|
if (!hasPresentationSample_ || largeGap ||
|
||||||
task.startRotY = state.rotationY;
|
currentPresentationSample_.isActive != sample.isActive) {
|
||||||
task.startScale = state.scale;
|
previousPresentationSample_ = sample;
|
||||||
task.targetPos = state.position;
|
} else {
|
||||||
task.targetRotY = targetRotY;
|
previousPresentationSample_ = currentPresentationSample_;
|
||||||
task.targetScale = state.scale;
|
|
||||||
task.durationMs = durationSec * 1000.f;
|
|
||||||
task.elapsedMs = 0.f;
|
|
||||||
task.onComplete = std::move(onComplete);
|
|
||||||
task.onCompleteName = std::move(callbackName);
|
|
||||||
state.animTask = std::move(task);
|
|
||||||
state.isAnimating = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
|
||||||
if (state.isAnimating) return;
|
|
||||||
AnimTask task;
|
|
||||||
task.type = AnimTask::Type::Scale;
|
|
||||||
task.startPos = state.position;
|
|
||||||
task.startRotY = state.rotationY;
|
|
||||||
task.startScale = state.scale;
|
|
||||||
task.targetPos = state.position;
|
|
||||||
task.targetRotY = state.rotationY;
|
|
||||||
task.targetScale = targetScale;
|
|
||||||
task.durationMs = durationSec * 1000.f;
|
|
||||||
task.elapsedMs = 0.f;
|
|
||||||
task.onComplete = std::move(onComplete);
|
|
||||||
task.onCompleteName = std::move(callbackName);
|
|
||||||
state.animTask = std::move(task);
|
|
||||||
state.isAnimating = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
|
||||||
if (state.isAnimating) return;
|
|
||||||
AnimTask task;
|
|
||||||
task.type = AnimTask::Type::Fade;
|
|
||||||
task.startPos = state.position;
|
|
||||||
task.startRotY = state.rotationY;
|
|
||||||
task.startScale = state.alpha; // reuse startScale to hold start alpha
|
|
||||||
task.targetPos = state.position;
|
|
||||||
task.targetRotY = state.rotationY;
|
|
||||||
task.targetScale = targetAlpha; // reuse targetScale to hold target alpha
|
|
||||||
task.durationMs = durationSec * 1000.f;
|
|
||||||
task.elapsedMs = 0.f;
|
|
||||||
task.onComplete = std::move(onComplete);
|
|
||||||
task.onCompleteName = std::move(callbackName);
|
|
||||||
state.animTask = std::move(task);
|
|
||||||
state.isAnimating = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractiveObject::update(int64_t deltaMs) {
|
|
||||||
if (!state.isAnimating || !state.animTask) return;
|
|
||||||
|
|
||||||
AnimTask& task = *state.animTask;
|
|
||||||
task.elapsedMs += static_cast<float>(deltaMs);
|
|
||||||
|
|
||||||
const float t = min(task.elapsedMs / task.durationMs, 1.0f);
|
|
||||||
|
|
||||||
switch (task.type) {
|
|
||||||
case AnimTask::Type::Move:
|
|
||||||
state.position = task.startPos + (task.targetPos - task.startPos) * t;
|
|
||||||
break;
|
|
||||||
case AnimTask::Type::Rotate:
|
|
||||||
state.rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
|
||||||
break;
|
|
||||||
case AnimTask::Type::Scale:
|
|
||||||
state.scale = task.startScale + (task.targetScale - task.startScale) * t;
|
|
||||||
break;
|
|
||||||
case AnimTask::Type::Fade:
|
|
||||||
state.alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
currentPresentationSample_ = sample;
|
||||||
|
lastPresentationUpdate_ = now;
|
||||||
|
hasPresentationSample_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (t >= 1.0f) {
|
InteractiveObject::PresentationSample
|
||||||
state.position = task.targetPos;
|
InteractiveObject::interpolatedPresentationState() const
|
||||||
state.rotationY = task.targetRotY;
|
{
|
||||||
if (task.type == AnimTask::Type::Fade)
|
PresentationSample result;
|
||||||
state.alpha = task.targetScale;
|
result.position = state.position;
|
||||||
else
|
result.rotationY = state.rotationY;
|
||||||
state.scale = task.targetScale;
|
result.scale = state.scale;
|
||||||
|
result.alpha = state.alpha;
|
||||||
|
result.isActive = state.isActive;
|
||||||
|
if (!hasPresentationSample_) return result;
|
||||||
|
|
||||||
std::function<void()> cb = std::move(task.onComplete);
|
constexpr float kSimulationTickMs = 100.0f;
|
||||||
state.animTask.reset();
|
const float elapsedMs = static_cast<float>(
|
||||||
state.isAnimating = false;
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - lastPresentationUpdate_).count());
|
||||||
|
const float alpha = (std::max)(0.0f,
|
||||||
|
(std::min)(1.0f, elapsedMs / kSimulationTickMs));
|
||||||
|
|
||||||
if (cb) cb();
|
result.position = previousPresentationSample_.position +
|
||||||
}
|
alpha * (currentPresentationSample_.position -
|
||||||
|
previousPresentationSample_.position);
|
||||||
|
result.scale = previousPresentationSample_.scale +
|
||||||
|
alpha * (currentPresentationSample_.scale -
|
||||||
|
previousPresentationSample_.scale);
|
||||||
|
result.alpha = previousPresentationSample_.alpha +
|
||||||
|
alpha * (currentPresentationSample_.alpha -
|
||||||
|
previousPresentationSample_.alpha);
|
||||||
|
|
||||||
|
constexpr float kPi = 3.14159265358979323846f;
|
||||||
|
constexpr float kTwoPi = 2.0f * kPi;
|
||||||
|
float rotationDelta = currentPresentationSample_.rotationY -
|
||||||
|
previousPresentationSample_.rotationY;
|
||||||
|
while (rotationDelta > kPi) rotationDelta -= kTwoPi;
|
||||||
|
while (rotationDelta < -kPi) rotationDelta += kTwoPi;
|
||||||
|
result.rotationY = previousPresentationSample_.rotationY +
|
||||||
|
alpha * rotationDelta;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveObject::drawDarklands(Renderer& renderer) const {
|
void InteractiveObject::drawDarklands(Renderer& renderer) const {
|
||||||
if (!state.isActive || !loadedObject.textureDarklands) return;
|
if (!state.isActive || !loadedObject.textureDarklands) return;
|
||||||
|
const PresentationSample presentation = interpolatedPresentationState();
|
||||||
|
|
||||||
if (state.alpha < 0.999f) {
|
if (state.alpha < 0.999f) {
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
@@ -195,16 +143,16 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.TranslateMatrix(state.position);
|
renderer.TranslateMatrix(presentation.position);
|
||||||
if (state.rotationY != 0.f) {
|
if (presentation.rotationY != 0.f) {
|
||||||
renderer.TranslateMatrix(state.pivot);
|
renderer.TranslateMatrix(state.pivot);
|
||||||
renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::AngleAxisf(presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix());
|
||||||
renderer.TranslateMatrix(-state.pivot);
|
renderer.TranslateMatrix(-state.pivot);
|
||||||
}
|
}
|
||||||
if (state.scale != 1.f)
|
if (presentation.scale != 1.f)
|
||||||
renderer.ScaleMatrix(state.scale);
|
renderer.ScaleMatrix(presentation.scale);
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
renderer.RenderUniform1f("uAlpha", state.alpha);
|
renderer.RenderUniform1f("uAlpha", presentation.alpha);
|
||||||
glBindTexture(GL_TEXTURE_2D, loadedObject.textureDarklands->getTexID());
|
glBindTexture(GL_TEXTURE_2D, loadedObject.textureDarklands->getTexID());
|
||||||
renderer.DrawVertexRenderStruct(loadedObject.mesh);
|
renderer.DrawVertexRenderStruct(loadedObject.mesh);
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
@@ -214,6 +162,7 @@ namespace FRG {
|
|||||||
|
|
||||||
void InteractiveObject::draw(Renderer& renderer) const {
|
void InteractiveObject::draw(Renderer& renderer) const {
|
||||||
if (!state.isActive || !loadedObject.texture) return;
|
if (!state.isActive || !loadedObject.texture) return;
|
||||||
|
const PresentationSample presentation = interpolatedPresentationState();
|
||||||
|
|
||||||
if (state.alpha < 0.999f) {
|
if (state.alpha < 0.999f) {
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
@@ -221,16 +170,16 @@ namespace FRG {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.TranslateMatrix(state.position);
|
renderer.TranslateMatrix(presentation.position);
|
||||||
if (state.rotationY != 0.f) {
|
if (presentation.rotationY != 0.f) {
|
||||||
renderer.TranslateMatrix(state.pivot);
|
renderer.TranslateMatrix(state.pivot);
|
||||||
renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::AngleAxisf(presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix());
|
||||||
renderer.TranslateMatrix(-state.pivot);
|
renderer.TranslateMatrix(-state.pivot);
|
||||||
}
|
}
|
||||||
if (state.scale != 1.f)
|
if (presentation.scale != 1.f)
|
||||||
renderer.ScaleMatrix(state.scale);
|
renderer.ScaleMatrix(presentation.scale);
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
renderer.RenderUniform1f("uAlpha", state.alpha);
|
renderer.RenderUniform1f("uAlpha", presentation.alpha);
|
||||||
glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID());
|
glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID());
|
||||||
renderer.DrawVertexRenderStruct(loadedObject.mesh);
|
renderer.DrawVertexRenderStruct(loadedObject.mesh);
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
@@ -238,66 +187,23 @@ namespace FRG {
|
|||||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveObjectState::save(nlohmann::json& out) const
|
void InteractiveObject::drawShadowDepth(Renderer& renderer) const
|
||||||
{
|
{
|
||||||
out["positionX"] = position.x();
|
if (!state.isActive) return;
|
||||||
out["positionY"] = position.y();
|
const PresentationSample presentation = interpolatedPresentationState();
|
||||||
out["positionZ"] = position.z();
|
|
||||||
out["rotationY"] = rotationY;
|
|
||||||
out["scale"] = scale;
|
|
||||||
out["alpha"] = alpha;
|
|
||||||
out["isActive"] = isActive;
|
|
||||||
out["isAnimating"] = isAnimating;
|
|
||||||
|
|
||||||
if (isAnimating && animTask.has_value()) {
|
renderer.PushMatrix();
|
||||||
nlohmann::json at;
|
renderer.TranslateMatrix(presentation.position);
|
||||||
at["type"] = static_cast<int>(animTask->type);
|
if (presentation.rotationY != 0.f) {
|
||||||
at["startPosX"] = animTask->startPos.x();
|
renderer.TranslateMatrix(state.pivot);
|
||||||
at["startPosY"] = animTask->startPos.y();
|
renderer.RotateMatrix(Eigen::AngleAxisf(
|
||||||
at["startPosZ"] = animTask->startPos.z();
|
presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix());
|
||||||
at["startRotY"] = animTask->startRotY;
|
renderer.TranslateMatrix(-state.pivot);
|
||||||
at["startScale"] = animTask->startScale;
|
|
||||||
at["targetPosX"] = animTask->targetPos.x();
|
|
||||||
at["targetPosY"] = animTask->targetPos.y();
|
|
||||||
at["targetPosZ"] = animTask->targetPos.z();
|
|
||||||
at["targetRotY"] = animTask->targetRotY;
|
|
||||||
at["targetScale"] = animTask->targetScale;
|
|
||||||
at["durationMs"] = animTask->durationMs;
|
|
||||||
at["elapsedMs"] = animTask->elapsedMs;
|
|
||||||
if (!animTask->onCompleteName.empty())
|
|
||||||
at["onCompleteName"] = animTask->onCompleteName;
|
|
||||||
out["animTask"] = std::move(at);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractiveObjectState::load(const nlohmann::json& in)
|
|
||||||
{
|
|
||||||
position.x() = in.value("positionX", position.x());
|
|
||||||
position.y() = in.value("positionY", position.y());
|
|
||||||
position.z() = in.value("positionZ", position.z());
|
|
||||||
rotationY = in.value("rotationY", rotationY);
|
|
||||||
scale = in.value("scale", scale);
|
|
||||||
alpha = in.value("alpha", alpha);
|
|
||||||
isActive = in.value("isActive", isActive);
|
|
||||||
isAnimating = in.value("isAnimating", false);
|
|
||||||
animTask.reset();
|
|
||||||
|
|
||||||
if (isAnimating && in.contains("animTask") && in["animTask"].is_object()) {
|
|
||||||
const auto& at = in["animTask"];
|
|
||||||
AnimTask task;
|
|
||||||
task.type = static_cast<AnimTask::Type>(at.value("type", 0));
|
|
||||||
task.startPos = { at.value("startPosX", 0.f), at.value("startPosY", 0.f), at.value("startPosZ", 0.f) };
|
|
||||||
task.startRotY = at.value("startRotY", 0.f);
|
|
||||||
task.startScale = at.value("startScale", 1.f);
|
|
||||||
task.targetPos = { at.value("targetPosX", 0.f), at.value("targetPosY", 0.f), at.value("targetPosZ", 0.f) };
|
|
||||||
task.targetRotY = at.value("targetRotY", 0.f);
|
|
||||||
task.targetScale = at.value("targetScale", 1.f);
|
|
||||||
task.durationMs = at.value("durationMs", 1000.f);
|
|
||||||
task.elapsedMs = at.value("elapsedMs", 0.f);
|
|
||||||
task.onCompleteName = at.value("onCompleteName", std::string());
|
|
||||||
// onComplete is non-serializable; re-wired at runtime by Lua script callbacks.
|
|
||||||
animTask = std::move(task);
|
|
||||||
}
|
}
|
||||||
|
if (presentation.scale != 1.f)
|
||||||
|
renderer.ScaleMatrix(presentation.scale);
|
||||||
|
renderer.DrawVertexRenderStruct(loadedObject.mesh);
|
||||||
|
renderer.PopMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace FRG
|
} // namespace FRG
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
|
||||||
#include <functional>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include "render/Renderer.h"
|
#include "render/Renderer.h"
|
||||||
#include "InteractiveObjectState.h"
|
#include "InteractiveObjectState.h"
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
@@ -21,25 +19,43 @@ namespace FRG {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct InteractiveObject {
|
struct InteractiveObject {
|
||||||
|
explicit InteractiveObject(InteractiveObjectState& boundState) : state(boundState) {}
|
||||||
|
std::string id;
|
||||||
LoadedGameObject loadedObject; // rendering assets
|
LoadedGameObject loadedObject; // rendering assets
|
||||||
InteractiveObjectState state; // all mutable + creation state
|
|
||||||
|
|
||||||
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
||||||
// Loads textures and mesh from state.objectInfo, computes mesh centering,
|
// Loads textures and centers the mesh around the state position.
|
||||||
// and sets state.position = jsonPosition + meshCenter.
|
static InteractiveObject createFromState(InteractiveObjectState& state,
|
||||||
static InteractiveObject createFromState(InteractiveObjectState st,
|
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
const std::string& zipPath = "");
|
const std::string& zipPath = "");
|
||||||
|
|
||||||
// callbackName, if given, is the global Lua function name backing onComplete —
|
|
||||||
// stored in the AnimTask so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
|
|
||||||
void moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
|
||||||
void rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
|
||||||
void scaleTo(float targetScale, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
|
||||||
void fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
|
||||||
void update(int64_t deltaMs);
|
|
||||||
void draw(Renderer& renderer) const;
|
void draw(Renderer& renderer) const;
|
||||||
void drawDarklands(Renderer& renderer) const;
|
void drawDarklands(Renderer& renderer) const;
|
||||||
|
void drawShadowDepth(Renderer& renderer) const;
|
||||||
|
void capturePresentationState();
|
||||||
|
const std::string& entityId() const { return id; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class GameObjectLoader;
|
||||||
|
friend class Location;
|
||||||
|
friend class LocationEditor;
|
||||||
|
friend class ScriptEngine;
|
||||||
|
InteractiveObjectState& state; // non-owning binding to LocationGameState
|
||||||
|
|
||||||
|
struct PresentationSample {
|
||||||
|
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||||
|
float rotationY = 0.f;
|
||||||
|
float scale = 1.f;
|
||||||
|
float alpha = 1.f;
|
||||||
|
bool isActive = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
PresentationSample previousPresentationSample_;
|
||||||
|
PresentationSample currentPresentationSample_;
|
||||||
|
std::chrono::steady_clock::time_point lastPresentationUpdate_;
|
||||||
|
bool hasPresentationSample_ = false;
|
||||||
|
|
||||||
|
PresentationSample interpolatedPresentationState() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace FRG
|
} // namespace FRG
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include "InteractiveObjectState.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace FRG {
|
||||||
|
|
||||||
|
void InteractiveObjectState::moveTo(
|
||||||
|
const Eigen::Vector3f& target, float durationSec, std::string onCompleteName)
|
||||||
|
{
|
||||||
|
if (isAnimating) return;
|
||||||
|
AnimTask task;
|
||||||
|
task.type = AnimTask::Type::Move;
|
||||||
|
task.startPos = position;
|
||||||
|
task.startRotY = rotationY;
|
||||||
|
task.startScale = scale;
|
||||||
|
task.targetPos = target;
|
||||||
|
task.targetRotY = rotationY;
|
||||||
|
task.targetScale = scale;
|
||||||
|
task.durationMs = durationSec * 1000.f;
|
||||||
|
task.onCompleteName = std::move(onCompleteName);
|
||||||
|
animTask = std::move(task);
|
||||||
|
isAnimating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractiveObjectState::rotateTo(
|
||||||
|
float targetRotY, float durationSec, std::string onCompleteName)
|
||||||
|
{
|
||||||
|
if (isAnimating) return;
|
||||||
|
AnimTask task;
|
||||||
|
task.type = AnimTask::Type::Rotate;
|
||||||
|
task.startPos = position;
|
||||||
|
task.startRotY = rotationY;
|
||||||
|
task.startScale = scale;
|
||||||
|
task.targetPos = position;
|
||||||
|
task.targetRotY = targetRotY;
|
||||||
|
task.targetScale = scale;
|
||||||
|
task.durationMs = durationSec * 1000.f;
|
||||||
|
task.onCompleteName = std::move(onCompleteName);
|
||||||
|
animTask = std::move(task);
|
||||||
|
isAnimating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractiveObjectState::scaleTo(
|
||||||
|
float targetScale, float durationSec, std::string onCompleteName)
|
||||||
|
{
|
||||||
|
if (isAnimating) return;
|
||||||
|
AnimTask task;
|
||||||
|
task.type = AnimTask::Type::Scale;
|
||||||
|
task.startPos = position;
|
||||||
|
task.startRotY = rotationY;
|
||||||
|
task.startScale = scale;
|
||||||
|
task.targetPos = position;
|
||||||
|
task.targetRotY = rotationY;
|
||||||
|
task.targetScale = targetScale;
|
||||||
|
task.durationMs = durationSec * 1000.f;
|
||||||
|
task.onCompleteName = std::move(onCompleteName);
|
||||||
|
animTask = std::move(task);
|
||||||
|
isAnimating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractiveObjectState::fadeTo(
|
||||||
|
float targetAlpha, float durationSec, std::string onCompleteName)
|
||||||
|
{
|
||||||
|
if (isAnimating) return;
|
||||||
|
AnimTask task;
|
||||||
|
task.type = AnimTask::Type::Fade;
|
||||||
|
task.startPos = position;
|
||||||
|
task.startRotY = rotationY;
|
||||||
|
task.startScale = alpha;
|
||||||
|
task.targetPos = position;
|
||||||
|
task.targetRotY = rotationY;
|
||||||
|
task.targetScale = targetAlpha;
|
||||||
|
task.durationMs = durationSec * 1000.f;
|
||||||
|
task.onCompleteName = std::move(onCompleteName);
|
||||||
|
animTask = std::move(task);
|
||||||
|
isAnimating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractiveObjectUpdateEvents InteractiveObjectState::update(std::int64_t deltaMs)
|
||||||
|
{
|
||||||
|
InteractiveObjectUpdateEvents events;
|
||||||
|
if (!isAnimating || !animTask) return events;
|
||||||
|
|
||||||
|
AnimTask& task = *animTask;
|
||||||
|
task.elapsedMs += static_cast<float>(deltaMs);
|
||||||
|
const float duration = (std::max)(task.durationMs, 0.001f);
|
||||||
|
const float t = (std::min)(task.elapsedMs / duration, 1.0f);
|
||||||
|
|
||||||
|
switch (task.type) {
|
||||||
|
case AnimTask::Type::Move:
|
||||||
|
position = task.startPos + (task.targetPos - task.startPos) * t;
|
||||||
|
break;
|
||||||
|
case AnimTask::Type::Rotate:
|
||||||
|
rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
||||||
|
break;
|
||||||
|
case AnimTask::Type::Scale:
|
||||||
|
scale = task.startScale + (task.targetScale - task.startScale) * t;
|
||||||
|
break;
|
||||||
|
case AnimTask::Type::Fade:
|
||||||
|
alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t < 1.0f) return events;
|
||||||
|
position = task.targetPos;
|
||||||
|
rotationY = task.targetRotY;
|
||||||
|
if (task.type == AnimTask::Type::Fade)
|
||||||
|
alpha = task.targetScale;
|
||||||
|
else
|
||||||
|
scale = task.targetScale;
|
||||||
|
events.onCompleteName = std::move(task.onCompleteName);
|
||||||
|
animTask.reset();
|
||||||
|
isAnimating = false;
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractiveObjectState::save(nlohmann::json& out) const
|
||||||
|
{
|
||||||
|
out["positionX"] = position.x();
|
||||||
|
out["positionY"] = position.y();
|
||||||
|
out["positionZ"] = position.z();
|
||||||
|
out["rotationY"] = rotationY;
|
||||||
|
out["scale"] = scale;
|
||||||
|
out["alpha"] = alpha;
|
||||||
|
out["isActive"] = isActive;
|
||||||
|
out["isAnimating"] = isAnimating;
|
||||||
|
|
||||||
|
if (isAnimating && animTask) {
|
||||||
|
out["animTask"] = {
|
||||||
|
{"type", static_cast<int>(animTask->type)},
|
||||||
|
{"startPosX", animTask->startPos.x()},
|
||||||
|
{"startPosY", animTask->startPos.y()},
|
||||||
|
{"startPosZ", animTask->startPos.z()},
|
||||||
|
{"startRotY", animTask->startRotY},
|
||||||
|
{"startScale", animTask->startScale},
|
||||||
|
{"targetPosX", animTask->targetPos.x()},
|
||||||
|
{"targetPosY", animTask->targetPos.y()},
|
||||||
|
{"targetPosZ", animTask->targetPos.z()},
|
||||||
|
{"targetRotY", animTask->targetRotY},
|
||||||
|
{"targetScale", animTask->targetScale},
|
||||||
|
{"durationMs", animTask->durationMs},
|
||||||
|
{"elapsedMs", animTask->elapsedMs},
|
||||||
|
{"onCompleteName", animTask->onCompleteName}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractiveObjectState::load(const nlohmann::json& in)
|
||||||
|
{
|
||||||
|
position.x() = in.value("positionX", position.x());
|
||||||
|
position.y() = in.value("positionY", position.y());
|
||||||
|
position.z() = in.value("positionZ", position.z());
|
||||||
|
rotationY = in.value("rotationY", rotationY);
|
||||||
|
scale = in.value("scale", scale);
|
||||||
|
alpha = in.value("alpha", alpha);
|
||||||
|
isActive = in.value("isActive", isActive);
|
||||||
|
isAnimating = in.value("isAnimating", false);
|
||||||
|
animTask.reset();
|
||||||
|
|
||||||
|
if (!isAnimating || !in.contains("animTask") ||
|
||||||
|
!in["animTask"].is_object()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& value = in["animTask"];
|
||||||
|
AnimTask task;
|
||||||
|
task.type = static_cast<AnimTask::Type>(value.value("type", 0));
|
||||||
|
task.startPos = {value.value("startPosX", 0.f),
|
||||||
|
value.value("startPosY", 0.f), value.value("startPosZ", 0.f)};
|
||||||
|
task.startRotY = value.value("startRotY", 0.f);
|
||||||
|
task.startScale = value.value("startScale", 1.f);
|
||||||
|
task.targetPos = {value.value("targetPosX", 0.f),
|
||||||
|
value.value("targetPosY", 0.f), value.value("targetPosZ", 0.f)};
|
||||||
|
task.targetRotY = value.value("targetRotY", 0.f);
|
||||||
|
task.targetScale = value.value("targetScale", 1.f);
|
||||||
|
task.durationMs = value.value("durationMs", 1000.f);
|
||||||
|
task.elapsedMs = value.value("elapsedMs", 0.f);
|
||||||
|
task.onCompleteName =
|
||||||
|
value.value("onCompleteName", std::string());
|
||||||
|
animTask = std::move(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FRG
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <functional>
|
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <Eigen/Geometry>
|
#include <Eigen/Geometry>
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
@@ -21,10 +21,11 @@ struct AnimTask {
|
|||||||
float targetScale = 1.f;
|
float targetScale = 1.f;
|
||||||
float durationMs = 1000.f;
|
float durationMs = 1000.f;
|
||||||
float elapsedMs = 0.f;
|
float elapsedMs = 0.f;
|
||||||
std::function<void()> onComplete; // non-serializable; re-wired at runtime by Lua callbacks
|
// Global Lua function called when the animation completes. Empty means none.
|
||||||
// Global Lua function name backing onComplete, if it was sourced from a script
|
std::string onCompleteName;
|
||||||
// (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback can be
|
};
|
||||||
// re-looked-up by name after a save/load; empty if none or non-Lua.
|
|
||||||
|
struct InteractiveObjectUpdateEvents {
|
||||||
std::string onCompleteName;
|
std::string onCompleteName;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,6 +71,12 @@ public:
|
|||||||
bool isAnimating = false;
|
bool isAnimating = false;
|
||||||
std::optional<AnimTask> animTask;
|
std::optional<AnimTask> animTask;
|
||||||
|
|
||||||
|
void moveTo(const Eigen::Vector3f& target, float durationSec, std::string onCompleteName = {});
|
||||||
|
void rotateTo(float targetRotY, float durationSec, std::string onCompleteName = {});
|
||||||
|
void scaleTo(float targetScale, float durationSec, std::string onCompleteName = {});
|
||||||
|
void fadeTo(float targetAlpha, float durationSec, std::string onCompleteName = {});
|
||||||
|
InteractiveObjectUpdateEvents update(std::int64_t deltaMs);
|
||||||
|
|
||||||
// --- Serialisation (runtime mutable fields only) ---
|
// --- Serialisation (runtime mutable fields only) ---
|
||||||
void save(nlohmann::json& out) const override;
|
void save(nlohmann::json& out) const override;
|
||||||
void load(const nlohmann::json& in) override;
|
void load(const nlohmann::json& in) override;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include "Item.h"
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace FRG {
|
||||||
|
|
||||||
|
bool Inventory::addItem(const std::string& itemId)
|
||||||
|
{
|
||||||
|
itemIds.push_back(itemId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inventory::removeItem(const std::string& itemId)
|
||||||
|
{
|
||||||
|
auto it = std::find(itemIds.begin(), itemIds.end(), itemId);
|
||||||
|
if (it == itemIds.end()) return false;
|
||||||
|
itemIds.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inventory::hasItem(const std::string& itemId) const
|
||||||
|
{
|
||||||
|
return std::find(itemIds.begin(), itemIds.end(), itemId) != itemIds.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Inventory::save(nlohmann::json& out) const
|
||||||
|
{
|
||||||
|
out["items"] = itemIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Inventory::load(const nlohmann::json& in)
|
||||||
|
{
|
||||||
|
itemIds.clear();
|
||||||
|
if (!in.contains("items") || !in["items"].is_array()) return;
|
||||||
|
for (const auto& item : in["items"]) {
|
||||||
|
if (!item.is_string())
|
||||||
|
throw std::runtime_error("Inventory item IDs must be strings");
|
||||||
|
itemIds.push_back(item.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FRG
|
||||||
@@ -1,31 +1,8 @@
|
|||||||
#include "Item.h"
|
#include "Item.h"
|
||||||
#include "ItemRegistry.h"
|
#include "ItemRegistry.h"
|
||||||
#include "external/nlohmann/json.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <iostream>
|
|
||||||
#include "../utils/Utils.h"
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
void Inventory::addItem(const std::string& itemId) {
|
|
||||||
itemIds.push_back(itemId);
|
|
||||||
logger() << "Item added to inventory: " << itemId << std::endl;
|
|
||||||
if (onItemAdded) onItemAdded(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Inventory::removeItem(const std::string& itemId) {
|
|
||||||
auto it = std::find(itemIds.begin(), itemIds.end(), itemId);
|
|
||||||
if (it != itemIds.end()) {
|
|
||||||
logger() << "Item removed from inventory: " << itemId << std::endl;
|
|
||||||
if (onItemRemoved) onItemRemoved(itemId);
|
|
||||||
itemIds.erase(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Inventory::hasItem(const std::string& itemId) const {
|
|
||||||
return std::find(itemIds.begin(), itemIds.end(), itemId) != itemIds.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Item> Inventory::getItems() const {
|
std::vector<Item> Inventory::getItems() const {
|
||||||
std::vector<Item> result;
|
std::vector<Item> result;
|
||||||
result.reserve(itemIds.size());
|
result.reserve(itemIds.size());
|
||||||
@@ -36,22 +13,4 @@ namespace FRG {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Inventory::save(nlohmann::json& out) const {
|
|
||||||
out["items"] = itemIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Inventory::load(const nlohmann::json& in) {
|
|
||||||
itemIds.clear();
|
|
||||||
if (!in.contains("items") || !in["items"].is_array()) return;
|
|
||||||
for (const auto& j : in["items"]) {
|
|
||||||
if (j.is_string()) {
|
|
||||||
itemIds.push_back(j.get<std::string>());
|
|
||||||
} else if (j.is_object()) {
|
|
||||||
// Backward-compat with old save files that stored full item structs.
|
|
||||||
const std::string id = j.value("id", "");
|
|
||||||
if (!id.empty()) itemIds.push_back(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace FRG
|
} // namespace FRG
|
||||||
+2
-7
@@ -2,7 +2,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
|
||||||
#include "ISaveable.h"
|
#include "ISaveable.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
@@ -22,8 +21,8 @@ namespace FRG {
|
|||||||
|
|
||||||
class Inventory : public ISaveable {
|
class Inventory : public ISaveable {
|
||||||
public:
|
public:
|
||||||
void addItem(const std::string& itemId);
|
bool addItem(const std::string& itemId);
|
||||||
void removeItem(const std::string& itemId);
|
bool removeItem(const std::string& itemId);
|
||||||
// Resolves current items from ItemRegistry by id, so returned data
|
// Resolves current items from ItemRegistry by id, so returned data
|
||||||
// always reflects the currently loaded (localized) definitions.
|
// always reflects the currently loaded (localized) definitions.
|
||||||
std::vector<Item> getItems() const;
|
std::vector<Item> getItems() const;
|
||||||
@@ -34,10 +33,6 @@ namespace FRG {
|
|||||||
void save(nlohmann::json& out) const override;
|
void save(nlohmann::json& out) const override;
|
||||||
void load(const nlohmann::json& in) override;
|
void load(const nlohmann::json& in) override;
|
||||||
|
|
||||||
// Called whenever an item is added/removed; receives the item id.
|
|
||||||
std::function<void(const std::string&)> onItemAdded;
|
|
||||||
std::function<void(const std::string&)> onItemRemoved;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<std::string> itemIds;
|
std::vector<std::string> itemIds;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
|
|
||||||
bool operator<(const QueueNode& other) const
|
bool operator<(const QueueNode& other) const
|
||||||
{
|
{
|
||||||
return priority > other.priority;
|
if (priority != other.priority) return priority > other.priority;
|
||||||
|
return index > other.index;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,7 +303,8 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
|
|
||||||
bool operator<(const QueueNode& other) const
|
bool operator<(const QueueNode& other) const
|
||||||
{
|
{
|
||||||
return priority > other.priority;
|
if (priority != other.priority) return priority > other.priority;
|
||||||
|
return index > other.index;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -412,7 +414,10 @@ std::vector<Eigen::Vector3f> PathFinder::findNearestReachableImpl(
|
|||||||
struct QueueNode {
|
struct QueueNode {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
float priority = 0.0f;
|
float priority = 0.0f;
|
||||||
bool operator<(const QueueNode& other) const { return priority > other.priority; }
|
bool operator<(const QueueNode& other) const {
|
||||||
|
if (priority != other.priority) return priority > other.priority;
|
||||||
|
return index > other.index;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const int cellCount = gridWidth * gridDepth;
|
const int cellCount = gridWidth * gridDepth;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render/Renderer.h"
|
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
+43
-26
@@ -106,38 +106,48 @@ bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::unlockQuest(const std::string& questId) {
|
QuestMutationResult QuestJournal::unlockQuest(const std::string& questId) {
|
||||||
|
QuestMutationResult result;
|
||||||
QuestState* quest = findQuest(questId);
|
QuestState* quest = findQuest(questId);
|
||||||
if (!quest) return false;
|
if (!quest) return result;
|
||||||
|
result.success = true;
|
||||||
if (quest->status == QuestStatus::Hidden) {
|
if (quest->status == QuestStatus::Hidden) {
|
||||||
quest->status = QuestStatus::Available;
|
quest->status = QuestStatus::Available;
|
||||||
if (onQuestUnlocked) onQuestUnlocked(questId);
|
result.events.push_back({QuestEventType::Unlocked, questId, {}});
|
||||||
}
|
}
|
||||||
return true;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::completeQuest(const std::string& questId) {
|
QuestMutationResult QuestJournal::completeQuest(const std::string& questId) {
|
||||||
bool changed = setStatus(questId, QuestStatus::Completed);
|
QuestMutationResult result;
|
||||||
if (changed && onQuestCompleted) onQuestCompleted(questId);
|
result.success = setStatus(questId, QuestStatus::Completed);
|
||||||
return changed;
|
if (result.success)
|
||||||
|
result.events.push_back({QuestEventType::Completed, questId, {}});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::failQuest(const std::string& questId) {
|
QuestMutationResult QuestJournal::failQuest(const std::string& questId) {
|
||||||
bool changed = setStatus(questId, QuestStatus::Failed);
|
QuestMutationResult result;
|
||||||
if (changed && onQuestFailed) onQuestFailed(questId);
|
result.success = setStatus(questId, QuestStatus::Failed);
|
||||||
return changed;
|
if (result.success)
|
||||||
|
result.events.push_back({QuestEventType::Failed, questId, {}});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed) {
|
QuestMutationResult QuestJournal::setObjectiveCompleted(
|
||||||
|
const std::string& questId, const std::string& objectiveId, bool completed) {
|
||||||
|
QuestMutationResult result;
|
||||||
QuestState* quest = findQuest(questId);
|
QuestState* quest = findQuest(questId);
|
||||||
if (!quest) return false;
|
if (!quest) return result;
|
||||||
|
|
||||||
for (auto& objective : quest->definition.objectives) {
|
for (auto& objective : quest->definition.objectives) {
|
||||||
if (objective.id == objectiveId) {
|
if (objective.id == objectiveId) {
|
||||||
objective.completed = completed;
|
objective.completed = completed;
|
||||||
|
result.success = true;
|
||||||
|
|
||||||
if (completed) {
|
if (completed) {
|
||||||
if (onObjectiveCompleted) onObjectiveCompleted(questId, objectiveId);
|
result.events.push_back(
|
||||||
|
{QuestEventType::ObjectiveCompleted, questId, objectiveId});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) {
|
if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) {
|
||||||
@@ -147,41 +157,48 @@ bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::
|
|||||||
[](const QuestObjective& o) { return o.completed; });
|
[](const QuestObjective& o) { return o.completed; });
|
||||||
if (allDone) {
|
if (allDone) {
|
||||||
quest->status = QuestStatus::Completed;
|
quest->status = QuestStatus::Completed;
|
||||||
if (onQuestCompleted) onQuestCompleted(questId);
|
result.events.push_back(
|
||||||
|
{QuestEventType::Completed, questId, {}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible) {
|
QuestMutationResult QuestJournal::setObjectiveVisible(
|
||||||
|
const std::string& questId, const std::string& objectiveId, bool visible) {
|
||||||
|
QuestMutationResult result;
|
||||||
QuestState* quest = findQuest(questId);
|
QuestState* quest = findQuest(questId);
|
||||||
if (!quest) return false;
|
if (!quest) return result;
|
||||||
|
|
||||||
for (auto& objective : quest->definition.objectives) {
|
for (auto& objective : quest->definition.objectives) {
|
||||||
if (objective.id == objectiveId) {
|
if (objective.id == objectiveId) {
|
||||||
objective.visible = visible;
|
objective.visible = visible;
|
||||||
return true;
|
result.success = true;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestJournal::setActiveObjective(const std::string& questId, int objectiveIndex) {
|
QuestMutationResult QuestJournal::setActiveObjective(
|
||||||
|
const std::string& questId, int objectiveIndex) {
|
||||||
|
QuestMutationResult result;
|
||||||
QuestState* quest = findQuest(questId);
|
QuestState* quest = findQuest(questId);
|
||||||
if (!quest) return false;
|
if (!quest) return result;
|
||||||
|
|
||||||
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
|
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
quest->activeObjectiveIndex = objectiveIndex;
|
quest->activeObjectiveIndex = objectiveIndex;
|
||||||
return true;
|
result.success = true;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestState* QuestJournal::findQuest(const std::string& questId) {
|
QuestState* QuestJournal::findQuest(const std::string& questId) {
|
||||||
|
|||||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user