From 70a8623331e8ca436df96beb2b39cfb42d7a7f13 Mon Sep 17 00:00:00 2001
From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com>
Date: Sun, 19 Apr 2026 15:25:53 -0600
Subject: [PATCH 1/4] Pixel Movement Complete
Should be working with all normal functions - though it hasn't been tested with Pathfinding yet
---
src/cute_c2.h | 2281 ++++++++++++++++++++++++++++
src/game_character.cpp | 3108 +++++++++++++++++++++++---------------
src/game_interpreter.cpp | 4 +-
3 files changed, 4158 insertions(+), 1235 deletions(-)
create mode 100644 src/cute_c2.h
diff --git a/src/cute_c2.h b/src/cute_c2.h
new file mode 100644
index 0000000000..be06fc9ee6
--- /dev/null
+++ b/src/cute_c2.h
@@ -0,0 +1,2281 @@
+/*
+
+* This file is part of EasyRPG Player.
+ *
+ * EasyRPG Player is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EasyRPG Player is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EasyRPG Player. If not, see .
+
+ ------------------------------------------------------------------------------
+ Licensing information can be found at the end of the file.
+ ------------------------------------------------------------------------------
+
+ cute_c2.h - v1.10
+
+ To create implementation (the function definitions)
+ #define CUTE_C2_IMPLEMENTATION
+ in *one* C/CPP file (translation unit) that includes this file
+
+
+ SUMMARY
+
+ cute_c2 is a single-file header that implements 2D collision detection routines
+ that test for overlap, and optionally can find the collision manifold. The
+ manifold contains all necessary information to prevent shapes from inter-
+ penetrating, which is useful for character controllers, general physics
+ simulation, and user-interface programming.
+
+ This header implements a group of "immediate mode" functions that should be
+ very easily adapted into pre-existing projects.
+
+
+ THE IMPORTANT PARTS
+
+ Most of the math types in this header are for internal use. Users care about
+ the shape types and the collision functions.
+
+ SHAPE TYPES:
+ * c2Circle
+ * c2Capsule
+ * c2AABB
+ * c2Ray
+ * c2Poly
+
+ COLLISION FUNCTIONS (*** is a shape name from the above list):
+ * c2***to*** - boolean YES/NO hittest
+ * c2***to***Manifold - construct manifold to describe how shapes hit
+ * c2GJK - runs GJK algorithm to find closest point pair between two shapes
+ * c2TOI - computes the time of impact between two shapes, useful for sweeping shapes, or doing shape casts
+ * c2MakePoly - Runs convex hull algorithm and computes normals on input point-set
+ * c2Collided - generic version of c2***to*** funcs
+ * c2Collide - generic version of c2***to***Manifold funcs
+ * c2CastRay - generic version of c2Rayto*** funcs
+
+ The rest of the header is more or less for internal use. Here is an example of
+ making some shapes and testing for collision:
+
+ c2Circle c;
+ c.p = position;
+ c.r = radius;
+
+ c2Capsule cap;
+ cap.a = first_endpoint;
+ cap.b = second_endpoint;
+ cap.r = radius;
+
+ int hit = c2CircletoCapsule(c, cap);
+ if (hit)
+ {
+ handle collision here...
+ }
+
+ For more code examples and tests please see:
+ https://github.com/RandyGaul/cute_header/tree/master/examples_cute_gl_and_c2
+
+ Here is a past discussion thread on this header:
+ https://www.reddit.com/r/gamedev/comments/5tqyey/tinyc2_2d_collision_detection_library_in_c/
+
+ Here is a very nice repo containing various tests and examples using SFML for rendering:
+ https://github.com/sro5h/tinyc2-tests
+
+
+ FEATURES
+
+ * Circles, capsules, AABBs, rays and convex polygons are supported
+ * Fast boolean only result functions (hit yes/no)
+ * Slghtly slower manifold generation for collision normals + depths +points
+ * GJK implementation (finds closest points for disjoint pairs of shapes)
+ * Shape casts/sweeps with c2TOI function (time of impact)
+ * Robust 2D convex hull generator
+ * Lots of correctly implemented and tested 2D math routines
+ * Implemented in portable C, and is readily portable to other languages
+ * Generic c2Collide, c2Collided and c2CastRay function (can pass in any shape type)
+ * Extensive examples at: https://github.com/RandyGaul/cute_headers/tree/master/examples_cute_gl_and_c2
+
+
+ Revision History
+
+ 1.0 (02/13/2017) initial release
+ 1.01 (02/13/2017) const crusade, minor optimizations, capsule degen
+ 1.02 (03/21/2017) compile fixes for c on more compilers
+ 1.03 (09/15/2017) various bugfixes and quality of life changes to manifolds
+ 1.04 (03/25/2018) fixed manifold bug in c2CircletoAABBManifold
+ 1.05 (11/01/2018) added c2TOI (time of impact) for shape cast/sweep test
+ 1.06 (08/23/2019) C2_*** types to C2_TYPE_***, and CUTE_C2_API
+ 1.07 (10/19/2019) Optimizations to c2TOI - breaking change to c2GJK API
+ 1.08 (12/22/2019) Remove contact point + normal from c2TOI, removed feather
+ radius from c2GJK, fixed various bugs in capsule to poly
+ manifold, did a pass on all docs
+ 1.09 (07/27/2019) Added c2Inflate - to inflate/deflate shapes for c2TOI
+ 1.10 (02/05/2022) Implemented GJK-Raycast for c2TOI (from E. Catto's Box2D)
+
+
+ Contributors
+
+ Plastburk 1.01 - const pointers pull request
+ mmozeiko 1.02 - 3 compile bugfixes
+ felipefs 1.02 - 3 compile bugfixes
+ seemk 1.02 - fix branching bug in c2Collide
+ sro5h 1.02 - bug reports for multiple manifold funcs
+ sro5h 1.03 - work involving quality of life fixes for manifolds
+ Wizzard033 1.06 - C2_*** types to C2_TYPE_***, and CUTE_C2_API
+ Tyler Glaeil 1.08 - Lots of bug reports and disussion on capsules + TOIs
+
+
+ DETAILS/ADVICE
+
+ BROAD PHASE
+
+ This header does not implement a broad-phase, and instead concerns itself with
+ the narrow-phase. This means this header just checks to see if two individual
+ shapes are touching, and can give information about how they are touching.
+
+ Very common 2D broad-phases are tree and grid approaches. Quad trees are good
+ for static geometry that does not move much if at all. Dynamic AABB trees are
+ good for general purpose use, and can handle moving objects very well. Grids
+ are great and are similar to quad trees.
+
+ If implementing a grid it can be wise to have each collideable grid cell hold
+ an integer. This integer refers to a 2D shape that can be passed into the
+ various functions in this header. The shape can be transformed from "model"
+ space to "world" space using c2x -- a transform struct. In this way a grid
+ can be implemented that holds any kind of convex shape (that this header
+ supports) while conserving memory with shape instancing.
+
+ NUMERIC ROBUSTNESS
+
+ Many of the functions in cute c2 use `c2GJK`, an implementation of the GJK
+ algorithm. Internally GJK computes signed area values, and these values are
+ very numerically sensitive to large shapes. This means the GJK function will
+ break down if input shapes are too large or too far away from the origin.
+
+ In general it is best to compute collision detection on small shapes very
+ close to the origin. One trick is to keep your collision information numerically
+ very tiny, and simply scale it up when rendering to the appropriate size.
+
+ For reference, if your shapes are all AABBs and contain a width and height
+ of somewhere between 1.0f and 10.0f, everything will be fine. However, once
+ your shapes start approaching a width/height of 100.0f to 1000.0f GJK can
+ start breaking down.
+
+ This is a complicated topic, so feel free to ask the author for advice here.
+
+ Here is an example demonstrating this problem with two large AABBs:
+ https://github.com/RandyGaul/cute_headers/issues/160
+
+ Please email at my address with any questions or comments at:
+ author's last name followed by 1748 at gmail
+*/
+
+#if !defined(CUTE_C2_H)
+
+// this can be adjusted as necessary, but is highly recommended to be kept at 8.
+// higher numbers will incur quite a bit of memory overhead, and convex shapes
+// over 8 verts start to just look like spheres, which can be implicitly rep-
+// resented as a point + radius. usually tools that generate polygons should be
+// constructed so they do not output polygons with too many verts.
+// Note: polygons in cute_c2 are all *convex*.
+#define C2_MAX_POLYGON_VERTS 8
+
+// 2d vector
+typedef struct c2v
+{
+ float x;
+ float y;
+} c2v;
+
+// 2d rotation composed of cos/sin pair for a single angle
+// We use two floats as a small optimization to avoid computing sin/cos unnecessarily
+typedef struct c2r
+{
+ float c;
+ float s;
+} c2r;
+
+// 2d rotation matrix
+typedef struct c2m
+{
+ c2v x;
+ c2v y;
+} c2m;
+
+// 2d transformation "x"
+// These are used especially for c2Poly when a c2Poly is passed to a function.
+// Since polygons are prime for "instancing" a c2x transform can be used to
+// transform a polygon from local space to world space. In functions that take
+// a c2x pointer (like c2PolytoPoly), these pointers can be NULL, which represents
+// an identity transformation and assumes the verts inside of c2Poly are already
+// in world space.
+typedef struct c2x
+{
+ c2v p;
+ c2r r;
+} c2x;
+
+// 2d halfspace (aka plane, aka line)
+typedef struct c2h
+{
+ c2v n; // normal, normalized
+ float d; // distance to origin from plane, or ax + by = d
+} c2h;
+
+typedef struct c2Circle
+{
+ c2v p;
+ float r;
+} c2Circle;
+
+typedef struct c2AABB
+{
+ c2v min;
+ c2v max;
+} c2AABB;
+
+// a capsule is defined as a line segment (from a to b) and radius r
+typedef struct c2Capsule
+{
+ c2v a;
+ c2v b;
+ float r;
+} c2Capsule;
+
+typedef struct c2Poly
+{
+ int count;
+ c2v verts[C2_MAX_POLYGON_VERTS];
+ c2v norms[C2_MAX_POLYGON_VERTS];
+} c2Poly;
+
+// IMPORTANT:
+// Many algorithms in this file are sensitive to the magnitude of the
+// ray direction (c2Ray::d). It is highly recommended to normalize the
+// ray direction and use t to specify a distance. Please see this link
+// for an in-depth explanation: https://github.com/RandyGaul/cute_headers/issues/30
+typedef struct c2Ray
+{
+ c2v p; // position
+ c2v d; // direction (normalized)
+ float t; // distance along d from position p to find endpoint of ray
+} c2Ray;
+
+typedef struct c2Raycast
+{
+ float t; // time of impact
+ c2v n; // normal of surface at impact (unit length)
+} c2Raycast;
+
+// position of impact p = ray.p + ray.d * raycast.t
+#define c2Impact(ray, t) c2Add(ray.p, c2Mulvs(ray.d, t))
+
+// contains all information necessary to resolve a collision, or in other words
+// this is the information needed to separate shapes that are colliding. Doing
+// the resolution step is *not* included in cute_c2.
+typedef struct c2Manifold
+{
+ int count;
+ float depths[2];
+ c2v contact_points[2];
+
+ // always points from shape A to shape B (first and second shapes passed into
+ // any of the c2***to***Manifold functions)
+ c2v n;
+} c2Manifold;
+
+// This define allows exporting/importing of the header to a dynamic library.
+// Here's an example.
+// #define CUTE_C2_API extern "C" __declspec(dllexport)
+#if !defined(CUTE_C2_API)
+# define CUTE_C2_API
+#endif
+
+// boolean collision detection
+// these versions are faster than the manifold versions, but only give a YES/NO result
+CUTE_C2_API int c2CircletoCircle(c2Circle A, c2Circle B);
+CUTE_C2_API int c2CircletoAABB(c2Circle A, c2AABB B);
+CUTE_C2_API int c2CircletoCapsule(c2Circle A, c2Capsule B);
+CUTE_C2_API int c2AABBtoAABB(c2AABB A, c2AABB B);
+CUTE_C2_API int c2AABBtoCapsule(c2AABB A, c2Capsule B);
+CUTE_C2_API int c2CapsuletoCapsule(c2Capsule A, c2Capsule B);
+CUTE_C2_API int c2CircletoPoly(c2Circle A, const c2Poly* B, const c2x* bx);
+CUTE_C2_API int c2AABBtoPoly(c2AABB A, const c2Poly* B, const c2x* bx);
+CUTE_C2_API int c2CapsuletoPoly(c2Capsule A, const c2Poly* B, const c2x* bx);
+CUTE_C2_API int c2PolytoPoly(const c2Poly* A, const c2x* ax, const c2Poly* B, const c2x* bx);
+
+// ray operations
+// output is placed into the c2Raycast struct, which represents the hit location
+// of the ray. the out param contains no meaningful information if these funcs
+// return 0
+CUTE_C2_API int c2RaytoCircle(c2Ray A, c2Circle B, c2Raycast* out);
+CUTE_C2_API int c2RaytoAABB(c2Ray A, c2AABB B, c2Raycast* out);
+CUTE_C2_API int c2RaytoCapsule(c2Ray A, c2Capsule B, c2Raycast* out);
+CUTE_C2_API int c2RaytoPoly(c2Ray A, const c2Poly* B, const c2x* bx_ptr, c2Raycast* out);
+
+// manifold generation
+// These functions are (generally) slower than the boolean versions, but will compute one
+// or two points that represent the plane of contact. This information is usually needed
+// to resolve and prevent shapes from colliding. If no collision occured the count member
+// of the manifold struct is set to 0.
+CUTE_C2_API void c2CircletoCircleManifold(c2Circle A, c2Circle B, c2Manifold* m);
+CUTE_C2_API void c2CircletoAABBManifold(c2Circle A, c2AABB B, c2Manifold* m);
+CUTE_C2_API void c2CircletoCapsuleManifold(c2Circle A, c2Capsule B, c2Manifold* m);
+CUTE_C2_API void c2AABBtoAABBManifold(c2AABB A, c2AABB B, c2Manifold* m);
+CUTE_C2_API void c2AABBtoCapsuleManifold(c2AABB A, c2Capsule B, c2Manifold* m);
+CUTE_C2_API void c2CapsuletoCapsuleManifold(c2Capsule A, c2Capsule B, c2Manifold* m);
+CUTE_C2_API void c2CircletoPolyManifold(c2Circle A, const c2Poly* B, const c2x* bx, c2Manifold* m);
+CUTE_C2_API void c2AABBtoPolyManifold(c2AABB A, const c2Poly* B, const c2x* bx, c2Manifold* m);
+CUTE_C2_API void c2CapsuletoPolyManifold(c2Capsule A, const c2Poly* B, const c2x* bx, c2Manifold* m);
+CUTE_C2_API void c2PolytoPolyManifold(const c2Poly* A, const c2x* ax, const c2Poly* B, const c2x* bx, c2Manifold* m);
+
+typedef enum
+{
+ C2_TYPE_CIRCLE,
+ C2_TYPE_AABB,
+ C2_TYPE_CAPSULE,
+ C2_TYPE_POLY
+} C2_TYPE;
+
+// This struct is only for advanced usage of the c2GJK function. See comments inside of the
+// c2GJK function for more details.
+typedef struct c2GJKCache
+{
+ float metric;
+ int count;
+ int iA[3];
+ int iB[3];
+ float div;
+} c2GJKCache;
+
+// This is an advanced function, intended to be used by people who know what they're doing.
+//
+// Runs the GJK algorithm to find closest points, returns distance between closest points.
+// outA and outB can be NULL, in this case only distance is returned. ax_ptr and bx_ptr
+// can be NULL, and represent local to world transformations for shapes A and B respectively.
+// use_radius will apply radii for capsules and circles (if set to false, spheres are
+// treated as points and capsules are treated as line segments i.e. rays). The cache parameter
+// should be NULL, as it is only for advanced usage (unless you know what you're doing, then
+// go ahead and use it). iterations is an optional parameter.
+//
+// IMPORTANT NOTE:
+// The GJK function is sensitive to large shapes, since it internally will compute signed area
+// values. `c2GJK` is called throughout cute c2 in many ways, so try to make sure all of your
+// collision shapes are not gigantic. For example, try to keep the volume of all your shapes
+// less than 100.0f. If you need large shapes, you should use tiny collision geometry for all
+// cute c2 function, and simply render the geometry larger on-screen by scaling it up.
+CUTE_C2_API float c2GJK(const void* A, C2_TYPE typeA, const c2x* ax_ptr, const void* B, C2_TYPE typeB, const c2x* bx_ptr, c2v* outA, c2v* outB, int use_radius, int* iterations, c2GJKCache* cache);
+
+// Stores results of a time of impact calculation done by `c2TOI`.
+typedef struct c2TOIResult
+{
+ int hit; // 1 if shapes were touching at the TOI, 0 if they never hit.
+ float toi; // The time of impact between two shapes.
+ c2v n; // Surface normal from shape A to B at the time of impact.
+ c2v p; // Point of contact between shapes A and B at time of impact.
+ int iterations; // Number of iterations the solver underwent.
+} c2TOIResult;
+
+// This is an advanced function, intended to be used by people who know what they're doing.
+//
+// Computes the time of impact from shape A and shape B. The velocity of each shape is provided
+// by vA and vB respectively. The shapes are *not* allowed to rotate over time. The velocity is
+// assumed to represent the change in motion from time 0 to time 1, and so the return value will
+// be a number from 0 to 1. To move each shape to the colliding configuration, multiply vA and vB
+// each by the return value. ax_ptr and bx_ptr are optional parameters to transforms for each shape,
+// and are typically used for polygon shapes to transform from model to world space. Set these to
+// NULL to represent identity transforms. iterations is an optional parameter. use_radius
+// will apply radii for capsules and circles (if set to false, spheres are treated as points and
+// capsules are treated as line segments i.e. rays).
+//
+// IMPORTANT NOTE:
+// The c2TOI function can be used to implement a "swept character controller", but it can be
+// difficult to do so. Say we compute a time of impact with `c2TOI` and move the shapes to the
+// time of impact, and adjust the velocity by zeroing out the velocity along the surface normal.
+// If we then call `c2TOI` again, it will fail since the shapes will be considered to start in
+// a colliding configuration. There are many styles of tricks to get around this problem, and
+// all of them involve giving the next call to `c2TOI` some breathing room. It is recommended
+// to use some variation of the following algorithm:
+//
+// 1. Call c2TOI.
+// 2. Move the shapes to the TOI.
+// 3. Slightly inflate the size of one, or both, of the shapes so they will be intersecting.
+// The purpose is to make the shapes numerically intersecting, but not visually intersecting.
+// Another option is to call c2TOI with slightly deflated shapes.
+// See the function `c2Inflate` for some more details.
+// 4. Compute the collision manifold between the inflated shapes (for example, use c2PolytoPolyManifold).
+// 5. Gently push the shapes apart. This will give the next call to c2TOI some breathing room.
+CUTE_C2_API c2TOIResult c2TOI(const void* A, C2_TYPE typeA, const c2x* ax_ptr, c2v vA, const void* B, C2_TYPE typeB, const c2x* bx_ptr, c2v vB, int use_radius);
+
+// Inflating a shape.
+//
+// This is useful to numerically grow or shrink a polytope. For example, when calling
+// a time of impact function it can be good to use a slightly smaller shape. Then, once
+// both shapes are moved to the time of impact a collision manifold can be made from the
+// slightly larger (and now overlapping) shapes.
+//
+// IMPORTANT NOTE
+// Inflating a shape with sharp corners can cause those corners to move dramatically.
+// Deflating a shape can avoid this problem, but deflating a very small shape can invert
+// the planes and result in something that is no longer convex. Make sure to pick an
+// appropriately small skin factor, for example 1.0e-6f.
+CUTE_C2_API void c2Inflate(void* shape, C2_TYPE type, float skin_factor);
+
+// Computes 2D convex hull. Will not do anything if less than two verts supplied. If
+// more than C2_MAX_POLYGON_VERTS are supplied extras are ignored.
+CUTE_C2_API int c2Hull(c2v* verts, int count);
+CUTE_C2_API void c2Norms(c2v* verts, c2v* norms, int count);
+
+// runs c2Hull and c2Norms, assumes p->verts and p->count are both set to valid values
+CUTE_C2_API void c2MakePoly(c2Poly* p);
+
+// Generic collision detection routines, useful for games that want to use some poly-
+// morphism to write more generic-styled code. Internally calls various above functions.
+// For AABBs/Circles/Capsules ax and bx are ignored. For polys ax and bx can define
+// model to world transformations (for polys only), or be NULL for identity transforms.
+CUTE_C2_API int c2Collided(const void* A, const c2x* ax, C2_TYPE typeA, const void* B, const c2x* bx, C2_TYPE typeB);
+CUTE_C2_API void c2Collide(const void* A, const c2x* ax, C2_TYPE typeA, const void* B, const c2x* bx, C2_TYPE typeB, c2Manifold* m);
+CUTE_C2_API int c2CastRay(c2Ray A, const void* B, const c2x* bx, C2_TYPE typeB, c2Raycast* out);
+
+#ifdef _MSC_VER
+ #define C2_INLINE __forceinline
+#else
+ #define C2_INLINE inline __attribute__((always_inline))
+#endif
+
+// adjust these primitives as seen fit
+#include // memcpy
+#include
+#define c2Sin(radians) sinf(radians)
+#define c2Cos(radians) cosf(radians)
+#define c2Sqrt(a) sqrtf(a)
+#define c2Min(a, b) ((a) < (b) ? (a) : (b))
+#define c2Max(a, b) ((a) > (b) ? (a) : (b))
+#define c2Abs(a) ((a) < 0 ? -(a) : (a))
+#define c2Clamp(a, lo, hi) c2Max(lo, c2Min(a, hi))
+C2_INLINE void c2SinCos(float radians, float* s, float* c) { *c = c2Cos(radians); *s = c2Sin(radians); }
+#define c2Sign(a) (a < 0 ? -1.0f : 1.0f)
+
+// The rest of the functions in the header-only portion are all for internal use
+// and use the author's personal naming conventions. It is recommended to use one's
+// own math library instead of the one embedded here in cute_c2, but for those
+// curious or interested in trying it out here's the details:
+
+// The Mul functions are used to perform multiplication. x stands for transform,
+// v stands for vector, s stands for scalar, r stands for rotation, h stands for
+// halfspace and T stands for transpose.For example c2MulxvT stands for "multiply
+// a transform with a vector, and transpose the transform".
+
+// vector ops
+C2_INLINE c2v c2V(float x, float y) { c2v a; a.x = x; a.y = y; return a; }
+C2_INLINE c2v c2Add(c2v a, c2v b) { a.x += b.x; a.y += b.y; return a; }
+C2_INLINE c2v c2Sub(c2v a, c2v b) { a.x -= b.x; a.y -= b.y; return a; }
+C2_INLINE float c2Dot(c2v a, c2v b) { return a.x * b.x + a.y * b.y; }
+C2_INLINE c2v c2Mulvs(c2v a, float b) { a.x *= b; a.y *= b; return a; }
+C2_INLINE c2v c2Mulvv(c2v a, c2v b) { a.x *= b.x; a.y *= b.y; return a; }
+C2_INLINE c2v c2Div(c2v a, float b) { return c2Mulvs(a, 1.0f / b); }
+C2_INLINE c2v c2Skew(c2v a) { c2v b; b.x = -a.y; b.y = a.x; return b; }
+C2_INLINE c2v c2CCW90(c2v a) { c2v b; b.x = a.y; b.y = -a.x; return b; }
+C2_INLINE float c2Det2(c2v a, c2v b) { return a.x * b.y - a.y * b.x; }
+C2_INLINE c2v c2Minv(c2v a, c2v b) { return c2V(c2Min(a.x, b.x), c2Min(a.y, b.y)); }
+C2_INLINE c2v c2Maxv(c2v a, c2v b) { return c2V(c2Max(a.x, b.x), c2Max(a.y, b.y)); }
+C2_INLINE c2v c2Clampv(c2v a, c2v lo, c2v hi) { return c2Maxv(lo, c2Minv(a, hi)); }
+C2_INLINE c2v c2Absv(c2v a) { return c2V(c2Abs(a.x), c2Abs(a.y)); }
+C2_INLINE float c2Hmin(c2v a) { return c2Min(a.x, a.y); }
+C2_INLINE float c2Hmax(c2v a) { return c2Max(a.x, a.y); }
+C2_INLINE float c2Len(c2v a) { return c2Sqrt(c2Dot(a, a)); }
+C2_INLINE c2v c2Norm(c2v a) { return c2Div(a, c2Len(a)); }
+C2_INLINE c2v c2SafeNorm(c2v a) { float sq = c2Dot(a, a); return sq ? c2Div(a, c2Len(a)) : c2V(0, 0); }
+C2_INLINE c2v c2Neg(c2v a) { return c2V(-a.x, -a.y); }
+C2_INLINE c2v c2Lerp(c2v a, c2v b, float t) { return c2Add(a, c2Mulvs(c2Sub(b, a), t)); }
+C2_INLINE int c2Parallel(c2v a, c2v b, float kTol)
+{
+ float k = c2Len(a) / c2Len(b);
+ b = c2Mulvs(b, k);
+ if (c2Abs(a.x - b.x) < kTol && c2Abs(a.y - b.y) < kTol) return 1;
+ return 0;
+}
+
+// rotation ops
+C2_INLINE c2r c2Rot(float radians) { c2r r; c2SinCos(radians, &r.s, &r.c); return r; }
+C2_INLINE c2r c2RotIdentity(void) { c2r r; r.c = 1.0f; r.s = 0; return r; }
+C2_INLINE c2v c2RotX(c2r r) { return c2V(r.c, r.s); }
+C2_INLINE c2v c2RotY(c2r r) { return c2V(-r.s, r.c); }
+C2_INLINE c2v c2Mulrv(c2r a, c2v b) { return c2V(a.c * b.x - a.s * b.y, a.s * b.x + a.c * b.y); }
+C2_INLINE c2v c2MulrvT(c2r a, c2v b) { return c2V(a.c * b.x + a.s * b.y, -a.s * b.x + a.c * b.y); }
+C2_INLINE c2r c2Mulrr(c2r a, c2r b) { c2r c; c.c = a.c * b.c - a.s * b.s; c.s = a.s * b.c + a.c * b.s; return c; }
+C2_INLINE c2r c2MulrrT(c2r a, c2r b) { c2r c; c.c = a.c * b.c + a.s * b.s; c.s = a.c * b.s - a.s * b.c; return c; }
+
+C2_INLINE c2v c2Mulmv(c2m a, c2v b) { c2v c; c.x = a.x.x * b.x + a.y.x * b.y; c.y = a.x.y * b.x + a.y.y * b.y; return c; }
+C2_INLINE c2v c2MulmvT(c2m a, c2v b) { c2v c; c.x = a.x.x * b.x + a.x.y * b.y; c.y = a.y.x * b.x + a.y.y * b.y; return c; }
+C2_INLINE c2m c2Mulmm(c2m a, c2m b) { c2m c; c.x = c2Mulmv(a, b.x); c.y = c2Mulmv(a, b.y); return c; }
+C2_INLINE c2m c2MulmmT(c2m a, c2m b) { c2m c; c.x = c2MulmvT(a, b.x); c.y = c2MulmvT(a, b.y); return c; }
+
+// transform ops
+C2_INLINE c2x c2xIdentity(void) { c2x x; x.p = c2V(0, 0); x.r = c2RotIdentity(); return x; }
+C2_INLINE c2v c2Mulxv(c2x a, c2v b) { return c2Add(c2Mulrv(a.r, b), a.p); }
+C2_INLINE c2v c2MulxvT(c2x a, c2v b) { return c2MulrvT(a.r, c2Sub(b, a.p)); }
+C2_INLINE c2x c2Mulxx(c2x a, c2x b) { c2x c; c.r = c2Mulrr(a.r, b.r); c.p = c2Add(c2Mulrv(a.r, b.p), a.p); return c; }
+C2_INLINE c2x c2MulxxT(c2x a, c2x b) { c2x c; c.r = c2MulrrT(a.r, b.r); c.p = c2MulrvT(a.r, c2Sub(b.p, a.p)); return c; }
+C2_INLINE c2x c2Transform(c2v p, float radians) { c2x x; x.r = c2Rot(radians); x.p = p; return x; }
+
+// halfspace ops
+C2_INLINE c2v c2Origin(c2h h) { return c2Mulvs(h.n, h.d); }
+C2_INLINE float c2Dist(c2h h, c2v p) { return c2Dot(h.n, p) - h.d; }
+C2_INLINE c2v c2Project(c2h h, c2v p) { return c2Sub(p, c2Mulvs(h.n, c2Dist(h, p))); }
+C2_INLINE c2h c2Mulxh(c2x a, c2h b) { c2h c; c.n = c2Mulrv(a.r, b.n); c.d = c2Dot(c2Mulxv(a, c2Origin(b)), c.n); return c; }
+C2_INLINE c2h c2MulxhT(c2x a, c2h b) { c2h c; c.n = c2MulrvT(a.r, b.n); c.d = c2Dot(c2MulxvT(a, c2Origin(b)), c.n); return c; }
+C2_INLINE c2v c2Intersect(c2v a, c2v b, float da, float db) { return c2Add(a, c2Mulvs(c2Sub(b, a), (da / (da - db)))); }
+
+C2_INLINE void c2BBVerts(c2v* out, c2AABB* bb)
+{
+ out[0] = bb->min;
+ out[1] = c2V(bb->max.x, bb->min.y);
+ out[2] = bb->max;
+ out[3] = c2V(bb->min.x, bb->max.y);
+}
+
+#define CUTE_C2_H
+#endif
+
+#ifdef CUTE_C2_IMPLEMENTATION
+#ifndef CUTE_C2_IMPLEMENTATION_ONCE
+#define CUTE_C2_IMPLEMENTATION_ONCE
+
+int c2Collided(const void* A, const c2x* ax, C2_TYPE typeA, const void* B, const c2x* bx, C2_TYPE typeB)
+{
+ switch (typeA)
+ {
+ case C2_TYPE_CIRCLE:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: return c2CircletoCircle(*(c2Circle*)A, *(c2Circle*)B);
+ case C2_TYPE_AABB: return c2CircletoAABB(*(c2Circle*)A, *(c2AABB*)B);
+ case C2_TYPE_CAPSULE: return c2CircletoCapsule(*(c2Circle*)A, *(c2Capsule*)B);
+ case C2_TYPE_POLY: return c2CircletoPoly(*(c2Circle*)A, (const c2Poly*)B, bx);
+ default: return 0;
+ }
+ break;
+
+ case C2_TYPE_AABB:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: return c2CircletoAABB(*(c2Circle*)B, *(c2AABB*)A);
+ case C2_TYPE_AABB: return c2AABBtoAABB(*(c2AABB*)A, *(c2AABB*)B);
+ case C2_TYPE_CAPSULE: return c2AABBtoCapsule(*(c2AABB*)A, *(c2Capsule*)B);
+ case C2_TYPE_POLY: return c2AABBtoPoly(*(c2AABB*)A, (const c2Poly*)B, bx);
+ default: return 0;
+ }
+ break;
+
+ case C2_TYPE_CAPSULE:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: return c2CircletoCapsule(*(c2Circle*)B, *(c2Capsule*)A);
+ case C2_TYPE_AABB: return c2AABBtoCapsule(*(c2AABB*)B, *(c2Capsule*)A);
+ case C2_TYPE_CAPSULE: return c2CapsuletoCapsule(*(c2Capsule*)A, *(c2Capsule*)B);
+ case C2_TYPE_POLY: return c2CapsuletoPoly(*(c2Capsule*)A, (const c2Poly*)B, bx);
+ default: return 0;
+ }
+ break;
+
+ case C2_TYPE_POLY:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: return c2CircletoPoly(*(c2Circle*)B, (const c2Poly*)A, ax);
+ case C2_TYPE_AABB: return c2AABBtoPoly(*(c2AABB*)B, (const c2Poly*)A, ax);
+ case C2_TYPE_CAPSULE: return c2CapsuletoPoly(*(c2Capsule*)B, (const c2Poly*)A, ax);
+ case C2_TYPE_POLY: return c2PolytoPoly((const c2Poly*)A, ax, (const c2Poly*)B, bx);
+ default: return 0;
+ }
+ break;
+
+ default:
+ return 0;
+ }
+}
+
+void c2Collide(const void* A, const c2x* ax, C2_TYPE typeA, const void* B, const c2x* bx, C2_TYPE typeB, c2Manifold* m)
+{
+ m->count = 0;
+
+ switch (typeA)
+ {
+ case C2_TYPE_CIRCLE:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: c2CircletoCircleManifold(*(c2Circle*)A, *(c2Circle*)B, m); break;
+ case C2_TYPE_AABB: c2CircletoAABBManifold(*(c2Circle*)A, *(c2AABB*)B, m); break;
+ case C2_TYPE_CAPSULE: c2CircletoCapsuleManifold(*(c2Circle*)A, *(c2Capsule*)B, m); break;
+ case C2_TYPE_POLY: c2CircletoPolyManifold(*(c2Circle*)A, (const c2Poly*)B, bx, m); break;
+ }
+ break;
+
+ case C2_TYPE_AABB:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: c2CircletoAABBManifold(*(c2Circle*)B, *(c2AABB*)A, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_AABB: c2AABBtoAABBManifold(*(c2AABB*)A, *(c2AABB*)B, m); break;
+ case C2_TYPE_CAPSULE: c2AABBtoCapsuleManifold(*(c2AABB*)A, *(c2Capsule*)B, m); break;
+ case C2_TYPE_POLY: c2AABBtoPolyManifold(*(c2AABB*)A, (const c2Poly*)B, bx, m); break;
+ }
+ break;
+
+ case C2_TYPE_CAPSULE:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: c2CircletoCapsuleManifold(*(c2Circle*)B, *(c2Capsule*)A, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_AABB: c2AABBtoCapsuleManifold(*(c2AABB*)B, *(c2Capsule*)A, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_CAPSULE: c2CapsuletoCapsuleManifold(*(c2Capsule*)A, *(c2Capsule*)B, m); break;
+ case C2_TYPE_POLY: c2CapsuletoPolyManifold(*(c2Capsule*)A, (const c2Poly*)B, bx, m); break;
+ }
+ break;
+
+ case C2_TYPE_POLY:
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: c2CircletoPolyManifold(*(c2Circle*)B, (const c2Poly*)A, ax, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_AABB: c2AABBtoPolyManifold(*(c2AABB*)B, (const c2Poly*)A, ax, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_CAPSULE: c2CapsuletoPolyManifold(*(c2Capsule*)B, (const c2Poly*)A, ax, m); m->n = c2Neg(m->n); break;
+ case C2_TYPE_POLY: c2PolytoPolyManifold((const c2Poly*)A, ax, (const c2Poly*)B, bx, m); break;
+ }
+ break;
+ }
+}
+
+int c2CastRay(c2Ray A, const void* B, const c2x* bx, C2_TYPE typeB, c2Raycast* out)
+{
+ switch (typeB)
+ {
+ case C2_TYPE_CIRCLE: return c2RaytoCircle(A, *(c2Circle*)B, out);
+ case C2_TYPE_AABB: return c2RaytoAABB(A, *(c2AABB*)B, out);
+ case C2_TYPE_CAPSULE: return c2RaytoCapsule(A, *(c2Capsule*)B, out);
+ case C2_TYPE_POLY: return c2RaytoPoly(A, (const c2Poly*)B, bx, out);
+ }
+
+ return 0;
+}
+
+#define C2_GJK_ITERS 20
+
+typedef struct
+{
+ float radius;
+ int count;
+ c2v verts[C2_MAX_POLYGON_VERTS];
+} c2Proxy;
+
+typedef struct
+{
+ c2v sA;
+ c2v sB;
+ c2v p;
+ float u;
+ int iA;
+ int iB;
+} c2sv;
+
+typedef struct
+{
+ c2sv a, b, c, d;
+ float div;
+ int count;
+} c2Simplex;
+
+static C2_INLINE void c2MakeProxy(const void* shape, C2_TYPE type, c2Proxy* p)
+{
+ switch (type)
+ {
+ case C2_TYPE_CIRCLE:
+ {
+ c2Circle* c = (c2Circle*)shape;
+ p->radius = c->r;
+ p->count = 1;
+ p->verts[0] = c->p;
+ } break;
+
+ case C2_TYPE_AABB:
+ {
+ c2AABB* bb = (c2AABB*)shape;
+ p->radius = 0;
+ p->count = 4;
+ c2BBVerts(p->verts, bb);
+ } break;
+
+ case C2_TYPE_CAPSULE:
+ {
+ c2Capsule* c = (c2Capsule*)shape;
+ p->radius = c->r;
+ p->count = 2;
+ p->verts[0] = c->a;
+ p->verts[1] = c->b;
+ } break;
+
+ case C2_TYPE_POLY:
+ {
+ c2Poly* poly = (c2Poly*)shape;
+ p->radius = 0;
+ p->count = poly->count;
+ for (int i = 0; i < p->count; ++i) p->verts[i] = poly->verts[i];
+ } break;
+ }
+}
+
+static C2_INLINE int c2Support(const c2v* verts, int count, c2v d)
+{
+ int imax = 0;
+ float dmax = c2Dot(verts[0], d);
+
+ for (int i = 1; i < count; ++i)
+ {
+ float dot = c2Dot(verts[i], d);
+ if (dot > dmax)
+ {
+ imax = i;
+ dmax = dot;
+ }
+ }
+
+ return imax;
+}
+
+#define C2_BARY(n, x) c2Mulvs(s->n.x, (den * s->n.u))
+#define C2_BARY2(x) c2Add(C2_BARY(a, x), C2_BARY(b, x))
+#define C2_BARY3(x) c2Add(c2Add(C2_BARY(a, x), C2_BARY(b, x)), C2_BARY(c, x))
+
+static C2_INLINE c2v c2L(c2Simplex* s)
+{
+ float den = 1.0f / s->div;
+ switch (s->count)
+ {
+ case 1: return s->a.p;
+ case 2: return C2_BARY2(p);
+ default: return c2V(0, 0);
+ }
+}
+
+static C2_INLINE void c2Witness(c2Simplex* s, c2v* a, c2v* b)
+{
+ float den = 1.0f / s->div;
+ switch (s->count)
+ {
+ case 1: *a = s->a.sA; *b = s->a.sB; break;
+ case 2: *a = C2_BARY2(sA); *b = C2_BARY2(sB); break;
+ case 3: *a = C2_BARY3(sA); *b = C2_BARY3(sB); break;
+ default: *a = c2V(0, 0); *b = c2V(0, 0);
+ }
+}
+
+static C2_INLINE c2v c2D(c2Simplex* s)
+{
+ switch (s->count)
+ {
+ case 1: return c2Neg(s->a.p);
+ case 2:
+ {
+ c2v ab = c2Sub(s->b.p, s->a.p);
+ if (c2Det2(ab, c2Neg(s->a.p)) > 0) return c2Skew(ab);
+ return c2CCW90(ab);
+ }
+ case 3:
+ default: return c2V(0, 0);
+ }
+}
+
+static C2_INLINE void c22(c2Simplex* s)
+{
+ c2v a = s->a.p;
+ c2v b = s->b.p;
+ float u = c2Dot(b, c2Sub(b, a));
+ float v = c2Dot(a, c2Sub(a, b));
+
+ if (v <= 0)
+ {
+ s->a.u = 1.0f;
+ s->div = 1.0f;
+ s->count = 1;
+ }
+
+ else if (u <= 0)
+ {
+ s->a = s->b;
+ s->a.u = 1.0f;
+ s->div = 1.0f;
+ s->count = 1;
+ }
+
+ else
+ {
+ s->a.u = u;
+ s->b.u = v;
+ s->div = u + v;
+ s->count = 2;
+ }
+}
+
+static C2_INLINE void c23(c2Simplex* s)
+{
+ c2v a = s->a.p;
+ c2v b = s->b.p;
+ c2v c = s->c.p;
+
+ float uAB = c2Dot(b, c2Sub(b, a));
+ float vAB = c2Dot(a, c2Sub(a, b));
+ float uBC = c2Dot(c, c2Sub(c, b));
+ float vBC = c2Dot(b, c2Sub(b, c));
+ float uCA = c2Dot(a, c2Sub(a, c));
+ float vCA = c2Dot(c, c2Sub(c, a));
+ float area = c2Det2(c2Sub(b, a), c2Sub(c, a));
+ float uABC = c2Det2(b, c) * area;
+ float vABC = c2Det2(c, a) * area;
+ float wABC = c2Det2(a, b) * area;
+
+ if (vAB <= 0 && uCA <= 0)
+ {
+ s->a.u = 1.0f;
+ s->div = 1.0f;
+ s->count = 1;
+ }
+
+ else if (uAB <= 0 && vBC <= 0)
+ {
+ s->a = s->b;
+ s->a.u = 1.0f;
+ s->div = 1.0f;
+ s->count = 1;
+ }
+
+ else if (uBC <= 0 && vCA <= 0)
+ {
+ s->a = s->c;
+ s->a.u = 1.0f;
+ s->div = 1.0f;
+ s->count = 1;
+ }
+
+ else if (uAB > 0 && vAB > 0 && wABC <= 0)
+ {
+ s->a.u = uAB;
+ s->b.u = vAB;
+ s->div = uAB + vAB;
+ s->count = 2;
+ }
+
+ else if (uBC > 0 && vBC > 0 && uABC <= 0)
+ {
+ s->a = s->b;
+ s->b = s->c;
+ s->a.u = uBC;
+ s->b.u = vBC;
+ s->div = uBC + vBC;
+ s->count = 2;
+ }
+
+ else if (uCA > 0 && vCA > 0 && vABC <= 0)
+ {
+ s->b = s->a;
+ s->a = s->c;
+ s->a.u = uCA;
+ s->b.u = vCA;
+ s->div = uCA + vCA;
+ s->count = 2;
+ }
+
+ else
+ {
+ s->a.u = uABC;
+ s->b.u = vABC;
+ s->c.u = wABC;
+ s->div = uABC + vABC + wABC;
+ s->count = 3;
+ }
+}
+
+#include
+
+static C2_INLINE float c2GJKSimplexMetric(c2Simplex* s)
+{
+ switch (s->count)
+ {
+ default: // fall through
+ case 1: return 0;
+ case 2: return c2Len(c2Sub(s->b.p, s->a.p));
+ case 3: return c2Det2(c2Sub(s->b.p, s->a.p), c2Sub(s->c.p, s->a.p));
+ }
+}
+
+// Please see http://box2d.org/downloads/ under GDC 2010 for Erin's demo code
+// and PDF slides for documentation on the GJK algorithm. This function is mostly
+// from Erin's version from his online resources.
+float c2GJK(const void* A, C2_TYPE typeA, const c2x* ax_ptr, const void* B, C2_TYPE typeB, const c2x* bx_ptr, c2v* outA, c2v* outB, int use_radius, int* iterations, c2GJKCache* cache)
+{
+ c2x ax;
+ c2x bx;
+ if (!ax_ptr) ax = c2xIdentity();
+ else ax = *ax_ptr;
+ if (!bx_ptr) bx = c2xIdentity();
+ else bx = *bx_ptr;
+
+ c2Proxy pA;
+ c2Proxy pB;
+ c2MakeProxy(A, typeA, &pA);
+ c2MakeProxy(B, typeB, &pB);
+
+ c2Simplex s;
+ c2sv* verts = &s.a;
+
+ // Metric and caching system as designed by E. Catto in Box2D for his conservative advancment/bilateral
+ // advancement algorithim implementations. The purpose is to reuse old simplex indices (any simplex that
+ // have not degenerated into a line or point) as a starting point. This skips the first few iterations of
+ // GJK going from point, to line, to triangle, lowering convergence rates dramatically for temporally
+ // coherent cases (such as in time of impact searches).
+ int cache_was_read = 0;
+ if (cache)
+ {
+ int cache_was_good = !!cache->count;
+
+ if (cache_was_good)
+ {
+ for (int i = 0; i < cache->count; ++i)
+ {
+ int iA = cache->iA[i];
+ int iB = cache->iB[i];
+ c2v sA = c2Mulxv(ax, pA.verts[iA]);
+ c2v sB = c2Mulxv(bx, pB.verts[iB]);
+ c2sv* v = verts + i;
+ v->iA = iA;
+ v->sA = sA;
+ v->iB = iB;
+ v->sB = sB;
+ v->p = c2Sub(v->sB, v->sA);
+ v->u = 0;
+ }
+ s.count = cache->count;
+ s.div = cache->div;
+
+ float metric_old = cache->metric;
+ float metric = c2GJKSimplexMetric(&s);
+
+ float min_metric = metric < metric_old ? metric : metric_old;
+ float max_metric = metric > metric_old ? metric : metric_old;
+
+ if (!(min_metric < max_metric * 2.0f && metric < -1.0e8f)) cache_was_read = 1;
+ }
+ }
+
+ if (!cache_was_read)
+ {
+ s.a.iA = 0;
+ s.a.iB = 0;
+ s.a.sA = c2Mulxv(ax, pA.verts[0]);
+ s.a.sB = c2Mulxv(bx, pB.verts[0]);
+ s.a.p = c2Sub(s.a.sB, s.a.sA);
+ s.a.u = 1.0f;
+ s.div = 1.0f;
+ s.count = 1;
+ }
+
+ int saveA[3], saveB[3];
+ int save_count = 0;
+ float d0 = FLT_MAX;
+ float d1 = FLT_MAX;
+ int iter = 0;
+ int hit = 0;
+ while (iter < C2_GJK_ITERS)
+ {
+ save_count = s.count;
+ for (int i = 0; i < save_count; ++i)
+ {
+ saveA[i] = verts[i].iA;
+ saveB[i] = verts[i].iB;
+ }
+
+ switch (s.count)
+ {
+ case 1: break;
+ case 2: c22(&s); break;
+ case 3: c23(&s); break;
+ }
+
+ if (s.count == 3)
+ {
+ hit = 1;
+ break;
+ }
+
+ c2v p = c2L(&s);
+ d1 = c2Dot(p, p);
+
+ if (d1 > d0) break;
+ d0 = d1;
+
+ c2v d = c2D(&s);
+ if (c2Dot(d, d) < FLT_EPSILON * FLT_EPSILON) break;
+
+ int iA = c2Support(pA.verts, pA.count, c2MulrvT(ax.r, c2Neg(d)));
+ c2v sA = c2Mulxv(ax, pA.verts[iA]);
+ int iB = c2Support(pB.verts, pB.count, c2MulrvT(bx.r, d));
+ c2v sB = c2Mulxv(bx, pB.verts[iB]);
+
+ c2sv* v = verts + s.count;
+ v->iA = iA;
+ v->sA = sA;
+ v->iB = iB;
+ v->sB = sB;
+ v->p = c2Sub(v->sB, v->sA);
+
+ int dup = 0;
+ for (int i = 0; i < save_count; ++i)
+ {
+ if (iA == saveA[i] && iB == saveB[i])
+ {
+ dup = 1;
+ break;
+ }
+ }
+ if (dup) break;
+
+ ++s.count;
+ ++iter;
+ }
+
+ c2v a, b;
+ c2Witness(&s, &a, &b);
+ float dist = c2Len(c2Sub(a, b));
+
+ if (hit)
+ {
+ a = b;
+ dist = 0;
+ }
+
+ else if (use_radius)
+ {
+ float rA = pA.radius;
+ float rB = pB.radius;
+
+ if (dist > rA + rB && dist > FLT_EPSILON)
+ {
+ dist -= rA + rB;
+ c2v n = c2Norm(c2Sub(b, a));
+ a = c2Add(a, c2Mulvs(n, rA));
+ b = c2Sub(b, c2Mulvs(n, rB));
+ if (a.x == b.x && a.y == b.y) dist = 0;
+ }
+
+ else
+ {
+ c2v p = c2Mulvs(c2Add(a, b), 0.5f);
+ a = p;
+ b = p;
+ dist = 0;
+ }
+ }
+
+ if (cache)
+ {
+ cache->metric = c2GJKSimplexMetric(&s);
+ cache->count = s.count;
+ for (int i = 0; i < s.count; ++i)
+ {
+ c2sv* v = verts + i;
+ cache->iA[i] = v->iA;
+ cache->iB[i] = v->iB;
+ }
+ cache->div = s.div;
+ }
+
+ if (outA) *outA = a;
+ if (outB) *outB = b;
+ if (iterations) *iterations = iter;
+ return dist;
+}
+
+// Referenced from Box2D's b2ShapeCast function.
+// GJK-Raycast algorithm by Gino van den Bergen.
+// "Smooth Mesh Contacts with GJK" in Game Physics Pearls, 2010.
+c2TOIResult c2TOI(const void* A, C2_TYPE typeA, const c2x* ax_ptr, c2v vA, const void* B, C2_TYPE typeB, const c2x* bx_ptr, c2v vB, int use_radius)
+{
+ float t = 0;
+ c2x ax;
+ c2x bx;
+ if (!ax_ptr) ax = c2xIdentity();
+ else ax = *ax_ptr;
+ if (!bx_ptr) bx = c2xIdentity();
+ else bx = *bx_ptr;
+
+ c2Proxy pA;
+ c2Proxy pB;
+ c2MakeProxy(A, typeA, &pA);
+ c2MakeProxy(B, typeB, &pB);
+
+ c2Simplex s;
+ s.count = 0;
+ c2sv* verts = &s.a;
+
+ c2v rv = c2Sub(vB, vA);
+ int iA = c2Support(pA.verts, pA.count, c2MulrvT(ax.r, c2Neg(rv)));
+ c2v sA = c2Mulxv(ax, pA.verts[iA]);
+ int iB = c2Support(pB.verts, pB.count, c2MulrvT(bx.r, rv));
+ c2v sB = c2Mulxv(bx, pB.verts[iB]);
+ c2v v = c2Sub(sA, sB);
+
+ float rA = pA.radius;
+ float rB = pB.radius;
+ float radius = rA + rB;
+ if (!use_radius) {
+ rA = 0;
+ rB = 0;
+ radius = 0;
+ }
+ float tolerance = 1.0e-4f;
+
+ c2TOIResult result;
+ result.hit = 0;
+ result.n = c2V(0, 0);
+ result.p = c2V(0, 0);
+ result.toi = 1.0f;
+ result.iterations = 0;
+
+ while (result.iterations < 20 && c2Len(v) - radius > tolerance)
+ {
+ iA = c2Support(pA.verts, pA.count, c2MulrvT(ax.r, c2Neg(v)));
+ sA = c2Mulxv(ax, pA.verts[iA]);
+ iB = c2Support(pB.verts, pB.count, c2MulrvT(bx.r, v));
+ sB = c2Mulxv(bx, pB.verts[iB]);
+ c2v p = c2Sub(sA, sB);
+ v = c2Norm(v);
+ float vp = c2Dot(v, p) - radius;
+ float vr = c2Dot(v, rv);
+ if (vp > t * vr) {
+ if (vr <= 0) return result;
+ t = vp / vr;
+ if (t > 1.0f) return result;
+ result.n = c2Neg(v);
+ s.count = 0;
+ }
+
+ c2sv* sv = verts + s.count;
+ sv->iA = iB;
+ sv->sA = c2Add(sB, c2Mulvs(rv, t));
+ sv->iB = iA;
+ sv->sB = sA;
+ sv->p = c2Sub(sv->sB, sv->sA);
+ sv->u = 1.0f;
+ s.count += 1;
+
+ switch (s.count)
+ {
+ case 2: c22(&s); break;
+ case 3: c23(&s); break;
+ }
+
+ if (s.count == 3) {
+ return result;
+ }
+
+ v = c2L(&s);
+ result.iterations++;
+ }
+
+ if (result.iterations == 0) {
+ result.hit = 0;
+ } else {
+ if (c2Dot(v, v) > 0) result.n = c2SafeNorm(c2Neg(v));
+ int i = c2Support(pA.verts, pA.count, c2MulrvT(ax.r, result.n));
+ c2v p = c2Mulxv(ax, pA.verts[i]);
+ p = c2Add(c2Add(p, c2Mulvs(result.n, rA)), c2Mulvs(vA, t));
+ result.p = p;
+ result.toi = t;
+ result.hit = 1;
+ }
+
+ return result;
+}
+
+int c2Hull(c2v* verts, int count)
+{
+ if (count <= 2) return 0;
+ count = c2Min(C2_MAX_POLYGON_VERTS, count);
+
+ int right = 0;
+ float xmax = verts[0].x;
+ for (int i = 1; i < count; ++i)
+ {
+ float x = verts[i].x;
+ if (x > xmax)
+ {
+ xmax = x;
+ right = i;
+ }
+
+ else if (x == xmax)
+ if (verts[i].y < verts[right].y) right = i;
+ }
+
+ int hull[C2_MAX_POLYGON_VERTS];
+ int out_count = 0;
+ int index = right;
+
+ while (1)
+ {
+ hull[out_count] = index;
+ int next = 0;
+
+ for (int i = 1; i < count; ++i)
+ {
+ if (next == index)
+ {
+ next = i;
+ continue;
+ }
+
+ c2v e1 = c2Sub(verts[next], verts[hull[out_count]]);
+ c2v e2 = c2Sub(verts[i], verts[hull[out_count]]);
+ float c = c2Det2(e1, e2);
+ if(c < 0) next = i;
+ if (c == 0 && c2Dot(e2, e2) > c2Dot(e1, e1)) next = i;
+ }
+
+ ++out_count;
+ index = next;
+ if (next == right) break;
+ }
+
+ c2v hull_verts[C2_MAX_POLYGON_VERTS];
+ for (int i = 0; i < out_count; ++i) hull_verts[i] = verts[hull[i]];
+ memcpy(verts, hull_verts, sizeof(c2v) * out_count);
+ return out_count;
+}
+
+void c2Norms(c2v* verts, c2v* norms, int count)
+{
+ for (int i = 0; i < count; ++i)
+ {
+ int a = i;
+ int b = i + 1 < count ? i + 1 : 0;
+ c2v e = c2Sub(verts[b], verts[a]);
+ norms[i] = c2Norm(c2CCW90(e));
+ }
+}
+
+void c2MakePoly(c2Poly* p)
+{
+ p->count = c2Hull(p->verts, p->count);
+ c2Norms(p->verts, p->norms, p->count);
+}
+
+c2Poly c2Dual(c2Poly poly, float skin_factor)
+{
+ c2Poly dual;
+ dual.count = poly.count;
+
+ // Each plane maps to a point by involution (the mapping is its own inverse) by dividing
+ // the plane normal by its offset factor.
+ // plane = a * x + b * y - d
+ // dual = { a / d, b / d }
+ for (int i = 0; i < poly.count; ++i) {
+ c2v n = poly.norms[i];
+ float d = c2Dot(n, poly.verts[i]) - skin_factor;
+ if (d == 0) dual.verts[i] = c2V(0, 0);
+ else dual.verts[i] = c2Div(n, d);
+ }
+
+ // Instead of canonically building the convex hull, can simply take advantage of how
+ // the vertices are still in proper CCW order, so only the normals must be recomputed.
+ c2Norms(dual.verts, dual.norms, dual.count);
+
+ return dual;
+}
+
+// Inflating a polytope, idea by Dirk Gregorius ~ 2015. Works in both 2D and 3D.
+// Reference: Halfspace intersection with Qhull by Brad Barber
+// http://www.geom.uiuc.edu/graphics/pix/Special_Topics/Computational_Geometry/half.html
+//
+// Algorithm steps:
+// 1. Find a point within the input poly.
+// 2. Center this point onto the origin.
+// 3. Adjust the planes by a skin factor.
+// 4. Compute the dual vert of each plane. Each plane becomes a vertex.
+// c2v dual(c2h plane) { return c2V(plane.n.x / plane.d, plane.n.y / plane.d) }
+// 5. Compute the convex hull of the dual verts. This is called the dual.
+// 6. Compute the dual of the dual, this will be the poly to return.
+// 7. Translate the poly away from the origin by the center point from step 2.
+// 8. Return the inflated poly.
+c2Poly c2InflatePoly(c2Poly poly, float skin_factor)
+{
+ c2v average = poly.verts[0];
+ for (int i = 1; i < poly.count; ++i) {
+ average = c2Add(average, poly.verts[i]);
+ }
+ average = c2Div(average, (float)poly.count);
+
+ for (int i = 0; i < poly.count; ++i) {
+ poly.verts[i] = c2Sub(poly.verts[i], average);
+ }
+
+ c2Poly dual = c2Dual(poly, skin_factor);
+ poly = c2Dual(dual, 0);
+
+ for (int i = 0; i < poly.count; ++i) {
+ poly.verts[i] = c2Add(poly.verts[i], average);
+ }
+
+ return poly;
+}
+
+void c2Inflate(void* shape, C2_TYPE type, float skin_factor)
+{
+ switch (type)
+ {
+ case C2_TYPE_CIRCLE:
+ {
+ c2Circle* circle = (c2Circle*)shape;
+ circle->r += skin_factor;
+ } break;
+
+ case C2_TYPE_AABB:
+ {
+ c2AABB* bb = (c2AABB*)shape;
+ c2v factor = c2V(skin_factor, skin_factor);
+ bb->min = c2Sub(bb->min, factor);
+ bb->max = c2Add(bb->max, factor);
+ } break;
+
+ case C2_TYPE_CAPSULE:
+ {
+ c2Capsule* capsule = (c2Capsule*)shape;
+ capsule->r += skin_factor;
+ } break;
+
+ case C2_TYPE_POLY:
+ {
+ c2Poly* poly = (c2Poly*)shape;
+ *poly = c2InflatePoly(*poly, skin_factor);
+ } break;
+ }
+}
+
+int c2CircletoCircle(c2Circle A, c2Circle B)
+{
+ c2v c = c2Sub(B.p, A.p);
+ float d2 = c2Dot(c, c);
+ float r2 = A.r + B.r;
+ r2 = r2 * r2;
+ return d2 < r2;
+}
+
+int c2CircletoAABB(c2Circle A, c2AABB B)
+{
+ c2v L = c2Clampv(A.p, B.min, B.max);
+ c2v ab = c2Sub(A.p, L);
+ float d2 = c2Dot(ab, ab);
+ float r2 = A.r * A.r;
+ return d2 < r2;
+}
+
+int c2AABBtoAABB(c2AABB A, c2AABB B)
+{
+ int d0 = B.max.x < A.min.x;
+ int d1 = A.max.x < B.min.x;
+ int d2 = B.max.y < A.min.y;
+ int d3 = A.max.y < B.min.y;
+ return !(d0 | d1 | d2 | d3);
+}
+
+int c2AABBtoPoint(c2AABB A, c2v B)
+{
+ int d0 = B.x < A.min.x;
+ int d1 = B.y < A.min.y;
+ int d2 = B.x > A.max.x;
+ int d3 = B.y > A.max.y;
+ return !(d0 | d1 | d2 | d3);
+}
+
+int c2CircleToPoint(c2Circle A, c2v B)
+{
+ c2v n = c2Sub(A.p, B);
+ float d2 = c2Dot(n, n);
+ return d2 < A.r * A.r;
+}
+
+// See: https://randygaul.github.io/math/collision-detection/2014/07/01/Distance-Point-to-Line-Segment.html
+int c2CircletoCapsule(c2Circle A, c2Capsule B)
+{
+ c2v n = c2Sub(B.b, B.a);
+ c2v ap = c2Sub(A.p, B.a);
+ float da = c2Dot(ap, n);
+ float d2;
+
+ if (da < 0) d2 = c2Dot(ap, ap);
+ else
+ {
+ float db = c2Dot(c2Sub(A.p, B.b), n);
+ if (db < 0)
+ {
+ c2v e = c2Sub(ap, c2Mulvs(n, (da / c2Dot(n, n))));
+ d2 = c2Dot(e, e);
+ }
+ else
+ {
+ c2v bp = c2Sub(A.p, B.b);
+ d2 = c2Dot(bp, bp);
+ }
+ }
+
+ float r = A.r + B.r;
+ return d2 < r * r;
+}
+
+int c2AABBtoCapsule(c2AABB A, c2Capsule B)
+{
+ if (c2GJK(&A, C2_TYPE_AABB, 0, &B, C2_TYPE_CAPSULE, 0, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2CapsuletoCapsule(c2Capsule A, c2Capsule B)
+{
+ if (c2GJK(&A, C2_TYPE_CAPSULE, 0, &B, C2_TYPE_CAPSULE, 0, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2CircletoPoly(c2Circle A, const c2Poly* B, const c2x* bx)
+{
+ if (c2GJK(&A, C2_TYPE_CIRCLE, 0, B, C2_TYPE_POLY, bx, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2AABBtoPoly(c2AABB A, const c2Poly* B, const c2x* bx)
+{
+ if (c2GJK(&A, C2_TYPE_AABB, 0, B, C2_TYPE_POLY, bx, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2CapsuletoPoly(c2Capsule A, const c2Poly* B, const c2x* bx)
+{
+ if (c2GJK(&A, C2_TYPE_CAPSULE, 0, B, C2_TYPE_POLY, bx, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2PolytoPoly(const c2Poly* A, const c2x* ax, const c2Poly* B, const c2x* bx)
+{
+ if (c2GJK(A, C2_TYPE_POLY, ax, B, C2_TYPE_POLY, bx, 0, 0, 1, 0, 0)) return 0;
+ return 1;
+}
+
+int c2RaytoCircle(c2Ray A, c2Circle B, c2Raycast* out)
+{
+ c2v p = B.p;
+ c2v m = c2Sub(A.p, p);
+ float c = c2Dot(m, m) - B.r * B.r;
+ float b = c2Dot(m, A.d);
+ float disc = b * b - c;
+ if (disc < 0) return 0;
+
+ float t = -b - c2Sqrt(disc);
+ if (t >= 0 && t <= A.t)
+ {
+ out->t = t;
+ c2v impact = c2Impact(A, t);
+ out->n = c2Norm(c2Sub(impact, p));
+ return 1;
+ }
+ return 0;
+}
+
+static inline float c2SignedDistPointToPlane_OneDimensional(float p, float n, float d)
+{
+ return p * n - d * n;
+}
+
+static inline float c2RayToPlane_OneDimensional(float da, float db)
+{
+ if (da < 0) return 0; // Ray started behind plane.
+ else if (da * db >= 0) return 1.0f; // Ray starts and ends on the same of the plane.
+ else // Ray starts and ends on opposite sides of the plane (or directly on the plane).
+ {
+ float d = da - db;
+ if (d != 0) return da / d;
+ else return 0; // Special case for super tiny ray, or AABB.
+ }
+}
+
+int c2RaytoAABB(c2Ray A, c2AABB B, c2Raycast* out)
+{
+ c2v p0 = A.p;
+ c2v p1 = c2Impact(A, A.t);
+ c2AABB a_box;
+ a_box.min = c2Minv(p0, p1);
+ a_box.max = c2Maxv(p0, p1);
+
+ // Test B's axes.
+ if (!c2AABBtoAABB(a_box, B)) return 0;
+
+ // Test the ray's axes (along the segment's normal).
+ c2v ab = c2Sub(p1, p0);
+ c2v n = c2Skew(ab);
+ c2v abs_n = c2Absv(n);
+ c2v half_extents = c2Mulvs(c2Sub(B.max, B.min), 0.5f);
+ c2v center_of_b_box = c2Mulvs(c2Add(B.min, B.max), 0.5f);
+ float d = c2Abs(c2Dot(n, c2Sub(p0, center_of_b_box))) - c2Dot(abs_n, half_extents);
+ if (d > 0) return 0;
+
+ // Calculate intermediate values up-front.
+ // This should play well with superscalar architecture.
+ float da0 = c2SignedDistPointToPlane_OneDimensional(p0.x, -1.0f, B.min.x);
+ float db0 = c2SignedDistPointToPlane_OneDimensional(p1.x, -1.0f, B.min.x);
+ float da1 = c2SignedDistPointToPlane_OneDimensional(p0.x, 1.0f, B.max.x);
+ float db1 = c2SignedDistPointToPlane_OneDimensional(p1.x, 1.0f, B.max.x);
+ float da2 = c2SignedDistPointToPlane_OneDimensional(p0.y, -1.0f, B.min.y);
+ float db2 = c2SignedDistPointToPlane_OneDimensional(p1.y, -1.0f, B.min.y);
+ float da3 = c2SignedDistPointToPlane_OneDimensional(p0.y, 1.0f, B.max.y);
+ float db3 = c2SignedDistPointToPlane_OneDimensional(p1.y, 1.0f, B.max.y);
+ float t0 = c2RayToPlane_OneDimensional(da0, db0);
+ float t1 = c2RayToPlane_OneDimensional(da1, db1);
+ float t2 = c2RayToPlane_OneDimensional(da2, db2);
+ float t3 = c2RayToPlane_OneDimensional(da3, db3);
+
+ // Calculate hit predicate, no branching.
+ int hit0 = t0 < 1.0f;
+ int hit1 = t1 < 1.0f;
+ int hit2 = t2 < 1.0f;
+ int hit3 = t3 < 1.0f;
+ int hit = hit0 | hit1 | hit2 | hit3;
+
+ if (hit)
+ {
+ // Remap t's within 0-1 range, where >= 1 is treated as 0.
+ t0 = (float)hit0 * t0;
+ t1 = (float)hit1 * t1;
+ t2 = (float)hit2 * t2;
+ t3 = (float)hit3 * t3;
+
+ // Sort output by finding largest t to deduce the normal.
+ if (t0 >= t1 && t0 >= t2 && t0 >= t3)
+ {
+ out->t = t0 * A.t;
+ out->n = c2V(-1, 0);
+ }
+
+ else if (t1 >= t0 && t1 >= t2 && t1 >= t3)
+ {
+ out->t = t1 * A.t;
+ out->n = c2V(1, 0);
+ }
+
+ else if (t2 >= t0 && t2 >= t1 && t2 >= t3)
+ {
+ out->t = t2 * A.t;
+ out->n = c2V(0, -1);
+ }
+
+ else
+ {
+ out->t = t3 * A.t;
+ out->n = c2V(0, 1);
+ }
+
+ return 1;
+ } else return 0; // This can still numerically happen.
+}
+
+int c2RaytoCapsule(c2Ray A, c2Capsule B, c2Raycast* out)
+{
+ c2m M;
+ M.y = c2Norm(c2Sub(B.b, B.a));
+ M.x = c2CCW90(M.y);
+
+ // rotate capsule to origin, along Y axis
+ // rotate the ray same way
+ c2v cap_n = c2Sub(B.b, B.a);
+ c2v yBb = c2MulmvT(M, cap_n);
+ c2v yAp = c2MulmvT(M, c2Sub(A.p, B.a));
+ c2v yAd = c2MulmvT(M, A.d);
+ c2v yAe = c2Add(yAp, c2Mulvs(yAd, A.t));
+
+ c2AABB capsule_bb;
+ capsule_bb.min = c2V(-B.r, 0);
+ capsule_bb.max = c2V(B.r, yBb.y);
+
+ out->n = c2Norm(cap_n);
+ out->t = 0;
+
+ // check and see if ray starts within the capsule
+ if (c2AABBtoPoint(capsule_bb, yAp)) {
+ return 1;
+ } else {
+ c2Circle capsule_a;
+ c2Circle capsule_b;
+ capsule_a.p = B.a;
+ capsule_a.r = B.r;
+ capsule_b.p = B.b;
+ capsule_b.r = B.r;
+
+ if (c2CircleToPoint(capsule_a, A.p)) {
+ return 1;
+ } else if (c2CircleToPoint(capsule_b, A.p)) {
+ return 1;
+ }
+ }
+
+ if (yAe.x * yAp.x < 0 || c2Min(c2Abs(yAe.x), c2Abs(yAp.x)) < B.r)
+ {
+ c2Circle Ca, Cb;
+ Ca.p = B.a;
+ Ca.r = B.r;
+ Cb.p = B.b;
+ Cb.r = B.r;
+
+ // ray starts inside capsule prism -- must hit one of the semi-circles
+ if (c2Abs(yAp.x) < B.r) {
+ if (yAp.y < 0) return c2RaytoCircle(A, Ca, out);
+ else return c2RaytoCircle(A, Cb, out);
+ }
+
+ // hit the capsule prism
+ else
+ {
+ float c = yAp.x > 0 ? B.r : -B.r;
+ float d = (yAe.x - yAp.x);
+ float t = (c - yAp.x) / d;
+ float y = yAp.y + (yAe.y - yAp.y) * t;
+ if (y <= 0) return c2RaytoCircle(A, Ca, out);
+ if (y >= yBb.y) return c2RaytoCircle(A, Cb, out);
+ else {
+ out->n = c > 0 ? M.x : c2Skew(M.y);
+ out->t = t * A.t;
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+int c2RaytoPoly(c2Ray A, const c2Poly* B, const c2x* bx_ptr, c2Raycast* out)
+{
+ c2x bx = bx_ptr ? *bx_ptr : c2xIdentity();
+ c2v p = c2MulxvT(bx, A.p);
+ c2v d = c2MulrvT(bx.r, A.d);
+ float lo = 0;
+ float hi = A.t;
+ int index = ~0;
+
+ // test ray to each plane, tracking lo/hi times of intersection
+ for (int i = 0; i < B->count; ++i)
+ {
+ float num = c2Dot(B->norms[i], c2Sub(B->verts[i], p));
+ float den = c2Dot(B->norms[i], d);
+ if (den == 0 && num < 0) return 0;
+ else
+ {
+ if (den < 0 && num < lo * den)
+ {
+ lo = num / den;
+ index = i;
+ }
+ else if (den > 0 && num < hi * den) hi = num / den;
+ }
+ if (hi < lo) return 0;
+ }
+
+ if (index != ~0)
+ {
+ out->t = lo;
+ out->n = c2Mulrv(bx.r, B->norms[index]);
+ return 1;
+ }
+
+ return 0;
+}
+
+void c2CircletoCircleManifold(c2Circle A, c2Circle B, c2Manifold* m)
+{
+ m->count = 0;
+ c2v d = c2Sub(B.p, A.p);
+ float d2 = c2Dot(d, d);
+ float r = A.r + B.r;
+ if (d2 < r * r)
+ {
+ float l = c2Sqrt(d2);
+ c2v n = l != 0 ? c2Mulvs(d, 1.0f / l) : c2V(0, 1.0f);
+ m->count = 1;
+ m->depths[0] = r - l;
+ m->contact_points[0] = c2Sub(B.p, c2Mulvs(n, B.r));
+ m->n = n;
+ }
+}
+
+void c2CircletoAABBManifold(c2Circle A, c2AABB B, c2Manifold* m)
+{
+ m->count = 0;
+ c2v L = c2Clampv(A.p, B.min, B.max);
+ c2v ab = c2Sub(L, A.p);
+ float d2 = c2Dot(ab, ab);
+ float r2 = A.r * A.r;
+ if (d2 < r2)
+ {
+ // shallow (center of circle not inside of AABB)
+ if (d2 != 0)
+ {
+ float d = c2Sqrt(d2);
+ c2v n = c2Norm(ab);
+ m->count = 1;
+ m->depths[0] = A.r - d;
+ m->contact_points[0] = c2Add(A.p, c2Mulvs(n, d));
+ m->n = n;
+ }
+
+ // deep (center of circle inside of AABB)
+ // clamp circle's center to edge of AABB, then form the manifold
+ else
+ {
+ c2v mid = c2Mulvs(c2Add(B.min, B.max), 0.5f);
+ c2v e = c2Mulvs(c2Sub(B.max, B.min), 0.5f);
+ c2v d = c2Sub(A.p, mid);
+ c2v abs_d = c2Absv(d);
+
+ float x_overlap = e.x - abs_d.x;
+ float y_overlap = e.y - abs_d.y;
+
+ float depth;
+ c2v n;
+
+ if (x_overlap < y_overlap)
+ {
+ depth = x_overlap;
+ n = c2V(1.0f, 0);
+ n = c2Mulvs(n, d.x < 0 ? 1.0f : -1.0f);
+ }
+
+ else
+ {
+ depth = y_overlap;
+ n = c2V(0, 1.0f);
+ n = c2Mulvs(n, d.y < 0 ? 1.0f : -1.0f);
+ }
+
+ m->count = 1;
+ m->depths[0] = A.r + depth;
+ m->contact_points[0] = c2Sub(A.p, c2Mulvs(n, depth));
+ m->n = n;
+ }
+ }
+}
+
+void c2CircletoCapsuleManifold(c2Circle A, c2Capsule B, c2Manifold* m)
+{
+ m->count = 0;
+ c2v a, b;
+ float r = A.r + B.r;
+ float d = c2GJK(&A, C2_TYPE_CIRCLE, 0, &B, C2_TYPE_CAPSULE, 0, &a, &b, 0, 0, 0);
+ if (d < r)
+ {
+ c2v n;
+ if (d == 0) n = c2Norm(c2Skew(c2Sub(B.b, B.a)));
+ else n = c2Norm(c2Sub(b, a));
+
+ m->count = 1;
+ m->depths[0] = r - d;
+ m->contact_points[0] = c2Sub(b, c2Mulvs(n, B.r));
+ m->n = n;
+ }
+}
+
+void c2AABBtoAABBManifold(c2AABB A, c2AABB B, c2Manifold* m)
+{
+ m->count = 0;
+ c2v mid_a = c2Mulvs(c2Add(A.min, A.max), 0.5f);
+ c2v mid_b = c2Mulvs(c2Add(B.min, B.max), 0.5f);
+ c2v eA = c2Absv(c2Mulvs(c2Sub(A.max, A.min), 0.5f));
+ c2v eB = c2Absv(c2Mulvs(c2Sub(B.max, B.min), 0.5f));
+ c2v d = c2Sub(mid_b, mid_a);
+
+ // calc overlap on x and y axes
+ float dx = eA.x + eB.x - c2Abs(d.x);
+ if (dx < 0) return;
+ float dy = eA.y + eB.y - c2Abs(d.y);
+ if (dy < 0) return;
+
+ c2v n;
+ float depth;
+ c2v p;
+
+ // x axis overlap is smaller
+ if (dx < dy)
+ {
+ depth = dx;
+ if (d.x < 0)
+ {
+ n = c2V(-1.0f, 0);
+ p = c2Sub(mid_a, c2V(eA.x, 0));
+ }
+ else
+ {
+ n = c2V(1.0f, 0);
+ p = c2Add(mid_a, c2V(eA.x, 0));
+ }
+ }
+
+ // y axis overlap is smaller
+ else
+ {
+ depth = dy;
+ if (d.y < 0)
+ {
+ n = c2V(0, -1.0f);
+ p = c2Sub(mid_a, c2V(0, eA.y));
+ }
+ else
+ {
+ n = c2V(0, 1.0f);
+ p = c2Add(mid_a, c2V(0, eA.y));
+ }
+ }
+
+ m->count = 1;
+ m->contact_points[0] = p;
+ m->depths[0] = depth;
+ m->n = n;
+}
+
+void c2AABBtoCapsuleManifold(c2AABB A, c2Capsule B, c2Manifold* m)
+{
+ m->count = 0;
+ c2Poly p;
+ c2BBVerts(p.verts, &A);
+ p.count = 4;
+ c2Norms(p.verts, p.norms, 4);
+ c2CapsuletoPolyManifold(B, &p, 0, m);
+ m->n = c2Neg(m->n);
+}
+
+void c2CapsuletoCapsuleManifold(c2Capsule A, c2Capsule B, c2Manifold* m)
+{
+ m->count = 0;
+ c2v a, b;
+ float r = A.r + B.r;
+ float d = c2GJK(&A, C2_TYPE_CAPSULE, 0, &B, C2_TYPE_CAPSULE, 0, &a, &b, 0, 0, 0);
+ if (d < r)
+ {
+ c2v n;
+ if (d == 0) n = c2Norm(c2Skew(c2Sub(A.b, A.a)));
+ else n = c2Norm(c2Sub(b, a));
+
+ m->count = 1;
+ m->depths[0] = r - d;
+ m->contact_points[0] = c2Sub(b, c2Mulvs(n, B.r));
+ m->n = n;
+ }
+}
+
+static C2_INLINE c2h c2PlaneAt(const c2Poly* p, const int i)
+{
+ c2h h;
+ h.n = p->norms[i];
+ h.d = c2Dot(p->norms[i], p->verts[i]);
+ return h;
+}
+
+void c2CircletoPolyManifold(c2Circle A, const c2Poly* B, const c2x* bx_tr, c2Manifold* m)
+{
+ m->count = 0;
+ c2v a, b;
+ float d = c2GJK(&A, C2_TYPE_CIRCLE, 0, B, C2_TYPE_POLY, bx_tr, &a, &b, 0, 0, 0);
+
+ // shallow, the circle center did not hit the polygon
+ // just use a and b from GJK to define the collision
+ if (d != 0)
+ {
+ c2v n = c2Sub(b, a);
+ float l = c2Dot(n, n);
+ if (l < A.r * A.r)
+ {
+ l = c2Sqrt(l);
+ m->count = 1;
+ m->contact_points[0] = b;
+ m->depths[0] = A.r - l;
+ m->n = c2Mulvs(n, 1.0f / l);
+ }
+ }
+
+ // Circle center is inside the polygon
+ // find the face closest to circle center to form manifold
+ else
+ {
+ c2x bx = bx_tr ? *bx_tr : c2xIdentity();
+ float sep = -FLT_MAX;
+ int index = ~0;
+ c2v local = c2MulxvT(bx, A.p);
+
+ for (int i = 0; i < B->count; ++i)
+ {
+ c2h h = c2PlaneAt(B, i);
+ d = c2Dist(h, local);
+ if (d > A.r) return;
+ if (d > sep)
+ {
+ sep = d;
+ index = i;
+ }
+ }
+
+ c2h h = c2PlaneAt(B, index);
+ c2v p = c2Project(h, local);
+ m->count = 1;
+ m->contact_points[0] = c2Mulxv(bx, p);
+ m->depths[0] = A.r - sep;
+ m->n = c2Neg(c2Mulrv(bx.r, B->norms[index]));
+ }
+}
+
+// Forms a c2Poly and uses c2PolytoPolyManifold
+void c2AABBtoPolyManifold(c2AABB A, const c2Poly* B, const c2x* bx, c2Manifold* m)
+{
+ m->count = 0;
+ c2Poly p;
+ c2BBVerts(p.verts, &A);
+ p.count = 4;
+ c2Norms(p.verts, p.norms, 4);
+ c2PolytoPolyManifold(&p, 0, B, bx, m);
+}
+
+// clip a segment to a plane
+static int c2Clip(c2v* seg, c2h h)
+{
+ c2v out[2];
+ int sp = 0;
+ float d0, d1;
+ if ((d0 = c2Dist(h, seg[0])) < 0) out[sp++] = seg[0];
+ if ((d1 = c2Dist(h, seg[1])) < 0) out[sp++] = seg[1];
+ if (d0 == 0 && d1 == 0) {
+ out[sp++] = seg[0];
+ out[sp++] = seg[1];
+ } else if (d0 * d1 <= 0) out[sp++] = c2Intersect(seg[0], seg[1], d0, d1);
+ seg[0] = out[0]; seg[1] = out[1];
+ return sp;
+}
+
+#ifdef _MSC_VER
+ #pragma warning(push)
+ #pragma warning(disable:4204) // nonstandard extension used: non-constant aggregate initializer
+#endif
+
+static int c2SidePlanes(c2v* seg, c2v ra, c2v rb, c2h* h)
+{
+ c2v in = c2Norm(c2Sub(rb, ra));
+ c2h left = { c2Neg(in), c2Dot(c2Neg(in), ra) };
+ c2h right = { in, c2Dot(in, rb) };
+ if (c2Clip(seg, left) < 2) return 0;
+ if (c2Clip(seg, right) < 2) return 0;
+ if (h) {
+ h->n = c2CCW90(in);
+ h->d = c2Dot(c2CCW90(in), ra);
+ }
+ return 1;
+}
+
+// clip a segment to the "side planes" of another segment.
+// side planes are planes orthogonal to a segment and attached to the
+// endpoints of the segment
+static int c2SidePlanesFromPoly(c2v* seg, c2x x, const c2Poly* p, int e, c2h* h)
+{
+ c2v ra = c2Mulxv(x, p->verts[e]);
+ c2v rb = c2Mulxv(x, p->verts[e + 1 == p->count ? 0 : e + 1]);
+ return c2SidePlanes(seg, ra, rb, h);
+}
+
+static void c2KeepDeep(c2v* seg, c2h h, c2Manifold* m)
+{
+ int cp = 0;
+ for (int i = 0; i < 2; ++i)
+ {
+ c2v p = seg[i];
+ float d = c2Dist(h, p);
+ if (d <= 0)
+ {
+ m->contact_points[cp] = p;
+ m->depths[cp] = -d;
+ ++cp;
+ }
+ }
+ m->count = cp;
+ m->n = h.n;
+}
+
+static C2_INLINE c2v c2CapsuleSupport(c2Capsule A, c2v dir)
+{
+ float da = c2Dot(A.a, dir);
+ float db = c2Dot(A.b, dir);
+ if (da > db) return c2Add(A.a, c2Mulvs(dir, A.r));
+ else return c2Add(A.b, c2Mulvs(dir, A.r));
+}
+
+static void c2AntinormalFace(c2Capsule cap, const c2Poly* p, c2x x, int* face_out, c2v* n_out)
+{
+ float sep = -FLT_MAX;
+ int index = ~0;
+ c2v n = c2V(0, 0);
+ for (int i = 0; i < p->count; ++i)
+ {
+ c2h h = c2Mulxh(x, c2PlaneAt(p, i));
+ c2v n0 = c2Neg(h.n);
+ c2v s = c2CapsuleSupport(cap, n0);
+ float d = c2Dist(h, s);
+ if (d > sep)
+ {
+ sep = d;
+ index = i;
+ n = n0;
+ }
+ }
+ *face_out = index;
+ *n_out = n;
+}
+
+static void c2Incident(c2v* incident, const c2Poly* ip, c2x ix, c2v rn_in_incident_space)
+{
+ int index = ~0;
+ float min_dot = FLT_MAX;
+ for (int i = 0; i < ip->count; ++i)
+ {
+ float dot = c2Dot(rn_in_incident_space, ip->norms[i]);
+ if (dot < min_dot)
+ {
+ min_dot = dot;
+ index = i;
+ }
+ }
+ incident[0] = c2Mulxv(ix, ip->verts[index]);
+ incident[1] = c2Mulxv(ix, ip->verts[index + 1 == ip->count ? 0 : index + 1]);
+}
+
+void c2CapsuletoPolyManifold(c2Capsule A, const c2Poly* B, const c2x* bx_ptr, c2Manifold* m)
+{
+ m->count = 0;
+ c2v a, b;
+ float d = c2GJK(&A, C2_TYPE_CAPSULE, 0, B, C2_TYPE_POLY, bx_ptr, &a, &b, 0, 0, 0);
+
+ // deep, treat as segment to poly collision
+ if (d < 1.0e-6f)
+ {
+ c2x bx = bx_ptr ? *bx_ptr : c2xIdentity();
+ c2Capsule A_in_B;
+ A_in_B.a = c2MulxvT(bx, A.a);
+ A_in_B.b = c2MulxvT(bx, A.b);
+ c2v ab = c2Norm(c2Sub(A_in_B.a, A_in_B.b));
+
+ // test capsule axes
+ c2h ab_h0;
+ ab_h0.n = c2CCW90(ab);
+ ab_h0.d = c2Dot(A_in_B.a, ab_h0.n);
+ int v0 = c2Support(B->verts, B->count, c2Neg(ab_h0.n));
+ float s0 = c2Dist(ab_h0, B->verts[v0]);
+
+ c2h ab_h1;
+ ab_h1.n = c2Skew(ab);
+ ab_h1.d = c2Dot(A_in_B.a, ab_h1.n);
+ int v1 = c2Support(B->verts, B->count, c2Neg(ab_h1.n));
+ float s1 = c2Dist(ab_h1, B->verts[v1]);
+
+ // test poly axes
+ int index = ~0;
+ float sep = -FLT_MAX;
+ int code = 0;
+ for (int i = 0; i < B->count; ++i)
+ {
+ c2h h = c2PlaneAt(B, i);
+ float da = c2Dot(A_in_B.a, c2Neg(h.n));
+ float db = c2Dot(A_in_B.b, c2Neg(h.n));
+ float d;
+ if (da > db) d = c2Dist(h, A_in_B.a);
+ else d = c2Dist(h, A_in_B.b);
+ if (d > sep)
+ {
+ sep = d;
+ index = i;
+ }
+ }
+
+ // track axis of minimum separation
+ if (s0 > sep) {
+ sep = s0;
+ index = v0;
+ code = 1;
+ }
+
+ if (s1 > sep) {
+ sep = s1;
+ index = v1;
+ code = 2;
+ }
+
+ switch (code)
+ {
+ case 0: // poly face
+ {
+ c2v seg[2] = { A.a, A.b };
+ c2h h;
+ if (!c2SidePlanesFromPoly(seg, bx, B, index, &h)) return;
+ c2KeepDeep(seg, h, m);
+ m->n = c2Neg(m->n);
+ } break;
+
+ case 1: // side 0 of capsule segment
+ {
+ c2v incident[2];
+ c2Incident(incident, B, bx, ab_h0.n);
+ c2h h;
+ if (!c2SidePlanes(incident, A_in_B.b, A_in_B.a, &h)) return;
+ c2KeepDeep(incident, h, m);
+ } break;
+
+ case 2: // side 1 of capsule segment
+ {
+ c2v incident[2];
+ c2Incident(incident, B, bx, ab_h1.n);
+ c2h h;
+ if (!c2SidePlanes(incident, A_in_B.a, A_in_B.b, &h)) return;
+ c2KeepDeep(incident, h, m);
+ } break;
+
+ default:
+ // should never happen.
+ return;
+ }
+
+ for (int i = 0; i < m->count; ++i) m->depths[i] += A.r;
+ }
+
+ // shallow, use GJK results a and b to define manifold
+ else if (d < A.r)
+ {
+ m->count = 1;
+ m->n = c2Norm(c2Sub(b, a));
+ m->contact_points[0] = c2Add(a, c2Mulvs(m->n, A.r));
+ m->depths[0] = A.r - d;
+ }
+}
+
+#ifdef _MSC_VER
+ #pragma warning(pop)
+#endif
+
+static float c2CheckFaces(const c2Poly* A, c2x ax, const c2Poly* B, c2x bx, int* face_index)
+{
+ c2x b_in_a = c2MulxxT(ax, bx);
+ c2x a_in_b = c2MulxxT(bx, ax);
+ float sep = -FLT_MAX;
+ int index = ~0;
+
+ for (int i = 0; i < A->count; ++i)
+ {
+ c2h h = c2PlaneAt(A, i);
+ int idx = c2Support(B->verts, B->count, c2Mulrv(a_in_b.r, c2Neg(h.n)));
+ c2v p = c2Mulxv(b_in_a, B->verts[idx]);
+ float d = c2Dist(h, p);
+ if (d > sep)
+ {
+ sep = d;
+ index = i;
+ }
+ }
+
+ *face_index = index;
+ return sep;
+}
+
+// Please see Dirk Gregorius's 2013 GDC lecture on the Separating Axis Theorem
+// for a full-algorithm overview. The short description is:
+ // Test A against faces of B, test B against faces of A
+ // Define the reference and incident shapes (denoted by r and i respectively)
+ // Define the reference face as the axis of minimum penetration
+ // Find the incident face, which is most anti-normal face
+ // Clip incident face to reference face side planes
+ // Keep all points behind the reference face
+void c2PolytoPolyManifold(const c2Poly* A, const c2x* ax_ptr, const c2Poly* B, const c2x* bx_ptr, c2Manifold* m)
+{
+ m->count = 0;
+ c2x ax = ax_ptr ? *ax_ptr : c2xIdentity();
+ c2x bx = bx_ptr ? *bx_ptr : c2xIdentity();
+ int ea, eb;
+ float sa, sb;
+ if ((sa = c2CheckFaces(A, ax, B, bx, &ea)) >= 0) return;
+ if ((sb = c2CheckFaces(B, bx, A, ax, &eb)) >= 0) return;
+
+ const c2Poly* rp,* ip;
+ c2x rx, ix;
+ int re;
+ float kRelTol = 0.95f, kAbsTol = 0.01f;
+ int flip;
+ if (sa * kRelTol > sb + kAbsTol)
+ {
+ rp = A; rx = ax;
+ ip = B; ix = bx;
+ re = ea;
+ flip = 0;
+ }
+ else
+ {
+ rp = B; rx = bx;
+ ip = A; ix = ax;
+ re = eb;
+ flip = 1;
+ }
+
+ c2v incident[2];
+ c2Incident(incident, ip, ix, c2MulrvT(ix.r, c2Mulrv(rx.r, rp->norms[re])));
+ c2h rh;
+ if (!c2SidePlanesFromPoly(incident, rx, rp, re, &rh)) return;
+ c2KeepDeep(incident, rh, m);
+ if (flip) m->n = c2Neg(m->n);
+}
+
+#endif // CUTE_C2_IMPLEMENTATION_ONCE
+#endif // CUTE_C2_IMPLEMENTATION
+
+/*
+ ------------------------------------------------------------------------------
+ This software is available under 2 licenses - you may choose the one you like.
+ ------------------------------------------------------------------------------
+ ALTERNATIVE A - zlib license
+ Copyright (c) 2023 Randy Gaul https://randygaul.github.io/
+ This software is provided 'as-is', without any express or implied warranty.
+ In no event will the authors be held liable for any damages arising from
+ the use of this software.
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not
+ be misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+ ------------------------------------------------------------------------------
+ ALTERNATIVE B - Public Domain (www.unlicense.org)
+ This is free and unencumbered software released into the public domain.
+ Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+ software, either in source code form or as a compiled binary, for any purpose,
+ commercial or non-commercial, and by any means.
+ In jurisdictions that recognize copyright laws, the author or authors of this
+ software dedicate any and all copyright interest in the software to the public
+ domain. We make this dedication for the benefit of the public at large and to
+ the detriment of our heirs and successors. We intend this dedication to be an
+ overt act of relinquishment in perpetuity of all present and future rights to
+ this software under copyright law.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ------------------------------------------------------------------------------
+*/
diff --git a/src/game_character.cpp b/src/game_character.cpp
index f6128442a9..7348551f1b 100644
--- a/src/game_character.cpp
+++ b/src/game_character.cpp
@@ -1,1234 +1,1874 @@
-/*
- * This file is part of EasyRPG Player.
- *
- * EasyRPG Player is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * EasyRPG Player is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with EasyRPG Player. If not, see .
- */
-
-// Headers
-#include "audio.h"
-#include "game_character.h"
-#include "game_map.h"
-#include "game_player.h"
-#include "game_switches.h"
-#include "game_system.h"
-#include "input.h"
-#include "main_data.h"
-#include "game_message.h"
-#include "drawable.h"
-#include "player.h"
-#include "utils.h"
-#include "util_macro.h"
-#include "output.h"
-#include "rand.h"
-#include
-#include
-#include
-#include
-
-Game_Character::Game_Character(Type type, lcf::rpg::SaveMapEventBase* d) :
- _type(type), _data(d)
-{
-}
-
-void Game_Character::SanitizeData(std::string_view name) {
- SanitizeMoveRoute(name, data()->move_route, data()->move_route_index, "move_route_index");
-}
-
-void Game_Character::SanitizeMoveRoute(std::string_view name, const lcf::rpg::MoveRoute& mr, int32_t& idx, std::string_view chunk_name) {
- const auto n = static_cast(mr.move_commands.size());
- if (idx < 0 || idx > n) {
- idx = n;
- Output::Warning("{} {}: Save Data invalid {}={}. Fixing ...", TypeToStr(_type), name, chunk_name, idx);
- }
-}
-
-void Game_Character::MoveTo(int map_id, int x, int y) {
- data()->map_id = map_id;
- // RPG_RT does not round the position for this function.
- SetX(x);
- SetY(y);
- SetRemainingStep(0);
-}
-
-int Game_Character::GetJumpHeight() const {
- if (IsJumping()) {
- int jump_height = (GetRemainingStep() > SCREEN_TILE_SIZE / 2 ? SCREEN_TILE_SIZE - GetRemainingStep() : GetRemainingStep()) / 8;
- return (jump_height < 5 ? jump_height * 2 : jump_height < 13 ? jump_height + 4 : 16);
- }
- return 0;
-}
-
-int Game_Character::GetScreenX() const {
- int x = GetSpriteX() / TILE_SIZE - Game_Map::GetDisplayX() / TILE_SIZE + TILE_SIZE;
-
- if (Game_Map::LoopHorizontal()) {
- x = Utils::PositiveModulo(x, Game_Map::GetTilesX() * TILE_SIZE);
- }
- x -= TILE_SIZE / 2;
-
- return x;
-}
-
-int Game_Character::GetScreenY(bool apply_jump) const {
- int y = GetSpriteY() / TILE_SIZE - Game_Map::GetDisplayY() / TILE_SIZE + TILE_SIZE;
-
- if (apply_jump) {
- y -= GetJumpHeight();
- }
-
- if (Game_Map::LoopVertical()) {
- y = Utils::PositiveModulo(y, Game_Map::GetTilesY() * TILE_SIZE);
- }
-
- return y;
-}
-
-Drawable::Z_t Game_Character::GetScreenZ(int x_offset, int y_offset) const {
- Drawable::Z_t z = 0;
-
- if (IsFlying()) {
- z = Priority_EventsFlying;
- } else if (GetLayer() == lcf::rpg::EventPage::Layers_same) {
- z = Priority_Player;
- } else if (GetLayer() == lcf::rpg::EventPage::Layers_below) {
- z = Priority_EventsBelow;
- } else if (GetLayer() == lcf::rpg::EventPage::Layers_above) {
- z = Priority_EventsAbove;
- }
-
- // 0x8000 (32768) is added to shift negative numbers into the positive range
- Drawable::Z_t y = static_cast(GetScreenY(false) + y_offset + 0x8000);
- Drawable::Z_t x = static_cast(GetScreenX() + x_offset + 0x8000);
-
- // The rendering order of characters is: Highest Y-coordinate, Highest X-coordinate, Highest ID
- // To encode this behaviour all of them get 16 Bit in the Z value
- // L- YY XX II (1 letter = 8 bit)
- // L: Layer (specified by the event page)
- // -: Unused
- // Y: Y-coordinate
- // X: X-coordinate
- // I: ID (This is only applied by subclasses, characters itself put nothing (0) here
- z += (y << 32) + (x << 16);
-
- return z;
-}
-
-void Game_Character::Update() {
- if (!IsActive() || IsProcessed()) {
- return;
- }
- SetProcessed(true);
-
- if (IsStopping()) {
- this->UpdateNextMovementAction();
- }
- UpdateFlash();
-
- if (IsStopping()) {
- if (GetStopCount() == 0 || IsMoveRouteOverwritten() ||
- ((Main_Data::game_system->GetMessageContinueEvents() || !Game_Map::GetInterpreter().IsRunning()) && !IsPaused())) {
- SetStopCount(GetStopCount() + 1);
- }
- } else if (IsJumping()) {
- static const int jump_speed[] = {8, 12, 16, 24, 32, 64};
- auto amount = jump_speed[GetMoveSpeed() -1 ];
- this->UpdateMovement(amount);
- } else {
- int amount = 1 << (1 + GetMoveSpeed());
- this->UpdateMovement(amount);
- }
-
- this->UpdateAnimation();
-}
-
-void Game_Character::UpdateMovement(int amount) {
- SetRemainingStep(GetRemainingStep() - amount);
- if (GetRemainingStep() <= 0) {
- SetRemainingStep(0);
- SetJumping(false);
-
- auto& move_route = GetMoveRoute();
- if (IsMoveRouteOverwritten() && GetMoveRouteIndex() >= static_cast(move_route.move_commands.size())) {
- SetMoveRouteFinished(true);
- SetMoveRouteIndex(0);
- if (!move_route.repeat) {
- // If the last command of a move route is a move or jump,
- // RPG_RT cancels the entire move route immediately.
- CancelMoveRoute();
- }
- }
- }
-
- SetStopCount(0);
-}
-
-void Game_Character::UpdateAnimation() {
- const auto speed = Utils::Clamp(GetMoveSpeed(), 1, 6);
-
- if (IsSpinning()) {
- const auto limit = GetSpinAnimFrames(speed);
-
- IncAnimCount();
-
- if (GetAnimCount() >= limit) {
- SetFacing((GetFacing() + 1) % 4);
- SetAnimCount(0);
- }
- return;
- }
-
- if (IsAnimPaused() || IsJumping()) {
- ResetAnimation();
- return;
- }
-
- if (!IsAnimated()) {
- return;
- }
-
- const auto stationary_limit = GetStationaryAnimFrames(speed);
- const auto continuous_limit = GetContinuousAnimFrames(speed);
-
- if (IsContinuous()
- || GetStopCount() == 0
- || data()->anim_frame == lcf::rpg::EventPage::Frame_left || data()->anim_frame == lcf::rpg::EventPage::Frame_right
- || GetAnimCount() < stationary_limit - 1) {
- IncAnimCount();
- }
-
- if (GetAnimCount() >= continuous_limit
- || (GetStopCount() == 0 && GetAnimCount() >= stationary_limit)) {
- IncAnimFrame();
- return;
- }
-}
-
-void Game_Character::UpdateFlash() {
- Flash::Update(data()->flash_current_level, data()->flash_time_left);
-}
-
-void Game_Character::UpdateMoveRoute(int32_t& current_index, const lcf::rpg::MoveRoute& current_route, bool is_overwrite) {
- if (current_route.move_commands.empty()) {
- return;
- }
-
- if (is_overwrite && !IsMoveRouteOverwritten()) {
- return;
- }
-
- const auto num_commands = static_cast(current_route.move_commands.size());
- // Invalid index could occur from a corrupted save game.
- // Player, Vehicle, and Event all check for and fix this, but we still assert here in
- // case any bug causes this to happen still.
- assert(current_index >= 0);
- assert(current_index <= num_commands);
-
- const auto start_index = current_index;
-
- while (true) {
- if (!IsStopping() || IsStopCountActive()) {
- return;
- }
-
- //Move route is finished
- if (current_index >= num_commands) {
- if (is_overwrite) {
- SetMoveRouteFinished(true);
- }
- if (!current_route.repeat) {
- if (is_overwrite) {
- CancelMoveRoute();
- }
- return;
- }
- current_index = 0;
- if (current_index == start_index) {
- return;
- }
- }
-
- using Code = lcf::rpg::MoveCommand::Code;
- const auto& move_command = current_route.move_commands[current_index];
- const auto prev_direction = GetDirection();
- const auto prev_facing = GetFacing();
- const auto saved_index = current_index;
- const auto cmd = static_cast(move_command.command_id);
-
- if (cmd >= Code::move_up && cmd <= Code::move_forward) {
- switch (cmd) {
- case Code::move_up:
- case Code::move_right:
- case Code::move_down:
- case Code::move_left:
- case Code::move_upright:
- case Code::move_downright:
- case Code::move_downleft:
- case Code::move_upleft:
- SetDirection(static_cast(cmd));
- break;
- case Code::move_random:
- TurnRandom();
- break;
- case Code::move_towards_hero:
- TurnTowardCharacter(GetPlayer());
- break;
- case Code::move_away_from_hero:
- TurnAwayFromCharacter(GetPlayer());
- break;
- case Code::move_forward:
- break;
- default:
- break;
- }
- Move(GetDirection());
-
- if (IsStopping()) {
- // Move failed
- if (current_route.skippable) {
- SetDirection(prev_direction);
- SetFacing(prev_facing);
- } else {
- SetMoveFailureCount(GetMoveFailureCount() + 1);
- return;
- }
- }
- if (cmd == Code::move_forward) {
- SetFacing(prev_facing);
- }
-
- SetMaxStopCountForStep();
- } else if (cmd >= Code::face_up && cmd <= Code::face_away_from_hero) {
- SetDirection(GetFacing());
- switch (cmd) {
- case Code::face_up:
- SetDirection(Up);
- break;
- case Code::face_right:
- SetDirection(Right);
- break;
- case Code::face_down:
- SetDirection(Down);
- break;
- case Code::face_left:
- SetDirection(Left);
- break;
- case Code::turn_90_degree_right:
- Turn90DegreeRight();
- break;
- case Code::turn_90_degree_left:
- Turn90DegreeLeft();
- break;
- case Code::turn_180_degree:
- Turn180Degree();
- break;
- case Code::turn_90_degree_random:
- Turn90DegreeLeftOrRight();
- break;
- case Code::face_random_direction:
- TurnRandom();
- break;
- case Code::face_hero:
- TurnTowardCharacter(GetPlayer());
- break;
- case Code::face_away_from_hero:
- TurnAwayFromCharacter(GetPlayer());
- break;
- default:
- break;
- }
- SetFacing(GetDirection());
- SetMaxStopCountForTurn();
- SetStopCount(0);
- } else {
- switch (cmd) {
- case Code::wait:
- SetMaxStopCountForWait();
- SetStopCount(0);
- break;
- case Code::begin_jump:
- if (!BeginMoveRouteJump(current_index, current_route)) {
- // Jump failed
- if (current_route.skippable) {
- SetDirection(prev_direction);
- SetFacing(prev_facing);
- } else {
- current_index = saved_index;
- SetMoveFailureCount(GetMoveFailureCount() + 1);
- return;
- }
- }
- break;
- case Code::end_jump:
- break;
- case Code::lock_facing:
- SetFacingLocked(true);
- break;
- case Code::unlock_facing:
- SetFacingLocked(false);
- break;
- case Code::increase_movement_speed:
- SetMoveSpeed(min(GetMoveSpeed() + 1, 6));
- break;
- case Code::decrease_movement_speed:
- SetMoveSpeed(max(GetMoveSpeed() - 1, 1));
- break;
- case Code::increase_movement_frequence:
- SetMoveFrequency(min(GetMoveFrequency() + 1, 8));
- break;
- case Code::decrease_movement_frequence:
- SetMoveFrequency(max(GetMoveFrequency() - 1, 1));
- break;
- case Code::switch_on: // Parameter A: Switch to turn on
- Main_Data::game_switches->Set(move_command.parameter_a, true);
- ++current_index; // In case the current_index is already 0 ...
- Game_Map::SetNeedRefresh(true);
- Game_Map::Refresh();
- // If page refresh has reset the current move route, abort now.
- if (current_index == 0) {
- return;
- }
- --current_index;
- break;
- case Code::switch_off: // Parameter A: Switch to turn off
- Main_Data::game_switches->Set(move_command.parameter_a, false);
- ++current_index; // In case the current_index is already 0 ...
- Game_Map::SetNeedRefresh(true);
- Game_Map::Refresh();
- // If page refresh has reset the current move route, abort now.
- if (current_index == 0) {
- return;
- }
- --current_index;
- break;
- case Code::change_graphic: // String: File, Parameter A: index
- MoveRouteSetSpriteGraphic(ToString(move_command.parameter_string), move_command.parameter_a);
- break;
- case Code::play_sound_effect: // String: File, Parameters: Volume, Tempo, Balance
- if (move_command.parameter_string != "(OFF)" && move_command.parameter_string != "(Brak)") {
- lcf::rpg::Sound sound;
- sound.name = ToString(move_command.parameter_string);
- sound.volume = move_command.parameter_a;
- sound.tempo = move_command.parameter_b;
- sound.balance = move_command.parameter_c;
-
- Main_Data::game_system->SePlay(sound);
- }
- break;
- case Code::walk_everywhere_on:
- SetThrough(true);
- data()->move_route_through = true;
- break;
- case Code::walk_everywhere_off:
- SetThrough(false);
- data()->move_route_through = false;
- break;
- case Code::stop_animation:
- SetAnimPaused(true);
- break;
- case Code::start_animation:
- SetAnimPaused(false);
- break;
- case Code::increase_transp:
- SetTransparency(GetTransparency() + 1);
- break;
- case Code::decrease_transp:
- SetTransparency(GetTransparency() - 1);
- break;
- default:
- break;
- }
- }
- SetMoveFailureCount(0);
- ++current_index;
-
- if (current_index == start_index) {
- return;
- }
- } // while (true)
-}
-
-
-bool Game_Character::MakeWay(int from_x, int from_y, int to_x, int to_y) {
- return Game_Map::MakeWay(*this, from_x, from_y, to_x, to_y);
-}
-
-
-bool Game_Character::CheckWay(int from_x, int from_y, int to_x, int to_y) {
- return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y);
-}
-
-
-bool Game_Character::CheckWay(
- int from_x, int from_y, int to_x, int to_y, bool ignore_all_events,
- Span ignore_some_events_by_id) {
- return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y,
- ignore_all_events, ignore_some_events_by_id);
-}
-
-
-bool Game_Character::Move(int dir) {
- if (!IsStopping()) {
- return true;
- }
-
- bool move_success = false;
-
- SetDirection(dir);
- UpdateFacing();
-
- const auto x = GetX();
- const auto y = GetY();
- const auto dx = GetDxFromDirection(dir);
- const auto dy = GetDyFromDirection(dir);
-
- if (dx && dy) {
- // For diagonal movement, RPG_RT trys vert -> horiz and if that fails, then horiz -> vert.
- move_success = (MakeWay(x, y, x, y + dy) && MakeWay(x, y + dy, x + dx, y + dy))
- || (MakeWay(x, y, x + dx, y) && MakeWay(x + dx, y, x + dx, y + dy));
- } else if (dx) {
- move_success = MakeWay(x, y, x + dx, y);
- } else if (dy) {
- move_success = MakeWay(x, y, x, y + dy);
- }
-
- if (!move_success) {
- return false;
- }
-
- const auto new_x = Game_Map::RoundX(x + dx);
- const auto new_y = Game_Map::RoundY(y + dy);
-
- SetX(new_x);
- SetY(new_y);
- SetRemainingStep(SCREEN_TILE_SIZE);
-
- return true;
-}
-
-void Game_Character::Turn90DegreeLeft() {
- SetDirection(GetDirection90DegreeLeft(GetDirection()));
-}
-
-void Game_Character::Turn90DegreeRight() {
- SetDirection(GetDirection90DegreeRight(GetDirection()));
-}
-
-void Game_Character::Turn180Degree() {
- SetDirection(GetDirection180Degree(GetDirection()));
-}
-
-void Game_Character::Turn90DegreeLeftOrRight() {
- if (Rand::ChanceOf(1,2)) {
- Turn90DegreeLeft();
- } else {
- Turn90DegreeRight();
- }
-}
-
-int Game_Character::GetDirectionToCharacter(const Game_Character& target) {
- int sx = GetDistanceXfromCharacter(target);
- int sy = GetDistanceYfromCharacter(target);
-
- if ( std::abs(sx) > std::abs(sy) ) {
- return (sx > 0) ? Left : Right;
- } else {
- return (sy > 0) ? Up : Down;
- }
-}
-
-int Game_Character::GetDirectionAwayCharacter(const Game_Character& target) {
- int sx = GetDistanceXfromCharacter(target);
- int sy = GetDistanceYfromCharacter(target);
-
- if ( std::abs(sx) > std::abs(sy) ) {
- return (sx > 0) ? Right : Left;
- } else {
- return (sy > 0) ? Down : Up;
- }
-}
-
-void Game_Character::TurnTowardCharacter(const Game_Character& target) {
- SetDirection(GetDirectionToCharacter(target));
-}
-
-void Game_Character::TurnAwayFromCharacter(const Game_Character& target) {
- SetDirection(GetDirectionAwayCharacter(target));
-}
-
-void Game_Character::TurnRandom() {
- SetDirection(Rand::GetRandomNumber(0, 3));
-}
-
-void Game_Character::Wait() {
- SetStopCount(0);
- SetMaxStopCountForWait();
-}
-
-bool Game_Character::BeginMoveRouteJump(int32_t& current_index, const lcf::rpg::MoveRoute& current_route) {
- int jdx = 0;
- int jdy = 0;
-
- for (++current_index; current_index < static_cast(current_route.move_commands.size()); ++current_index) {
- using Code = lcf::rpg::MoveCommand::Code;
- const auto& move_command = current_route.move_commands[current_index];
- const auto cmd = static_cast(move_command.command_id);
- if (cmd >= Code::move_up && cmd <= Code::move_forward) {
- switch (cmd) {
- case Code::move_up:
- case Code::move_right:
- case Code::move_down:
- case Code::move_left:
- case Code::move_upright:
- case Code::move_downright:
- case Code::move_downleft:
- case Code::move_upleft:
- SetDirection(move_command.command_id);
- break;
- case Code::move_random:
- TurnRandom();
- break;
- case Code::move_towards_hero:
- TurnTowardCharacter(GetPlayer());
- break;
- case Code::move_away_from_hero:
- TurnAwayFromCharacter(GetPlayer());
- break;
- case Code::move_forward:
- break;
- default:
- break;
- }
- jdx += GetDxFromDirection(GetDirection());
- jdy += GetDyFromDirection(GetDirection());
- }
-
- if (cmd >= Code::face_up && cmd <= Code::face_away_from_hero) {
- switch (cmd) {
- case Code::face_up:
- SetDirection(Up);
- break;
- case Code::face_right:
- SetDirection(Right);
- break;
- case Code::face_down:
- SetDirection(Down);
- break;
- case Code::face_left:
- SetDirection(Left);
- break;
- case Code::turn_90_degree_right:
- Turn90DegreeRight();
- break;
- case Code::turn_90_degree_left:
- Turn90DegreeLeft();
- break;
- case Code::turn_180_degree:
- Turn180Degree();
- break;
- case Code::turn_90_degree_random:
- Turn90DegreeLeftOrRight();
- break;
- case Code::face_random_direction:
- TurnRandom();
- break;
- case Code::face_hero:
- TurnTowardCharacter(GetPlayer());
- break;
- case Code::face_away_from_hero:
- TurnAwayFromCharacter(GetPlayer());
- break;
- default:
- break;
- }
- }
-
- if (cmd == Code::end_jump) {
- int new_x = GetX() + jdx;
- int new_y = GetY() + jdy;
-
- auto rc = Jump(new_x, new_y);
- if (rc) {
- SetMaxStopCountForStep();
- }
- // Note: outer function increment will cause the end jump to pass after the return.
- return rc;
- }
- }
-
- // Commands finished with no end jump. Back up the index by 1 to allow outer loop increment to work.
- --current_index;
-
- // Jump is skipped
- return true;
-}
-
-bool Game_Character::Jump(int x, int y) {
- if (!IsStopping()) {
- return true;
- }
-
- auto begin_x = GetX();
- auto begin_y = GetY();
- const auto dx = x - begin_x;
- const auto dy = y - begin_y;
-
- if (std::abs(dy) >= std::abs(dx)) {
- SetDirection(dy >= 0 ? Down : Up);
- } else {
- SetDirection(dx >= 0 ? Right : Left);
- }
-
- SetJumping(true);
-
- if (dx != 0 || dy != 0) {
- if (!IsFacingLocked()) {
- SetFacing(GetDirection());
- }
-
- // FIXME: Remove dependency on jump from within Game_Map::MakeWay?
- // RPG_RT passes INT_MAX into from_x to tell it to skip self tile checks, which is hacky..
- if (!MakeWay(begin_x, begin_y, x, y)) {
- SetJumping(false);
- return false;
- }
- }
-
- // Adjust positions for looping maps. jump begin positions
- // get set off the edge of the map to preserve direction.
- if (Game_Map::LoopHorizontal()
- && (x < 0 || x >= Game_Map::GetTilesX()))
- {
- const auto old_x = x;
- x = Game_Map::RoundX(x);
- begin_x += x - old_x;
- }
-
- if (Game_Map::LoopVertical()
- && (y < 0 || y >= Game_Map::GetTilesY()))
- {
- auto old_y = y;
- y = Game_Map::RoundY(y);
- begin_y += y - old_y;
- }
-
- SetBeginJumpX(begin_x);
- SetBeginJumpY(begin_y);
- SetX(x);
- SetY(y);
- SetJumping(true);
- SetRemainingStep(SCREEN_TILE_SIZE);
-
- return true;
-}
-
-int Game_Character::GetDistanceXfromCharacter(const Game_Character& target) const {
- int sx = GetX() - target.GetX();
- if (Game_Map::LoopHorizontal()) {
- if (std::abs(sx) > Game_Map::GetTilesX() / 2) {
- if (sx > 0)
- sx -= Game_Map::GetTilesX();
- else
- sx += Game_Map::GetTilesX();
- }
- }
- return sx;
-}
-
-int Game_Character::GetDistanceYfromCharacter(const Game_Character& target) const {
- int sy = GetY() - target.GetY();
- if (Game_Map::LoopVertical()) {
- if (std::abs(sy) > Game_Map::GetTilesY() / 2) {
- if (sy > 0)
- sy -= Game_Map::GetTilesY();
- else
- sy += Game_Map::GetTilesY();
- }
- }
- return sy;
-}
-
-void Game_Character::ForceMoveRoute(const lcf::rpg::MoveRoute& new_route,
- int frequency) {
- if (!IsMoveRouteOverwritten()) {
- original_move_frequency = GetMoveFrequency();
- }
-
- SetPaused(false);
- SetStopCount(0xFFFF);
- SetMoveRouteIndex(0);
- SetMoveRouteFinished(false);
- SetMoveFrequency(frequency);
- SetMoveRouteOverwritten(true);
- SetMoveRoute(new_route);
- SetMoveFailureCount(0);
- if (frequency != original_move_frequency) {
- SetMaxStopCountForStep();
- }
-
- if (GetMoveRoute().move_commands.empty()) {
- CancelMoveRoute();
- return;
- }
-}
-
-void Game_Character::CancelMoveRoute() {
- if (IsMoveRouteOverwritten()) {
- SetMoveFrequency(original_move_frequency);
- SetMaxStopCountForStep();
- }
- SetMoveRouteOverwritten(false);
- SetMoveRouteFinished(false);
-}
-
-struct SearchNode {
- int x = 0;
- int y = 0;
- int cost = 0;
- int direction = 0;
-
- int id = 0;
- int parent_id = -1;
- int parent_x = -1;
- int parent_y = -1;
-
- friend bool operator==(const SearchNode& n1, const SearchNode& n2)
- {
- return n1.x == n2.x && n1.y == n2.y;
- }
-
- bool operator()(SearchNode const& a, SearchNode const& b)
- {
- return a.id > b.id;
- }
-};
-
-struct SearchNodeHash {
- size_t operator()(const SearchNode &p) const {
- return (p.x ^ (p.y + (p.y >> 12)));
- }
-};
-
-bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) {
- CancelMoveRoute();
-
- // Set up helper variables:
- SearchNode start = {GetX(), GetY(), 0, -1};
- if ((start.x == args.dest_x && start.y == args.dest_y) || args.steps_max == 0) {
- return true;
- }
- std::vector queue;
- std::unordered_map graph;
- std::map, SearchNode> graph_by_coord;
- queue.push_back(start);
- int id = 0;
- int idd = 0;
- int steps_taken = 0;
- SearchNode closest_node = {args.dest_x, args.dest_y, std::numeric_limits::max(), -1}; // Initialize with a very high cost.
- int closest_distance = std::numeric_limits::max(); // Initialize with a very high distance.
- std::unordered_set seen;
-
- int steps_max = args.steps_max;
- if (steps_max == -1) {
- steps_max = std::numeric_limits::max();
- }
-
- if (args.debug_print) {
- Output::Debug("Game_Interpreter::CommandSearchPath: "
- "start search, character x{} y{}, to x{}, y{}, "
- "ignored event ids count: {}",
- start.x, start.y, args.dest_x, args.dest_y, args.event_id_ignore_list.size());
- }
-
- bool loops_horizontal = Game_Map::LoopHorizontal();
- bool loops_vertical = Game_Map::LoopVertical();
- std::vector neighbour;
- neighbour.reserve(8);
- while (!queue.empty() && steps_taken < args.search_max) {
- SearchNode n = queue[0];
- queue.erase(queue.begin());
- steps_taken++;
- graph[n.id] = n;
- graph_by_coord.insert({{n.x, n.y}, n});
-
- if (n.x == args.dest_x && n.y == args.dest_y) {
- // Reached the destination.
- closest_node = n;
- closest_distance = 0;
- break; // Exit the loop to build final route.
- }
- else {
- neighbour.clear();
- SearchNode nn = {n.x + 1, n.y, n.cost + 1, 1}; // Right
- neighbour.push_back(nn);
- nn = {n.x, n.y - 1, n.cost + 1, 0}; // Up
- neighbour.push_back(nn);
- nn = {n.x - 1, n.y, n.cost + 1, 3}; // Left
- neighbour.push_back(nn);
- nn = {n.x, n.y + 1, n.cost + 1, 2}; // Down
- neighbour.push_back(nn);
-
- if (args.allow_diagonal) {
- nn = {n.x - 1, n.y + 1, n.cost + 1, 6}; // Down Left
- neighbour.push_back(nn);
- nn = {n.x + 1, n.y - 1, n.cost + 1, 4}; // Up Right
- neighbour.push_back(nn);
- nn = {n.x - 1, n.y - 1, n.cost + 1, 7}; // Up Left
- neighbour.push_back(nn);
- nn = {n.x + 1, n.y + 1, n.cost + 1, 5}; // Down Right
- neighbour.push_back(nn);
- }
-
- for (SearchNode a : neighbour) {
- idd++;
- a.parent_x = n.x;
- a.parent_y = n.y;
- a.id = idd;
- a.parent_id = n.id;
-
- // Adjust neighbor coordinates for map looping
- if (loops_horizontal) {
- if (a.x >= Game_Map::GetTilesX())
- a.x -= Game_Map::GetTilesX();
- else if (a.x < 0)
- a.x += Game_Map::GetTilesX();
- }
-
- if (loops_vertical) {
- if (a.y >= Game_Map::GetTilesY())
- a.y -= Game_Map::GetTilesY();
- else if (a.y < 0)
- a.y += Game_Map::GetTilesY();
- }
-
- auto check = seen.find(a);
- if (check != seen.end()) {
- SearchNode old_entry = graph[(*check).id];
- if (a.cost < old_entry.cost) {
- // Found a shorter path to previous node, update & reinsert:
- if (args.debug_print) {
- Output::Debug("Game_Interpreter::CommandSearchPath: "
- "found shorter path to x:{} y:{}"
- "from x:{} y:{} direction: {}",
- a.x, a.y, n.x, n.y, a.direction);
- }
- graph.erase(old_entry.id);
- old_entry.cost = a.cost;
- old_entry.parent_id = n.id;
- old_entry.parent_x = n.x;
- old_entry.parent_y = n.y;
- old_entry.direction = a.direction;
- graph[old_entry.id] = old_entry;
- }
- continue;
- } else if (a.x == start.x && a.y == start.y) {
- continue;
- }
- bool added = false;
- if (CheckWay(n.x, n.y, a.x, a.y, true, args.event_id_ignore_list) ||
- (a.x == args.dest_x && a.y == args.dest_y &&
- CheckWay(n.x, n.y, a.x, a.y, false, {}))) {
- if (a.direction == 4) {
- if (CheckWay(n.x, n.y, n.x + 1, n.y,
- true, args.event_id_ignore_list) ||
- CheckWay(n.x, n.y, n.x, n.y - 1,
- true, args.event_id_ignore_list)) {
- added = true;
- queue.push_back(a);
- seen.insert(a);
- }
- }
- else if (a.direction == 5) {
- if (CheckWay(n.x, n.y, n.x + 1, n.y,
- true, args.event_id_ignore_list) ||
- CheckWay(n.x, n.y, n.x, n.y + 1,
- true, args.event_id_ignore_list)) {
- added = true;
- queue.push_back(a);
- seen.insert(a);
- }
- }
- else if (a.direction == 6) {
- if (CheckWay(n.x, n.y, n.x - 1, n.y,
- true, args.event_id_ignore_list) ||
- CheckWay(n.x, n.y, n.x, n.y + 1,
- true, args.event_id_ignore_list)) {
- added = true;
- queue.push_back(a);
- seen.insert(a);
- }
- }
- else if (a.direction == 7) {
- if (CheckWay(n.x, n.y, n.x - 1, n.y,
- true, args.event_id_ignore_list) ||
- CheckWay(n.x, n.y, n.x, n.y - 1,
- true, args.event_id_ignore_list)) {
- added = true;
- queue.push_back(a);
- seen.insert(a);
- }
- }
- else {
- added = true;
- queue.push_back(a);
- seen.insert(a);
- }
- }
- if (added && args.debug_print) {
- Output::Debug("Game_Interpreter::CommandSearchPath: "
- "discovered id:{} x:{} y:{} parentX:{} parentY:{}"
- "parentID:{} direction: {}",
- queue[queue.size() - 1].id,
- queue[queue.size() - 1].x, queue[queue.size() - 1].y,
- queue[queue.size() - 1].parent_x,
- queue[queue.size() - 1].parent_y,
- queue[queue.size() - 1].parent_id,
- queue[queue.size() - 1].direction);
- }
- }
- }
- id++;
- // Calculate the Manhattan distance between the current node and the destination
- int manhattan_dist = abs(args.dest_x - n.x) + abs(args.dest_y - n.y);
-
- // Check if this node is closer to the destination
- if (manhattan_dist < closest_distance) {
- closest_node = n;
- closest_distance = manhattan_dist;
- if (args.debug_print) {
- Output::Debug("Game_Interpreter::CommandSearchPath: "
- "new closest node at x:{} y:{} id:{}",
- closest_node.x, closest_node.y,
- closest_node.id);
- }
- }
- }
-
- // Check if a path to the closest node was found.
- if (closest_distance != std::numeric_limits::max()) {
- // Build a route to the closest reachable node.
- if (args.debug_print) {
- Output::Debug("Game_Interpreter::CommandSearchPath: "
- "trying to return route from x:{} y:{} to "
- "x:{} y:{} (id:{})",
- start.x, start.y, closest_node.x, closest_node.y,
- closest_node.id);
- }
- std::vector list_move;
-
- SearchNode node = closest_node;
- while (static_cast(list_move.size()) < steps_max) {
- list_move.push_back(node);
- if (graph_by_coord.find({node.parent_x,
- node.parent_y}) == graph_by_coord.end())
- break;
- SearchNode node2 = graph_by_coord[
- {node.parent_x, node.parent_y}
- ];
- if (args.debug_print) {
- Output::Debug(
- "Game_Interpreter::CommandSearchPath: "
- "found parent leading to x:{} y:{}, "
- "it's at x:{} y:{} dir:{}",
- node.x, node.y,
- node2.x, node2.y, node2.direction);
- }
- node = node2;
- }
-
- std::reverse(list_move.rbegin(), list_move.rend());
-
- std::string debug_output_path("");
- if (list_move.size() > 0) {
- lcf::rpg::MoveRoute route;
- route.skippable = args.skip_when_failed;
- route.repeat = false;
-
- for (SearchNode const& node2 : list_move) {
- if (node2.direction >= 0) {
- lcf::rpg::MoveCommand cmd;
- cmd.command_id = node2.direction;
- route.move_commands.push_back(cmd);
- if (args.debug_print >= 1) {
- if (debug_output_path.length() > 0)
- debug_output_path += ",";
- std::ostringstream dirnum;
- dirnum << node2.direction;
- debug_output_path += std::string(dirnum.str());
- }
- }
- }
-
- lcf::rpg::MoveCommand cmd;
- cmd.command_id = 23;
- route.move_commands.push_back(cmd);
-
- ForceMoveRoute(route, args.frequency);
- }
- if (args.debug_print) {
- Output::Debug(
- "Game_Interpreter::CommandSearchPath: "
- "setting route {} for character x{} y{}",
- " (ignored event ids count: {})",
- debug_output_path, start.x, start.y,
- args.event_id_ignore_list.size()
- );
- }
- return true;
- }
-
- // No path to the destination, return failure.
- return false;
-}
-
-int Game_Character::GetSpriteX() const {
- int x = GetX() * SCREEN_TILE_SIZE;
-
- if (IsMoving()) {
- int d = GetDirection();
- if (d == Right || d == UpRight || d == DownRight)
- x -= GetRemainingStep();
- else if (d == Left || d == UpLeft || d == DownLeft)
- x += GetRemainingStep();
- } else if (IsJumping()) {
- x -= ((GetX() - GetBeginJumpX()) * GetRemainingStep());
- }
-
- return x;
-}
-
-int Game_Character::GetSpriteY() const {
- int y = GetY() * SCREEN_TILE_SIZE;
-
- if (IsMoving()) {
- int d = GetDirection();
- if (d == Down || d == DownRight || d == DownLeft)
- y -= GetRemainingStep();
- else if (d == Up || d == UpRight || d == UpLeft)
- y += GetRemainingStep();
- } else if (IsJumping()) {
- y -= (GetY() - GetBeginJumpY()) * GetRemainingStep();
- }
-
- return y;
-}
-
-bool Game_Character::IsInPosition(int x, int y) const {
- return ((GetX() == x) && (GetY() == y));
-}
-
-int Game_Character::GetOpacity() const {
- return Utils::Clamp((8 - GetTransparency()) * 32 - 1, 0, 255);
-}
-
-bool Game_Character::IsAnimated() const {
- auto at = GetAnimationType();
- return !IsAnimPaused()
- && at != lcf::rpg::EventPage::AnimType_fixed_graphic
- && at != lcf::rpg::EventPage::AnimType_step_frame_fix;
-}
-
-bool Game_Character::IsContinuous() const {
- auto at = GetAnimationType();
- return
- at == lcf::rpg::EventPage::AnimType_continuous ||
- at == lcf::rpg::EventPage::AnimType_fixed_continuous;
-}
-
-bool Game_Character::IsSpinning() const {
- return GetAnimationType() == lcf::rpg::EventPage::AnimType_spin;
-}
-
-int Game_Character::GetBushDepth() const {
- if ((GetLayer() != lcf::rpg::EventPage::Layers_same) || IsJumping() || IsFlying()) {
- return 0;
- }
-
- return Game_Map::GetBushDepth(GetX(), GetY());
-}
-
-void Game_Character::Flash(int r, int g, int b, int power, int frames) {
- data()->flash_red = r;
- data()->flash_green = g;
- data()->flash_blue = b;
- data()->flash_current_level = power;
- data()->flash_time_left = frames;
-}
-
-// Gets Character
-Game_Character* Game_Character::GetCharacter(int character_id, int event_id) {
- switch (character_id) {
- case CharPlayer:
- // Player/Hero
- return Main_Data::game_player.get();
- case CharBoat:
- return Game_Map::GetVehicle(Game_Vehicle::Boat);
- case CharShip:
- return Game_Map::GetVehicle(Game_Vehicle::Ship);
- case CharAirship:
- return Game_Map::GetVehicle(Game_Vehicle::Airship);
- case CharThisEvent:
- // This event
- return Game_Map::GetEvent(event_id);
- default:
- // Other events
- return Game_Map::GetEvent(character_id);
- }
-}
-
-Game_Character& Game_Character::GetPlayer() {
- assert(Main_Data::game_player);
-
- return *Main_Data::game_player;
-}
-
-int Game_Character::ReverseDir(int dir) {
- constexpr static char reversed[] =
- { Down, Left, Up, Right, DownLeft, UpLeft, UpRight, DownRight };
- return reversed[dir];
-}
-
-void Game_Character::SetMaxStopCountForStep() {
- SetMaxStopCount(GetMaxStopCountForStep(GetMoveFrequency()));
-}
-
-void Game_Character::SetMaxStopCountForTurn() {
- SetMaxStopCount(GetMaxStopCountForTurn(GetMoveFrequency()));
-}
-
-void Game_Character::SetMaxStopCountForWait() {
- SetMaxStopCount(GetMaxStopCountForWait(GetMoveFrequency()));
-}
-
-void Game_Character::UpdateFacing() {
- // RPG_RT only does the IsSpinning() check for Game_Event. We did it for all types here
- // in order to avoid a virtual call and because normally with RPG_RT, spinning
- // player or vehicle is impossible.
- if (IsFacingLocked() || IsSpinning()) {
- return;
- }
- const auto dir = GetDirection();
- const auto facing = GetFacing();
- if (dir >= 4) /* is diagonal */ {
- // [UR, DR, DL, UL] -> [U, D, D, U]
- const auto f1 = ((dir + (dir >= 6)) % 2) * 2;
- // [UR, DR, DL, UL] -> [R, R, L, L]
- const auto f2 = (dir / 2) - (dir < 6);
- if (facing != f1 && facing != f2) {
- // Reverse the direction.
- SetFacing((facing + 2) % 4);
- }
- } else {
- SetFacing(dir);
- }
-}
+/*
+ * This file is part of EasyRPG Player.
+ *
+ * EasyRPG Player is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EasyRPG Player is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EasyRPG Player. If not, see .
+ */
+
+#define CUTE_C2_IMPLEMENTATION
+// Headers
+#include "audio.h"
+#include "game_character.h"
+#include "game_map.h"
+#include "game_player.h"
+#include "game_switches.h"
+#include "game_system.h"
+#include "input.h"
+#include "main_data.h"
+#include "game_message.h"
+#include "drawable.h"
+#include "player.h"
+#include "utils.h"
+#include "util_macro.h"
+#include "output.h"
+#include "rand.h"
+#include
+#include
+#include
+#include
+
+#include "cute_c2.h"
+
+#include
+#include
+#include
+#include "tilemap.h"
+#include "tilemap_layer.h"
+
+
+
+Game_Character::Game_Character(Type type, lcf::rpg::SaveMapEventBase* d) :
+ _type(type), _data(d)
+{
+}
+
+float Game_Character::GetRealX() const {
+ return real_x;
+}
+
+float Game_Character::GetRealY() const {
+ return real_y;
+}// Game_Character::~Game_Character() {}
+
+void Game_Character::SanitizeData(std::string_view name) {
+ SanitizeMoveRoute(name, data()->move_route, data()->move_route_index, "move_route_index");
+}
+
+void Game_Character::SanitizeMoveRoute(std::string_view name, const lcf::rpg::MoveRoute& mr, int32_t& idx, std::string_view chunk_name) {
+ const auto n = static_cast(mr.move_commands.size());
+ if (idx < 0 || idx > n) {
+ idx = n;
+ Output::Warning("{} {}: Save Data invalid {}={}. Fixing ...", TypeToStr(_type), name, chunk_name, idx);
+ }
+}
+
+void Game_Character::MoveTo(int map_id, int x, int y) {
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+ is_moving_toward_target = false;
+ real_x = (float)x;
+ real_y = (float)y;
+ //Output::Warning("Char Pos = {}x{}", real_x, real_y);
+ }// END - PIXELMOVE
+
+ data()->map_id = map_id;
+ // RPG_RT does not round the position for this function.
+ SetX(x);
+ SetY(y);
+ SetRemainingStep(0);
+}
+
+// int Game_Character::GetYOffset() const {
+// return GetJumpHeight();
+// }
+
+
+int Game_Character::GetJumpHeight() const {
+ if (IsJumping()) {
+ int jump_height = (GetRemainingStep() > SCREEN_TILE_SIZE / 2 ? SCREEN_TILE_SIZE - GetRemainingStep() : GetRemainingStep()) / 8;
+ return (jump_height < 5 ? jump_height * 2 : jump_height < 13 ? jump_height + 4 : 16);
+ }
+ return 0;
+}
+
+int Game_Character::GetScreenX() const {
+ if (Player::game_config.allow_pixel_movement.Get()) {
+ float val = real_x * TILE_SIZE - floor((float)Game_Map::GetDisplayX() / (float)TILE_SIZE) + TILE_SIZE / 2.0f;
+
+ // Wraps the screen coordinate if the map loops.
+ // This keeps the sprite visible when its world coordinate is wrapped (e.g. 0.1 vs 19.9).
+ // The Mode7 renderer handles the "ghosting" logic separately using a radius check.
+ if (Game_Map::LoopHorizontal()) {
+ val = Utils::PositiveModulo(static_cast(val), Game_Map::GetTilesX() * TILE_SIZE);
+ }
+
+ return floor(val);
+ }
+
+ // Legacy tile-based logic
+ int x = GetSpriteX() / TILE_SIZE - Game_Map::GetDisplayX() / TILE_SIZE + TILE_SIZE;
+
+ if (Game_Map::LoopHorizontal()) {
+ x = Utils::PositiveModulo(x, Game_Map::GetTilesX() * TILE_SIZE);
+ }
+ x -= TILE_SIZE / 2;
+
+ return x;
+}
+
+int Game_Character::GetScreenY(bool apply_jump) const {
+ if (Player::game_config.allow_pixel_movement.Get()) {
+ float val = real_y * TILE_SIZE - floor((float)Game_Map::GetDisplayY() / (float)TILE_SIZE) + TILE_SIZE;
+
+ if (apply_jump) {
+ val -= GetJumpHeight();
+ }
+
+ // Wraps the screen coordinate if the map loops.
+ if (Game_Map::LoopVertical()) {
+ val = Utils::PositiveModulo(static_cast(val), Game_Map::GetTilesY() * TILE_SIZE);
+ }
+
+ return floor(val);
+ }
+
+ // Legacy tile-based logic
+ int y = GetSpriteY() / TILE_SIZE - Game_Map::GetDisplayY() / TILE_SIZE + TILE_SIZE;
+
+ if (apply_jump) {
+ y -= GetJumpHeight();
+ }
+
+ if (Game_Map::LoopVertical()) {
+ y = Utils::PositiveModulo(y, Game_Map::GetTilesY() * TILE_SIZE);
+ }
+ return y;
+}
+
+Drawable::Z_t Game_Character::GetScreenZ(int x_offset, int y_offset) const {
+ Drawable::Z_t z = 0;
+
+ if (IsFlying()) {
+ z = Priority_EventsFlying;
+ } else if (GetLayer() == lcf::rpg::EventPage::Layers_same) {
+ z = Priority_Player;
+ } else if (GetLayer() == lcf::rpg::EventPage::Layers_below) {
+ z = Priority_EventsBelow;
+ } else if (GetLayer() == lcf::rpg::EventPage::Layers_above) {
+ z = Priority_EventsAbove;
+ }
+
+ // 0x8000 (32768) is added to shift negative numbers into the positive range
+ Drawable::Z_t y = static_cast(GetScreenY(false) + y_offset + 0x8000);
+ Drawable::Z_t x = static_cast(GetScreenX() + x_offset + 0x8000);
+
+ // The rendering order of characters is: Highest Y-coordinate, Highest X-coordinate, Highest ID
+ // To encode this behaviour all of them get 16 Bit in the Z value
+ // L- YY XX II (1 letter = 8 bit)
+ // L: Layer (specified by the event page)
+ // -: Unused
+ // Y: Y-coordinate
+ // X: X-coordinate
+ // I: ID (This is only applied by subclasses, characters itself put nothing (0) here
+ z += (y << 32) + (x << 16);
+
+ return z;
+}
+
+void Game_Character::Update() {
+ if (!IsActive() || IsProcessed()) {
+ return;
+ }
+ SetProcessed(true);
+
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+ UpdateMoveTowardTarget();
+ } // END - PIXELMOVE
+
+
+ if (IsStopping()) {
+ this->UpdateNextMovementAction();
+ }
+ UpdateFlash();
+
+ if (IsStopping()) {
+ if (GetStopCount() == 0 || IsMoveRouteOverwritten() ||
+ ((Main_Data::game_system->GetMessageContinueEvents() || !Game_Map::GetInterpreter().IsRunning()) && !IsPaused())) {
+ SetStopCount(GetStopCount() + 1);
+ }
+ } else if (IsJumping()) {
+ static const int jump_speed[] = {8, 12, 16, 24, 32, 64};
+ auto amount = jump_speed[GetMoveSpeed() -1 ];
+ this->UpdateMovement(amount);
+ } else {
+ int amount = 1 << (1 + GetMoveSpeed());
+ this->UpdateMovement(amount);
+ }
+
+ this->UpdateAnimation();
+}
+
+void Game_Character::UpdateMovement(int amount) {
+
+ // --- START: Pixel Movement Jump Interpolation Fix ---
+ // If a jump is in progress and pixel movement is active, we need to manually
+ // interpolate the real_x and real_y coordinates from the start to the end point.
+ if (IsJumping() && Player::game_config.allow_pixel_movement.Get()) {
+ // total_duration is the number of "steps" a jump takes, which is always SCREEN_TILE_SIZE
+ const int total_duration = SCREEN_TILE_SIZE;
+ // elapsed is how many steps have passed since the jump started
+ int elapsed = total_duration - GetRemainingStep();
+ // progress is a float from 0.0 to 1.0 representing jump completion
+ float progress = static_cast(elapsed) / static_cast(total_duration);
+
+ // Linearly interpolate the real coordinates based on the jump's progress
+ real_x = jump_start_real_x + (jump_end_real_x - jump_start_real_x) * progress;
+ real_y = jump_start_real_y + (jump_end_real_y - jump_start_real_y) * progress;
+ }
+ // --- END: Pixel Movement Jump Interpolation Fix ---
+
+ SetRemainingStep(GetRemainingStep() - amount);
+ if (GetRemainingStep() <= 0) {
+ SetRemainingStep(0);
+ bool was_jumping = IsJumping();
+ SetJumping(false);
+
+ if (was_jumping && Player::game_config.allow_pixel_movement.Get()) {
+ real_x = jump_end_real_x;
+ real_y = jump_end_real_y;
+ SetX(static_cast(round(real_x)));
+ SetY(static_cast(round(real_y)));
+ }
+
+
+ auto& move_route = GetMoveRoute();
+ if (IsMoveRouteOverwritten() && GetMoveRouteIndex() >= static_cast(move_route.move_commands.size())) {
+ SetMoveRouteFinished(true);
+ SetMoveRouteIndex(0);
+ if (!move_route.repeat) {
+ // If the last command of a move route is a move or jump,
+ // RPG_RT cancels the entire move route immediately.
+ CancelMoveRoute();
+ }
+ }
+ }
+
+ SetStopCount(0);
+}
+
+void Game_Character::UpdateAnimation() {
+ const auto speed = Utils::Clamp(GetMoveSpeed(), 1, 6);
+
+ if (IsSpinning()) {
+ const auto limit = GetSpinAnimFrames(speed);
+
+ IncAnimCount();
+
+ if (GetAnimCount() >= limit) {
+ SetFacing((GetFacing() + 1) % 4);
+ SetAnimCount(0);
+ }
+ return;
+ }
+
+ if (IsAnimPaused() || IsJumping()) {
+ ResetAnimation();
+ return;
+ }
+
+ if (!IsAnimated()) {
+ return;
+ }
+
+ const auto stationary_limit = GetStationaryAnimFrames(speed);
+ const auto continuous_limit = GetContinuousAnimFrames(speed);
+
+ if (IsContinuous()
+ || GetStopCount() == 0
+ || data()->anim_frame == lcf::rpg::EventPage::Frame_left || data()->anim_frame == lcf::rpg::EventPage::Frame_right
+ || GetAnimCount() < stationary_limit - 1) {
+ IncAnimCount();
+ }
+
+ if (GetAnimCount() >= continuous_limit
+ || (GetStopCount() == 0 && GetAnimCount() >= stationary_limit)) {
+ IncAnimFrame();
+ return;
+ }
+}
+
+void Game_Character::UpdateFlash() {
+ Flash::Update(data()->flash_current_level, data()->flash_time_left);
+}
+
+void Game_Character::UpdateMoveRoute(int32_t& current_index, const lcf::rpg::MoveRoute& current_route, bool is_overwrite) {
+
+ if (true && is_moving_toward_target && !current_route.skippable) { // TODO - PIXELMOVE
+ return;
+ } // END - PIXELMOVE
+
+
+
+ if (current_route.move_commands.empty()) {
+ return;
+ }
+
+ if (is_overwrite && !IsMoveRouteOverwritten()) {
+ return;
+ }
+
+ const auto num_commands = static_cast(current_route.move_commands.size());
+ // Invalid index could occur from a corrupted save game.
+ // Player, Vehicle, and Event all check for and fix this, but we still assert here in
+ // case any bug causes this to happen still.
+ assert(current_index >= 0);
+ assert(current_index <= num_commands);
+
+ const auto start_index = current_index;
+
+ while (true) {
+ if (!IsStopping() || IsStopCountActive()) {
+ return;
+ }
+
+ //Move route is finished
+ if (current_index >= num_commands) {
+ if (is_overwrite) {
+ SetMoveRouteFinished(true);
+ }
+ if (!current_route.repeat) {
+ if (is_overwrite) {
+ CancelMoveRoute();
+ }
+ return;
+ }
+ current_index = 0;
+ if (current_index == start_index) {
+ return;
+ }
+ }
+
+ using Code = lcf::rpg::MoveCommand::Code;
+ const auto& move_command = current_route.move_commands[current_index];
+ const auto prev_direction = GetDirection();
+ const auto prev_facing = GetFacing();
+ const auto saved_index = current_index;
+ const auto cmd = static_cast(move_command.command_id);
+
+ if (cmd >= Code::move_up && cmd <= Code::move_forward) {
+ switch (cmd) {
+ case Code::move_up:
+ case Code::move_right:
+ case Code::move_down:
+ case Code::move_left:
+ case Code::move_upright:
+ case Code::move_downright:
+ case Code::move_downleft:
+ case Code::move_upleft:
+ SetDirection(static_cast(cmd));
+ break;
+ case Code::move_random:
+ TurnRandom();
+ break;
+ case Code::move_towards_hero:
+ TurnTowardCharacter(GetPlayer());
+ break;
+ case Code::move_away_from_hero:
+ TurnAwayFromCharacter(GetPlayer());
+ break;
+ case Code::move_forward:
+ break;
+ default:
+ break;
+ }
+ /*
+ Move(GetDirection());
+ */
+
+
+// if (true && (cmd >= Code::move_towards_hero && cmd <= Code::move_away_from_hero)) { // TODO - PIXELMOVE
+ if (Player::game_config.allow_pixel_movement.Get() && (cmd >= Code::move_towards_hero && cmd <= Code::move_away_from_hero)) {
+ int flag = (1 - (cmd == Code::move_away_from_hero) * 2);
+ float vx = (Main_Data::game_player->real_x - real_x) * flag;
+ float vy = (Main_Data::game_player->real_y - real_y) * flag;
+ float length = sqrt(vx * vx + vy * vy);
+ float step_size = GetStepSize();
+ MoveVector(step_size * (vx / length), step_size * (vy / length));
+ }
+// else if (true) {
+ else if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+ float vx = (float)GetDxFromDirection(GetDirection());
+ float vy = (float)GetDyFromDirection(GetDirection());
+ c2v target;
+ if (forced_skip) {
+ forced_skip = false;
+ target = c2V(round(target_x + vx), round(target_y + vy));
+ }
+ else {
+ target = c2V(round(real_x + vx), round(real_y + vy));
+ }
+ SetMoveTowardTarget(target, current_route.skippable);
+ UpdateMoveTowardTarget();
+ if (!current_route.skippable) {
+ SetMaxStopCountForStep();
+ ++current_index;
+ return;
+ }
+ }
+ else {
+ Move(GetDirection());
+ } // END - PIXELMOV
+
+
+ static const int move_speed[] = { 16, 8, 6, 4, 3, 2 };
+ doomWait = move_speed[GetMoveSpeed() - 1];
+
+ if (IsStopping()) {
+ // Move failed
+ if (current_route.skippable) {
+ SetDirection(prev_direction);
+ SetFacing(prev_facing);
+ } else {
+ SetMoveFailureCount(GetMoveFailureCount() + 1);
+ return;
+ }
+ }
+ if (cmd == Code::move_forward) {
+ SetFacing(prev_facing);
+ }
+
+ SetMaxStopCountForStep();
+ } else if (cmd >= Code::face_up && cmd <= Code::face_away_from_hero) {
+ SetDirection(GetFacing());
+ switch (cmd) {
+ case Code::face_up:
+ SetDirection(Up);
+ break;
+ case Code::face_right:
+ SetDirection(Right);
+ break;
+ case Code::face_down:
+ SetDirection(Down);
+ break;
+ case Code::face_left:
+ SetDirection(Left);
+ break;
+ case Code::turn_90_degree_right:
+ Turn90DegreeRight();
+ break;
+ case Code::turn_90_degree_left:
+ Turn90DegreeLeft();
+ break;
+ case Code::turn_180_degree:
+ Turn180Degree();
+ break;
+ case Code::turn_90_degree_random:
+ Turn90DegreeLeftOrRight();
+ break;
+ case Code::face_random_direction:
+ TurnRandom();
+ break;
+ case Code::face_hero:
+ TurnTowardCharacter(GetPlayer());
+ break;
+ case Code::face_away_from_hero:
+ TurnAwayFromCharacter(GetPlayer());
+ break;
+ default:
+ break;
+ }
+ SetFacing(GetDirection());
+ SetMaxStopCountForTurn();
+ SetStopCount(0);
+
+ static const int turn_speed[] = { 64, 32, 24, 16, 12, 8 };
+ doomWait = turn_speed[GetMoveSpeed() - 1];
+
+
+ } else {
+ switch (cmd) {
+ case Code::wait:
+ SetMaxStopCountForWait();
+ SetStopCount(0);
+ break;
+ case Code::begin_jump:
+ if (!BeginMoveRouteJump(current_index, current_route)) {
+ // Jump failed
+ if (current_route.skippable) {
+ SetDirection(prev_direction);
+ SetFacing(prev_facing);
+ } else {
+ current_index = saved_index;
+ SetMoveFailureCount(GetMoveFailureCount() + 1);
+ return;
+ }
+ }
+ break;
+ case Code::end_jump:
+ break;
+ case Code::lock_facing:
+ SetFacingLocked(true);
+ break;
+ case Code::unlock_facing:
+ SetFacingLocked(false);
+ break;
+ case Code::increase_movement_speed:
+ SetMoveSpeed(min(GetMoveSpeed() + 1, 6));
+ break;
+ case Code::decrease_movement_speed:
+ SetMoveSpeed(max(GetMoveSpeed() - 1, 1));
+ break;
+ case Code::increase_movement_frequence:
+ SetMoveFrequency(min(GetMoveFrequency() + 1, 8));
+ break;
+ case Code::decrease_movement_frequence:
+ SetMoveFrequency(max(GetMoveFrequency() - 1, 1));
+ break;
+ case Code::switch_on: // Parameter A: Switch to turn on
+ Main_Data::game_switches->Set(move_command.parameter_a, true);
+ ++current_index; // In case the current_index is already 0 ...
+ Game_Map::SetNeedRefresh(true);
+ Game_Map::Refresh();
+ // If page refresh has reset the current move route, abort now.
+ if (current_index == 0) {
+ return;
+ }
+ --current_index;
+ break;
+ case Code::switch_off: // Parameter A: Switch to turn off
+ Main_Data::game_switches->Set(move_command.parameter_a, false);
+ ++current_index; // In case the current_index is already 0 ...
+ Game_Map::SetNeedRefresh(true);
+ Game_Map::Refresh();
+ // If page refresh has reset the current move route, abort now.
+ if (current_index == 0) {
+ return;
+ }
+ --current_index;
+ break;
+ case Code::change_graphic: // String: File, Parameter A: index
+ MoveRouteSetSpriteGraphic(ToString(move_command.parameter_string), move_command.parameter_a);
+ break;
+ case Code::play_sound_effect: // String: File, Parameters: Volume, Tempo, Balance
+ if (move_command.parameter_string != "(OFF)" && move_command.parameter_string != "(Brak)") {
+ lcf::rpg::Sound sound;
+ sound.name = ToString(move_command.parameter_string);
+ sound.volume = move_command.parameter_a;
+ sound.tempo = move_command.parameter_b;
+ sound.balance = move_command.parameter_c;
+
+ Main_Data::game_system->SePlay(sound);
+ }
+ break;
+ case Code::walk_everywhere_on:
+ SetThrough(true);
+ data()->move_route_through = true;
+ break;
+ case Code::walk_everywhere_off:
+ SetThrough(false);
+ data()->move_route_through = false;
+ break;
+ case Code::stop_animation:
+ SetAnimPaused(true);
+ break;
+ case Code::start_animation:
+ SetAnimPaused(false);
+ break;
+ case Code::increase_transp:
+ SetTransparency(GetTransparency() + 1);
+ break;
+ case Code::decrease_transp:
+ SetTransparency(GetTransparency() - 1);
+ break;
+ default:
+ break;
+ }
+ }
+ SetMoveFailureCount(0);
+ ++current_index;
+
+ if (current_index == start_index) {
+ return;
+ }
+ } // while (true)
+}
+
+
+bool Game_Character::MakeWay(int from_x, int from_y, int to_x, int to_y) {
+ return Game_Map::MakeWay(*this, from_x, from_y, to_x, to_y);
+}
+
+
+bool Game_Character::CheckWay(int from_x, int from_y, int to_x, int to_y) {
+ return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y);
+}
+
+
+bool Game_Character::CheckWay(
+ int from_x, int from_y, int to_x, int to_y, bool ignore_all_events,
+ Span ignore_some_events_by_id) {
+ return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y,
+ ignore_all_events, ignore_some_events_by_id);
+}
+
+void Game_Character::SetMoveTowardTarget(c2v position, bool skippable) {
+ SetMoveTowardTarget(position.x, position.y, skippable);
+}
+
+void Game_Character::SetMoveTowardTarget(float x, float y, bool skippable) {
+ is_moving_toward_target = true;
+ is_move_toward_target_skippable = skippable;
+ target_x = x;
+ target_y = y;
+ move_direction = c2Norm(c2V(target_x - real_x, target_y - real_y));
+}
+
+bool Game_Character::UpdateMoveTowardTarget() {
+ if (!is_moving_toward_target || IsPaused()) {
+ return false;
+ }
+ //forced_skip = false;
+ bool move_success = false;
+ c2v vector = c2V(target_x - real_x, target_y - real_y);
+ float length = c2Len(vector);
+ c2v vectorNorm = c2Div(vector, length);
+ float step_size = GetStepSize();
+ if (length > step_size) {
+ move_success = MoveVector(c2Mulvs(vectorNorm, step_size));
+ }
+ else {
+ move_success = MoveVector(vector);
+ is_moving_toward_target = false;
+ }
+ if (!move_success) {
+ if (is_move_toward_target_skippable) {
+ is_moving_toward_target = false;
+ }
+ else if (c2Dot(vectorNorm, move_direction) <= 0) {
+ is_moving_toward_target = false;
+ forced_skip = true;
+ }
+ }
+ return move_success;
+}
+
+bool Game_Character::MoveVector(c2v vector) {
+ return MoveVector(vector.x, vector.y);
+}
+
+bool Game_Character::MoveVector(float vx, float vy) { // TODO - PIXELMOVE
+// if (abs(vx) <= Epsilon && abs(vy) <= Epsilon) {
+// return false;
+// }
+
+ auto& player = Main_Data::game_player;
+ auto player_x = player->GetX();
+ auto player_y = player->GetY();
+
+
+ bool vehicle = Main_Data::game_player->InVehicle();
+ bool airship = Main_Data::game_player->InAirship();
+ bool flying = Main_Data::game_player->IsFlying();
+ bool boarding = Main_Data::game_player->IsBoardingOrUnboarding();
+ bool isAboard = Main_Data::game_player->IsAboard();
+ bool ascending = Game_Map::GetVehicle(Game_Vehicle::Airship)->IsAscending();
+ bool descending = Game_Map::GetVehicle(Game_Vehicle::Airship)->IsDescending();
+ bool airshipUse = Game_Map::GetVehicle(Game_Vehicle::Airship)->IsInUse();
+
+ auto boatFront = Game_Map::GetVehicle(Game_Vehicle::Boat)->GetDirection();
+ auto playerFront = Main_Data::game_player->GetDirection();
+ auto airshipFront = Game_Map::GetVehicle(Game_Vehicle::Airship)->GetDirection();
+
+ auto MapID = Main_Data::game_player->GetMapId();
+
+ if (boarding || ascending || IsJumping() || descending) // this is to try and stop events from going to NaNland.
+ {
+ return false;
+ }
+
+ if (!GetThrough() && !IsFlying() && !Game_Map::IsPassableTile(this, 0x0F, Game_Map::RoundX(GetX()), Game_Map::RoundY(GetY()))) {
+ return false;
+ }
+
+ if (!IsFacingLocked()) {
+ if (std::abs(vx) > std::abs(vy)) {
+ SetDirection(vx > 0 ? Right : Left);
+ } else if (std::abs(vy) > 0) {
+ SetDirection(vy > 0 ? Down : Up);
+ }
+ }
+
+ UpdateFacing();
+ SetRemainingStep(1); //little hack to make the character step anim
+ float last_x = real_x;
+ float last_y = real_y;
+ real_x += vx;
+ real_y += vy;
+ if (GetThrough()) {
+ return true;
+ }
+ c2Circle self;
+ c2Circle other;
+ c2Circle hero;
+ self.p = c2V(real_x + 0.5, real_y + 0.5);
+ self.r = 0.5;
+ other.r = 0.5;
+ c2AABB tile;
+
+ c2Manifold manifold;
+
+ /*
+ c2Poly poly;
+ poly.count = 4;
+ poly.verts[0] = c2V(0, 0);
+ poly.verts[1] = c2V(1, 0);
+ poly.verts[2] = c2V(1, 1);
+ poly.verts[3] = c2V(0, 1);
+ c2MakePoly(&poly);
+ c2x transform = c2xIdentity();
+ //
+ c2Poly poly;
+ poly.count = 3;
+ poly.verts[0] = c2V(0, 1);
+ poly.verts[1] = c2V(1, 0);
+ poly.verts[2] = c2V(1, 1);
+ c2MakePoly(&poly);
+ c2x transform = c2xIdentity();
+ transform.p = c2V(14, 16);
+ c2CircletoPolyManifold(self, &poly, &transform, &manifold);
+ if (manifold.count > 0) {
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+ }
+ transform.p = c2V(15, 15);
+ c2CircletoPolyManifold(self, &poly, &transform, &manifold);
+ if (manifold.count > 0) {
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+ }
+ transform.p = c2V(16, 14);
+ c2CircletoPolyManifold(self, &poly, &transform, &manifold);
+ if (manifold.count > 0) {
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+ }
+ */
+
+ //Test Collision With Events
+ for (auto& ev : Game_Map::GetEvents()) {
+ if (!Game_Map::WouldCollideWithCharacter(*this, ev, false)) {
+ continue;
+ }
+ other.p.x = ev.real_x + 0.5;
+ other.p.y = ev.real_y + 0.5;
+ c2CircletoCircleManifold(self, other, &manifold);
+ if (manifold.count > 0) {
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+ }
+ }
+ //Test Collision With Player
+
+ if (Game_Map::WouldCollideWithCharacter(*this, *Main_Data::game_player, false) && !Main_Data::game_player->IsFlying()) {
+ other.p.x = player->real_x + 0.5;
+ other.p.y = player->real_y + 0.5;
+ c2CircletoCircleManifold(self, other, &manifold);
+ if (manifold.count > 0) {
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+// Now, check if this collision should trigger an event.
+ // This only applies if 'this' character is an event bumping into the player.
+ if (GetType() == Game_Character::Event) {
+ Game_Event* self_as_event = static_cast(this);
+
+ // Check for "Event Touch" on the "Same as Hero" layer
+ if (self_as_event->GetTrigger() == lcf::rpg::EventPage::Trigger_collision &&
+ self_as_event->GetLayer() == lcf::rpg::EventPage::Layers_same &&
+ !Game_Map::GetInterpreter().IsRunning())
+ {
+ // Collision is confirmed, trigger the event!
+ self_as_event->ScheduleForegroundExecution(false, true);
+ }
+ }
+ }
+ }
+//Test Collision With Map - Map collision has high priority, so it is tested last
+
+ // MODIFIED: Airships that are flying should ignore map collision entirely.
+ if (IsFlying()) {
+ real_x = self.p.x - 0.5f;
+ real_y = self.p.y - 0.5f;
+
+ if (Game_Map::LoopHorizontal()) {
+ const float map_width_f = static_cast(Game_Map::GetTilesX());
+ // Use fmod to wrap the coordinate into the [-map_width, map_width] range
+ real_x = fmod(real_x, map_width_f);
+ // If the result is negative, add map_width to bring it into the [0, map_width] range
+ if (real_x < 0.0f) {
+ real_x += map_width_f;
+ }
+ }
+
+ else if (this == Main_Data::game_player.get()) {
+ // If not looping, clamp to map bounds (0 to Width - 1)
+ float map_width_f = static_cast(Game_Map::GetTilesX());
+ if (real_x < 0.0f) real_x = 0.0f;
+ if (real_x > map_width_f - 1.0f) real_x = map_width_f - 1.0f;
+ }
+
+ if (Game_Map::LoopVertical()) {
+ const float map_height_f = static_cast(Game_Map::GetTilesY());
+ real_y = fmod(real_y, map_height_f);
+ if (real_y < 0.0f) {
+ real_y += map_height_f;
+ }
+ }
+ else if (this == Main_Data::game_player.get()) {
+ // If not looping, clamp to map bounds (0 to Height - 1)
+ float map_height_f = static_cast(Game_Map::GetTilesY());
+ if (real_y < 0.0f) real_y = 0.0f;
+ if (real_y > map_height_f - 1.0f) real_y = map_height_f - 1.0f;
+ }
+
+ SetX(round(real_x));
+ SetY(round(real_y));
+
+ // Check for landing possibility with decision key
+ if (Input::IsTriggered(Input::DECISION) && GetType() == Game_Character::Player) {
+ Game_Map::GetVehicle(Game_Vehicle::Airship)->StartDescent();
+ }
+
+ return true; // Skip all further map collision checks
+ }
+
+ int map_width = Game_Map::GetTilesX();
+ int map_height = Game_Map::GetTilesY();
+
+ // Clamp the player's position to the map boundaries on non-looping maps.
+ // This check applies to the player directly and when in a vehicle.
+ if (Player::game_config.allow_pixel_movement.Get() && this == Main_Data::game_player.get()) {
+ if (!Game_Map::LoopHorizontal()) {
+ float map_width_f = static_cast(Game_Map::GetTilesX());
+ // Clamp the center of the character so its edges (radius 0.5) don't go past the map boundary.
+ self.p.x = std::max(0.5f, std::min(self.p.x, map_width_f - 0.5f));
+ }
+
+ if (!Game_Map::LoopVertical()) {
+ float map_height_f = static_cast(Game_Map::GetTilesY());
+ self.p.y = std::max(0.5f, std::min(self.p.y, map_height_f - 0.5f));
+ }
+ }
+
+ int left = floor(self.p.x - 0.5f);
+ int right = floor((self.p.x - 0.5f) + 1.0f);
+ int top = floor(self.p.y - 0.5f);
+ int bottom = floor((self.p.y - 0.5f) + 1.0f);
+
+ for (int y = top; y <= bottom; y++) {
+ for (int x = left; x <= right; x++) {
+ int tile_x = x;
+ int tile_y = y;
+
+ if (Game_Map::LoopHorizontal()) {
+ tile_x = (tile_x % map_width + map_width) % map_width;
+ }
+ if (Game_Map::LoopVertical()) {
+ tile_y = (tile_y % map_height + map_height) % map_height;
+ }
+
+ // MODIFIED: Use IsPassableTile, which already contains all the logic for
+ // different vehicles. We check passability from all cardinal directions (0x0F)
+ // as a proxy for "can this character be on this tile at all?".
+ if (!Game_Map::IsPassableTile(&(*this), 0x0F, tile_x, tile_y)) {
+ c2AABB tile_aabb;
+ tile_aabb.min = c2V(x, y);
+ tile_aabb.max = c2V(x + 1, y + 1);
+ c2CircletoAABBManifold(self, tile_aabb, &manifold);
+ if (manifold.count > 0) {
+ // Simplified collision resolution
+ self.p.x -= manifold.n.x * manifold.depths[0];
+ self.p.y -= manifold.n.y * manifold.depths[0];
+ }
+ }
+ }
+ }
+
+
+ real_x = self.p.x - 0.5f;
+ real_y = self.p.y - 0.5f;
+
+ if (Game_Map::LoopHorizontal()) {
+ const float map_width_f = static_cast(Game_Map::GetTilesX());
+ // Use fmod to wrap the coordinate into the [-map_width, map_width] range
+ real_x = fmod(real_x, map_width_f);
+ // If the result is negative, add map_width to bring it into the [0, map_width] range
+ if (real_x < 0.0f) {
+ real_x += map_width_f;
+ }
+ }
+
+ if (Game_Map::LoopVertical()) {
+ const float map_height_f = static_cast(Game_Map::GetTilesY());
+ real_y = fmod(real_y, map_height_f);
+ if (real_y < 0.0f) {
+ real_y += map_height_f;
+ }
+ }
+
+ SetX(round(real_x));
+ SetY(round(real_y));
+
+ if (abs(real_x - last_x) <= Epsilon && abs(real_y - last_y) <= Epsilon) {
+ SetRemainingStep(0);
+ return false; // If there is no expressive change, treat as no movement.
+ }
+
+ return true;
+}
+
+bool Game_Character::Move(int dir) {
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){// TODO - PIXELMOVE
+ SetDirection(dir);
+ c2v vector = c2V(GetDxFromDirection(dir), GetDyFromDirection(dir));
+ float step_size = GetStepSize();
+ return MoveVector(c2Mulvs(c2Norm(vector), step_size));
+ }
+
+ if (!IsStopping()) {
+ return true;
+ }
+
+ bool move_success = false;
+
+ SetDirection(dir);
+ UpdateFacing();
+
+ const auto x = GetX();
+ const auto y = GetY();
+ const auto dx = GetDxFromDirection(dir);
+ const auto dy = GetDyFromDirection(dir);
+
+ if (dx && dy) {
+ // For diagonal movement, RPG_RT trys vert -> horiz and if that fails, then horiz -> vert.
+ move_success = (MakeWay(x, y, x, y + dy) && MakeWay(x, y + dy, x + dx, y + dy))
+ || (MakeWay(x, y, x + dx, y) && MakeWay(x + dx, y, x + dx, y + dy));
+ } else if (dx) {
+ move_success = MakeWay(x, y, x + dx, y);
+ } else if (dy) {
+ move_success = MakeWay(x, y, x, y + dy);
+ }
+
+ if (!move_success) {
+ return false;
+ }
+
+ const auto new_x = Game_Map::RoundX(x + dx);
+ const auto new_y = Game_Map::RoundY(y + dy);
+
+ SetX(new_x);
+ SetY(new_y);
+ SetRemainingStep(SCREEN_TILE_SIZE);
+
+ return true;
+}
+
+void Game_Character::Turn90DegreeLeft() {
+ SetDirection(GetDirection90DegreeLeft(GetDirection()));
+}
+
+void Game_Character::Turn90DegreeRight() {
+ SetDirection(GetDirection90DegreeRight(GetDirection()));
+}
+
+void Game_Character::Turn180Degree() {
+ SetDirection(GetDirection180Degree(GetDirection()));
+}
+
+void Game_Character::Turn90DegreeLeftOrRight() {
+ if (Rand::ChanceOf(1,2)) {
+ Turn90DegreeLeft();
+ } else {
+ Turn90DegreeRight();
+ }
+}
+
+int Game_Character::GetDirectionToCharacter(const Game_Character& target) {
+ int sx = GetDistanceXfromCharacter(target);
+ int sy = GetDistanceYfromCharacter(target);
+
+ if ( std::abs(sx) > std::abs(sy) ) {
+ return (sx > 0) ? Left : Right;
+ } else {
+ return (sy > 0) ? Up : Down;
+ }
+}
+
+int Game_Character::GetDirectionAwayCharacter(const Game_Character& target) {
+ int sx = GetDistanceXfromCharacter(target);
+ int sy = GetDistanceYfromCharacter(target);
+
+ if ( std::abs(sx) > std::abs(sy) ) {
+ return (sx > 0) ? Right : Left;
+ } else {
+ return (sy > 0) ? Down : Up;
+ }
+}
+
+void Game_Character::TurnTowardCharacter(const Game_Character& target) {
+ SetDirection(GetDirectionToCharacter(target));
+}
+
+void Game_Character::TurnAwayFromCharacter(const Game_Character& target) {
+ SetDirection(GetDirectionAwayCharacter(target));
+}
+
+void Game_Character::TurnRandom() {
+ SetDirection(Rand::GetRandomNumber(0, 3));
+}
+
+void Game_Character::Wait() {
+ SetStopCount(0);
+ SetMaxStopCountForWait();
+}
+
+bool Game_Character::BeginMoveRouteJump(int32_t& current_index, const lcf::rpg::MoveRoute& current_route) {
+ int jdx = 0;
+ int jdy = 0;
+
+ for (++current_index; current_index < static_cast(current_route.move_commands.size()); ++current_index) {
+ using Code = lcf::rpg::MoveCommand::Code;
+ const auto& move_command = current_route.move_commands[current_index];
+ const auto cmd = static_cast(move_command.command_id);
+ if (cmd >= Code::move_up && cmd <= Code::move_forward) {
+ switch (cmd) {
+ case Code::move_up:
+ case Code::move_right:
+ case Code::move_down:
+ case Code::move_left:
+ case Code::move_upright:
+ case Code::move_downright:
+ case Code::move_downleft:
+ case Code::move_upleft:
+ SetDirection(move_command.command_id);
+ break;
+ case Code::move_random:
+ TurnRandom();
+ break;
+ case Code::move_towards_hero:
+ TurnTowardCharacter(GetPlayer());
+ break;
+ case Code::move_away_from_hero:
+ TurnAwayFromCharacter(GetPlayer());
+ break;
+ case Code::move_forward:
+ break;
+ default:
+ break;
+ }
+ jdx += GetDxFromDirection(GetDirection());
+ jdy += GetDyFromDirection(GetDirection());
+ }
+
+ if (cmd >= Code::face_up && cmd <= Code::face_away_from_hero) {
+ switch (cmd) {
+ case Code::face_up:
+ SetDirection(Up);
+ break;
+ case Code::face_right:
+ SetDirection(Right);
+ break;
+ case Code::face_down:
+ SetDirection(Down);
+ break;
+ case Code::face_left:
+ SetDirection(Left);
+ break;
+ case Code::turn_90_degree_right:
+ Turn90DegreeRight();
+ break;
+ case Code::turn_90_degree_left:
+ Turn90DegreeLeft();
+ break;
+ case Code::turn_180_degree:
+ Turn180Degree();
+ break;
+ case Code::turn_90_degree_random:
+ Turn90DegreeLeftOrRight();
+ break;
+ case Code::face_random_direction:
+ TurnRandom();
+ break;
+ case Code::face_hero:
+ TurnTowardCharacter(GetPlayer());
+ break;
+ case Code::face_away_from_hero:
+ TurnAwayFromCharacter(GetPlayer());
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (cmd == Code::end_jump) {
+ bool rc;
+ if (Player::game_config.allow_pixel_movement.Get()) {
+ float new_x = GetRealX() + jdx;
+ float new_y = GetRealY() + jdy;
+ rc = Jump(new_x, new_y);
+ } else {
+ int new_x = GetX() + jdx;
+ int new_y = GetY() + jdy;
+ rc = Jump(new_x, new_y);
+ }
+ if (rc) {
+ SetMaxStopCountForStep();
+ }
+ // Note: outer function increment will cause the end jump to pass after the return.
+ return rc;
+ }
+ }
+
+ // Commands finished with no end jump. Back up the index by 1 to allow outer loop increment to work.
+ --current_index;
+
+ // Jump is skipped
+ return true;
+}
+
+// --- START: New Pixel-Perfect Jump Function ---
+bool Game_Character::Jump(float x, float y) {
+ if (!IsStopping()) {
+ return true;
+ }
+
+ // Store the precise floating-point start and end coordinates
+ jump_start_real_x = GetRealX();
+ jump_start_real_y = GetRealY();
+ jump_end_real_x = x;
+ jump_end_real_y = y;
+
+ // For compatibility, also store the tile-based start/end points
+ auto begin_x = GetX();
+ auto begin_y = GetY();
+ const auto final_tile_x = static_cast(round(x));
+ const auto final_tile_y = static_cast(round(y));
+ const auto dx = final_tile_x - begin_x;
+ const auto dy = final_tile_y - begin_y;
+
+ // Determine facing direction based on jump vector
+ if (std::abs(dy) >= std::abs(dx)) {
+ SetDirection(dy >= 0 ? Down : Up);
+ } else {
+ SetDirection(dx >= 0 ? Right : Left);
+ }
+
+ SetJumping(true);
+
+ if (dx != 0 || dy != 0) {
+ if (!IsFacingLocked()) {
+ SetFacing(GetDirection());
+ }
+
+ // Pathfinding still uses the tile grid. A pixel jump is only
+ // allowed if the underlying tile path is clear.
+ if (!MakeWay(begin_x, begin_y, final_tile_x, final_tile_y)) {
+ SetJumping(false);
+ return false; // Jump failed, path is blocked
+ }
+ }
+
+ // Update the integer tile coordinates to the final destination tile
+ SetBeginJumpX(begin_x);
+ SetBeginJumpY(begin_y);
+ SetX(final_tile_x);
+ SetY(final_tile_y);
+
+ SetRemainingStep(SCREEN_TILE_SIZE);
+
+ return true;
+}
+// --- END: New Pixel-Perfect Jump Function ---
+
+bool Game_Character::Jump(int x, int y) {
+
+
+ if (Player::game_config.allow_pixel_movement.Get()) {
+ // If pixel movement is on, call the new float-based version
+ return Jump(static_cast(x), static_cast(y));
+ }
+
+// real_x = (float)x;
+// real_y = (float)y;
+
+ if (!IsStopping()) {
+ return true;
+ }
+
+ auto begin_x = GetX();
+ auto begin_y = GetY();
+ const auto dx = x - begin_x;
+ const auto dy = y - begin_y;
+
+ if (std::abs(dy) >= std::abs(dx)) {
+ SetDirection(dy >= 0 ? Down : Up);
+ } else {
+ SetDirection(dx >= 0 ? Right : Left);
+ }
+
+ SetJumping(true);
+
+ if (dx != 0 || dy != 0) {
+ if (!IsFacingLocked()) {
+ SetFacing(GetDirection());
+ }
+
+ // FIXME: Remove dependency on jump from within Game_Map::MakeWay?
+ // RPG_RT passes INT_MAX into from_x to tell it to skip self tile checks, which is hacky..
+ if (!MakeWay(begin_x, begin_y, x, y)) {
+ SetJumping(false);
+ return false;
+ }
+ }
+
+ // Adjust positions for looping maps. jump begin positions
+ // get set off the edge of the map to preserve direction.
+ if (Game_Map::LoopHorizontal()
+ && (x < 0 || x >= Game_Map::GetTilesX()))
+ {
+ const auto old_x = x;
+ x = Game_Map::RoundX(x);
+ begin_x += x - old_x;
+ }
+
+ if (Game_Map::LoopVertical()
+ && (y < 0 || y >= Game_Map::GetTilesY()))
+ {
+ auto old_y = y;
+ y = Game_Map::RoundY(y);
+ begin_y += y - old_y;
+ }
+
+ SetBeginJumpX(begin_x);
+ SetBeginJumpY(begin_y);
+
+ if (Player::game_config.allow_pixel_movement.Get()) {
+ real_x = static_cast(begin_x);
+ real_y = static_cast(begin_y);
+ }
+
+ SetX(x);
+ SetY(y);
+
+// SetX(real_x);
+// SetY(real_y);
+ SetJumping(true);
+ SetRemainingStep(SCREEN_TILE_SIZE);
+
+ /* if (true) { // TODO - PIXELMOVE
+
+
+
+// SetDirection(GetDirection());
+ c2v vector = c2V(GetDxFromDirection(GetDirection()), GetDyFromDirection(GetDirection()));
+// c2v vector = c2V(real_x - begin_x, real_y - begin_y);
+ float length = c2Len(vector);
+ c2v vectorNorm = c2Div(vector, length);
+ float step_size = GetStepSize();
+// MoveVector(c2Mulvs(vectorNorm, step_size));
+ MoveVector(c2Mulvs(c2Norm(vectorNorm), step_size));
+// SetRemainingStep(0);
+}
+*/
+
+/* Reference material
+ c2v vector = c2V(GetDxFromDirection(GetDirection()), GetDyFromDirection(GetDirection()));
+ c2v vector = c2V(target_x - real_x, target_y - real_y);
+ float length = c2Len(vector);
+ c2v vectorNorm = c2Div(vector, length);
+ float step_size = GetStepSize();
+ if (length > step_size) {
+ move_success = MoveVector(c2Mulvs(vectorNorm, step_size));
+ }
+ else {
+ move_success = MoveVector(vector);
+ is_moving_toward_target = false;
+ }
+ if (!move_success) {
+ if (is_move_toward_target_skippable) {
+ is_moving_toward_target = false;
+ }
+ else if (c2Dot(vectorNorm, move_direction) <= 0) {
+ is_moving_toward_target = false;
+ forced_skip = true;
+ }
+ }
+
+*/
+
+
+ return true;
+}
+
+int Game_Character::GetDistanceXfromCharacter(const Game_Character& target) const {
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+
+ float sx = real_x - Main_Data::game_player->real_x;
+
+ if (Game_Map::LoopHorizontal()) {
+ if (std::abs(sx) > Game_Map::GetTilesX() / 2) {
+ if (sx > 0)
+ sx -= Game_Map::GetTilesX();
+ else
+ sx += Game_Map::GetTilesX();
+ }
+ }
+ return round(sx * SCREEN_TILE_SIZE);
+ } //END - PIXELMOVE
+
+
+ int sx = GetX() - target.GetX();
+ if (Game_Map::LoopHorizontal()) {
+ if (std::abs(sx) > Game_Map::GetTilesX() / 2) {
+ if (sx > 0)
+ sx -= Game_Map::GetTilesX();
+ else
+ sx += Game_Map::GetTilesX();
+ }
+ }
+ return sx;
+}
+
+int Game_Character::GetDistanceYfromCharacter(const Game_Character& target) const {
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+ float sy = real_y - Main_Data::game_player->real_y;
+
+ if (Game_Map::LoopVertical()) {
+ if (std::abs(sy) > Game_Map::GetTilesY() / 2) {
+ if (sy > 0)
+ sy -= Game_Map::GetTilesY();
+ else
+ sy += Game_Map::GetTilesY();
+ }
+ }
+ return round(sy * SCREEN_TILE_SIZE);
+ } // END - PIXELMOVE
+
+
+
+ int sy = GetY() - target.GetY();
+ if (Game_Map::LoopVertical()) {
+ if (std::abs(sy) > Game_Map::GetTilesY() / 2) {
+ if (sy > 0)
+ sy -= Game_Map::GetTilesY();
+ else
+ sy += Game_Map::GetTilesY();
+ }
+ }
+ return sy;
+}
+
+void Game_Character::ForceMoveRoute(const lcf::rpg::MoveRoute& new_route,
+ int frequency) {
+ if (!IsMoveRouteOverwritten()) {
+ original_move_frequency = GetMoveFrequency();
+ }
+
+ SetPaused(false);
+ SetStopCount(0xFFFF);
+ SetMoveRouteIndex(0);
+ SetMoveRouteFinished(false);
+ SetMoveFrequency(frequency);
+ SetMoveRouteOverwritten(true);
+ SetMoveRoute(new_route);
+ SetMoveFailureCount(0);
+ if (frequency != original_move_frequency) {
+ SetMaxStopCountForStep();
+ }
+
+ if (GetMoveRoute().move_commands.empty()) {
+ CancelMoveRoute();
+ return;
+ }
+}
+
+void Game_Character::CancelMoveRoute() {
+ if (IsMoveRouteOverwritten()) {
+ SetMoveFrequency(original_move_frequency);
+ SetMaxStopCountForStep();
+ }
+ SetMoveRouteOverwritten(false);
+ SetMoveRouteFinished(false);
+}
+
+struct SearchNode {
+ int x = 0;
+ int y = 0;
+ int cost = 0;
+ int direction = 0;
+
+ int id = 0;
+ int parent_id = -1;
+ int parent_x = -1;
+ int parent_y = -1;
+
+ friend bool operator==(const SearchNode& n1, const SearchNode& n2)
+ {
+ return n1.x == n2.x && n1.y == n2.y;
+ }
+
+ bool operator()(SearchNode const& a, SearchNode const& b)
+ {
+ return a.id > b.id;
+ }
+};
+
+struct SearchNodeHash {
+ size_t operator()(const SearchNode &p) const {
+ return (p.x ^ (p.y + (p.y >> 12)));
+ }
+};
+
+bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) {
+ CancelMoveRoute();
+
+ // Set up helper variables:
+ SearchNode start = {GetX(), GetY(), 0, -1};
+ if ((start.x == args.dest_x && start.y == args.dest_y) || args.steps_max == 0) {
+ return true;
+ }
+ std::vector queue;
+ std::unordered_map graph;
+ std::map, SearchNode> graph_by_coord;
+ queue.push_back(start);
+ int id = 0;
+ int idd = 0;
+ int steps_taken = 0;
+ SearchNode closest_node = {args.dest_x, args.dest_y, std::numeric_limits::max(), -1}; // Initialize with a very high cost.
+ int closest_distance = std::numeric_limits::max(); // Initialize with a very high distance.
+ std::unordered_set seen;
+
+ int steps_max = args.steps_max;
+ if (steps_max == -1) {
+ steps_max = std::numeric_limits::max();
+ }
+
+ if (args.debug_print) {
+ Output::Debug("Game_Interpreter::CommandSearchPath: "
+ "start search, character x{} y{}, to x{}, y{}, "
+ "ignored event ids count: {}",
+ start.x, start.y, args.dest_x, args.dest_y, args.event_id_ignore_list.size());
+ }
+
+ bool loops_horizontal = Game_Map::LoopHorizontal();
+ bool loops_vertical = Game_Map::LoopVertical();
+ std::vector neighbour;
+ neighbour.reserve(8);
+ while (!queue.empty() && steps_taken < args.search_max) {
+ SearchNode n = queue[0];
+ queue.erase(queue.begin());
+ steps_taken++;
+ graph[n.id] = n;
+ graph_by_coord.insert({{n.x, n.y}, n});
+
+ if (n.x == args.dest_x && n.y == args.dest_y) {
+ // Reached the destination.
+ closest_node = n;
+ closest_distance = 0;
+ break; // Exit the loop to build final route.
+ }
+ else {
+ neighbour.clear();
+ SearchNode nn = {n.x + 1, n.y, n.cost + 1, 1}; // Right
+ neighbour.push_back(nn);
+ nn = {n.x, n.y - 1, n.cost + 1, 0}; // Up
+ neighbour.push_back(nn);
+ nn = {n.x - 1, n.y, n.cost + 1, 3}; // Left
+ neighbour.push_back(nn);
+ nn = {n.x, n.y + 1, n.cost + 1, 2}; // Down
+ neighbour.push_back(nn);
+
+ if (args.allow_diagonal) {
+ nn = {n.x - 1, n.y + 1, n.cost + 1, 6}; // Down Left
+ neighbour.push_back(nn);
+ nn = {n.x + 1, n.y - 1, n.cost + 1, 4}; // Up Right
+ neighbour.push_back(nn);
+ nn = {n.x - 1, n.y - 1, n.cost + 1, 7}; // Up Left
+ neighbour.push_back(nn);
+ nn = {n.x + 1, n.y + 1, n.cost + 1, 5}; // Down Right
+ neighbour.push_back(nn);
+ }
+
+ for (SearchNode a : neighbour) {
+ idd++;
+ a.parent_x = n.x;
+ a.parent_y = n.y;
+ a.id = idd;
+ a.parent_id = n.id;
+
+ // Adjust neighbor coordinates for map looping
+ if (loops_horizontal) {
+ if (a.x >= Game_Map::GetTilesX())
+ a.x -= Game_Map::GetTilesX();
+ else if (a.x < 0)
+ a.x += Game_Map::GetTilesX();
+ }
+
+ if (loops_vertical) {
+ if (a.y >= Game_Map::GetTilesY())
+ a.y -= Game_Map::GetTilesY();
+ else if (a.y < 0)
+ a.y += Game_Map::GetTilesY();
+ }
+
+ auto check = seen.find(a);
+ if (check != seen.end()) {
+ SearchNode old_entry = graph[(*check).id];
+ if (a.cost < old_entry.cost) {
+ // Found a shorter path to previous node, update & reinsert:
+ if (args.debug_print) {
+ Output::Debug("Game_Interpreter::CommandSearchPath: "
+ "found shorter path to x:{} y:{}"
+ "from x:{} y:{} direction: {}",
+ a.x, a.y, n.x, n.y, a.direction);
+ }
+ graph.erase(old_entry.id);
+ old_entry.cost = a.cost;
+ old_entry.parent_id = n.id;
+ old_entry.parent_x = n.x;
+ old_entry.parent_y = n.y;
+ old_entry.direction = a.direction;
+ graph[old_entry.id] = old_entry;
+ }
+ continue;
+ } else if (a.x == start.x && a.y == start.y) {
+ continue;
+ }
+ bool added = false;
+ if (CheckWay(n.x, n.y, a.x, a.y, true, args.event_id_ignore_list) ||
+ (a.x == args.dest_x && a.y == args.dest_y &&
+ CheckWay(n.x, n.y, a.x, a.y, false, {}))) {
+ if (a.direction == 4) {
+ if (CheckWay(n.x, n.y, n.x + 1, n.y,
+ true, args.event_id_ignore_list) ||
+ CheckWay(n.x, n.y, n.x, n.y - 1,
+ true, args.event_id_ignore_list)) {
+ added = true;
+ queue.push_back(a);
+ seen.insert(a);
+ }
+ }
+ else if (a.direction == 5) {
+ if (CheckWay(n.x, n.y, n.x + 1, n.y,
+ true, args.event_id_ignore_list) ||
+ CheckWay(n.x, n.y, n.x, n.y + 1,
+ true, args.event_id_ignore_list)) {
+ added = true;
+ queue.push_back(a);
+ seen.insert(a);
+ }
+ }
+ else if (a.direction == 6) {
+ if (CheckWay(n.x, n.y, n.x - 1, n.y,
+ true, args.event_id_ignore_list) ||
+ CheckWay(n.x, n.y, n.x, n.y + 1,
+ true, args.event_id_ignore_list)) {
+ added = true;
+ queue.push_back(a);
+ seen.insert(a);
+ }
+ }
+ else if (a.direction == 7) {
+ if (CheckWay(n.x, n.y, n.x - 1, n.y,
+ true, args.event_id_ignore_list) ||
+ CheckWay(n.x, n.y, n.x, n.y - 1,
+ true, args.event_id_ignore_list)) {
+ added = true;
+ queue.push_back(a);
+ seen.insert(a);
+ }
+ }
+ else {
+ added = true;
+ queue.push_back(a);
+ seen.insert(a);
+ }
+ }
+ if (added && args.debug_print) {
+ Output::Debug("Game_Interpreter::CommandSearchPath: "
+ "discovered id:{} x:{} y:{} parentX:{} parentY:{}"
+ "parentID:{} direction: {}",
+ queue[queue.size() - 1].id,
+ queue[queue.size() - 1].x, queue[queue.size() - 1].y,
+ queue[queue.size() - 1].parent_x,
+ queue[queue.size() - 1].parent_y,
+ queue[queue.size() - 1].parent_id,
+ queue[queue.size() - 1].direction);
+ }
+ }
+ }
+ id++;
+ // Calculate the Manhattan distance between the current node and the destination
+ int manhattan_dist = abs(args.dest_x - n.x) + abs(args.dest_y - n.y);
+
+ // Check if this node is closer to the destination
+ if (manhattan_dist < closest_distance) {
+ closest_node = n;
+ closest_distance = manhattan_dist;
+ if (args.debug_print) {
+ Output::Debug("Game_Interpreter::CommandSearchPath: "
+ "new closest node at x:{} y:{} id:{}",
+ closest_node.x, closest_node.y,
+ closest_node.id);
+ }
+ }
+ }
+
+ // Check if a path to the closest node was found.
+ if (closest_distance != std::numeric_limits::max()) {
+ // Build a route to the closest reachable node.
+ if (args.debug_print) {
+ Output::Debug("Game_Interpreter::CommandSearchPath: "
+ "trying to return route from x:{} y:{} to "
+ "x:{} y:{} (id:{})",
+ start.x, start.y, closest_node.x, closest_node.y,
+ closest_node.id);
+ }
+ std::vector list_move;
+
+ SearchNode node = closest_node;
+ while (static_cast(list_move.size()) < steps_max) {
+ list_move.push_back(node);
+ if (graph_by_coord.find({node.parent_x,
+ node.parent_y}) == graph_by_coord.end())
+ break;
+ SearchNode node2 = graph_by_coord[
+ {node.parent_x, node.parent_y}
+ ];
+ if (args.debug_print) {
+ Output::Debug(
+ "Game_Interpreter::CommandSearchPath: "
+ "found parent leading to x:{} y:{}, "
+ "it's at x:{} y:{} dir:{}",
+ node.x, node.y,
+ node2.x, node2.y, node2.direction);
+ }
+ node = node2;
+ }
+
+ std::reverse(list_move.rbegin(), list_move.rend());
+
+ std::string debug_output_path("");
+ if (list_move.size() > 0) {
+ lcf::rpg::MoveRoute route;
+ route.skippable = args.skip_when_failed;
+ route.repeat = false;
+
+ for (SearchNode node2 : list_move) {
+ if (node2.direction >= 0) {
+ lcf::rpg::MoveCommand cmd;
+ cmd.command_id = node2.direction;
+ route.move_commands.push_back(cmd);
+ if (args.debug_print >= 1) {
+ if (debug_output_path.length() > 0)
+ debug_output_path += ",";
+ std::ostringstream dirnum;
+ dirnum << node2.direction;
+ debug_output_path += std::string(dirnum.str());
+ }
+ }
+ }
+
+ lcf::rpg::MoveCommand cmd;
+ cmd.command_id = 23;
+ route.move_commands.push_back(cmd);
+
+ ForceMoveRoute(route, args.frequency);
+ }
+ if (args.debug_print) {
+ Output::Debug(
+ "Game_Interpreter::CommandSearchPath: "
+ "setting route {} for character x{} y{}",
+ " (ignored event ids count: {})",
+ debug_output_path, start.x, start.y,
+ args.event_id_ignore_list.size()
+ );
+ }
+ return true;
+ }
+
+ // No path to the destination, return failure.
+ return false;
+}
+
+int Game_Character::GetSpriteX() const {
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXEL MOVE
+ return round(real_x * SCREEN_TILE_SIZE);
+ } // END - PIXELMOVE
+
+
+ int x = GetX() * SCREEN_TILE_SIZE;
+
+ if (IsMoving()) {
+ int d = GetDirection();
+ if (d == Right || d == UpRight || d == DownRight)
+ x -= GetRemainingStep();
+ else if (d == Left || d == UpLeft || d == DownLeft)
+ x += GetRemainingStep();
+ } else if (IsJumping()) {
+ x -= ((GetX() - GetBeginJumpX()) * GetRemainingStep());
+ }
+
+ return x;
+}
+
+int Game_Character::GetSpriteY() const {
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXEL MOVE
+ return round(real_x * SCREEN_TILE_SIZE);
+ } // END - PIXELMOVE
+
+
+ int y = GetY() * SCREEN_TILE_SIZE;
+
+ if (IsMoving()) {
+ int d = GetDirection();
+ if (d == Down || d == DownRight || d == DownLeft)
+ y -= GetRemainingStep();
+ else if (d == Up || d == UpRight || d == UpLeft)
+ y += GetRemainingStep();
+ } else if (IsJumping()) {
+ y -= (GetY() - GetBeginJumpY()) * GetRemainingStep();
+ }
+
+ return y;
+}
+
+bool Game_Character::IsInPosition(int x, int y) const {
+ return ((GetX() == x) && (GetY() == y));
+}
+
+int Game_Character::GetOpacity() const {
+ return Utils::Clamp((8 - GetTransparency()) * 32 - 1, 0, 255);
+}
+
+bool Game_Character::IsAnimated() const {
+ auto at = GetAnimationType();
+ return !IsAnimPaused()
+ && at != lcf::rpg::EventPage::AnimType_fixed_graphic
+ && at != lcf::rpg::EventPage::AnimType_step_frame_fix;
+}
+
+bool Game_Character::IsContinuous() const {
+ auto at = GetAnimationType();
+ return
+ at == lcf::rpg::EventPage::AnimType_continuous ||
+ at == lcf::rpg::EventPage::AnimType_fixed_continuous;
+}
+
+bool Game_Character::IsSpinning() const {
+ return GetAnimationType() == lcf::rpg::EventPage::AnimType_spin;
+}
+
+int Game_Character::GetBushDepth() const {
+ if ((GetLayer() != lcf::rpg::EventPage::Layers_same) || IsJumping() || IsFlying()) {
+ return 0;
+ }
+
+ return Game_Map::GetBushDepth(GetX(), GetY());
+}
+
+void Game_Character::Flash(int r, int g, int b, int power, int frames) {
+ data()->flash_red = r;
+ data()->flash_green = g;
+ data()->flash_blue = b;
+ data()->flash_current_level = power;
+ data()->flash_time_left = frames;
+}
+
+// Gets Character
+Game_Character* Game_Character::GetCharacter(int character_id, int event_id) {
+ switch (character_id) {
+ case CharPlayer:
+ // Player/Hero
+ return Main_Data::game_player.get();
+ case CharBoat:
+ return Game_Map::GetVehicle(Game_Vehicle::Boat);
+ case CharShip:
+ return Game_Map::GetVehicle(Game_Vehicle::Ship);
+ case CharAirship:
+ return Game_Map::GetVehicle(Game_Vehicle::Airship);
+ case CharThisEvent:
+ // This event
+ return Game_Map::GetEvent(event_id);
+ default:
+ // Other events
+ return Game_Map::GetEvent(character_id);
+ }
+}
+
+Game_Character& Game_Character::GetPlayer() {
+ assert(Main_Data::game_player);
+
+ return *Main_Data::game_player;
+}
+
+int Game_Character::ReverseDir(int dir) {
+ constexpr static char reversed[] =
+ { Down, Left, Up, Right, DownLeft, UpLeft, UpRight, DownRight };
+ return reversed[dir];
+}
+
+void Game_Character::SetMaxStopCountForStep() {
+ SetMaxStopCount(GetMaxStopCountForStep(GetMoveFrequency()));
+}
+
+void Game_Character::SetMaxStopCountForTurn() {
+ SetMaxStopCount(GetMaxStopCountForTurn(GetMoveFrequency()));
+}
+
+void Game_Character::SetMaxStopCountForWait() {
+ SetMaxStopCount(GetMaxStopCountForWait(GetMoveFrequency()));
+}
+
+void Game_Character::UpdateFacing() {
+ // RPG_RT only does the IsSpinning() check for Game_Event. We did it for all types here
+ // in order to avoid a virtual call and because normally with RPG_RT, spinning
+ // player or vehicle is impossible.
+ if (IsFacingLocked() || IsSpinning()) {
+ return;
+ }
+ const auto dir = GetDirection();
+ const auto facing = GetFacing();
+ if (dir >= 4) /* is diagonal */ {
+ // [UR, DR, DL, UL] -> [U, D, D, U]
+ const auto f1 = ((dir + (dir >= 6)) % 2) * 2;
+ // [UR, DR, DL, UL] -> [R, R, L, L]
+ const auto f2 = (dir / 2) - (dir < 6);
+ if (facing != f1 && facing != f2) {
+ // Reverse the direction.
+ SetFacing((facing + 2) % 4);
+ }
+ } else {
+ SetFacing(dir);
+ }
+}
diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp
index 1bbe7877e3..cfe56c0fb8 100644
--- a/src/game_interpreter.cpp
+++ b/src/game_interpreter.cpp
@@ -992,7 +992,9 @@ void Game_Interpreter::SetupChoices(const std::vector& choices, int
}
pm.SetChoiceContinuation([this, indent](int choice_result) {
- SetSubcommandIndex(indent, choice_result);
+ if (IsRunning()) {
+ SetSubcommandIndex(indent, choice_result);
+ }
return AsyncOp();
});
From ed59eb42f809488f3ca2df9dbda54d36beb9c5eb Mon Sep 17 00:00:00 2001
From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com>
Date: Mon, 20 Apr 2026 18:58:04 -0600
Subject: [PATCH 2/4] Pixel Movement Complete
The remaining files.
---
src/game_character.h | 65 +-
src/game_config_game.cpp | 9 +
src/game_config_game.h | 2 +-
src/game_event.cpp | 112 +-
src/game_event.h | 3 +-
src/game_map.cpp | 5053 +++++++++++++++++++++-----------------
src/game_map.h | 88 +-
src/game_player.cpp | 2300 ++++++++++-------
src/game_player.h | 38 +-
src/game_vehicle.cpp | 30 +-
src/game_vehicle.h | 3 +-
11 files changed, 4433 insertions(+), 3270 deletions(-)
diff --git a/src/game_character.h b/src/game_character.h
index 2549fe516d..3c5ed6da66 100644
--- a/src/game_character.h
+++ b/src/game_character.h
@@ -28,13 +28,46 @@
#include
#include
#include "drawable.h"
-#include "utils.h"
+#include "utils.h"
+#include "cute_c2.h"
/**
* Game_Character class.
*/
class Game_Character {
-public:
+public:
+
+
+ //TODO - PIXELMOVE
+ float real_x;
+ float real_y;
+
+ float target_x;
+ float target_y;
+ c2v move_direction;
+ bool forced_skip = false; //When a movement is not skippable, but it is forced to be skiped.
+ bool is_moving_toward_target = false;
+ bool is_move_toward_target_skippable = false;
+
+ bool MoveVector(c2v vector);
+ bool MoveVector(float vx, float vy);
+ float GetCustomZoom() const { return custom_zoom; }
+ void SetCustomZoom(float z) { custom_zoom = z; }
+
+ float GetRealX() const;
+ float GetRealY() const;
+
+ void SetMoveTowardTarget(c2v position, bool skippable);
+ void SetMoveTowardTarget(float x, float y, bool skippable);
+ bool UpdateMoveTowardTarget();
+
+ float GetStepSize() const;
+
+
+ // END - PIXELMOVE
+
+
+
using AnimType = lcf::rpg::EventPage::AnimType;
enum Type {
@@ -584,7 +617,8 @@ class Game_Character {
* @return Whether jump was successful or a move or jump is already in progress.
* @post If successful, IsStopping() == false.
*/
- bool Jump(int x, int y);
+ bool Jump(int x, int y);
+ bool Jump(float x, float y);
/**
* Check if this can move to the given tile.
@@ -912,6 +946,9 @@ class Game_Character {
DownLeft,
UpLeft
};
+
+ float Epsilon = pow(256, -2); //TODO - PIXELMOVE
+
static bool IsDirectionDiagonal(int d);
@@ -923,6 +960,10 @@ class Game_Character {
static constexpr int GetDxFromDirection(int dir);
static constexpr int GetDyFromDirection(int dir);
+
+ /** Wait time for DOOM mode */
+ int doomWait = 0;
+
protected:
explicit Game_Character(Type type, lcf::rpg::SaveMapEventBase* d);
@@ -943,11 +984,19 @@ class Game_Character {
void IncAnimFrame();
void UpdateFlash();
bool BeginMoveRouteJump(int32_t& current_index, const lcf::rpg::MoveRoute& current_route);
+
+// For pixel-perfect jumping
+ float jump_start_real_x = 0.0f;
+ float jump_start_real_y = 0.0f;
+ float jump_end_real_x = 0.0f;
+ float jump_end_real_y = 0.0f;
lcf::rpg::SaveMapEventBase* data();
const lcf::rpg::SaveMapEventBase* data() const;
- int original_move_frequency = 2;
+ int original_move_frequency = 2;
+
+ float custom_zoom = 1.0f; // Default is 100%
// contains if any movement (<= step_forward) of a forced move route was successful
Type _type = {};
@@ -993,6 +1042,14 @@ inline const lcf::rpg::SaveMapEventBase* Game_Character::data() const {
inline Game_Character::Type Game_Character::GetType() const {
return _type;
}
+
+
+//TODO - PIXELMOVE
+inline float Game_Character::GetStepSize() const {
+ return (float)(1 << (1 + GetMoveSpeed())) / 256.0; // SCREEN_TILE_SIZE == 265
+}
+//END - PIXELMOVE
+
inline int Game_Character::GetX() const {
return data()->position_x;
diff --git a/src/game_config_game.cpp b/src/game_config_game.cpp
index 0789bd17f9..f9d9ca38e8 100644
--- a/src/game_config_game.cpp
+++ b/src/game_config_game.cpp
@@ -69,6 +69,11 @@ void Game_ConfigGame::LoadFromArgs(CmdlineParser& cp) {
if (cp.ParseNext(arg, 0, {"--new-game", "--no-new-game"})) {
new_game.Set(arg.ArgIsOn());
continue;
+ }
+ if (cp.ParseNext(arg, 0, {"--pixel-movement", "--no-pixel-movement"})) {
+ allow_pixel_movement.Set(arg.ArgIsOn());
+ patch_override = true;
+ continue;
}
if (cp.ParseNext(arg, 1, "--engine")) {
if (arg.NumValues() > 0) {
@@ -234,6 +239,10 @@ void Game_ConfigGame::LoadFromStream(Filesystem_Stream::InputStream& is) {
if (patch_direct_menu.FromIni(ini)) {
patch_override = true;
}
+
+ if (allow_pixel_movement.FromIni(ini)) {
+ patch_override = true;
+ }
if (RuntimePatches::ParseFromIni(ini)) {
patch_override = true;
diff --git a/src/game_config_game.h b/src/game_config_game.h
index 354d1d9bb8..3e7a71d118 100644
--- a/src/game_config_game.h
+++ b/src/game_config_game.h
@@ -70,7 +70,7 @@ struct Game_ConfigGame {
ConfigParam patch_guardrevamp_normal{ "GuardRevamp", "Changes damage calculation for defense situations (Normal)", "Patch", "GuardRevamp.NormalDefense", 0 };
ConfigParam patch_guardrevamp_strong{ "GuardRevamp", "Changes damage calculation for defense situations (Strong)", "Patch", "GuardRevamp.StrongDefense", 0 };
-
+ ConfigParam allow_pixel_movement{"allow_pixel_movement", "Allow pixel-based movement instead of tile-based.", "Patch", "allow_pixel_movement", false};
// Command line only
BoolConfigParam patch_support{ "Support patches", "When OFF all patch support is disabled", "", "", true };
diff --git a/src/game_event.cpp b/src/game_event.cpp
index 039e232104..abf20c5796 100644
--- a/src/game_event.cpp
+++ b/src/game_event.cpp
@@ -33,7 +33,8 @@
#include "rand.h"
#include "output.h"
#include
-#include
+#include
+#include "cute_c2.h"
Game_Event::Game_Event(int map_id, const lcf::rpg::Event* event) :
Game_EventBase(Event),
@@ -43,8 +44,19 @@ Game_Event::Game_Event(int map_id, const lcf::rpg::Event* event) :
SetMapId(map_id);
SetX(event->x);
SetY(event->y);
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ //TODO - PIXELMOVE
+ real_x = (float)GetX();
+ real_y = (float)GetY();
+ //Output::Warning("Event Pos = {}x{}", real_x, real_y);
+ }// END - PIXELMOVE
+
+
+ RefreshPage();
+
+ Output::Debug("event[{}].name: {}", data()->ID, GetSpriteName()); //TODO - PIXELMOVE
- RefreshPage();
}
void Game_Event::SanitizeData() {
@@ -91,7 +103,14 @@ void Game_Event::SetSaveData(lcf::rpg::SaveMapEvent save)
if (has_state) {
interpreter->SetState(state);
}
- }
+ }
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE
+ real_x = (float)GetX();
+ real_y = (float)GetY();
+ } // END - PIXELMOVE
+
}
lcf::rpg::SaveMapEvent Game_Event::GetSaveData() const {
@@ -468,8 +487,25 @@ void Game_Event::MoveTypeRandom() {
SetStopCount(Rand::GetRandomNumber(0, GetMaxStopCount()));
return;
}
-
+
+/*
Move(GetDirection());
+*/
+
+// if (true) { // TODO - PIXELMOVE
+ if (Player::game_config.allow_pixel_movement.Get()){
+ c2v target = c2V(
+ round(real_x + GetDxFromDirection(GetDirection())),
+ round(real_y + GetDyFromDirection(GetDirection()))
+ );
+ SetMoveTowardTarget(target, true);
+ UpdateMoveTowardTarget();
+ }
+ else {
+ Move(GetDirection());
+ } // END - PIXELMOVE
+
+
if (IsStopping()) {
if (IsWaitingForegroundExecution() || (GetStopCount() >= GetMaxStopCount() + 60)) {
@@ -534,7 +570,8 @@ void Game_Event::MoveTypeTowardsOrAwayPlayer(bool towards) {
&& sy >= -offset && sy <= Player::screen_height + offset);
const auto prev_dir = GetDirection();
-
+
+ /*
int dir = 0;
if (!in_sight) {
dir = Rand::GetRandomNumber(0, 3);
@@ -552,6 +589,71 @@ void Game_Event::MoveTypeTowardsOrAwayPlayer(bool towards) {
}
Move(dir);
+ */
+
+
+// if (true) {
+ if (Player::game_config.allow_pixel_movement.Get()){ //TODO - PIXELMOVE
+ int dir = 0;
+ int draw = 0;
+ c2v target;
+ if (!in_sight) {
+ dir = Rand::GetRandomNumber(0, 3);
+ }
+ else {
+ draw = Rand::GetRandomNumber(0, 9);
+ if (draw == 0) {
+ dir = GetDirection();
+ }
+ else if (draw == 1) {
+ dir = Rand::GetRandomNumber(0, 3);
+ }
+ }
+ if (towards) {
+ TurnTowardCharacter(GetPlayer());
+ }
+ else {
+ TurnAwayFromCharacter(GetPlayer());
+ }
+ if (draw > 1) {
+ int flag = towards ? 1 : -1;
+ target.x = (Main_Data::game_player->real_x - real_x) * flag;
+ target.y = (Main_Data::game_player->real_y - real_y) * flag;
+ target = c2Add(c2Norm(target), c2V(real_x, real_y));
+ }
+ else {
+ SetDirection(dir);
+ target.x = round(real_x + GetDxFromDirection(dir));
+ target.y = round(real_y + GetDyFromDirection(dir));
+ }
+ SetMoveTowardTarget(target, true);
+ UpdateMoveTowardTarget();
+ }
+ else {
+ int dir = 0;
+ if (!in_sight) {
+ dir = Rand::GetRandomNumber(0, 3);
+ }
+ else {
+ int draw = Rand::GetRandomNumber(0, 9);
+ if (draw == 0) {
+ dir = GetDirection();
+ }
+ else if (draw == 1) {
+ dir = Rand::GetRandomNumber(0, 3);
+ }
+ else {
+ dir = towards
+ ? GetDirectionToCharacter(GetPlayer())
+ : GetDirectionAwayCharacter(GetPlayer());
+ }
+ }
+
+ Move(dir);
+ } // END - PIXEL MOVE
+
+
+
if (IsStopping()) {
if (IsWaitingForegroundExecution() || (GetStopCount() >= GetMaxStopCount() + 60)) {
diff --git a/src/game_event.h b/src/game_event.h
index b2f3dfd07f..60e288f01c 100644
--- a/src/game_event.h
+++ b/src/game_event.h
@@ -46,7 +46,8 @@ class Game_Event : public Game_EventBase {
}
/** Load from saved game */
- void SetSaveData(lcf::rpg::SaveMapEvent save);
+ void SetSaveData(lcf::rpg::SaveMapEvent save);
+
/** @return save game data */
lcf::rpg::SaveMapEvent GetSaveData() const;
diff --git a/src/game_map.cpp b/src/game_map.cpp
index 861b037e86..c44c0d20c0 100644
--- a/src/game_map.cpp
+++ b/src/game_map.cpp
@@ -1,2306 +1,2747 @@
-/*
- * This file is part of EasyRPG Player.
- *
- * EasyRPG Player is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * EasyRPG Player is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with EasyRPG Player. If not, see .
- */
-
-// Headers
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "async_handler.h"
-#include "options.h"
-#include "system.h"
-#include "game_battle.h"
-#include "game_battler.h"
-#include "game_map.h"
-#include "game_interpreter_map.h"
-#include "game_switches.h"
-#include "game_player.h"
-#include "game_party.h"
-#include "game_message.h"
-#include "game_screen.h"
-#include "game_pictures.h"
-#include "game_variables.h"
-#include "scene_battle.h"
-#include "scene_map.h"
-#include
-#include
-#include "map_data.h"
-#include "main_data.h"
-#include "output.h"
-#include "util_macro.h"
-#include "game_system.h"
-#include "filefinder.h"
-#include "player.h"
-#include "input.h"
-#include "utils.h"
-#include "rand.h"
-#include
-#include
-#include "scene_gameover.h"
-#include "feature.h"
-
-namespace {
- // Intended bad value, Game_Map::Init sets them correctly
- int screen_width = -1;
- int screen_height = -1;
-
- lcf::rpg::SaveMapInfo map_info;
- lcf::rpg::SavePanorama panorama;
-
- bool need_refresh;
-
- int animation_type;
- bool animation_fast;
- std::vector passages_down;
- std::vector passages_up;
- std::vector events;
- std::vector common_events;
- std::unique_ptr map_cache;
-
- std::unique_ptr map;
-
- std::unique_ptr interpreter;
- std::vector vehicles;
-
- lcf::rpg::Chipset* chipset;
-
- //FIXME: Find a better way to do this.
- bool panorama_on_map_init = true;
- bool reset_panorama_x_on_next_init = true;
- bool reset_panorama_y_on_next_init = true;
-
- bool translation_changed = false;
-
- // Used when the current map is not in the maptree
- const lcf::rpg::MapInfo empty_map_info;
-}
-
-namespace Game_Map {
-void SetupCommon();
-}
-
-void Game_Map::OnContinueFromBattle() {
- Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeBattleMusic());
-}
-
-static Game_Map::Parallax::Params GetParallaxParams();
-
-void Game_Map::Init() {
- Dispose();
-
- map_info = {};
- panorama = {};
- SetNeedRefresh(true);
-
- interpreter.reset(new Game_Interpreter_Map(true));
- map_cache.reset(new Caching::MapCache());
-
- InitCommonEvents();
-
- vehicles.clear();
- vehicles.emplace_back(Game_Vehicle::Boat);
- vehicles.emplace_back(Game_Vehicle::Ship);
- vehicles.emplace_back(Game_Vehicle::Airship);
-}
-
-void Game_Map::InitCommonEvents() {
- common_events.clear();
- common_events.reserve(lcf::Data::commonevents.size());
- for (const lcf::rpg::CommonEvent& ev : lcf::Data::commonevents) {
- common_events.emplace_back(ev.ID);
- }
- translation_changed = false;
-}
-
-void Game_Map::Dispose() {
- events.clear();
- map.reset();
- map_info = {};
- panorama = {};
-}
-
-void Game_Map::Quit() {
- Dispose();
- common_events.clear();
- interpreter.reset();
- map_cache.reset();
-}
-
-int Game_Map::GetMapSaveCount() {
- return (Player::IsRPG2k3() && map->save_count_2k3e > 0)
- ? map->save_count_2k3e
- : map->save_count;
-}
-
-void Game_Map::Setup(std::unique_ptr map_in) {
- Dispose();
-
- map = std::move(map_in);
-
- SetupCommon();
-
- panorama_on_map_init = true;
- Parallax::ClearChangedBG();
-
- SetEncounterSteps(GetMapInfo().encounter_steps);
- SetChipset(map->chipset_id);
-
- std::iota(map_info.lower_tiles.begin(), map_info.lower_tiles.end(), 0);
- std::iota(map_info.upper_tiles.begin(), map_info.upper_tiles.end(), 0);
-
- // Save allowed
- const auto* current_info = &GetMapInfo();
- int current_index = current_info->ID;
- int can_save = current_info->save;
- int can_escape = current_info->escape;
- int can_teleport = current_info->teleport;
-
- while (can_save == lcf::rpg::MapInfo::TriState_parent
- || can_escape == lcf::rpg::MapInfo::TriState_parent
- || can_teleport == lcf::rpg::MapInfo::TriState_parent)
- {
- const auto* parent_info = &GetParentMapInfo(*current_info);
- int parent_index = parent_info->ID;
- if (parent_index == 0) {
- // If parent is 0 and flag is parent, it's implicitly enabled.
- break;
- }
- if (parent_index == current_index) {
- Output::Warning("Map {} has parent pointing to itself!", current_index);
- break;
- }
- current_info = parent_info;
- if (can_save == lcf::rpg::MapInfo::TriState_parent) {
- can_save = current_info->save;
- }
- if (can_escape == lcf::rpg::MapInfo::TriState_parent) {
- can_escape = current_info->escape;
- }
- if (can_teleport == lcf::rpg::MapInfo::TriState_parent) {
- can_teleport = current_info->teleport;
- }
- }
- Main_Data::game_system->SetAllowSave(can_save != lcf::rpg::MapInfo::TriState_forbid);
- Main_Data::game_system->SetAllowEscape(can_escape != lcf::rpg::MapInfo::TriState_forbid);
- Main_Data::game_system->SetAllowTeleport(can_teleport != lcf::rpg::MapInfo::TriState_forbid);
-
- auto& player = *Main_Data::game_player;
-
- SetPositionX(player.GetX() * SCREEN_TILE_SIZE - player.GetPanX());
- SetPositionY(player.GetY() * SCREEN_TILE_SIZE - player.GetPanY());
-
- // Update the save counts so that if the player saves the game
- // events will properly resume upon loading.
- Main_Data::game_player->UpdateSaveCounts(lcf::Data::system.save_count, GetMapSaveCount());
-}
-
-void Game_Map::SetupFromSave(
- std::unique_ptr map_in,
- lcf::rpg::SaveMapInfo save_map,
- lcf::rpg::SaveVehicleLocation save_boat,
- lcf::rpg::SaveVehicleLocation save_ship,
- lcf::rpg::SaveVehicleLocation save_airship,
- lcf::rpg::SaveEventExecState save_fg_exec,
- lcf::rpg::SavePanorama save_pan,
- std::vector save_ce) {
-
- map = std::move(map_in);
- map_info = std::move(save_map);
- panorama = std::move(save_pan);
-
- SetupCommon();
-
- const bool is_db_save_compat = Main_Data::game_player->IsDatabaseCompatibleWithSave(lcf::Data::system.save_count);
- const bool is_map_save_compat = Main_Data::game_player->IsMapCompatibleWithSave(GetMapSaveCount());
-
- InitCommonEvents();
-
- if (is_db_save_compat && is_map_save_compat) {
- for (size_t i = 0; i < std::min(save_ce.size(), common_events.size()); ++i) {
- common_events[i].SetSaveData(save_ce[i].parallel_event_execstate);
- }
- }
-
- if (is_map_save_compat) {
- std::vector destroyed_event_ids;
-
- for (size_t i = 0, j = 0; i < events.size() && j < map_info.events.size(); ++i) {
- auto& ev = events[i];
- auto& save_ev = map_info.events[j];
- if (ev.GetId() == save_ev.ID) {
- ev.SetSaveData(save_ev);
- ++j;
- } else {
- if (save_ev.ID > ev.GetId()) {
- // assume that the event has been destroyed during gameplay via "DestroyMapEvent"
- destroyed_event_ids.emplace_back(ev.GetId());
- } else {
- Output::Debug("SetupFromSave: Unexpected ID {}/{}", save_ev.ID, ev.GetId());
- }
- }
- }
- for (size_t i = 0; i < destroyed_event_ids.size(); ++i) {
- DestroyMapEvent(destroyed_event_ids[i], true);
- }
- if (destroyed_event_ids.size() > 0) {
- UpdateUnderlyingEventReferences();
- }
- }
-
- // Handle cloned events in a separate loop, regardless of "is_map_save_compat"
- if (Player::HasEasyRpgExtensions()) {
- for (size_t i = 0; i < map_info.events.size(); ++i) {
- auto& save_ev = map_info.events[i];
- bool is_cloned_evt = save_ev.easyrpg_clone_map_id > 0 || save_ev.easyrpg_clone_event_id > 0;
- if (is_cloned_evt && CloneMapEvent(
- save_ev.easyrpg_clone_map_id, save_ev.easyrpg_clone_event_id,
- save_ev.position_x, save_ev.position_y,
- save_ev.ID, "")) { // FIXME: Customized event names for saved events aren't part of liblcf/SaveMapEvent at the moment & thus cannot be restored
- if (auto new_event = GetEvent(save_ev.ID); new_event != nullptr) {
- new_event->SetSaveData(save_ev);
- }
- }
- }
- UpdateUnderlyingEventReferences();
- }
- map_info.events.clear();
- interpreter->Clear();
-
- GetVehicle(Game_Vehicle::Boat)->SetSaveData(std::move(save_boat));
- GetVehicle(Game_Vehicle::Ship)->SetSaveData(std::move(save_ship));
- GetVehicle(Game_Vehicle::Airship)->SetSaveData(std::move(save_airship));
-
- if (is_map_save_compat) {
- // Make main interpreter "busy" if save contained events to prevent auto-events from starting
- interpreter->SetState(std::move(save_fg_exec));
- }
-
- SetEncounterSteps(map_info.encounter_steps);
-
- // RPG_RT bug: Chipset is not loaded. Fixed in 2k3E
- if (Player::IsRPG2k3E()) {
- SetChipset(map_info.chipset_id);
- } else {
- SetChipset(0);
- }
-
- if (!is_map_save_compat) {
- panorama = {};
- }
-
- // We want to support loading rm2k3e panning chunks
- // but also not break other saves which don't have them.
- // To solve this problem, we reuse the scrolling methods
- // which always reset the position anyways when scroll_horz/vert
- // is false.
- // This produces compatible behavior for old RPG_RT saves, namely
- // the pan_x/y is always forced to 0.
- // If the later async code will load panorama, set the flag to not clear the offsets.
- // FIXME: RPG_RT compatibility bug: Everytime we load a savegame with default panorama chunks,
- // this causes them to get overwritten
- // FIXME: RPG_RT compatibility bug: On async platforms, panorama async loading can
- // cause panorama chunks to be out of sync.
- Game_Map::Parallax::ChangeBG(GetParallaxParams());
-}
-
-std::unique_ptr Game_Map::LoadMapFile(int map_id) {
- std::unique_ptr map;
-
- // Attempt to load either the EasyRPG map file or the RPG Maker map file first, depending on config.
- // If it fails, try the other one.
- // FIXME: Assert map was cached for async platforms
- bool map_is_easyrpg_file = Player::player_config.prefer_easyrpg_map_files.Get();
- std::string map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file);
- std::string map_file = FileFinder::Game().FindFile(map_name);
- if (map_file.empty()) {
- map_is_easyrpg_file = !map_is_easyrpg_file;
- map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file);
- map_file = FileFinder::Game().FindFile(map_name);
-
- if (map_file.empty()) {
- Output::Error("Loading of Map {} failed.\nThe map was not found.", map_name);
- return nullptr;
- }
- }
-
- auto map_stream = FileFinder::Game().OpenInputStream(map_file);
- if (!map_stream) {
- Output::Error("Loading of Map {} failed.\nMap not readable.", map_name);
- return nullptr;
- }
-
- if (map_is_easyrpg_file) {
- map = lcf::LMU_Reader::LoadXml(map_stream);
- } else {
- map = lcf::LMU_Reader::Load(map_stream, Player::encoding);
- if (Input::IsRecording()) {
- map_stream.clear();
- map_stream.seekg(0);
- Input::AddRecordingData(Input::RecordingData::Hash,
- fmt::format("map{:04} {:#08x}", map_id, Utils::CRC32(map_stream)));
- }
- }
-
- Output::Debug("Loaded Map {}", map_name);
-
- if (map.get() == NULL) {
- Output::ErrorStr(lcf::LcfReader::GetError());
- }
-
- return map;
-}
-
-void Game_Map::SetupCommon() {
- screen_width = (Player::screen_width / 16.0) * SCREEN_TILE_SIZE;
- screen_height = (Player::screen_height / 16.0) * SCREEN_TILE_SIZE;
-
- if (!Tr::GetCurrentTranslationId().empty()) {
- TranslateMapMessages(GetMapId(), *map);
- }
- SetNeedRefresh(true);
-
- PrintPathToMap();
-
- if (translation_changed) {
- InitCommonEvents();
- }
-
- map_cache->Clear();
-
- CreateMapEvents();
-}
-
-void Game_Map::CreateMapEvents() {
- events.reserve(map->events.size());
- for (auto& ev : map->events) {
- events.emplace_back(GetMapId(), &ev);
- AddEventToCache(ev);
- }
-}
-
-void Game_Map::AddEventToCache(const lcf::rpg::Event& ev) {
- using Op = Caching::ObservedVarOps;
-
- for (const auto& pg : ev.pages) {
- if (pg.condition.flags.switch_a) {
- map_cache->AddEventAsRefreshTarget(pg.condition.switch_a_id, ev);
- }
- if (pg.condition.flags.switch_b) {
- map_cache->AddEventAsRefreshTarget(pg.condition.switch_b_id, ev);
- }
- if (pg.condition.flags.variable) {
- map_cache->AddEventAsRefreshTarget(pg.condition.variable_id, ev);
- }
- }
-}
-
-void Game_Map::RemoveEventFromCache(const lcf::rpg::Event& ev) {
- using Op = Caching::ObservedVarOps;
-
- for (const auto& pg : ev.pages) {
- if (pg.condition.flags.switch_a) {
- map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_a_id, ev);
- }
- if (pg.condition.flags.switch_b) {
- map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_b_id, ev);
- }
- if (pg.condition.flags.variable) {
- map_cache->RemoveEventAsRefreshTarget(pg.condition.variable_id, ev);
- }
- }
-}
-
-void Game_Map::Caching::MapCache::Clear() {
- for (int i = 0; i < static_cast(ObservedVarOps_END); i++) {
- refresh_targets_by_varid[i].clear();
- }
-}
-
-bool Game_Map::CloneMapEvent(int src_map_id, int src_event_id, int target_x, int target_y, int target_event_id, std::string_view target_name) {
- std::unique_ptr source_map_storage;
- const lcf::rpg::Map* source_map;
-
- if (src_map_id == GetMapId()) {
- source_map = &GetMap();
- } else {
- source_map_storage = Game_Map::LoadMapFile(src_map_id);
- source_map = source_map_storage.get();
-
- if (source_map_storage == nullptr) {
- Output::Warning("CloneMapEvent: Invalid source map ID {}", src_map_id);
- return false;
- }
-
- if (!Tr::GetCurrentTranslationId().empty()) {
- TranslateMapMessages(src_map_id, *source_map_storage);
- }
- }
-
- const lcf::rpg::Event* source_event = FindEventById(source_map->events, src_event_id);
- if (source_event == nullptr) {
- Output::Warning("CloneMapEvent: Event ID {} not found on source map {}", src_event_id, src_map_id);
- return false;
- }
-
- lcf::rpg::Event new_event = *source_event;
- if (target_event_id > 0) {
- DestroyMapEvent(target_event_id, true);
- new_event.ID = target_event_id;
- } else {
- new_event.ID = GetNextAvailableEventId();
- }
- new_event.x = target_x;
- new_event.y = target_y;
-
- if (!target_name.empty()) {
- new_event.name = lcf::DBString(target_name);
- }
-
- // sorted insert
- auto insert_it = map->events.insert(
- std::upper_bound(map->events.begin(), map->events.end(), new_event, [](const auto& e, const auto& e2) {
- return e.ID < e2.ID;
- }), new_event);
-
- auto game_event = Game_Event(GetMapId(), &*insert_it);
- game_event.data()->easyrpg_clone_event_id = src_event_id;
- game_event.data()->easyrpg_clone_map_id = src_map_id;
-
- events.insert(
- std::upper_bound(events.begin(), events.end(), game_event, [](const auto& e, const auto& e2) {
- return e.GetId() < e2.GetId();
- }), std::move(game_event));
-
- UpdateUnderlyingEventReferences();
-
- AddEventToCache(new_event);
-
- Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
- if (scene) {
- scene->spriteset->Refresh();
- SetNeedRefresh(true);
- }
-
- return true;
-}
-
-bool Game_Map::DestroyMapEvent(const int event_id, bool from_clone) {
- const lcf::rpg::Event* event = FindEventById(map->events, event_id);
-
- if (event == nullptr) {
- if (!from_clone) {
- Output::Warning("DestroyMapEvent: Event ID {} not found on current map", event_id);
- }
- return true;
- }
-
- // Remove event from cache
- RemoveEventFromCache(*event);
-
- // Remove event from events vector
- for (auto it = events.begin(); it != events.end(); ++it) {
- if (it->GetId() == event_id) {
- events.erase(it);
- break;
- }
- }
-
- // Remove event from map
- for (auto it = map->events.begin(); it != map->events.end(); ++it) {
- if (it->ID == event_id) {
- map->events.erase(it);
- break;
- }
- }
-
- if (!from_clone) {
- UpdateUnderlyingEventReferences();
-
- Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
- scene->spriteset->Refresh();
- SetNeedRefresh(true);
- }
-
- if (GetInterpreter().GetOriginalEventId() == event_id) {
- // Prevent triggering "invalid event on stack" sanity check
- GetInterpreter().ClearOriginalEventId();
- }
-
- return true;
-}
-
-void Game_Map::TranslateMapMessages(int mapId, lcf::rpg::Map& map) {
- std::stringstream ss;
- ss << "map" << std::setfill('0') << std::setw(4) << mapId << ".po";
- Player::translation.RewriteMapMessages(ss.str(), map);
-}
-
-
-void Game_Map::UpdateUnderlyingEventReferences() {
- // Update references because modifying the vector can reallocate
- size_t idx = 0;
- for (auto& ev : events) {
- ev.SetUnderlyingEvent(&map->events.at(idx++));
- }
-
- Main_Data::game_screen->UpdateUnderlyingEventReferences();
-}
-
-const lcf::rpg::Event* Game_Map::FindEventById(const std::vector& events, int eventId) {
- for (const auto& ev : events) {
- if (ev.ID == eventId) {
- return &ev;
- }
- }
- return nullptr;
-}
-
-int Game_Map::GetNextAvailableEventId() {
- if (map->events.empty()) {
- return 1;
- } else {
- return map->events.back().ID + 1;
- }
-}
-
-void Game_Map::PrepareSave(lcf::rpg::Save& save) {
- save.foreground_event_execstate = interpreter->GetSaveState();
-
- save.airship_location = GetVehicle(Game_Vehicle::Airship)->GetSaveData();
- save.ship_location = GetVehicle(Game_Vehicle::Ship)->GetSaveData();
- save.boat_location = GetVehicle(Game_Vehicle::Boat)->GetSaveData();
-
- save.map_info = map_info;
- save.map_info.chipset_id = GetChipset();
- if (save.map_info.chipset_id == GetOriginalChipset()) {
- // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset.
- save.map_info.chipset_id = 0;
- }
- if (save.map_info.encounter_steps == GetOriginalEncounterSteps()) {
- save.map_info.encounter_steps = -1;
- }
- // Note: RPG_RT does not use a sentinel for parallax parameters. Once the parallax BG is changed, it stays that way forever.
-
- save.map_info.events.clear();
- save.map_info.events.reserve(events.size());
- for (Game_Event& ev : events) {
- save.map_info.events.push_back(ev.GetSaveData());
- }
-
- save.panorama = panorama;
-
- save.common_events.clear();
- save.common_events.reserve(common_events.size());
- for (Game_CommonEvent& ev : common_events) {
- save.common_events.push_back(lcf::rpg::SaveCommonEvent());
- save.common_events.back().ID = ev.GetIndex();
- save.common_events.back().parallel_event_execstate = ev.GetSaveData();
- }
-}
-
-void Game_Map::PlayBgm() {
- const auto* current_info = &GetMapInfo();
- while (current_info->music_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) {
- current_info = &GetParentMapInfo(*current_info);
- }
-
- if ((current_info->ID > 0) && !current_info->music.name.empty()) {
- if (current_info->music_type == 1) {
- return;
- }
- auto& music = current_info->music;
- if (!Main_Data::game_player->IsAboard()) {
- Main_Data::game_system->BgmPlay(music);
- } else {
- Main_Data::game_system->SetBeforeVehicleMusic(music);
- }
- }
-}
-
-std::vector Game_Map::GetTilesLayer(int layer) {
- return layer >= 1 ? map_info.upper_tiles : map_info.lower_tiles;
-}
-
-void Game_Map::Refresh() {
- if (GetMapId() > 0) {
- for (Game_Event& ev : events) {
- ev.RefreshPage();
- }
- }
-
- need_refresh = false;
-}
-
-Game_Interpreter_Map& Game_Map::GetInterpreter() {
- assert(interpreter);
- return *interpreter;
-}
-
-void Game_Map::Scroll(int dx, int dy) {
- int x = map_info.position_x;
- AddScreenX(x, dx);
- map_info.position_x = x;
-
- int y = map_info.position_y;
- AddScreenY(y, dy);
- map_info.position_y = y;
-
- if (dx == 0 && dy == 0) {
- return;
- }
-
- Main_Data::game_screen->OnMapScrolled(dx, dy);
- Main_Data::game_pictures->OnMapScrolled(dx, dy);
- Game_Map::Parallax::ScrollRight(dx);
- Game_Map::Parallax::ScrollDown(dy);
-}
-
-// Add inc to acc, clamping the result into the range [low, high].
-// If the result is clamped, inc is also modified to be actual amount
-// that acc changed by.
-static void ClampingAdd(int low, int high, int& acc, int& inc) {
- int original_acc = acc;
- // Do not use std::clamp here. When the map is smaller than the screen the
- // upper bound is smaller than the lower bound making the function fail.
- acc = std::max(low, std::min(high, acc + inc));
- inc = acc - original_acc;
-}
-
-void Game_Map::AddScreenX(int& screen_x, int& inc) {
- int map_width = GetTilesX() * SCREEN_TILE_SIZE;
- if (LoopHorizontal()) {
- screen_x = (screen_x + inc) % map_width;
- } else {
- ClampingAdd(0, map_width - screen_width, screen_x, inc);
- }
-}
-
-void Game_Map::AddScreenY(int& screen_y, int& inc) {
- int map_height = GetTilesY() * SCREEN_TILE_SIZE;
- if (LoopVertical()) {
- screen_y = (screen_y + inc) % map_height;
- } else {
- ClampingAdd(0, map_height - screen_height, screen_y, inc);
- }
-}
-
-bool Game_Map::IsValid(int x, int y) {
- return (x >= 0 && x < GetTilesX() && y >= 0 && y < GetTilesY());
-}
-
-static int GetPassableMask(int old_x, int old_y, int new_x, int new_y) {
- int bit = 0;
- if (new_x > old_x) { bit |= Passable::Right; }
- if (new_x < old_x) { bit |= Passable::Left; }
- if (new_y > old_y) { bit |= Passable::Down; }
- if (new_y < old_y) { bit |= Passable::Up; }
- return bit;
-}
-
-static bool WouldCollide(const Game_Character& self, const Game_Character& other, bool self_conflict) {
- if (self.GetThrough() || other.GetThrough()) {
- return false;
- }
-
- if (self.IsFlying() || other.IsFlying()) {
- return false;
- }
-
- if (!self.IsActive() || !other.IsActive()) {
- return false;
- }
-
- if (self.GetType() == Game_Character::Event
- && other.GetType() == Game_Character::Event
- && (self.IsOverlapForbidden() || other.IsOverlapForbidden())) {
- return true;
- }
-
- if (other.GetLayer() == lcf::rpg::EventPage::Layers_same && self_conflict) {
- return true;
- }
-
- if (self.GetLayer() == other.GetLayer()) {
- return true;
- }
-
- return false;
-}
-
-template
-static void MakeWayUpdate(T& other) {
- other.Update();
-}
-
-static void MakeWayUpdate(Game_Event& other) {
- other.Update(false);
-}
-
-template
-static bool CheckWayTestCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) {
- if (&self == &other) {
- return false;
- }
-
- if (!other.IsInPosition(x, y)) {
- return false;
- }
-
- return WouldCollide(self, other, self_conflict);
-}
-
-template
-static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) {
- if (&self == &other) {
- return false;
- }
-
- if (!other.IsInPosition(x, y)) {
- return false;
- }
-
- // Force the other event to update, allowing them to possibly move out of the way.
- MakeWayUpdate(other);
-
- if (!other.IsInPosition(x, y)) {
- return false;
- }
-
- return WouldCollide(self, other, self_conflict);
-}
-
-static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) {
- if (ch && ch->GetType() == Game_Character::Vehicle) {
- return static_cast(static_cast(ch)->GetVehicleType());
- }
- return Game_Vehicle::None;
-}
-
-bool Game_Map::CheckWay(const Game_Character& self,
- int from_x, int from_y,
- int to_x, int to_y
- )
-{
- return CheckOrMakeWayEx(
- self, from_x, from_y, to_x, to_y, true, {}, false
- );
-}
-
-bool Game_Map::CheckWay(const Game_Character& self,
- int from_x, int from_y,
- int to_x, int to_y,
- bool check_events_and_vehicles,
- Span ignore_some_events_by_id) {
- return CheckOrMakeWayEx(
- self, from_x, from_y, to_x, to_y,
- check_events_and_vehicles,
- ignore_some_events_by_id, false
- );
-}
-
-bool Game_Map::CheckOrMakeWayEx(const Game_Character& self,
- int from_x, int from_y,
- int to_x, int to_y,
- bool check_events_and_vehicles,
- Span ignore_some_events_by_id,
- bool make_way
- )
-{
- // Infer directions before we do any rounding.
- const int bit_from = GetPassableMask(from_x, from_y, to_x, to_y);
- const int bit_to = GetPassableMask(to_x, to_y, from_x, from_y);
-
- // Now round for looping maps.
- to_x = Game_Map::RoundX(to_x);
- to_y = Game_Map::RoundY(to_y);
-
- // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first!
- if (!Game_Map::IsValid(to_x, to_y)) {
- return false;
- }
-
- if (self.GetThrough()) {
- return true;
- }
-
- const auto vehicle_type = GetCollisionVehicleType(&self);
- bool self_conflict = false;
-
- // Depending on whether we're supposed to call MakeWayCollideEvent
- // (which might change the map) or not, choose what to call:
- auto CheckOrMakeCollideEvent = [&](auto& other) {
- if (make_way) {
- return MakeWayCollideEvent(to_x, to_y, self, other, self_conflict);
- } else {
- return CheckWayTestCollideEvent(
- to_x, to_y, self, other, self_conflict
- );
- }
- };
-
- if (!self.IsJumping()) {
- // Check for self conflict.
- // If this event has a tile graphic and the tile itself has passage blocked in the direction
- // we want to move, flag it as "self conflicting" for use later.
- if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) {
- int tile_id = self.GetTileId();
- if ((passages_up[tile_id] & bit_from) == 0) {
- self_conflict = true;
- }
- }
-
- if (vehicle_type == Game_Vehicle::None) {
- // Check that we are allowed to step off of the current tile.
- // Note: Vehicles can always step off a tile.
-
- // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event.
- // Round it for looping maps to ensure the check passes
- // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving
- // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it
- // inbounds after the first move.
- from_x = Game_Map::RoundX(from_x);
- from_y = Game_Map::RoundY(from_y);
- if (!IsPassableTile(&self, bit_from, from_x, from_y)) {
- return false;
- }
- }
- }
- if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) {
- // Check for collision with events on the target tile.
- if (ignore_some_events_by_id.empty()) {
- for (auto& other: GetEvents()) {
- if (CheckOrMakeCollideEvent(other)) {
- return false;
- }
- }
- } else {
- for (auto& other: GetEvents()) {
- if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end())
- continue;
- if (CheckOrMakeCollideEvent(other)) {
- return false;
- }
- }
- }
-
- auto& player = Main_Data::game_player;
- if (player->GetVehicleType() == Game_Vehicle::None) {
- if (CheckOrMakeCollideEvent(*Main_Data::game_player)) {
- return false;
- }
- }
- for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship}) {
- auto& other = vehicles[vid - 1];
- if (other.IsInCurrentMap()) {
- if (CheckOrMakeCollideEvent(other)) {
- return false;
- }
- }
- }
- auto& airship = vehicles[Game_Vehicle::Airship - 1];
- if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) {
- if (CheckOrMakeCollideEvent(airship)) {
- return false;
- }
- }
- }
- int bit = bit_to;
- if (self.IsJumping()) {
- bit = Passable::Down | Passable::Up | Passable::Left | Passable::Right;
- }
-
- return IsPassableTile(
- &self, bit, to_x, to_y, check_events_and_vehicles, true
- );
-}
-
-bool Game_Map::MakeWay(const Game_Character& self,
- int from_x, int from_y,
- int to_x, int to_y
- )
-{
- return CheckOrMakeWayEx(
- self, from_x, from_y, to_x, to_y, true, {}, true
- );
-}
-
-
-bool Game_Map::CanLandAirship(int x, int y) {
- if (!Game_Map::IsValid(x, y)) return false;
-
- const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
- if (!terrain) {
- Output::Warning("CanLandAirship: Invalid terrain at ({}, {})", x, y);
- return false;
- }
- if (!terrain->airship_land) {
- return false;
- }
-
- for (auto& ev: events) {
- if (ev.IsInPosition(x, y)
- && ev.IsActive()
- && ev.GetActivePage() != nullptr) {
- return false;
- }
- }
- for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship }) {
- auto& vehicle = vehicles[vid - 1];
- if (vehicle.IsInCurrentMap() && vehicle.IsInPosition(x, y)) {
- return false;
- }
- }
-
- const int bit = Passable::Down | Passable::Right | Passable::Left | Passable::Up;
-
- int tile_index = x + y * GetTilesX();
-
- if (!IsPassableLowerTile(bit, tile_index)) {
- return false;
- }
-
- int tile_id = map->upper_layer[tile_index] - BLOCK_F;
- tile_id = map_info.upper_tiles[tile_id];
-
- return (passages_up[tile_id] & bit) != 0;
-}
-
-bool Game_Map::CanEmbarkShip(Game_Player& player, int x, int y) {
- auto bit = GetPassableMask(player.GetX(), player.GetY(), x, y);
- return IsPassableTile(&player, bit, player.GetX(), player.GetY());
-}
-
-bool Game_Map::CanDisembarkShip(Game_Player& player, int x, int y) {
- if (!Game_Map::IsValid(x, y)) {
- return false;
- }
-
- for (auto& ev: GetEvents()) {
- if (ev.IsInPosition(x, y)
- && ev.GetLayer() == lcf::rpg::EventPage::Layers_same
- && ev.IsActive()
- && ev.GetActivePage() != nullptr) {
- return false;
- }
- }
-
- int bit = GetPassableMask(x, y, player.GetX(), player.GetY());
-
- return IsPassableTile(nullptr, bit, x, y);
-}
-
-bool Game_Map::IsPassableLowerTile(int bit, int tile_index) {
- int tile_raw_id = map->lower_layer[tile_index];
- int tile_id = 0;
-
- if (tile_raw_id >= BLOCK_E) {
- tile_id = tile_raw_id - BLOCK_E;
- tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX;
-
- } else if (tile_raw_id >= BLOCK_D) {
- tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX;
- int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE;
-
- if (((passages_down[tile_id] & Passable::Wall) != 0) && (
- (autotile_id >= 20 && autotile_id <= 23) ||
- (autotile_id >= 33 && autotile_id <= 37) ||
- autotile_id == 42 || autotile_id == 43 ||
- autotile_id == 45 || autotile_id == 46))
- return true;
-
- } else if (tile_raw_id >= BLOCK_C) {
- tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX;
-
- } else if (map->lower_layer[tile_index] < BLOCK_C) {
- tile_id = tile_raw_id / BLOCK_B_STRIDE;
- }
-
- return (passages_down[tile_id] & bit) != 0;
-}
-
-bool Game_Map::IsPassableTile(
- const Game_Character* self, int bit, int x, int y
- ) {
- return IsPassableTile(
- self, bit, x, y, true, true
- );
-}
-
-bool Game_Map::IsPassableTile(
- const Game_Character* self, int bit, int x, int y,
- bool check_events_and_vehicles, bool check_map_geometry
- ) {
- if (!IsValid(x, y)) return false;
-
- const auto vehicle_type = GetCollisionVehicleType(self);
- if (check_events_and_vehicles) {
- if (vehicle_type != Game_Vehicle::None) {
- const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
- if (!terrain) {
- Output::Warning("IsPassableTile: Invalid terrain at ({}, {})", x, y);
- return false;
- }
- if (vehicle_type == Game_Vehicle::Boat && !terrain->boat_pass) {
- return false;
- }
- if (vehicle_type == Game_Vehicle::Ship && !terrain->ship_pass) {
- return false;
- }
- if (vehicle_type == Game_Vehicle::Airship) {
- return terrain->airship_pass;
- }
- }
-
- // Highest ID event with layer=below, not through, and a tile graphic wins.
- int event_tile_id = 0;
- for (auto& ev: events) {
- if (self == &ev) {
- continue;
- }
- if (!ev.IsActive() || ev.GetActivePage() == nullptr || ev.GetThrough()) {
- continue;
- }
- if (ev.IsInPosition(x, y) && ev.GetLayer() == lcf::rpg::EventPage::Layers_below) {
- if (ev.HasTileSprite()) {
- event_tile_id = ev.GetTileId();
- }
- }
- }
-
- // If there was a below tile event, and the tile is not above
- // Override the chipset with event tile behavior.
- if (event_tile_id > 0
- && ((passages_up[event_tile_id] & Passable::Above) == 0)) {
- switch (vehicle_type) {
- case Game_Vehicle::None:
- return ((passages_up[event_tile_id] & bit) != 0);
- case Game_Vehicle::Boat:
- case Game_Vehicle::Ship:
- return false;
- case Game_Vehicle::Airship:
- break;
- };
- }
- }
-
- if (check_map_geometry) {
- int tile_index = x + y * GetTilesX();
- int tile_id = map->upper_layer[tile_index] - BLOCK_F;
- tile_id = map_info.upper_tiles[tile_id];
-
- if (vehicle_type == Game_Vehicle::Boat || vehicle_type == Game_Vehicle::Ship) {
- if ((passages_up[tile_id] & Passable::Above) == 0)
- return false;
- return true;
- }
-
- if ((passages_up[tile_id] & bit) == 0)
- return false;
-
- if ((passages_up[tile_id] & Passable::Above) == 0)
- return true;
-
- return IsPassableLowerTile(bit, tile_index);
- } else {
- return true;
- }
-}
-
-int Game_Map::GetBushDepth(int x, int y) {
- if (!Game_Map::IsValid(x, y)) return 0;
-
- const lcf::rpg::Terrain* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x,y));
- if (!terrain) {
- Output::Warning("GetBushDepth: Invalid terrain at ({}, {})", x, y);
- return 0;
- }
- return terrain->bush_depth;
-}
-
-bool Game_Map::IsCounter(int x, int y) {
- if (!Game_Map::IsValid(x, y)) return false;
-
- int const tile_id = map->upper_layer[x + y * GetTilesX()];
- if (tile_id < BLOCK_F) return false;
- int const index = map_info.upper_tiles[tile_id - BLOCK_F];
- return !!(passages_up[index] & Passable::Counter);
-}
-
-int Game_Map::GetTerrainTag(int x, int y) {
- if (!chipset) {
- // FIXME: Is this ever possible?
- return 1;
- }
-
- auto& terrain_data = chipset->terrain_data;
-
- if (terrain_data.empty()) {
- // RPG_RT optimisation: When the terrain is all 1, no terrain data is stored
- return 1;
- }
-
- // Terrain tag wraps on looping maps
- if (Game_Map::LoopHorizontal()) {
- x = RoundX(x);
- }
- if (Game_Map::LoopVertical()) {
- y = RoundY(y);
- }
-
- // RPG_RT always uses the terrain of the first lower tile
- // for out of bounds coordinates.
- unsigned chip_index = 0;
-
- if (Game_Map::IsValid(x, y)) {
- const auto chip_id = map->lower_layer[x + y * GetTilesX()];
- chip_index = ChipIdToIndex(chip_id);
-
- // Apply tile substitution
- if (chip_index >= BLOCK_E_INDEX && chip_index < NUM_LOWER_TILES) {
- chip_index = map_info.lower_tiles[chip_index - BLOCK_E_INDEX] + BLOCK_E_INDEX;
- }
- }
-
- assert(chip_index < terrain_data.size());
-
- return terrain_data[chip_index];
-}
-
-Game_Event* Game_Map::GetEventAt(int x, int y, bool require_active) {
- auto& events = GetEvents();
- for (auto iter = events.rbegin(); iter != events.rend(); ++iter) {
- auto& ev = *iter;
- if (ev.IsInPosition(x, y) && (!require_active || ev.IsActive())) {
- return &ev;
- }
- }
- return nullptr;
-}
-
-bool Game_Map::LoopHorizontal() {
- return map->scroll_type == lcf::rpg::Map::ScrollType_horizontal || map->scroll_type == lcf::rpg::Map::ScrollType_both;
-}
-
-bool Game_Map::LoopVertical() {
- return map->scroll_type == lcf::rpg::Map::ScrollType_vertical || map->scroll_type == lcf::rpg::Map::ScrollType_both;
-}
-
-int Game_Map::RoundX(int x, int units) {
- if (LoopHorizontal()) {
- return Utils::PositiveModulo(x, GetTilesX() * units);
- } else {
- return x;
- }
-}
-
-int Game_Map::RoundY(int y, int units) {
- if (LoopVertical()) {
- return Utils::PositiveModulo(y, GetTilesY() * units);
- } else {
- return y;
- }
-}
-
-int Game_Map::RoundDx(int dx, int units) {
- if (LoopHorizontal()) {
- return Utils::PositiveModulo(std::abs(dx), GetTilesX() * units) * Utils::Sign(dx);
- } else {
- return dx;
- }
-}
-
-int Game_Map::RoundDy(int dy, int units) {
- if (LoopVertical()) {
- return Utils::PositiveModulo(std::abs(dy), GetTilesY() * units) * Utils::Sign(dy);
- } else {
- return dy;
- }
-}
-
-int Game_Map::XwithDirection(int x, int direction) {
- return RoundX(x + (direction == lcf::rpg::EventPage::Direction_right ? 1 : direction == lcf::rpg::EventPage::Direction_left ? -1 : 0));
-}
-
-int Game_Map::YwithDirection(int y, int direction) {
- return RoundY(y + (direction == lcf::rpg::EventPage::Direction_down ? 1 : direction == lcf::rpg::EventPage::Direction_up ? -1 : 0));
-}
-
-int Game_Map::CheckEvent(int x, int y) {
- for (const Game_Event& ev : events) {
- if (ev.IsInPosition(x, y)) {
- return ev.GetId();
- }
- }
-
- return 0;
-}
-
-void Game_Map::Update(MapUpdateAsyncContext& actx, bool is_preupdate) {
- if (GetNeedRefresh()) {
- Refresh();
- }
-
- if (!actx.IsActive()) {
- //If not resuming from async op ...
- UpdateProcessedFlags(is_preupdate);
- }
-
- if (!actx.IsActive() || actx.IsParallelCommonEvent()) {
- if (!UpdateCommonEvents(actx)) {
- // Suspend due to common event async op ...
- return;
- }
- }
-
- if (!actx.IsActive() || actx.IsParallelMapEvent()) {
- if (!UpdateMapEvents(actx)) {
- // Suspend due to map event async op ...
- return;
- }
- }
-
- if (is_preupdate) {
- return;
- }
-
- if (!actx.IsActive()) {
- //If not resuming from async op ...
- Main_Data::game_player->Update();
-
- for (auto& vehicle: vehicles) {
- if (vehicle.GetMapId() == GetMapId()) {
- vehicle.Update();
- }
- }
- }
-
- if (!actx.IsActive() || actx.IsMessage()) {
- if (!UpdateMessage(actx)) {
- // Suspend due to message async op ...
- return;
- }
- }
-
- if (!actx.IsActive()) {
- Main_Data::game_party->UpdateTimers();
- Main_Data::game_screen->Update();
- Main_Data::game_pictures->Update(false);
- }
-
- if (!actx.IsActive() || actx.IsForegroundEvent()) {
- if (!UpdateForegroundEvents(actx)) {
- // Suspend due to foreground event async op ...
- return;
- }
- }
-
- Parallax::Update();
-
- actx = {};
-}
-
-void Game_Map::UpdateProcessedFlags(bool is_preupdate) {
- for (Game_Event& ev : events) {
- ev.SetProcessed(false);
- }
- if (!is_preupdate) {
- Main_Data::game_player->SetProcessed(false);
- for (auto& vehicle: vehicles) {
- if (vehicle.IsInCurrentMap()) {
- vehicle.SetProcessed(false);
- }
- }
- }
-}
-
-
-bool Game_Map::UpdateCommonEvents(MapUpdateAsyncContext& actx) {
- int resume_ce = actx.GetParallelCommonEvent();
-
- for (Game_CommonEvent& ev : common_events) {
- bool resume_async = false;
- if (resume_ce != 0) {
- // If resuming, skip all until the event to resume from ..
- if (ev.GetIndex() != resume_ce) {
- continue;
- } else {
- resume_ce = 0;
- resume_async = true;
- }
- }
-
- auto aop = ev.Update(resume_async);
- if (aop.IsActive()) {
- // Suspend due to this event ..
- actx = MapUpdateAsyncContext::FromCommonEvent(ev.GetIndex(), aop);
- return false;
- }
- }
-
- actx = {};
- return true;
-}
-
-bool Game_Map::UpdateMapEvents(MapUpdateAsyncContext& actx) {
- int resume_ev = actx.GetParallelMapEvent();
-
- for (Game_Event& ev : events) {
- bool resume_async = false;
- if (resume_ev != 0) {
- // If resuming, skip all until the event to resume from ..
- if (ev.GetId() != resume_ev) {
- continue;
- } else {
- resume_ev = 0;
- resume_async = true;
- }
- }
-
- auto aop = ev.Update(resume_async);
- if (aop.IsActive()) {
- // Suspend due to this event ..
- actx = MapUpdateAsyncContext::FromMapEvent(ev.GetId(), aop);
- return false;
- }
- }
-
- actx = {};
- return true;
-}
-
-bool Game_Map::UpdateMessage(MapUpdateAsyncContext& actx) {
- // Message system does not support suspend and resume internally. So if the last frame the message
- // produced an async event, the message loop finished completely. Therefore this frame we should
- // resume *after* the message and not run it again.
- if (!actx.IsActive()) {
- auto aop = Game_Message::Update();
- if (aop.IsActive()) {
- actx = MapUpdateAsyncContext::FromMessage(aop);
- return false;
- }
- }
-
- actx = {};
- return true;
-}
-
-bool Game_Map::UpdateForegroundEvents(MapUpdateAsyncContext& actx) {
- auto& interp = GetInterpreter();
-
- // If we resume from async op, we don't clear the loop index.
- const bool resume_fg = actx.IsForegroundEvent();
-
- // Run any event loaded from last frame.
- interp.Update(!resume_fg);
- if (interp.IsAsyncPending()) {
- // Suspend due to this event ..
- actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp());
- return false;
- }
-
- while (!interp.IsRunning() && !interp.ReachedLoopLimit()) {
- interp.Clear();
-
- // This logic is probably one big loop in RPG_RT. We have to replicate
- // it here because once we stop executing from this we should not
- // clear anymore waiting flags.
- if (Scene::instance->HasRequestedScene() && interp.GetLoopCount() > 0) {
- break;
- }
- Game_CommonEvent* run_ce = nullptr;
-
- for (auto& ce: common_events) {
- if (ce.IsWaitingForegroundExecution()) {
- run_ce = &ce;
- break;
- }
- }
- if (run_ce) {
- interp.Push(run_ce);
- }
-
- Game_Event* run_ev = nullptr;
- for (auto& ev: events) {
- if (ev.IsWaitingForegroundExecution()) {
- if (!ev.IsActive()) {
- ev.ClearWaitingForegroundExecution();
- continue;
- }
- run_ev = &ev;
- break;
- }
- }
- if (run_ev) {
- if (run_ev->WasStartedByDecisionKey()) {
- interp.Push(run_ev);
- } else {
- switch (run_ev->GetTrigger()) {
- case lcf::rpg::EventPage::Trigger_touched:
- interp.Push(run_ev);
- break;
- case lcf::rpg::EventPage::Trigger_collision:
- interp.Push(run_ev);
- break;
- case lcf::rpg::EventPage::Trigger_auto_start:
- interp.Push(run_ev);
- break;
- case lcf::rpg::EventPage::Trigger_action:
- default:
- interp.Push(run_ev);
- break;
- }
- }
- run_ev->ClearWaitingForegroundExecution();
- }
-
- // If no events to run we're finished.
- if (!interp.IsRunning()) {
- break;
- }
-
- interp.Update(false);
- if (interp.IsAsyncPending()) {
- // Suspend due to this event ..
- actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp());
- return false;
- }
- }
-
- actx = {};
- return true;
-}
-
-lcf::rpg::MapInfo const& Game_Map::GetMapInfo() {
- return GetMapInfo(GetMapId());
-}
-
-lcf::rpg::MapInfo const& Game_Map::GetMapInfo(int map_id) {
- for (const auto& mi: lcf::Data::treemap.maps) {
- if (mi.ID == map_id) {
- return mi;
- }
- }
-
- Output::Debug("Map {} not in Maptree", map_id);
- return empty_map_info;
-}
-
-const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo() {
- return GetParentMapInfo(GetMapInfo());
-}
-
-const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo(const lcf::rpg::MapInfo& map_info) {
- return GetMapInfo(map_info.parent_map);
-}
-
-lcf::rpg::Map const& Game_Map::GetMap() {
- return *map;
-}
-
-int Game_Map::GetMapId() {
- return Main_Data::game_player->GetMapId();
-}
-
-void Game_Map::PrintPathToMap() {
- const auto* current_info = &GetMapInfo();
- std::ostringstream ss;
- ss << current_info->name;
-
- current_info = &GetParentMapInfo(*current_info);
- while (current_info->ID != 0 && current_info->ID != GetMapId()) {
- ss << " < " << current_info->name;
- current_info = &GetParentMapInfo(*current_info);
- }
-
- Output::Debug("Tree: {}", ss.str());
-}
-
-int Game_Map::GetTilesX() {
- return map->width;
-}
-
-int Game_Map::GetTilesY() {
- return map->height;
-}
-
-int Game_Map::GetOriginalEncounterSteps() {
- return GetMapInfo().encounter_steps;
-}
-
-int Game_Map::GetEncounterSteps() {
- return map_info.encounter_steps;
-}
-
-void Game_Map::SetEncounterSteps(int step) {
- if (step < 0) {
- step = GetOriginalEncounterSteps();
- }
- map_info.encounter_steps = step;
-}
-
-std::vector Game_Map::GetEncountersAt(int x, int y) {
- int terrain_tag = GetTerrainTag(Main_Data::game_player->GetX(), Main_Data::game_player->GetY());
-
- std::function is_acceptable = [=](int troop_id) {
- const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id);
- if (!troop) {
- Output::Warning("GetEncountersAt: Invalid troop ID {} in encounter list", troop_id);
- return false;
- }
-
- const auto& terrain_set = troop->terrain_set;
-
- // RPG_RT optimisation: Omitted entries are the default value (true)
- return terrain_set.size() <= (unsigned)(terrain_tag - 1) ||
- terrain_set[terrain_tag - 1];
- };
-
- std::vector out;
-
- for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) {
- lcf::rpg::MapInfo& map = lcf::Data::treemap.maps[i];
-
- if (map.ID == GetMapId()) {
- for (const auto& enc : map.encounters) {
- if (is_acceptable(enc.troop_id)) {
- out.push_back(enc.troop_id);
- }
- }
- } else if (map.parent_map == GetMapId() && map.type == lcf::rpg::TreeMap::MapType_area) {
- // Area
- Rect area_rect(map.area_rect.l, map.area_rect.t, map.area_rect.r - map.area_rect.l, map.area_rect.b - map.area_rect.t);
- Rect player_rect(x, y, 1, 1);
-
- if (!player_rect.IsOutOfBounds(area_rect)) {
- for (const lcf::rpg::Encounter& enc : map.encounters) {
- if (is_acceptable(enc.troop_id)) {
- out.push_back(enc.troop_id);
- }
- }
- }
- }
- }
-
- return out;
-}
-
-static void OnEncounterEnd(BattleResult result) {
- if (result != BattleResult::Defeat) {
- return;
- }
-
- if (!Game_Battle::HasDeathHandler()) {
- Scene::Push(std::make_shared());
- return;
- }
-
- //2k3 death handler
-
- auto* ce = lcf::ReaderUtil::GetElement(common_events, Game_Battle::GetDeathHandlerCommonEvent());
- if (ce) {
- auto& interp = Game_Map::GetInterpreter();
- interp.Push(ce);
- }
-
- auto tt = Game_Battle::GetDeathHandlerTeleport();
- if (tt.IsActive()) {
- Main_Data::game_player->ReserveTeleport(tt.GetMapId(), tt.GetX(), tt.GetY(), tt.GetDirection(), tt.GetType());
- }
-}
-
-bool Game_Map::PrepareEncounter(BattleArgs& args) {
- int x = Main_Data::game_player->GetX();
- int y = Main_Data::game_player->GetY();
-
- std::vector encounters = GetEncountersAt(x, y);
-
- if (encounters.empty()) {
- // No enemies on this map :(
- return false;
- }
-
- args.troop_id = encounters[Rand::GetRandomNumber(0, encounters.size() - 1)];
-
- if (RuntimePatches::EncounterRandomnessAlert::HandleEncounter(args.troop_id)) {
- //Cancel the battle setup
- return false;
- }
-
- if (Feature::HasRpg2kBattleSystem()) {
- if (Rand::ChanceOf(1, 32)) {
- args.first_strike = true;
- }
- } else {
- const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
- if (!terrain) {
- Output::Warning("PrepareEncounter: Invalid terrain at ({}, {})", x, y);
- } else {
- if (terrain->special_flags.back_party && Rand::PercentChance(terrain->special_back_party)) {
- args.condition = lcf::rpg::System::BattleCondition_initiative;
- } else if (terrain->special_flags.back_enemies && Rand::PercentChance(terrain->special_back_enemies)) {
- args.condition = lcf::rpg::System::BattleCondition_back;
- } else if (terrain->special_flags.lateral_party && Rand::PercentChance(terrain->special_lateral_party)) {
- args.condition = lcf::rpg::System::BattleCondition_surround;
- } else if (terrain->special_flags.lateral_enemies && Rand::PercentChance(terrain->special_lateral_enemies)) {
- args.condition = lcf::rpg::System::BattleCondition_pincers;
- }
- }
- }
-
- SetupBattle(args);
- args.on_battle_end = OnEncounterEnd;
- args.allow_escape = true;
-
- return true;
-}
-
-void Game_Map::SetupBattle(BattleArgs& args) {
- int x = Main_Data::game_player->GetX();
- int y = Main_Data::game_player->GetY();
-
- args.terrain_id = GetTerrainTag(x, y);
-
- const auto* current_info = &GetMapInfo();
- while (current_info->background_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) {
- current_info = &GetParentMapInfo(*current_info);
- }
-
- if (current_info->background_type == 2) {
- args.background = ToString(current_info->background_name);
- }
-}
-
-std::vector& Game_Map::GetMapDataDown() {
- return map->lower_layer;
-}
-
-std::vector& Game_Map::GetMapDataUp() {
- return map->upper_layer;
-}
-
-int Game_Map::GetOriginalChipset() {
- return map != nullptr ? map->chipset_id : 0;
-}
-
-int Game_Map::GetChipset() {
- return chipset != nullptr ? chipset->ID : 0;
-}
-
-std::string_view Game_Map::GetChipsetName() {
- return chipset != nullptr
- ? std::string_view(chipset->chipset_name)
- : std::string_view("");
-}
-
-int Game_Map::GetPositionX() {
- return map_info.position_x;
-}
-
-int Game_Map::GetDisplayX() {
- return map_info.position_x + Main_Data::game_screen->GetShakeOffsetX() * 16;
-}
-
-void Game_Map::SetPositionX(int x, bool reset_panorama) {
- const int map_width = GetTilesX() * SCREEN_TILE_SIZE;
- if (LoopHorizontal()) {
- x = Utils::PositiveModulo(x, map_width);
- } else {
- // Do not use std::clamp here. When the map is smaller than the screen the
- // upper bound is smaller than the lower bound making the function fail.
- x = std::max(0, std::min(map_width - screen_width, x));
- }
- map_info.position_x = x;
- if (reset_panorama) {
- Parallax::SetPositionX(map_info.position_x);
- Parallax::ResetPositionX();
- }
-}
-
-int Game_Map::GetPositionY() {
- return map_info.position_y;
-}
-
-int Game_Map::GetDisplayY() {
- return map_info.position_y + Main_Data::game_screen->GetShakeOffsetY() * 16;
-}
-
-void Game_Map::SetPositionY(int y, bool reset_panorama) {
- const int map_height = GetTilesY() * SCREEN_TILE_SIZE;
- if (LoopVertical()) {
- y = Utils::PositiveModulo(y, map_height);
- } else {
- // Do not use std::clamp here. When the map is smaller than the screen the
- // upper bound is smaller than the lower bound making the function fail.
- y = std::max(0, std::min(map_height - screen_height, y));
- }
- map_info.position_y = y;
- if (reset_panorama) {
- Parallax::SetPositionY(map_info.position_y);
- Parallax::ResetPositionY();
- }
-}
-
-bool Game_Map::GetNeedRefresh() {
- int anti_lag_switch = Player::game_config.patch_anti_lag_switch.Get();
- if (anti_lag_switch > 0 && Main_Data::game_switches->Get(anti_lag_switch)) {
- return false;
- }
-
- return need_refresh;
-}
-
-void Game_Map::SetNeedRefresh(bool refresh) {
- need_refresh = refresh;
-}
-
-void Game_Map::SetNeedRefreshForSwitchChange(int switch_id) {
- if (need_refresh)
- return;
- if (map_cache->GetNeedRefresh(switch_id))
- SetNeedRefresh(true);
-}
-
-void Game_Map::SetNeedRefreshForVarChange(int var_id) {
- if (need_refresh)
- return;
- if (map_cache->GetNeedRefresh(var_id))
- SetNeedRefresh(true);
-}
-
-void Game_Map::SetNeedRefreshForSwitchChange(std::initializer_list switch_ids) {
- for (auto switch_id: switch_ids) {
- SetNeedRefreshForSwitchChange(switch_id);
- }
-}
-
-void Game_Map::SetNeedRefreshForVarChange(std::initializer_list var_ids) {
- for (auto var_id: var_ids) {
- SetNeedRefreshForVarChange(var_id);
- }
-}
-
-std::vector& Game_Map::GetPassagesDown() {
- return passages_down;
-}
-
-std::vector& Game_Map::GetPassagesUp() {
- return passages_up;
-}
-
-int Game_Map::GetAnimationType() {
- return animation_type;
-}
-
-int Game_Map::GetAnimationSpeed() {
- return (animation_fast ? 12 : 24);
-}
-
-std::vector& Game_Map::GetEvents() {
- return events;
-}
-
-int Game_Map::GetHighestEventId() {
- int id = 0;
- for (auto& ev: events) {
- id = std::max(id, ev.GetId());
- }
- return id;
-}
-
-Game_Event* Game_Map::GetEvent(int event_id) {
- auto it = std::find_if(events.begin(), events.end(),
- [&event_id](Game_Event& ev) {return ev.GetId() == event_id;});
- return it == events.end() ? nullptr : &(*it);
-}
-
-std::vector& Game_Map::GetCommonEvents() {
- return common_events;
-}
-
-std::string_view Game_Map::GetMapName(int id) {
- for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) {
- if (lcf::Data::treemap.maps[i].ID == id) {
- return lcf::Data::treemap.maps[i].name;
- }
- }
- // nothing found
- return {};
-}
-
-void Game_Map::SetChipset(int id) {
- if (id == 0) {
- // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset.
- id = GetOriginalChipset();
- }
- map_info.chipset_id = id;
-
- if (!ReloadChipset()) {
- Output::Warning("SetChipset: Invalid chipset ID {}", map_info.chipset_id);
- } else {
- passages_down = chipset->passable_data_lower;
- passages_up = chipset->passable_data_upper;
- animation_type = chipset->animation_type;
- animation_fast = chipset->animation_speed != 0;
- }
-
- if (passages_down.size() < 162)
- passages_down.resize(162, (unsigned char) 0x0F);
- if (passages_up.size() < 144)
- passages_up.resize(144, (unsigned char) 0x0F);
-}
-
-bool Game_Map::ReloadChipset() {
- chipset = lcf::ReaderUtil::GetElement(lcf::Data::chipsets, map_info.chipset_id);
- if (!chipset) {
- return false;
- }
- return true;
-}
-
-void Game_Map::OnTranslationChanged() {
- ReloadChipset();
- // Marks common events for reload on map change
- // This is not save to do while they are executing
- translation_changed = true;
-}
-
-Game_Vehicle* Game_Map::GetVehicle(Game_Vehicle::Type which) {
- if (which == Game_Vehicle::Boat ||
- which == Game_Vehicle::Ship ||
- which == Game_Vehicle::Airship) {
- return &vehicles[which - 1];
- }
-
- return nullptr;
-}
-
-bool Game_Map::IsAnyEventStarting() {
- for (Game_Event& ev : events)
- if (ev.IsWaitingForegroundExecution() && !ev.GetList().empty() && ev.IsActive())
- return true;
-
- for (Game_CommonEvent& ev : common_events)
- if (ev.IsWaitingForegroundExecution())
- return true;
-
- return false;
-}
-
-bool Game_Map::IsAnyMovePending() {
- auto check = [](auto& ev) {
- return ev.IsMoveRouteOverwritten() && !ev.IsMoveRouteFinished();
- };
- const auto map_id = GetMapId();
- if (check(*Main_Data::game_player)) {
- return true;
- }
- for (auto& vh: vehicles) {
- if (vh.GetMapId() == map_id && check(vh)) {
- return true;
- }
- }
- for (auto& ev: events) {
- if (check(ev)) {
- return true;
- }
- }
-
- return false;
-}
-
-void Game_Map::RemoveAllPendingMoves() {
- const auto map_id = GetMapId();
- Main_Data::game_player->CancelMoveRoute();
- for (auto& vh: vehicles) {
- if (vh.GetMapId() == map_id) {
- vh.CancelMoveRoute();
- }
- }
- for (auto& ev: events) {
- ev.CancelMoveRoute();
- }
-}
-
-static int DoSubstitute(std::vector& tiles, int old_id, int new_id) {
- int num_subst = 0;
- for (size_t i = 0; i < tiles.size(); ++i) {
- if (tiles[i] == old_id) {
- tiles[i] = (uint8_t) new_id;
- ++num_subst;
- }
- }
- return num_subst;
-}
-
-int Game_Map::SubstituteDown(int old_id, int new_id) {
- return DoSubstitute(map_info.lower_tiles, old_id, new_id);
-}
-
-int Game_Map::SubstituteUp(int old_id, int new_id) {
- return DoSubstitute(map_info.upper_tiles, old_id, new_id);
-}
-
-void Game_Map::ReplaceTileAt(int x, int y, int new_id, int layer) {
- auto pos = x + y * map->width;
- auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer;
- layer_vec[pos] = static_cast(new_id);
-}
-
-int Game_Map::GetTileIdAt(int x, int y, int layer, bool chip_id_or_index) {
- if (x < 0 || x >= map->width || y < 0 || y >= map->height) {
- return 0; // Return 0 for out-of-bounds coordinates
- }
-
- auto pos = x + y * map->width;
- auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer;
-
- int tile_output = chip_id_or_index ? layer_vec[pos] : ChipIdToIndex(layer_vec[pos]);
- if (layer >= 1) tile_output -= BLOCK_F_INDEX;
-
- return tile_output;
-}
-
-std::vector Game_Map::GetTilesIdAt(Rect coords, int layer, bool chip_id_or_index) {
- std::vector tiles_collection;
- for (int i = 0; i < coords.height; ++i) {
- for (int j = 0; j < coords.width; ++j) {
- tiles_collection.emplace_back(Game_Map::GetTileIdAt(coords.x + j, coords.y + i, layer, chip_id_or_index));
- }
- }
- return tiles_collection;
-}
-
-std::string Game_Map::ConstructMapName(int map_id, bool is_easyrpg) {
- std::stringstream ss;
- ss << "Map" << std::setfill('0') << std::setw(4) << map_id;
- if (is_easyrpg) {
- return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_EMU);
- } else {
- return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_LMU);
- }
-}
-
-FileRequestAsync* Game_Map::RequestMap(int map_id) {
-#ifdef EMSCRIPTEN
- Player::translation.RequestAndAddMap(map_id);
-#endif
-
- auto* request = AsyncHandler::RequestFile(Game_Map::ConstructMapName(map_id, false));
- request->SetImportantFile(true);
- return request;
-}
-
-// MapEventCache
-//////////////////
-void Game_Map::Caching::MapEventCache::AddEvent(const lcf::rpg::Event& ev) {
- auto id = ev.ID;
-
- if (std::find(event_ids.begin(), event_ids.end(), id) == event_ids.end()) {
- event_ids.emplace_back(id);
- }
-}
-
-void Game_Map::Caching::MapEventCache::RemoveEvent(const lcf::rpg::Event& ev) {
- auto id = ev.ID;
-
- auto it = std::find(event_ids.begin(), event_ids.end(), id);
-
- if (it != event_ids.end()) {
- event_ids.erase(it);
- }
-}
-
-// Parallax
-/////////////
-
-namespace {
- int parallax_width;
- int parallax_height;
-
- bool parallax_fake_x;
- bool parallax_fake_y;
-}
-
-/* Helper function to get the current parallax parameters. If the default
- * parallax for the current map was overridden by a "Change Parallax BG"
- * command, the result is filled out from those values in the SaveMapInfo.
- * Otherwise, the result is filled out from the default for the current map.
- */
-static Game_Map::Parallax::Params GetParallaxParams() {
- Game_Map::Parallax::Params params = {};
-
- if (!map_info.parallax_name.empty()) {
- params.name = map_info.parallax_name;
- params.scroll_horz = map_info.parallax_horz;
- params.scroll_horz_auto = map_info.parallax_horz_auto;
- params.scroll_horz_speed = map_info.parallax_horz_speed;
- params.scroll_vert = map_info.parallax_vert;
- params.scroll_vert_auto = map_info.parallax_vert_auto;
- params.scroll_vert_speed = map_info.parallax_vert_speed;
- } else if (map->parallax_flag) {
- // Default case when map parallax hasn't been overwritten.
- params.name = ToString(map->parallax_name);
- params.scroll_horz = map->parallax_loop_x;
- params.scroll_horz_auto = map->parallax_auto_loop_x;
- params.scroll_horz_speed = map->parallax_sx;
- params.scroll_vert = map->parallax_loop_y;
- params.scroll_vert_auto = map->parallax_auto_loop_y;
- params.scroll_vert_speed = map->parallax_sy;
- } else {
- // No BG; use default-constructed Param
- }
-
- return params;
-}
-
-std::string Game_Map::Parallax::GetName() {
- return GetParallaxParams().name;
-}
-
-int Game_Map::Parallax::GetX() {
- return (-panorama.pan_x / TILE_SIZE) / 2;
-}
-
-int Game_Map::Parallax::GetY() {
- return (-panorama.pan_y / TILE_SIZE) / 2;
-}
-
-void Game_Map::Parallax::Initialize(int width, int height) {
- parallax_width = width;
- parallax_height = height;
-
- if (panorama_on_map_init) {
- SetPositionX(map_info.position_x);
- SetPositionY(map_info.position_y);
- }
-
- if (reset_panorama_x_on_next_init) {
- ResetPositionX();
- }
- if (reset_panorama_y_on_next_init) {
- ResetPositionY();
- }
-
- if (Player::IsRPG2k() && !panorama_on_map_init) {
- SetPositionX(panorama.pan_x);
- SetPositionY(panorama.pan_y);
- }
-
- panorama_on_map_init = false;
-}
-
-void Game_Map::Parallax::AddPositionX(int off_x) {
- SetPositionX(panorama.pan_x + off_x);
-}
-
-void Game_Map::Parallax::AddPositionY(int off_y) {
- SetPositionY(panorama.pan_y + off_y);
-}
-
-void Game_Map::Parallax::SetPositionX(int x) {
- // FIXME: Fixes a crash with ChangeBG commands in events, but not correct.
- // Real fix TBD
- if (parallax_width) {
- const int w = parallax_width * TILE_SIZE * 2;
- panorama.pan_x = (x + w) % w;
- }
-}
-
-void Game_Map::Parallax::SetPositionY(int y) {
- // FIXME: Fixes a crash with ChangeBG commands in events, but not correct.
- // Real fix TBD
- if (parallax_height) {
- const int h = parallax_height * TILE_SIZE * 2;
- panorama.pan_y = (y + h) % h;
- }
-}
-
-void Game_Map::Parallax::ResetPositionX() {
- Params params = GetParallaxParams();
-
- if (params.name.empty()) {
- return;
- }
-
- parallax_fake_x = false;
-
- if (!params.scroll_horz && !LoopHorizontal()) {
- int pan_screen_width = Player::screen_width;
- if (Player::game_config.fake_resolution.Get()) {
- pan_screen_width = SCREEN_TARGET_WIDTH;
- }
-
- int tiles_per_screen = pan_screen_width / TILE_SIZE;
- if (pan_screen_width % TILE_SIZE != 0) {
- ++tiles_per_screen;
- }
-
- if (GetTilesX() > tiles_per_screen && parallax_width > pan_screen_width) {
- const int w = (GetTilesX() - tiles_per_screen) * TILE_SIZE;
- const int ph = 2 * std::min(w, parallax_width - pan_screen_width) * map_info.position_x / w;
- if (Player::IsRPG2k()) {
- SetPositionX(ph);
- } else {
- // 2k3 does not do the (% parallax_width * TILE_SIZE * 2) here
- panorama.pan_x = ph;
- }
- } else {
- panorama.pan_x = 0;
- parallax_fake_x = true;
- }
- } else {
- parallax_fake_x = true;
- }
-}
-
-void Game_Map::Parallax::ResetPositionY() {
- Params params = GetParallaxParams();
-
- if (params.name.empty()) {
- return;
- }
-
- parallax_fake_y = false;
-
- if (!params.scroll_vert && !Game_Map::LoopVertical()) {
- int pan_screen_height = Player::screen_height;
- if (Player::game_config.fake_resolution.Get()) {
- pan_screen_height = SCREEN_TARGET_HEIGHT;
- }
-
- int tiles_per_screen = pan_screen_height / TILE_SIZE;
- if (pan_screen_height % TILE_SIZE != 0) {
- ++tiles_per_screen;
- }
-
- if (GetTilesY() > tiles_per_screen && parallax_height > pan_screen_height) {
- const int h = (GetTilesY() - tiles_per_screen) * TILE_SIZE;
- const int pv = 2 * std::min(h, parallax_height - pan_screen_height) * map_info.position_y / h;
- SetPositionY(pv);
- } else {
- panorama.pan_y = 0;
- parallax_fake_y = true;
- }
- } else {
- parallax_fake_y = true;
- }
-}
-
-void Game_Map::Parallax::ScrollRight(int distance) {
- if (!distance) {
- return;
- }
-
- Params params = GetParallaxParams();
- if (params.name.empty()) {
- return;
- }
-
- if (params.scroll_horz) {
- AddPositionX(distance);
- return;
- }
-
- if (Game_Map::LoopHorizontal()) {
- return;
- }
-
- ResetPositionX();
-}
-
-void Game_Map::Parallax::ScrollDown(int distance) {
- if (!distance) {
- return;
- }
-
- Params params = GetParallaxParams();
- if (params.name.empty()) {
- return;
- }
-
- if (params.scroll_vert) {
- AddPositionY(distance);
- return;
- }
-
- if (Game_Map::LoopVertical()) {
- return;
- }
-
- ResetPositionY();
-}
-
-void Game_Map::Parallax::Update() {
- Params params = GetParallaxParams();
-
- if (params.name.empty())
- return;
-
- auto scroll_amt = [](int speed) {
- return speed < 0 ? (1 << -speed) : -(1 << speed);
- };
-
- if (params.scroll_horz
- && params.scroll_horz_auto
- && params.scroll_horz_speed != 0) {
- AddPositionX(scroll_amt(params.scroll_horz_speed));
- }
-
- if (params.scroll_vert
- && params.scroll_vert_auto
- && params.scroll_vert_speed != 0) {
- if (parallax_height != 0) {
- AddPositionY(scroll_amt(params.scroll_vert_speed));
- }
- }
-}
-
-void Game_Map::Parallax::ChangeBG(const Params& params) {
- map_info.parallax_name = params.name;
- map_info.parallax_horz = params.scroll_horz;
- map_info.parallax_horz_auto = params.scroll_horz_auto;
- map_info.parallax_horz_speed = params.scroll_horz_speed;
- map_info.parallax_vert = params.scroll_vert;
- map_info.parallax_vert_auto = params.scroll_vert_auto;
- map_info.parallax_vert_speed = params.scroll_vert_speed;
-
- reset_panorama_x_on_next_init = !Game_Map::LoopHorizontal() && !map_info.parallax_horz;
- reset_panorama_y_on_next_init = !Game_Map::LoopVertical() && !map_info.parallax_vert;
-
- Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
- if (!scene || !scene->spriteset)
- return;
- scene->spriteset->ParallaxUpdated();
-}
-
-void Game_Map::Parallax::ClearChangedBG() {
- Params params {}; // default Param indicates no override
- ChangeBG(params);
-}
-
-bool Game_Map::Parallax::FakeXPosition() {
- return parallax_fake_x;
-}
-
-bool Game_Map::Parallax::FakeYPosition() {
- return parallax_fake_y;
-}
+/*
+ * This file is part of EasyRPG Player.
+ *
+ * EasyRPG Player is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EasyRPG Player is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EasyRPG Player. If not, see .
+ */
+
+// Headers
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "async_handler.h"
+#include "options.h"
+#include "system.h"
+#include "game_battle.h"
+#include "game_battler.h"
+#include "game_map.h"
+#include "game_interpreter_map.h"
+#include "game_switches.h"
+#include "game_player.h"
+#include "game_party.h"
+#include "game_message.h"
+#include "game_screen.h"
+#include "game_pictures.h"
+#include "game_variables.h"
+#include "scene_battle.h"
+#include "scene_map.h"
+#include
+#include
+#include "map_data.h"
+#include "main_data.h"
+#include "output.h"
+#include "util_macro.h"
+#include "game_system.h"
+#include "filefinder.h"
+#include "player.h"
+#include "input.h"
+#include "utils.h"
+#include "rand.h"
+#include
+#include
+#include "scene_gameover.h"
+#include "feature.h"
+
+namespace {
+ // Intended bad value, Game_Map::Init sets them correctly
+ int screen_width = -1;
+ int screen_height = -1;
+
+ lcf::rpg::SaveMapInfo map_info;
+ lcf::rpg::SavePanorama panorama;
+
+ bool need_refresh;
+
+ bool isMode7 = false;
+ float mode7Slant = 60;
+ float mode7Yaw = 0;
+ int mode7Horizon = 20;
+ double mode7Scale = 200.0;
+
+ float mode7Zoom = 1.0f;
+ float mode7ZoomTarget = 1.0f;
+ float mode7ZoomSpeed = 0.0f;
+ int mode7ZOffset = 0;
+ //
+ float mode7SlantTarget = 0;
+ float mode7SlantSpeed = 0;
+ float mode7YawTarget = 0;
+ float mode7YawSpeed = 0;
+
+ std::string mode7BackgroundName = "";
+
+ int mode7FadeWidth = 16;
+
+ std::map mode7SkyLayers;
+
+ int animation_type;
+ bool animation_fast;
+ std::vector passages_down;
+ std::vector passages_up;
+ std::vector events;
+ std::vector common_events;
+ std::unique_ptr map_cache;
+
+ std::unique_ptr map;
+
+ std::unique_ptr interpreter;
+ std::vector vehicles;
+
+ lcf::rpg::Chipset* chipset;
+
+ //FIXME: Find a better way to do this.
+ bool panorama_on_map_init = true;
+ bool reset_panorama_x_on_next_init = true;
+ bool reset_panorama_y_on_next_init = true;
+
+ bool translation_changed = false;
+
+ // Used when the current map is not in the maptree
+ const lcf::rpg::MapInfo empty_map_info;
+}
+
+namespace Game_Map {
+void SetupCommon();
+}
+
+void Game_Map::OnContinueFromBattle() {
+ Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeBattleMusic());
+}
+
+static Game_Map::Parallax::Params GetParallaxParams();
+
+void Game_Map::Init() {
+
+ screen_width = (Player::screen_width / 16) * SCREEN_TILE_SIZE;
+ screen_height = (Player::screen_height / 16) * SCREEN_TILE_SIZE;
+
+ Dispose();
+
+ map_info = {};
+ panorama = {};
+ SetNeedRefresh(true);
+
+ interpreter.reset(new Game_Interpreter_Map(true));
+ map_cache.reset(new Caching::MapCache());
+
+ InitCommonEvents();
+
+ vehicles.clear();
+ vehicles.emplace_back(Game_Vehicle::Boat);
+ vehicles.emplace_back(Game_Vehicle::Ship);
+ vehicles.emplace_back(Game_Vehicle::Airship);
+}
+
+void Game_Map::InitCommonEvents() {
+ common_events.clear();
+ common_events.reserve(lcf::Data::commonevents.size());
+ for (const lcf::rpg::CommonEvent& ev : lcf::Data::commonevents) {
+ common_events.emplace_back(ev.ID);
+ }
+ translation_changed = false;
+}
+
+void Game_Map::Dispose() {
+ events.clear();
+ map.reset();
+ map_info = {};
+ panorama = {};
+}
+
+void Game_Map::Quit() {
+ Dispose();
+ common_events.clear();
+ interpreter.reset();
+ map_cache.reset();
+
+ vehicles.clear();
+
+
+// Reset all Mode7 parameters to their default state
+ isMode7 = false;
+ mode7Slant = 60; // Reset to default
+ mode7Yaw = 0;
+ mode7Horizon = 20; // Reset to default
+ mode7Zoom = 1.0f;
+ mode7Scale = 200.0; // Reset to default
+
+ // Reset any timed movement parameters for Mode7
+ mode7SlantTarget = 0;
+ mode7SlantSpeed = 0;
+ mode7YawTarget = 0;
+ mode7YawSpeed = 0;
+}
+
+int Game_Map::GetMapSaveCount() {
+ return (Player::IsRPG2k3() && map->save_count_2k3e > 0)
+ ? map->save_count_2k3e
+ : map->save_count;
+}
+
+void Game_Map::Setup(std::unique_ptr map_in) {
+
+ Dispose();
+
+
+ screen_width = (Player::screen_width / 16) * SCREEN_TILE_SIZE;
+ screen_height = (Player::screen_height / 16) * SCREEN_TILE_SIZE;
+
+ map = std::move(map_in);
+
+ SetupCommon();
+
+ panorama_on_map_init = true;
+ Parallax::ClearChangedBG();
+
+ SetEncounterSteps(GetMapInfo().encounter_steps);
+ SetChipset(map->chipset_id);
+
+ std::iota(map_info.lower_tiles.begin(), map_info.lower_tiles.end(), 0);
+ std::iota(map_info.upper_tiles.begin(), map_info.upper_tiles.end(), 0);
+
+ // Save allowed
+ const auto* current_info = &GetMapInfo();
+ int current_index = current_info->ID;
+ int can_save = current_info->save;
+ int can_escape = current_info->escape;
+ int can_teleport = current_info->teleport;
+
+ while (can_save == lcf::rpg::MapInfo::TriState_parent
+ || can_escape == lcf::rpg::MapInfo::TriState_parent
+ || can_teleport == lcf::rpg::MapInfo::TriState_parent)
+ {
+ const auto* parent_info = &GetParentMapInfo(*current_info);
+ int parent_index = parent_info->ID;
+ if (parent_index == 0) {
+ // If parent is 0 and flag is parent, it's implicitly enabled.
+ break;
+ }
+ if (parent_index == current_index) {
+ Output::Warning("Map {} has parent pointing to itself!", current_index);
+ break;
+ }
+ current_info = parent_info;
+ if (can_save == lcf::rpg::MapInfo::TriState_parent) {
+ can_save = current_info->save;
+ }
+ if (can_escape == lcf::rpg::MapInfo::TriState_parent) {
+ can_escape = current_info->escape;
+ }
+ if (can_teleport == lcf::rpg::MapInfo::TriState_parent) {
+ can_teleport = current_info->teleport;
+ }
+ }
+ Main_Data::game_system->SetAllowSave(can_save != lcf::rpg::MapInfo::TriState_forbid);
+ Main_Data::game_system->SetAllowEscape(can_escape != lcf::rpg::MapInfo::TriState_forbid);
+ Main_Data::game_system->SetAllowTeleport(can_teleport != lcf::rpg::MapInfo::TriState_forbid);
+
+ auto& player = *Main_Data::game_player;
+
+ SetPositionX(player.GetX() * SCREEN_TILE_SIZE - player.GetPanX());
+ SetPositionY(player.GetY() * SCREEN_TILE_SIZE - player.GetPanY());
+
+ // Set Mode7 flag
+ RefreshMode7();
+
+
+
+ // Update the save counts so that if the player saves the game
+ // events will properly resume upon loading.
+ Main_Data::game_player->UpdateSaveCounts(lcf::Data::system.save_count, GetMapSaveCount());
+}
+
+void Game_Map::SetupFromSave(
+ std::unique_ptr map_in,
+ lcf::rpg::SaveMapInfo save_map,
+ lcf::rpg::SaveVehicleLocation save_boat,
+ lcf::rpg::SaveVehicleLocation save_ship,
+ lcf::rpg::SaveVehicleLocation save_airship,
+ lcf::rpg::SaveEventExecState save_fg_exec,
+ lcf::rpg::SavePanorama save_pan,
+ std::vector save_ce) {
+
+ map = std::move(map_in);
+ map_info = std::move(save_map);
+ panorama = std::move(save_pan);
+
+ SetupCommon();
+
+ const bool is_db_save_compat = Main_Data::game_player->IsDatabaseCompatibleWithSave(lcf::Data::system.save_count);
+ const bool is_map_save_compat = Main_Data::game_player->IsMapCompatibleWithSave(GetMapSaveCount());
+
+ InitCommonEvents();
+
+ if (is_db_save_compat && is_map_save_compat) {
+ for (size_t i = 0; i < std::min(save_ce.size(), common_events.size()); ++i) {
+ common_events[i].SetSaveData(save_ce[i].parallel_event_execstate);
+ }
+ }
+
+ if (is_map_save_compat) {
+ std::vector destroyed_event_ids;
+
+ for (size_t i = 0, j = 0; i < events.size() && j < map_info.events.size(); ++i) {
+ auto& ev = events[i];
+ auto& save_ev = map_info.events[j];
+ if (ev.GetId() == save_ev.ID) {
+ ev.SetSaveData(save_ev);
+ ++j;
+ } else {
+ if (save_ev.ID > ev.GetId()) {
+ // assume that the event has been destroyed during gameplay via "DestroyMapEvent"
+ destroyed_event_ids.emplace_back(ev.GetId());
+ } else {
+ Output::Debug("SetupFromSave: Unexpected ID {}/{}", save_ev.ID, ev.GetId());
+ }
+ }
+ }
+ for (size_t i = 0; i < destroyed_event_ids.size(); ++i) {
+ DestroyMapEvent(destroyed_event_ids[i], true);
+ }
+ if (destroyed_event_ids.size() > 0) {
+ UpdateUnderlyingEventReferences();
+ }
+ }
+
+ // Handle cloned events in a separate loop, regardless of "is_map_save_compat"
+ if (Player::HasEasyRpgExtensions()) {
+ for (size_t i = 0; i < map_info.events.size(); ++i) {
+ auto& save_ev = map_info.events[i];
+ bool is_cloned_evt = save_ev.easyrpg_clone_map_id > 0 || save_ev.easyrpg_clone_event_id > 0;
+ if (is_cloned_evt && CloneMapEvent(
+ save_ev.easyrpg_clone_map_id, save_ev.easyrpg_clone_event_id,
+ save_ev.position_x, save_ev.position_y,
+ save_ev.ID, "")) { // FIXME: Customized event names for saved events aren't part of liblcf/SaveMapEvent at the moment & thus cannot be restored
+ if (auto new_event = GetEvent(save_ev.ID); new_event != nullptr) {
+ new_event->SetSaveData(save_ev);
+ }
+ }
+ }
+ UpdateUnderlyingEventReferences();
+ }
+ map_info.events.clear();
+ interpreter->Clear();
+
+ GetVehicle(Game_Vehicle::Boat)->SetSaveData(std::move(save_boat));
+ GetVehicle(Game_Vehicle::Ship)->SetSaveData(std::move(save_ship));
+ GetVehicle(Game_Vehicle::Airship)->SetSaveData(std::move(save_airship));
+
+ if (is_map_save_compat) {
+ // Make main interpreter "busy" if save contained events to prevent auto-events from starting
+ interpreter->SetState(std::move(save_fg_exec));
+ }
+
+ SetEncounterSteps(map_info.encounter_steps);
+
+ // RPG_RT bug: Chipset is not loaded. Fixed in 2k3E
+ if (Player::IsRPG2k3E()) {
+ SetChipset(map_info.chipset_id);
+ } else {
+ SetChipset(0);
+ }
+
+ if (!is_map_save_compat) {
+ panorama = {};
+ }
+
+ // We want to support loading rm2k3e panning chunks
+ // but also not break other saves which don't have them.
+ // To solve this problem, we reuse the scrolling methods
+ // which always reset the position anyways when scroll_horz/vert
+ // is false.
+ // This produces compatible behavior for old RPG_RT saves, namely
+ // the pan_x/y is always forced to 0.
+ // If the later async code will load panorama, set the flag to not clear the offsets.
+ // FIXME: RPG_RT compatibility bug: Everytime we load a savegame with default panorama chunks,
+ // this causes them to get overwritten
+ // FIXME: RPG_RT compatibility bug: On async platforms, panorama async loading can
+ // cause panorama chunks to be out of sync.
+ Game_Map::Parallax::ChangeBG(GetParallaxParams());
+}
+
+std::unique_ptr Game_Map::LoadMapFile(int map_id) {
+ std::unique_ptr map;
+
+ // Try loading EasyRPG map files first, then fallback to normal RPG Maker
+ // FIXME: Assert map was cached for async platforms
+ std::string map_name = Game_Map::ConstructMapName(map_id, true);
+ std::string map_file = FileFinder::Game().FindFile(map_name);
+ if (map_file.empty()) {
+ map_name = Game_Map::ConstructMapName(map_id, false);
+ map_file = FileFinder::Game().FindFile(map_name);
+
+ if (map_file.empty()) {
+ Output::Error("Loading of Map {} failed.\nThe map was not found.", map_name);
+ return nullptr;
+ }
+
+ auto map_stream = FileFinder::Game().OpenInputStream(map_file);
+ if (!map_stream) {
+ Output::Error("Loading of Map {} failed.\nMap not readable.", map_name);
+ return nullptr;
+ }
+
+ map = lcf::LMU_Reader::Load(map_stream, Player::encoding);
+
+ if (Input::IsRecording()) {
+ map_stream.clear();
+ map_stream.seekg(0);
+ Input::AddRecordingData(Input::RecordingData::Hash,
+ fmt::format("map{:04} {:#08x}", map_id, Utils::CRC32(map_stream)));
+ }
+ } else {
+ auto map_stream = FileFinder::Game().OpenInputStream(map_file);
+ if (!map_stream) {
+ Output::Error("Loading of Map {} failed.\nMap not readable.", map_name);
+ return nullptr;
+ }
+ map = lcf::LMU_Reader::LoadXml(map_stream);
+ }
+
+ Output::Debug("Loaded Map {}", map_name);
+
+ if (map.get() == NULL) {
+ Output::ErrorStr(lcf::LcfReader::GetError());
+ }
+
+ return map;
+}
+
+void Game_Map::SetupCommon() {
+ screen_width = (Player::screen_width / 16.0) * SCREEN_TILE_SIZE;
+ screen_height = (Player::screen_height / 16.0) * SCREEN_TILE_SIZE;
+
+ if (!Tr::GetCurrentTranslationId().empty()) {
+ TranslateMapMessages(GetMapId(), *map);
+ }
+ SetNeedRefresh(true);
+
+ PrintPathToMap();
+
+ if (translation_changed) {
+ InitCommonEvents();
+ }
+
+ map_cache->Clear();
+
+ CreateMapEvents();
+}
+
+void Game_Map::CreateMapEvents() {
+ events.reserve(map->events.size());
+ for (auto& ev : map->events) {
+ events.emplace_back(GetMapId(), &ev);
+ AddEventToCache(ev);
+ }
+}
+
+void Game_Map::AddEventToCache(const lcf::rpg::Event& ev) {
+ using Op = Caching::ObservedVarOps;
+
+ for (const auto& pg : ev.pages) {
+ if (pg.condition.flags.switch_a) {
+ map_cache->AddEventAsRefreshTarget(pg.condition.switch_a_id, ev);
+ }
+ if (pg.condition.flags.switch_b) {
+ map_cache->AddEventAsRefreshTarget(pg.condition.switch_b_id, ev);
+ }
+ if (pg.condition.flags.variable) {
+ map_cache->AddEventAsRefreshTarget(pg.condition.variable_id, ev);
+ }
+ }
+}
+
+void Game_Map::RemoveEventFromCache(const lcf::rpg::Event& ev) {
+ using Op = Caching::ObservedVarOps;
+
+ for (const auto& pg : ev.pages) {
+ if (pg.condition.flags.switch_a) {
+ map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_a_id, ev);
+ }
+ if (pg.condition.flags.switch_b) {
+ map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_b_id, ev);
+ }
+ if (pg.condition.flags.variable) {
+ map_cache->RemoveEventAsRefreshTarget(pg.condition.variable_id, ev);
+ }
+ }
+}
+
+void Game_Map::Caching::MapCache::Clear() {
+ for (int i = 0; i < static_cast(ObservedVarOps_END); i++) {
+ refresh_targets_by_varid[i].clear();
+ }
+}
+
+bool Game_Map::CloneMapEvent(int src_map_id, int src_event_id, int target_x, int target_y, int target_event_id, std::string_view target_name) {
+ std::unique_ptr source_map_storage;
+ const lcf::rpg::Map* source_map;
+
+ if (src_map_id == GetMapId()) {
+ source_map = &GetMap();
+ } else {
+ source_map_storage = Game_Map::LoadMapFile(src_map_id);
+ source_map = source_map_storage.get();
+
+ if (source_map_storage == nullptr) {
+ Output::Warning("CloneMapEvent: Invalid source map ID {}", src_map_id);
+ return false;
+ }
+
+ if (!Tr::GetCurrentTranslationId().empty()) {
+ TranslateMapMessages(src_map_id, *source_map_storage);
+ }
+ }
+
+ const lcf::rpg::Event* source_event = FindEventById(source_map->events, src_event_id);
+ if (source_event == nullptr) {
+ Output::Warning("CloneMapEvent: Event ID {} not found on source map {}", src_event_id, src_map_id);
+ return false;
+ }
+
+ lcf::rpg::Event new_event = *source_event;
+ if (target_event_id > 0) {
+ DestroyMapEvent(target_event_id, true);
+ new_event.ID = target_event_id;
+ } else {
+ new_event.ID = GetNextAvailableEventId();
+ }
+ new_event.x = target_x;
+ new_event.y = target_y;
+
+ if (!target_name.empty()) {
+ new_event.name = lcf::DBString(target_name);
+ }
+
+ // sorted insert
+ auto insert_it = map->events.insert(
+ std::upper_bound(map->events.begin(), map->events.end(), new_event, [](const auto& e, const auto& e2) {
+ return e.ID < e2.ID;
+ }), new_event);
+
+ auto game_event = Game_Event(GetMapId(), &*insert_it);
+ game_event.data()->easyrpg_clone_event_id = src_event_id;
+ game_event.data()->easyrpg_clone_map_id = src_map_id;
+
+ events.insert(
+ std::upper_bound(events.begin(), events.end(), game_event, [](const auto& e, const auto& e2) {
+ return e.GetId() < e2.GetId();
+ }), std::move(game_event));
+
+ UpdateUnderlyingEventReferences();
+
+ AddEventToCache(new_event);
+
+ Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
+ if (scene) {
+ scene->spriteset->Refresh();
+ SetNeedRefresh(true);
+ }
+
+ return true;
+}
+
+bool Game_Map::DestroyMapEvent(const int event_id, bool from_clone) {
+ const lcf::rpg::Event* event = FindEventById(map->events, event_id);
+
+ if (event == nullptr) {
+ if (!from_clone) {
+ Output::Warning("DestroyMapEvent: Event ID {} not found on current map", event_id);
+ }
+ return true;
+ }
+
+ // Remove event from cache
+ RemoveEventFromCache(*event);
+
+ // Remove event from events vector
+ for (auto it = events.begin(); it != events.end(); ++it) {
+ if (it->GetId() == event_id) {
+ events.erase(it);
+ break;
+ }
+ }
+
+ // Remove event from map
+ for (auto it = map->events.begin(); it != map->events.end(); ++it) {
+ if (it->ID == event_id) {
+ map->events.erase(it);
+ break;
+ }
+ }
+
+ if (!from_clone) {
+ UpdateUnderlyingEventReferences();
+
+ Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
+ scene->spriteset->Refresh();
+ SetNeedRefresh(true);
+ }
+
+ if (GetInterpreter().GetOriginalEventId() == event_id) {
+ // Prevent triggering "invalid event on stack" sanity check
+ GetInterpreter().ClearOriginalEventId();
+ }
+
+ return true;
+}
+
+void Game_Map::TranslateMapMessages(int mapId, lcf::rpg::Map& map) {
+ std::stringstream ss;
+ ss << "map" << std::setfill('0') << std::setw(4) << mapId << ".po";
+ Player::translation.RewriteMapMessages(ss.str(), map);
+}
+
+
+void Game_Map::UpdateUnderlyingEventReferences() {
+ // Update references because modifying the vector can reallocate
+ size_t idx = 0;
+ for (auto& ev : events) {
+ ev.SetUnderlyingEvent(&map->events.at(idx++));
+ }
+
+ Main_Data::game_screen->UpdateUnderlyingEventReferences();
+}
+
+const lcf::rpg::Event* Game_Map::FindEventById(const std::vector& events, int eventId) {
+ for (const auto& ev : events) {
+ if (ev.ID == eventId) {
+ return &ev;
+ }
+ }
+ return nullptr;
+}
+
+int Game_Map::GetNextAvailableEventId() {
+ return map->events.back().ID + 1;
+}
+
+void Game_Map::PrepareSave(lcf::rpg::Save& save) {
+ save.foreground_event_execstate = interpreter->GetSaveState();
+
+ save.airship_location = GetVehicle(Game_Vehicle::Airship)->GetSaveData();
+ save.ship_location = GetVehicle(Game_Vehicle::Ship)->GetSaveData();
+ save.boat_location = GetVehicle(Game_Vehicle::Boat)->GetSaveData();
+
+ save.map_info = map_info;
+ save.map_info.chipset_id = GetChipset();
+ if (save.map_info.chipset_id == GetOriginalChipset()) {
+ // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset.
+ save.map_info.chipset_id = 0;
+ }
+ if (save.map_info.encounter_steps == GetOriginalEncounterSteps()) {
+ save.map_info.encounter_steps = -1;
+ }
+ // Note: RPG_RT does not use a sentinel for parallax parameters. Once the parallax BG is changed, it stays that way forever.
+
+ save.map_info.events.clear();
+ save.map_info.events.reserve(events.size());
+ for (Game_Event& ev : events) {
+ save.map_info.events.push_back(ev.GetSaveData());
+ }
+
+ save.panorama = panorama;
+
+ save.common_events.clear();
+ save.common_events.reserve(common_events.size());
+ for (Game_CommonEvent& ev : common_events) {
+ save.common_events.push_back(lcf::rpg::SaveCommonEvent());
+ save.common_events.back().ID = ev.GetIndex();
+ save.common_events.back().parallel_event_execstate = ev.GetSaveData();
+ }
+}
+
+void Game_Map::PlayBgm() {
+ const auto* current_info = &GetMapInfo();
+ while (current_info->music_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) {
+ current_info = &GetParentMapInfo(*current_info);
+ }
+
+ if ((current_info->ID > 0) && !current_info->music.name.empty()) {
+ if (current_info->music_type == 1) {
+ return;
+ }
+ auto& music = current_info->music;
+ if (!Main_Data::game_player->IsAboard()) {
+ Main_Data::game_system->BgmPlay(music);
+ } else {
+ Main_Data::game_system->SetBeforeVehicleMusic(music);
+ }
+ }
+}
+
+std::vector Game_Map::GetTilesLayer(int layer) {
+ return layer >= 1 ? map_info.upper_tiles : map_info.lower_tiles;
+}
+
+void Game_Map::Refresh() {
+ if (GetMapId() > 0) {
+ for (Game_Event& ev : events) {
+ ev.RefreshPage();
+ }
+ }
+
+ need_refresh = false;
+}
+
+Game_Interpreter_Map& Game_Map::GetInterpreter() {
+ assert(interpreter);
+ return *interpreter;
+}
+
+void Game_Map::Scroll(int dx, int dy) {
+ int x = map_info.position_x;
+ AddScreenX(x, dx);
+ map_info.position_x = x;
+
+ int y = map_info.position_y;
+ AddScreenY(y, dy);
+ map_info.position_y = y;
+
+ if (dx == 0 && dy == 0) {
+ return;
+ }
+
+ Main_Data::game_screen->OnMapScrolled(dx, dy);
+ Main_Data::game_pictures->OnMapScrolled(dx, dy);
+ Game_Map::Parallax::ScrollRight(dx);
+ Game_Map::Parallax::ScrollDown(dy);
+}
+
+// Add inc to acc, clamping the result into the range [low, high].
+// If the result is clamped, inc is also modified to be actual amount
+// that acc changed by.
+static void ClampingAdd(int low, int high, int& acc, int& inc) {
+ int original_acc = acc;
+ // Do not use std::clamp here. When the map is smaller than the screen the
+ // upper bound is smaller than the lower bound making the function fail.
+ acc = std::max(low, std::min(high, acc + inc));
+ inc = acc - original_acc;
+}
+
+void Game_Map::AddScreenX(int& screen_x, int& inc) {
+ int map_width = GetTilesX() * SCREEN_TILE_SIZE;
+ if (LoopHorizontal()) {
+ screen_x = (screen_x + inc) % map_width;
+ } else {
+ ClampingAdd(0, map_width - screen_width, screen_x, inc);
+ }
+}
+
+void Game_Map::AddScreenY(int& screen_y, int& inc) {
+ int map_height = GetTilesY() * SCREEN_TILE_SIZE;
+ if (LoopVertical()) {
+ screen_y = (screen_y + inc) % map_height;
+ } else {
+ ClampingAdd(0, map_height - screen_height, screen_y, inc);
+ }
+}
+
+bool Game_Map::IsValid(int x, int y) {
+ return (x >= 0 && x < GetTilesX() && y >= 0 && y < GetTilesY());
+}
+
+static int GetPassableMask(int old_x, int old_y, int new_x, int new_y) {
+ int bit = 0;
+ if (new_x > old_x) { bit |= Passable::Right; }
+ if (new_x < old_x) { bit |= Passable::Left; }
+ if (new_y > old_y) { bit |= Passable::Down; }
+ if (new_y < old_y) { bit |= Passable::Up; }
+ return bit;
+}
+
+static bool WouldCollide(const Game_Character& self, const Game_Character& other, bool self_conflict) {
+ if (self.GetThrough() || other.GetThrough()) {
+ return false;
+ }
+
+ if (self.IsFlying() || other.IsFlying()) {
+ return false;
+ }
+
+ if (!self.IsActive() || !other.IsActive()) {
+ return false;
+ }
+
+ if (self.GetType() == Game_Character::Event
+ && other.GetType() == Game_Character::Event
+ && (self.IsOverlapForbidden() || other.IsOverlapForbidden())) {
+ return true;
+ }
+
+ if (other.GetLayer() == lcf::rpg::EventPage::Layers_same && self_conflict) {
+ return true;
+ }
+
+ if (self.GetLayer() == other.GetLayer()) {
+ return true;
+ }
+
+ return false;
+}
+
+bool Game_Map::WouldCollideWithCharacter(const Game_Character& self, const Game_Character& other, bool self_conflict) { // TODO - PIXELMOVE
+ if (&self == &other) {
+ return false;
+ }
+ return WouldCollide(self, other, self_conflict);
+} // END - PIXELMOVE
+
+
+template
+static void MakeWayUpdate(T& other) {
+ other.Update();
+}
+
+static void MakeWayUpdate(Game_Event& other) {
+ other.Update(false);
+}
+
+template
+static bool CheckWayTestCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) {
+ if (&self == &other) {
+ return false;
+ }
+
+ if (!other.IsInPosition(x, y)) {
+ return false;
+ }
+
+ return WouldCollide(self, other, self_conflict);
+}
+
+template
+static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) {
+ if (&self == &other) {
+ return false;
+ }
+
+ if (!other.IsInPosition(x, y)) {
+ return false;
+ }
+
+ // Force the other event to update, allowing them to possibly move out of the way.
+ MakeWayUpdate(other);
+
+ if (!other.IsInPosition(x, y)) {
+ return false;
+ }
+
+ return WouldCollide(self, other, self_conflict);
+}
+
+static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) {
+ if (ch) {
+ if (ch->GetType() == Game_Character::Vehicle) {
+ return static_cast(static_cast(ch)->GetVehicleType());
+ }
+ // ADDED: Check if the character is the player and if they are in a vehicle.
+ if (ch->GetType() == Game_Character::Player) {
+ return static_cast(static_cast(ch)->GetVehicleType());
+ }
+ }
+ return Game_Vehicle::None;
+}
+
+bool Game_Map::CheckWay(const Game_Character& self,
+ int from_x, int from_y,
+ int to_x, int to_y
+ )
+{
+ return CheckOrMakeWayEx(
+ self, from_x, from_y, to_x, to_y, true, {}, false
+ );
+}
+
+bool Game_Map::CheckWay(const Game_Character& self,
+ int from_x, int from_y,
+ int to_x, int to_y,
+ bool check_events_and_vehicles,
+ Span ignore_some_events_by_id) {
+ return CheckOrMakeWayEx(
+ self, from_x, from_y, to_x, to_y,
+ check_events_and_vehicles,
+ ignore_some_events_by_id, false
+ );
+}
+
+bool Game_Map::CheckOrMakeWayEx(const Game_Character& self,
+ int from_x, int from_y,
+ int to_x, int to_y,
+ bool check_events_and_vehicles,
+ Span ignore_some_events_by_id,
+ bool make_way
+ )
+{
+ // Infer directions before we do any rounding.
+ const int bit_from = GetPassableMask(from_x, from_y, to_x, to_y);
+ const int bit_to = GetPassableMask(to_x, to_y, from_x, from_y);
+
+ // Now round for looping maps.
+ to_x = Game_Map::RoundX(to_x);
+ to_y = Game_Map::RoundY(to_y);
+
+ // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first!
+ if (!Game_Map::IsValid(to_x, to_y)) {
+ return false;
+ }
+
+ if (self.GetThrough()) {
+ return true;
+ }
+
+ const auto vehicle_type = GetCollisionVehicleType(&self);
+ bool self_conflict = false;
+
+ // Depending on whether we're supposed to call MakeWayCollideEvent
+ // (which might change the map) or not, choose what to call:
+ auto CheckOrMakeCollideEvent = [&](auto& other) {
+ if (make_way) {
+ return MakeWayCollideEvent(to_x, to_y, self, other, self_conflict);
+ } else {
+ return CheckWayTestCollideEvent(
+ to_x, to_y, self, other, self_conflict
+ );
+ }
+ };
+
+ if (!self.IsJumping()) {
+ // Check for self conflict.
+ // If this event has a tile graphic and the tile itself has passage blocked in the direction
+ // we want to move, flag it as "self conflicting" for use later.
+ if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) {
+ int tile_id = self.GetTileId();
+ if ((passages_up[tile_id] & bit_from) == 0) {
+ self_conflict = true;
+ }
+ }
+
+ if (vehicle_type == Game_Vehicle::None) {
+ // Check that we are allowed to step off of the current tile.
+ // Note: Vehicles can always step off a tile.
+
+ // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event.
+ // Round it for looping maps to ensure the check passes
+ // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving
+ // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it
+ // inbounds after the first move.
+ from_x = Game_Map::RoundX(from_x);
+ from_y = Game_Map::RoundY(from_y);
+ if (!IsPassableTile(&self, bit_from, from_x, from_y)) {
+ return false;
+ }
+ }
+ }
+ if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) {
+ // Check for collision with events on the target tile.
+ if (ignore_some_events_by_id.empty()) {
+ for (auto& other: GetEvents()) {
+ if (CheckOrMakeCollideEvent(other)) {
+ return false;
+ }
+ }
+ } else {
+ for (auto& other: GetEvents()) {
+ if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end())
+ continue;
+ if (CheckOrMakeCollideEvent(other)) {
+ return false;
+ }
+ }
+ }
+
+ auto& player = Main_Data::game_player;
+ if (player->GetVehicleType() == Game_Vehicle::None) {
+ if (CheckOrMakeCollideEvent(*Main_Data::game_player)) {
+ return false;
+ }
+ }
+ for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship}) {
+ auto& other = vehicles[vid - 1];
+ if (other.IsInCurrentMap()) {
+ if (CheckOrMakeCollideEvent(other)) {
+ return false;
+ }
+ }
+ }
+ auto& airship = vehicles[Game_Vehicle::Airship - 1];
+ if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) {
+ if (CheckOrMakeCollideEvent(airship)) {
+ return false;
+ }
+ }
+ }
+ int bit = bit_to;
+ if (self.IsJumping()) {
+ bit = Passable::Down | Passable::Up | Passable::Left | Passable::Right;
+ }
+
+ return IsPassableTile(
+ &self, bit, to_x, to_y, check_events_and_vehicles, true
+ );
+}
+
+bool Game_Map::MakeWay(const Game_Character& self,
+ int from_x, int from_y,
+ int to_x, int to_y
+ )
+{
+ return CheckOrMakeWayEx(
+ self, from_x, from_y, to_x, to_y, true, {}, true
+ );
+}
+
+
+bool Game_Map::CanLandAirship(int x, int y) {
+ if (!Game_Map::IsValid(x, y)) return false;
+
+ const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
+ if (!terrain) {
+ Output::Warning("CanLandAirship: Invalid terrain at ({}, {})", x, y);
+ return false;
+ }
+ if (!terrain->airship_land) {
+ return false;
+ }
+
+ for (auto& ev: events) {
+ if (ev.IsInPosition(x, y)
+ && ev.IsActive()
+ && ev.GetActivePage() != nullptr) {
+ return false;
+ }
+ }
+ for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship }) {
+ auto& vehicle = vehicles[vid - 1];
+ if (vehicle.IsInCurrentMap() && vehicle.IsInPosition(x, y)) {
+ return false;
+ }
+ }
+
+ const int bit = Passable::Down | Passable::Right | Passable::Left | Passable::Up;
+
+ int tile_index = x + y * GetTilesX();
+
+ if (!IsPassableLowerTile(bit, tile_index)) {
+ return false;
+ }
+
+ int tile_id = map->upper_layer[tile_index] - BLOCK_F;
+ tile_id = map_info.upper_tiles[tile_id];
+
+ return (passages_up[tile_id] & bit) != 0;
+}
+
+bool Game_Map::CanEmbarkShip(Game_Player& player, int x, int y) {
+ auto bit = GetPassableMask(player.GetX(), player.GetY(), x, y);
+ return IsPassableTile(&player, bit, player.GetX(), player.GetY());
+}
+
+bool Game_Map::CanDisembarkShip(Game_Player& player, int x, int y) {
+ if (!Game_Map::IsValid(x, y)) {
+ return false;
+ }
+
+ for (auto& ev: GetEvents()) {
+ if (ev.IsInPosition(x, y)
+ && ev.GetLayer() == lcf::rpg::EventPage::Layers_same
+ && ev.IsActive()
+ && ev.GetActivePage() != nullptr) {
+ return false;
+ }
+ }
+
+ int bit = GetPassableMask(x, y, player.GetX(), player.GetY());
+
+ return IsPassableTile(nullptr, bit, x, y);
+}
+
+bool Game_Map::IsPassableLowerTile(int bit, int tile_index) {
+ int tile_raw_id = map->lower_layer[tile_index];
+ int tile_id = 0;
+
+ if (tile_raw_id >= BLOCK_E) {
+ tile_id = tile_raw_id - BLOCK_E;
+ tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX;
+
+ } else if (tile_raw_id >= BLOCK_D) {
+ tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX;
+ int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE;
+
+ if (((passages_down[tile_id] & Passable::Wall) != 0) && (
+ (autotile_id >= 20 && autotile_id <= 23) ||
+ (autotile_id >= 33 && autotile_id <= 37) ||
+ autotile_id == 42 || autotile_id == 43 ||
+ autotile_id == 45 || autotile_id == 46))
+ return true;
+
+ } else if (tile_raw_id >= BLOCK_C) {
+ tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX;
+
+ } else if (map->lower_layer[tile_index] < BLOCK_C) {
+ tile_id = tile_raw_id / BLOCK_B_STRIDE;
+ }
+
+ return (passages_down[tile_id] & bit) != 0;
+}
+
+bool Game_Map::IsPassableTile(
+ const Game_Character* self, int bit, int x, int y
+ ) {
+ return IsPassableTile(
+ self, bit, x, y, true, true
+ );
+}
+
+bool Game_Map::IsPassableTile(
+ const Game_Character* self, int bit, int x, int y,
+ bool check_events_and_vehicles, bool check_map_geometry
+ ) {
+ if (!IsValid(x, y)) return false;
+
+ const auto vehicle_type = GetCollisionVehicleType(self);
+ if (check_events_and_vehicles) {
+ if (vehicle_type != Game_Vehicle::None) {
+ const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
+ if (!terrain) {
+ Output::Warning("IsPassableTile: Invalid terrain at ({}, {})", x, y);
+ return false;
+ }
+ if (vehicle_type == Game_Vehicle::Boat && !terrain->boat_pass) {
+ return false;
+ }
+ if (vehicle_type == Game_Vehicle::Ship && !terrain->ship_pass) {
+ return false;
+ }
+ if (vehicle_type == Game_Vehicle::Airship) {
+ return terrain->airship_pass;
+ }
+ }
+
+ // Highest ID event with layer=below, not through, and a tile graphic wins.
+ int event_tile_id = 0;
+ for (auto& ev: events) {
+ if (self == &ev) {
+ continue;
+ }
+ if (!ev.IsActive() || ev.GetActivePage() == nullptr || ev.GetThrough()) {
+ continue;
+ }
+ if (ev.IsInPosition(x, y) && ev.GetLayer() == lcf::rpg::EventPage::Layers_below) {
+ if (ev.HasTileSprite()) {
+ event_tile_id = ev.GetTileId();
+ }
+ }
+ }
+
+ // If there was a below tile event, and the tile is not above
+ // Override the chipset with event tile behavior.
+ if (event_tile_id > 0
+ && ((passages_up[event_tile_id] & Passable::Above) == 0)) {
+ switch (vehicle_type) {
+ case Game_Vehicle::None:
+ return ((passages_up[event_tile_id] & bit) != 0);
+ case Game_Vehicle::Boat:
+ case Game_Vehicle::Ship:
+ return false;
+ case Game_Vehicle::Airship:
+ break;
+ };
+ }
+ }
+
+ if (check_map_geometry) {
+ int tile_index = x + y * GetTilesX();
+ // --- MODIFIED BLOCK START ---
+ int tile_raw_id = map->upper_layer[tile_index];
+
+ // If the tile on the upper layer is actually a Lower Layer tile (A-E)
+ if (tile_raw_id < BLOCK_F) {
+ // Reuse the lower layer passability logic for this specific tile ID
+ // We pass the tile_index, but IsPassableLowerTile usually looks at map->lower_layer.
+ // We need to verify IsPassableLowerTile handles arbitrary IDs or if we need to copy logic.
+
+ // IsPassableLowerTile reads directly from map->lower_layer[tile_index].
+ // We can't use it directly without modification because we want to test 'tile_raw_id'.
+ // So we replicate the LowerTile logic here for the upper layer slot:
+
+ int tile_id = 0;
+ if (tile_raw_id >= BLOCK_E) {
+ tile_id = tile_raw_id - BLOCK_E;
+ tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX;
+ } else if (tile_raw_id >= BLOCK_D) {
+ tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX;
+ int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE;
+
+ // Wall check logic for autotiles
+ if (((passages_down[tile_id] & Passable::Wall) != 0) && (
+ (autotile_id >= 20 && autotile_id <= 23) ||
+ (autotile_id >= 33 && autotile_id <= 37) ||
+ autotile_id == 42 || autotile_id == 43 ||
+ autotile_id == 45 || autotile_id == 46))
+ return true; // Walls block "bit" check below, effectively returning false for movement
+ } else if (tile_raw_id >= BLOCK_C) {
+ tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX;
+ } else {
+ tile_id = tile_raw_id / BLOCK_B_STRIDE;
+ }
+
+ // Check collision
+ if (vehicle_type == Game_Vehicle::Boat || vehicle_type == Game_Vehicle::Ship) {
+ // Boats can only pass if it's NOT an "Above" tile (Star)
+ // But for lower tiles, Star usually means "Overhead", so boats pass UNDER it?
+ // Standard behavior: Boats fail if not Star.
+ if ((passages_down[tile_id] & Passable::Above) == 0) return false;
+ return true;
+ }
+
+ if ((passages_down[tile_id] & bit) == 0) return false;
+
+ // If it's a Star tile on the upper layer, we treat it as passable but check the layer below
+ if ((passages_down[tile_id] & Passable::Above) == 0) return true;
+
+ } else {
+ // Standard Upper Layer Logic (Block F)
+ int tile_id = tile_raw_id - BLOCK_F;
+ tile_id = map_info.upper_tiles[tile_id];
+
+ if (vehicle_type == Game_Vehicle::Boat || vehicle_type == Game_Vehicle::Ship) {
+ if ((passages_up[tile_id] & Passable::Above) == 0)
+ return false;
+ return true;
+ }
+
+ if ((passages_up[tile_id] & bit) == 0)
+ return false;
+
+ if ((passages_up[tile_id] & Passable::Above) == 0)
+ return true;
+ }
+ // --- MODIFIED BLOCK END ---
+
+ return IsPassableLowerTile(bit, tile_index);
+ } else {
+ return true;
+ }
+}
+
+int Game_Map::GetBushDepth(int x, int y) {
+ if (!Game_Map::IsValid(x, y)) return 0;
+
+ const lcf::rpg::Terrain* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x,y));
+ if (!terrain) {
+ Output::Warning("GetBushDepth: Invalid terrain at ({}, {})", x, y);
+ return 0;
+ }
+ return terrain->bush_depth;
+}
+
+bool Game_Map::IsCounter(int x, int y) {
+ if (!Game_Map::IsValid(x, y)) return false;
+
+ int const tile_id = map->upper_layer[x + y * GetTilesX()];
+ if (tile_id < BLOCK_F) return false;
+ int const index = map_info.upper_tiles[tile_id - BLOCK_F];
+ return !!(passages_up[index] & Passable::Counter);
+}
+
+int Game_Map::GetTerrainTag(int x, int y) {
+ if (!chipset) {
+ // FIXME: Is this ever possible?
+ return 1;
+ }
+
+ auto& terrain_data = chipset->terrain_data;
+
+ if (terrain_data.empty()) {
+ // RPG_RT optimisation: When the terrain is all 1, no terrain data is stored
+ return 1;
+ }
+
+ // Terrain tag wraps on looping maps
+ if (Game_Map::LoopHorizontal()) {
+ x = RoundX(x);
+ }
+ if (Game_Map::LoopVertical()) {
+ y = RoundY(y);
+ }
+
+ // RPG_RT always uses the terrain of the first lower tile
+ // for out of bounds coordinates.
+ unsigned chip_index = 0;
+
+ if (Game_Map::IsValid(x, y)) {
+ const auto chip_id = map->lower_layer[x + y * GetTilesX()];
+ chip_index = ChipIdToIndex(chip_id);
+
+ // Apply tile substitution
+ if (chip_index >= BLOCK_E_INDEX && chip_index < NUM_LOWER_TILES) {
+ chip_index = map_info.lower_tiles[chip_index - BLOCK_E_INDEX] + BLOCK_E_INDEX;
+ }
+ }
+
+ assert(chip_index < terrain_data.size());
+
+ return terrain_data[chip_index];
+}
+
+Game_Event* Game_Map::GetEventAt(int x, int y, bool require_active) {
+ auto& events = GetEvents();
+ for (auto iter = events.rbegin(); iter != events.rend(); ++iter) {
+ auto& ev = *iter;
+ if (ev.IsInPosition(x, y) && (!require_active || ev.IsActive())) {
+ return &ev;
+ }
+ }
+ return nullptr;
+}
+
+bool Game_Map::LoopHorizontal() {
+ return map->scroll_type == lcf::rpg::Map::ScrollType_horizontal || map->scroll_type == lcf::rpg::Map::ScrollType_both;
+}
+
+bool Game_Map::LoopVertical() {
+ return map->scroll_type == lcf::rpg::Map::ScrollType_vertical || map->scroll_type == lcf::rpg::Map::ScrollType_both;
+}
+
+int Game_Map::RoundX(int x, int units) {
+ if (LoopHorizontal()) {
+ return Utils::PositiveModulo(x, GetTilesX() * units);
+ } else {
+ return x;
+ }
+}
+
+int Game_Map::RoundY(int y, int units) {
+ if (LoopVertical()) {
+ return Utils::PositiveModulo(y, GetTilesY() * units);
+ } else {
+ return y;
+ }
+}
+
+int Game_Map::RoundDx(int dx, int units) {
+ if (LoopHorizontal()) {
+ return Utils::PositiveModulo(std::abs(dx), GetTilesX() * units) * Utils::Sign(dx);
+ } else {
+ return dx;
+ }
+}
+
+int Game_Map::RoundDy(int dy, int units) {
+ if (LoopVertical()) {
+ return Utils::PositiveModulo(std::abs(dy), GetTilesY() * units) * Utils::Sign(dy);
+ } else {
+ return dy;
+ }
+}
+
+int Game_Map::XwithDirection(int x, int direction) {
+ return RoundX(x + (direction == lcf::rpg::EventPage::Direction_right ? 1 : direction == lcf::rpg::EventPage::Direction_left ? -1 : 0));
+}
+
+int Game_Map::YwithDirection(int y, int direction) {
+ return RoundY(y + (direction == lcf::rpg::EventPage::Direction_down ? 1 : direction == lcf::rpg::EventPage::Direction_up ? -1 : 0));
+}
+
+int Game_Map::CheckEvent(int x, int y) {
+ for (const Game_Event& ev : events) {
+ if (ev.IsInPosition(x, y)) {
+ return ev.GetId();
+ }
+ }
+
+ return 0;
+}
+
+void Game_Map::Update(MapUpdateAsyncContext& actx, bool is_preupdate) {
+ if (GetNeedRefresh()) {
+ Refresh();
+ }
+
+ if (!actx.IsActive()) {
+ //If not resuming from async op ...
+ UpdateProcessedFlags(is_preupdate);
+ }
+
+ if (!actx.IsActive() || actx.IsParallelCommonEvent()) {
+ if (!UpdateCommonEvents(actx)) {
+ // Suspend due to common event async op ...
+ return;
+ }
+ }
+
+ if (!actx.IsActive() || actx.IsParallelMapEvent()) {
+ if (!UpdateMapEvents(actx)) {
+ // Suspend due to map event async op ...
+ return;
+ }
+ }
+
+ if (is_preupdate) {
+ return;
+ }
+
+ if (!actx.IsActive()) {
+ //If not resuming from async op ...
+ Main_Data::game_player->Update();
+
+ for (auto& vehicle: vehicles) {
+ if (vehicle.GetMapId() == GetMapId()) {
+ vehicle.Update();
+ }
+ }
+ }
+
+ if (!actx.IsActive() || actx.IsMessage()) {
+ if (!UpdateMessage(actx)) {
+ // Suspend due to message async op ...
+ return;
+ }
+ }
+
+ if (!actx.IsActive()) {
+ Main_Data::game_party->UpdateTimers();
+ Main_Data::game_screen->Update();
+ Main_Data::game_pictures->Update(false);
+ }
+
+ if (!actx.IsActive() || actx.IsForegroundEvent()) {
+ if (!UpdateForegroundEvents(actx)) {
+ // Suspend due to foreground event async op ...
+ return;
+ }
+ }
+
+ Parallax::Update();
+
+ if (isMode7) {
+ UpdateMode7();
+ }
+
+ actx = {};
+}
+
+void Game_Map::UpdateMode7() {
+ if (mode7SlantSpeed > 0) {
+ if (mode7SlantTarget > mode7Slant) {
+ mode7Slant += mode7SlantSpeed;
+ if (mode7SlantTarget <= mode7Slant) {
+ mode7Slant = mode7SlantTarget;
+ mode7SlantSpeed = 0;
+ }
+ }
+ else {
+ mode7Slant -= mode7SlantSpeed;
+ if (mode7SlantTarget >= mode7Slant) {
+ mode7Slant = mode7SlantTarget;
+ mode7SlantSpeed = 0;
+ }
+ }
+ }
+ if (mode7YawSpeed > 0) {
+ float tt = mode7YawTarget;
+ float left = (mode7Yaw < tt) ? 360 - tt + mode7Yaw : mode7Yaw - tt;
+ float right = (mode7Yaw < tt) ? tt - mode7Yaw : 360 - mode7Yaw + tt;
+
+ bool rotLeft = (left < right);
+
+ if (rotLeft) {
+ mode7Yaw -= mode7YawSpeed;
+ if (mode7Yaw < 0) mode7Yaw += 360;
+
+ // Check if we passed the target (handling wraparound)
+ float newDist = (mode7Yaw < tt) ? 360 - tt + mode7Yaw : mode7Yaw - tt;
+ if (newDist > left) { // Distance increased means we passed it
+ mode7Yaw = mode7YawTarget;
+ mode7YawSpeed = 0;
+ }
+ }
+ else {
+ mode7Yaw += mode7YawSpeed;
+ if (mode7Yaw >= 360) mode7Yaw -= 360;
+
+ // Check if we passed the target
+ float newDist = (mode7Yaw < tt) ? tt - mode7Yaw : 360 - mode7Yaw + tt;
+ if (newDist > right) {
+ mode7Yaw = mode7YawTarget;
+ mode7YawSpeed = 0;
+ }
+ }
+ }
+}
+
+
+void Game_Map::UpdateProcessedFlags(bool is_preupdate) {
+ for (Game_Event& ev : events) {
+ ev.SetProcessed(false);
+ }
+ if (!is_preupdate) {
+ Main_Data::game_player->SetProcessed(false);
+ for (auto& vehicle: vehicles) {
+ if (vehicle.IsInCurrentMap()) {
+ vehicle.SetProcessed(false);
+ }
+ }
+ }
+}
+
+
+bool Game_Map::UpdateCommonEvents(MapUpdateAsyncContext& actx) {
+ int resume_ce = actx.GetParallelCommonEvent();
+
+ for (Game_CommonEvent& ev : common_events) {
+ bool resume_async = false;
+ if (resume_ce != 0) {
+ // If resuming, skip all until the event to resume from ..
+ if (ev.GetIndex() != resume_ce) {
+ continue;
+ } else {
+ resume_ce = 0;
+ resume_async = true;
+ }
+ }
+
+ auto aop = ev.Update(resume_async);
+ if (aop.IsActive()) {
+ // Suspend due to this event ..
+ actx = MapUpdateAsyncContext::FromCommonEvent(ev.GetIndex(), aop);
+ return false;
+ }
+ }
+
+ actx = {};
+ return true;
+}
+
+bool Game_Map::UpdateMapEvents(MapUpdateAsyncContext& actx) {
+ int resume_ev = actx.GetParallelMapEvent();
+
+ for (Game_Event& ev : events) {
+ bool resume_async = false;
+ if (resume_ev != 0) {
+ // If resuming, skip all until the event to resume from ..
+ if (ev.GetId() != resume_ev) {
+ continue;
+ } else {
+ resume_ev = 0;
+ resume_async = true;
+ }
+ }
+
+ auto aop = ev.Update(resume_async);
+ if (aop.IsActive()) {
+ // Suspend due to this event ..
+ actx = MapUpdateAsyncContext::FromMapEvent(ev.GetId(), aop);
+ return false;
+ }
+ }
+
+ actx = {};
+ return true;
+}
+
+bool Game_Map::UpdateMessage(MapUpdateAsyncContext& actx) {
+ // Message system does not support suspend and resume internally. So if the last frame the message
+ // produced an async event, the message loop finished completely. Therefore this frame we should
+ // resume *after* the message and not run it again.
+ if (!actx.IsActive()) {
+ auto aop = Game_Message::Update();
+ if (aop.IsActive()) {
+ actx = MapUpdateAsyncContext::FromMessage(aop);
+ return false;
+ }
+ }
+
+ actx = {};
+ return true;
+}
+
+bool Game_Map::UpdateForegroundEvents(MapUpdateAsyncContext& actx) {
+ auto& interp = GetInterpreter();
+
+ // If we resume from async op, we don't clear the loop index.
+ const bool resume_fg = actx.IsForegroundEvent();
+
+ // Run any event loaded from last frame.
+ interp.Update(!resume_fg);
+ if (interp.IsAsyncPending()) {
+ // Suspend due to this event ..
+ actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp());
+ return false;
+ }
+
+ while (!interp.IsRunning() && !interp.ReachedLoopLimit()) {
+ interp.Clear();
+
+ // This logic is probably one big loop in RPG_RT. We have to replicate
+ // it here because once we stop executing from this we should not
+ // clear anymore waiting flags.
+ if (Scene::instance->HasRequestedScene() && interp.GetLoopCount() > 0) {
+ break;
+ }
+ Game_CommonEvent* run_ce = nullptr;
+
+ for (auto& ce: common_events) {
+ if (ce.IsWaitingForegroundExecution()) {
+ run_ce = &ce;
+ break;
+ }
+ }
+ if (run_ce) {
+ interp.Push(run_ce);
+ }
+
+ Game_Event* run_ev = nullptr;
+ for (auto& ev: events) {
+ if (ev.IsWaitingForegroundExecution()) {
+ if (!ev.IsActive()) {
+ ev.ClearWaitingForegroundExecution();
+ continue;
+ }
+ run_ev = &ev;
+ break;
+ }
+ }
+ if (run_ev) {
+ if (run_ev->WasStartedByDecisionKey()) {
+ interp.Push(run_ev);
+ } else {
+ switch (run_ev->GetTrigger()) {
+ case lcf::rpg::EventPage::Trigger_touched:
+ interp.Push(run_ev);
+ break;
+ case lcf::rpg::EventPage::Trigger_collision:
+ interp.Push(run_ev);
+ break;
+ case lcf::rpg::EventPage::Trigger_auto_start:
+ interp.Push(run_ev);
+ break;
+ case lcf::rpg::EventPage::Trigger_action:
+ default:
+ interp.Push(run_ev);
+ break;
+ }
+ }
+ run_ev->ClearWaitingForegroundExecution();
+ }
+
+ // If no events to run we're finished.
+ if (!interp.IsRunning()) {
+ break;
+ }
+
+ interp.Update(false);
+ if (interp.IsAsyncPending()) {
+ // Suspend due to this event ..
+ actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp());
+ return false;
+ }
+ }
+
+ actx = {};
+ return true;
+}
+
+lcf::rpg::MapInfo const& Game_Map::GetMapInfo() {
+ return GetMapInfo(GetMapId());
+}
+
+lcf::rpg::MapInfo const& Game_Map::GetMapInfo(int map_id) {
+ for (const auto& mi: lcf::Data::treemap.maps) {
+ if (mi.ID == map_id) {
+ return mi;
+ }
+ }
+
+ Output::Debug("Map {} not in Maptree", map_id);
+ return empty_map_info;
+}
+
+const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo() {
+ return GetParentMapInfo(GetMapInfo());
+}
+
+const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo(const lcf::rpg::MapInfo& map_info) {
+ return GetMapInfo(map_info.parent_map);
+}
+
+lcf::rpg::Map const& Game_Map::GetMap() {
+ return *map;
+}
+
+int Game_Map::GetMapId() {
+ return Main_Data::game_player->GetMapId();
+}
+
+void Game_Map::PrintPathToMap() {
+ const auto* current_info = &GetMapInfo();
+ std::ostringstream ss;
+ ss << current_info->name;
+
+ current_info = &GetParentMapInfo(*current_info);
+ while (current_info->ID != 0 && current_info->ID != GetMapId()) {
+ ss << " < " << current_info->name;
+ current_info = &GetParentMapInfo(*current_info);
+ }
+
+ Output::Debug("Tree: {}", ss.str());
+}
+
+int Game_Map::GetTilesX() {
+ return map->width;
+}
+
+int Game_Map::GetTilesY() {
+ return map->height;
+}
+
+int Game_Map::GetOriginalEncounterSteps() {
+ return GetMapInfo().encounter_steps;
+}
+
+int Game_Map::GetEncounterSteps() {
+ return map_info.encounter_steps;
+}
+
+int Game_Map::GetMoveDirection(int dir) {
+ if (dir == 0) return 0;
+ if (isMode7) {
+ int idx = 0;
+ for (int i = 0; i < 8; i++) {
+ if (INPUT8_VALUES[i] == dir) {
+ idx = i;
+ break;
+ }
+ }
+
+ float yaw = mode7Yaw;
+ yaw = fmodf(yaw + 22.5f, 360.0f);
+ if (yaw < 0) yaw += 360.0f; // Handle negative result from fmodf
+
+ idx = static_cast(idx + (yaw / 45.0f)) % 8;
+
+ dir = INPUT8_VALUES[idx];
+ }
+ return dir;
+}
+
+int Game_Map::GetGraphicDirection(int d) {
+ if (isMode7) {
+ float yaw = mode7Yaw;
+ yaw = fmodf(yaw + 22.5f, 360.0f);
+ if (yaw < 0) yaw += 360.0f;
+
+ int idx = (d + static_cast(yaw / 90.0f)) % 4;
+ return idx;
+ }
+ return d;
+}
+
+bool Game_Map::GetIsMode7() {
+ return isMode7;
+}
+
+void Game_Map::SetIsMode7(bool v) {
+ isMode7 = v;
+}
+
+float Game_Map::GetMode7Slant() {
+ return mode7Slant;
+}
+
+void Game_Map::TiltMode7(int v) {
+ // Clear any active transition first
+ mode7SlantSpeed = 0;
+ SetMode7Slant(static_cast(mode7Slant * 100) + v);
+}
+
+void Game_Map::TiltTowardsMode7(int v, int duration) {
+ float vv = v / 100.0f;
+ mode7SlantTarget = vv;
+ float delta = abs(mode7Slant - mode7SlantTarget);
+ mode7SlantSpeed = (duration > 0) ? delta / duration : delta;
+}
+
+void Game_Map::SetMode7Slant(int v) {
+ // Clear any active transition
+ mode7SlantSpeed = 0;
+
+ float vv = v / 100.0f;
+ mode7Slant = vv;
+ if (mode7Slant < 25) mode7Slant = 25;
+ if (mode7Slant > 90) mode7Slant = 90;
+}
+
+float Game_Map::GetMode7Yaw() {
+ return mode7Yaw;
+}
+
+void Game_Map::RotateMode7(int v) {
+ // Clear any active transition
+ mode7YawSpeed = 0;
+
+ float vv = v / 100.0f;
+ mode7Yaw += vv;
+ while (mode7Yaw >= 360.0f) mode7Yaw -= 360.0f;
+ while (mode7Yaw < 0.0f) mode7Yaw += 360.0f;
+}
+
+void Game_Map::RotateTowardsMode7(int v, int duration) {
+ float vv = v / 100.0f;
+ // Normalize target to [0, 360)
+ while (vv >= 360.0f) vv -= 360.0f;
+ while (vv < 0.0f) vv += 360.0f;
+ mode7YawTarget = vv;
+
+ // Calculate shortest path
+ float diff = mode7YawTarget - mode7Yaw;
+ while (diff <= -180.0f) diff += 360.0f;
+ while (diff > 180.0f) diff -= 360.0f;
+
+ // Set speed based on absolute difference
+ mode7YawSpeed = (duration > 0) ? std::abs(diff) / duration : std::abs(diff);
+}
+
+void Game_Map::SetMode7Yaw(int v) {
+ // Clear any active transition
+ mode7YawSpeed = 0;
+
+ float vv = v / 100.0f;
+ mode7Yaw = vv;
+ while (mode7Yaw < 0) mode7Yaw += 360;
+ while (mode7Yaw >= 360) mode7Yaw -= 360;
+}
+
+int Game_Map::GetMode7Horizon() {
+ return mode7Horizon;
+}
+
+int Game_Map::GetMode7Baseline() {
+ return 4;
+}
+
+double Game_Map::GetMode7Scale() {
+ return mode7Scale;
+}
+
+void Game_Map::SetMode7Scale(int scale_factor) {
+ // Value is passed as an integer multiplied by 100.
+ mode7Scale = scale_factor / 100.0;
+ if (mode7Scale <= 0) {
+ mode7Scale = 0.1; // Prevent division by zero or negative values.
+ }
+}
+
+
+void Game_Map::RefreshMode7() {
+ isMode7 = false;
+ const auto* current_info = &GetMapInfo();
+ std::string s = current_info->name.data();
+ int v = s.find("[M7]");
+ if (v != std::string::npos) {
+ isMode7 = true;
+ mode7Yaw = 0;
+ printf("Mode7 Enabled!");
+ }
+}
+
+
+
+
+void Game_Map::SetEncounterSteps(int step) {
+ if (step < 0) {
+ step = GetOriginalEncounterSteps();
+ }
+ map_info.encounter_steps = step;
+}
+
+std::vector Game_Map::GetEncountersAt(int x, int y) {
+ int terrain_tag = GetTerrainTag(Main_Data::game_player->GetX(), Main_Data::game_player->GetY());
+
+ std::function is_acceptable = [=](int troop_id) {
+ const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id);
+ if (!troop) {
+ Output::Warning("GetEncountersAt: Invalid troop ID {} in encounter list", troop_id);
+ return false;
+ }
+
+ const auto& terrain_set = troop->terrain_set;
+
+ // RPG_RT optimisation: Omitted entries are the default value (true)
+ return terrain_set.size() <= (unsigned)(terrain_tag - 1) ||
+ terrain_set[terrain_tag - 1];
+ };
+
+ std::vector out;
+
+ for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) {
+ lcf::rpg::MapInfo& map = lcf::Data::treemap.maps[i];
+
+ if (map.ID == GetMapId()) {
+ for (const auto& enc : map.encounters) {
+ if (is_acceptable(enc.troop_id)) {
+ out.push_back(enc.troop_id);
+ }
+ }
+ } else if (map.parent_map == GetMapId() && map.type == lcf::rpg::TreeMap::MapType_area) {
+ // Area
+ Rect area_rect(map.area_rect.l, map.area_rect.t, map.area_rect.r - map.area_rect.l, map.area_rect.b - map.area_rect.t);
+ Rect player_rect(x, y, 1, 1);
+
+ if (!player_rect.IsOutOfBounds(area_rect)) {
+ for (const lcf::rpg::Encounter& enc : map.encounters) {
+ if (is_acceptable(enc.troop_id)) {
+ out.push_back(enc.troop_id);
+ }
+ }
+ }
+ }
+ }
+
+ return out;
+}
+
+static void OnEncounterEnd(BattleResult result) {
+ if (result != BattleResult::Defeat) {
+ return;
+ }
+
+ if (!Game_Battle::HasDeathHandler()) {
+ Scene::Push(std::make_shared());
+ return;
+ }
+
+ //2k3 death handler
+
+ auto* ce = lcf::ReaderUtil::GetElement(common_events, Game_Battle::GetDeathHandlerCommonEvent());
+ if (ce) {
+ auto& interp = Game_Map::GetInterpreter();
+ interp.Push(ce);
+ }
+
+ auto tt = Game_Battle::GetDeathHandlerTeleport();
+ if (tt.IsActive()) {
+ Main_Data::game_player->ReserveTeleport(tt.GetMapId(), tt.GetX(), tt.GetY(), tt.GetDirection(), tt.GetType());
+ }
+}
+
+bool Game_Map::PrepareEncounter(BattleArgs& args) {
+ int x = Main_Data::game_player->GetX();
+ int y = Main_Data::game_player->GetY();
+
+ std::vector encounters = GetEncountersAt(x, y);
+
+ if (encounters.empty()) {
+ // No enemies on this map :(
+ return false;
+ }
+
+ args.troop_id = encounters[Rand::GetRandomNumber(0, encounters.size() - 1)];
+
+ if (RuntimePatches::EncounterRandomnessAlert::HandleEncounter(args.troop_id)) {
+ //Cancel the battle setup
+ return false;
+ }
+
+ if (Feature::HasRpg2kBattleSystem()) {
+ if (Rand::ChanceOf(1, 32)) {
+ args.first_strike = true;
+ }
+ } else {
+ const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y));
+ if (!terrain) {
+ Output::Warning("PrepareEncounter: Invalid terrain at ({}, {})", x, y);
+ } else {
+ if (terrain->special_flags.back_party && Rand::PercentChance(terrain->special_back_party)) {
+ args.condition = lcf::rpg::System::BattleCondition_initiative;
+ } else if (terrain->special_flags.back_enemies && Rand::PercentChance(terrain->special_back_enemies)) {
+ args.condition = lcf::rpg::System::BattleCondition_back;
+ } else if (terrain->special_flags.lateral_party && Rand::PercentChance(terrain->special_lateral_party)) {
+ args.condition = lcf::rpg::System::BattleCondition_surround;
+ } else if (terrain->special_flags.lateral_enemies && Rand::PercentChance(terrain->special_lateral_enemies)) {
+ args.condition = lcf::rpg::System::BattleCondition_pincers;
+ }
+ }
+ }
+
+ SetupBattle(args);
+ args.on_battle_end = OnEncounterEnd;
+ args.allow_escape = true;
+
+ return true;
+}
+
+void Game_Map::SetupBattle(BattleArgs& args) {
+ int x = Main_Data::game_player->GetX();
+ int y = Main_Data::game_player->GetY();
+
+ args.terrain_id = GetTerrainTag(x, y);
+
+ const auto* current_info = &GetMapInfo();
+ while (current_info->background_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) {
+ current_info = &GetParentMapInfo(*current_info);
+ }
+
+ if (current_info->background_type == 2) {
+ args.background = ToString(current_info->background_name);
+ }
+}
+
+std::vector& Game_Map::GetMapDataDown() {
+ return map->lower_layer;
+}
+
+std::vector& Game_Map::GetMapDataUp() {
+ return map->upper_layer;
+}
+
+int Game_Map::GetOriginalChipset() {
+ return map != nullptr ? map->chipset_id : 0;
+}
+
+int Game_Map::GetChipset() {
+ return chipset != nullptr ? chipset->ID : 0;
+}
+
+std::string_view Game_Map::GetChipsetName() {
+ return chipset != nullptr
+ ? std::string_view(chipset->chipset_name)
+ : std::string_view("");
+}
+
+int Game_Map::GetPositionX() {
+ return map_info.position_x;
+}
+
+int Game_Map::GetDisplayX() {
+ return map_info.position_x + Main_Data::game_screen->GetShakeOffsetX() * 16;
+}
+
+void Game_Map::SetPositionX(int x, bool reset_panorama) {
+ const int map_width = GetTilesX() * SCREEN_TILE_SIZE;
+ if (LoopHorizontal()) {
+ x = Utils::PositiveModulo(x, map_width);
+ } else {
+ // Do not use std::clamp here. When the map is smaller than the screen the
+ // upper bound is smaller than the lower bound making the function fail.
+ x = std::max(0, std::min(map_width - screen_width, x));
+ }
+ map_info.position_x = x;
+ if (reset_panorama) {
+ Parallax::SetPositionX(map_info.position_x);
+ Parallax::ResetPositionX();
+ }
+}
+
+int Game_Map::GetPositionY() {
+ return map_info.position_y;
+}
+
+int Game_Map::GetDisplayY() {
+ return map_info.position_y + Main_Data::game_screen->GetShakeOffsetY() * 16;
+}
+
+void Game_Map::SetPositionY(int y, bool reset_panorama) {
+ const int map_height = GetTilesY() * SCREEN_TILE_SIZE;
+ if (LoopVertical()) {
+ y = Utils::PositiveModulo(y, map_height);
+ } else {
+ // Do not use std::clamp here. When the map is smaller than the screen the
+ // upper bound is smaller than the lower bound making the function fail.
+ y = std::max(0, std::min(map_height - screen_height, y));
+ }
+ map_info.position_y = y;
+ if (reset_panorama) {
+ Parallax::SetPositionY(map_info.position_y);
+ Parallax::ResetPositionY();
+ }
+}
+
+bool Game_Map::GetNeedRefresh() {
+ int anti_lag_switch = Player::game_config.patch_anti_lag_switch.Get();
+ if (anti_lag_switch > 0 && Main_Data::game_switches->Get(anti_lag_switch)) {
+ return false;
+ }
+
+ return need_refresh;
+}
+
+void Game_Map::SetNeedRefresh(bool refresh) {
+ need_refresh = refresh;
+}
+
+void Game_Map::SetNeedRefreshForSwitchChange(int switch_id) {
+ if (need_refresh)
+ return;
+ if (map_cache->GetNeedRefresh(switch_id))
+ SetNeedRefresh(true);
+}
+
+void Game_Map::SetNeedRefreshForVarChange(int var_id) {
+ if (need_refresh)
+ return;
+ if (map_cache->GetNeedRefresh(var_id))
+ SetNeedRefresh(true);
+}
+
+void Game_Map::SetNeedRefreshForSwitchChange(std::initializer_list switch_ids) {
+ for (auto switch_id: switch_ids) {
+ SetNeedRefreshForSwitchChange(switch_id);
+ }
+}
+
+void Game_Map::SetNeedRefreshForVarChange(std::initializer_list var_ids) {
+ for (auto var_id: var_ids) {
+ SetNeedRefreshForVarChange(var_id);
+ }
+}
+
+std::vector& Game_Map::GetPassagesDown() {
+ return passages_down;
+}
+
+std::vector& Game_Map::GetPassagesUp() {
+ return passages_up;
+}
+
+int Game_Map::GetAnimationType() {
+ return animation_type;
+}
+
+int Game_Map::GetAnimationSpeed() {
+ return (animation_fast ? 12 : 24);
+}
+
+std::vector& Game_Map::GetEvents() {
+ return events;
+}
+
+int Game_Map::GetHighestEventId() {
+ int id = 0;
+ for (auto& ev: events) {
+ id = std::max(id, ev.GetId());
+ }
+ return id;
+}
+
+Game_Event* Game_Map::GetEvent(int event_id) {
+ auto it = std::find_if(events.begin(), events.end(),
+ [&event_id](Game_Event& ev) {return ev.GetId() == event_id;});
+ return it == events.end() ? nullptr : &(*it);
+}
+
+std::vector& Game_Map::GetCommonEvents() {
+ return common_events;
+}
+
+std::string_view Game_Map::GetMapName(int id) {
+ for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) {
+ if (lcf::Data::treemap.maps[i].ID == id) {
+ return lcf::Data::treemap.maps[i].name;
+ }
+ }
+ // nothing found
+ return {};
+}
+
+void Game_Map::SetChipset(int id) {
+ if (id == 0) {
+ // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset.
+ id = GetOriginalChipset();
+ }
+ map_info.chipset_id = id;
+
+ if (!ReloadChipset()) {
+ Output::Warning("SetChipset: Invalid chipset ID {}", map_info.chipset_id);
+ } else {
+ passages_down = chipset->passable_data_lower;
+ passages_up = chipset->passable_data_upper;
+ animation_type = chipset->animation_type;
+ animation_fast = chipset->animation_speed != 0;
+ }
+
+ if (passages_down.size() < 162)
+ passages_down.resize(162, (unsigned char) 0x0F);
+ if (passages_up.size() < 144)
+ passages_up.resize(144, (unsigned char) 0x0F);
+}
+
+bool Game_Map::ReloadChipset() {
+ chipset = lcf::ReaderUtil::GetElement(lcf::Data::chipsets, map_info.chipset_id);
+ if (!chipset) {
+ return false;
+ }
+ return true;
+}
+
+void Game_Map::OnTranslationChanged() {
+ ReloadChipset();
+ // Marks common events for reload on map change
+ // This is not save to do while they are executing
+ translation_changed = true;
+}
+
+Game_Vehicle* Game_Map::GetVehicle(Game_Vehicle::Type which) {
+ if (which == Game_Vehicle::Boat ||
+ which == Game_Vehicle::Ship ||
+ which == Game_Vehicle::Airship) {
+ return &vehicles[which - 1];
+ }
+
+ return nullptr;
+}
+
+bool Game_Map::IsAnyEventStarting() {
+ for (Game_Event& ev : events)
+ if (ev.IsWaitingForegroundExecution() && !ev.GetList().empty() && ev.IsActive())
+ return true;
+
+ for (Game_CommonEvent& ev : common_events)
+ if (ev.IsWaitingForegroundExecution())
+ return true;
+
+ return false;
+}
+
+bool Game_Map::IsAnyMovePending() {
+ auto check = [](auto& ev) {
+ return ev.IsMoveRouteOverwritten() && !ev.IsMoveRouteFinished();
+ };
+ const auto map_id = GetMapId();
+ if (check(*Main_Data::game_player)) {
+ return true;
+ }
+ for (auto& vh: vehicles) {
+ if (vh.GetMapId() == map_id && check(vh)) {
+ return true;
+ }
+ }
+ for (auto& ev: events) {
+ if (check(ev)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Game_Map::RemoveAllPendingMoves() {
+ const auto map_id = GetMapId();
+ Main_Data::game_player->CancelMoveRoute();
+ for (auto& vh: vehicles) {
+ if (vh.GetMapId() == map_id) {
+ vh.CancelMoveRoute();
+ }
+ }
+ for (auto& ev: events) {
+ ev.CancelMoveRoute();
+ }
+}
+
+static int DoSubstitute(std::vector& tiles, int old_id, int new_id) {
+ int num_subst = 0;
+ for (size_t i = 0; i < tiles.size(); ++i) {
+ if (tiles[i] == old_id) {
+ tiles[i] = (uint8_t) new_id;
+ ++num_subst;
+ }
+ }
+ return num_subst;
+}
+
+int Game_Map::SubstituteDown(int old_id, int new_id) {
+ return DoSubstitute(map_info.lower_tiles, old_id, new_id);
+}
+
+int Game_Map::SubstituteUp(int old_id, int new_id) {
+ return DoSubstitute(map_info.upper_tiles, old_id, new_id);
+}
+
+void Game_Map::ReplaceTileAt(int x, int y, int new_id, int layer) {
+ auto pos = x + y * map->width;
+ auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer;
+ layer_vec[pos] = static_cast(new_id);
+}
+
+int Game_Map::GetTileIdAt(int x, int y, int layer, bool chip_id_or_index) {
+ if (x < 0 || x >= map->width || y < 0 || y >= map->height) {
+ return 0; // Return 0 for out-of-bounds coordinates
+ }
+
+ auto pos = x + y * map->width;
+ auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer;
+
+ int tile_output = chip_id_or_index ? layer_vec[pos] : ChipIdToIndex(layer_vec[pos]);
+ if (layer >= 1) tile_output -= BLOCK_F_INDEX;
+
+ return tile_output;
+}
+
+std::vector Game_Map::GetTilesIdAt(Rect coords, int layer, bool chip_id_or_index) {
+ std::vector tiles_collection;
+ for (int i = 0; i < coords.height; ++i) {
+ for (int j = 0; j < coords.width; ++j) {
+ tiles_collection.emplace_back(Game_Map::GetTileIdAt(coords.x + j, coords.y + i, layer, chip_id_or_index));
+ }
+ }
+ return tiles_collection;
+}
+
+std::string Game_Map::ConstructMapName(int map_id, bool is_easyrpg) {
+ std::stringstream ss;
+ ss << "Map" << std::setfill('0') << std::setw(4) << map_id;
+ if (is_easyrpg) {
+ return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_EMU);
+ } else {
+ return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_LMU);
+ }
+}
+
+FileRequestAsync* Game_Map::RequestMap(int map_id) {
+#ifdef EMSCRIPTEN
+ Player::translation.RequestAndAddMap(map_id);
+#endif
+
+ auto* request = AsyncHandler::RequestFile(Game_Map::ConstructMapName(map_id, false));
+ request->SetImportantFile(true);
+ return request;
+}
+
+// MapEventCache
+//////////////////
+void Game_Map::Caching::MapEventCache::AddEvent(const lcf::rpg::Event& ev) {
+ auto id = ev.ID;
+
+ if (std::find(event_ids.begin(), event_ids.end(), id) == event_ids.end()) {
+ event_ids.emplace_back(id);
+ }
+}
+
+void Game_Map::Caching::MapEventCache::RemoveEvent(const lcf::rpg::Event& ev) {
+ auto id = ev.ID;
+
+ auto it = std::find(event_ids.begin(), event_ids.end(), id);
+
+ if (it != event_ids.end()) {
+ event_ids.erase(it);
+ }
+}
+
+// Parallax
+/////////////
+
+namespace {
+ int parallax_width;
+ int parallax_height;
+
+ bool parallax_fake_x;
+ bool parallax_fake_y;
+}
+
+/* Helper function to get the current parallax parameters. If the default
+ * parallax for the current map was overridden by a "Change Parallax BG"
+ * command, the result is filled out from those values in the SaveMapInfo.
+ * Otherwise, the result is filled out from the default for the current map.
+ */
+static Game_Map::Parallax::Params GetParallaxParams() {
+ Game_Map::Parallax::Params params = {};
+
+ if (!map_info.parallax_name.empty()) {
+ params.name = map_info.parallax_name;
+ params.scroll_horz = map_info.parallax_horz;
+ params.scroll_horz_auto = map_info.parallax_horz_auto;
+ params.scroll_horz_speed = map_info.parallax_horz_speed;
+ params.scroll_vert = map_info.parallax_vert;
+ params.scroll_vert_auto = map_info.parallax_vert_auto;
+ params.scroll_vert_speed = map_info.parallax_vert_speed;
+ } else if (map->parallax_flag) {
+ // Default case when map parallax hasn't been overwritten.
+ params.name = ToString(map->parallax_name);
+ params.scroll_horz = map->parallax_loop_x;
+ params.scroll_horz_auto = map->parallax_auto_loop_x;
+ params.scroll_horz_speed = map->parallax_sx;
+ params.scroll_vert = map->parallax_loop_y;
+ params.scroll_vert_auto = map->parallax_auto_loop_y;
+ params.scroll_vert_speed = map->parallax_sy;
+ } else {
+ // No BG; use default-constructed Param
+ }
+
+ return params;
+}
+
+std::string Game_Map::Parallax::GetName() {
+ return GetParallaxParams().name;
+}
+
+int Game_Map::Parallax::GetX() {
+ return (-panorama.pan_x / TILE_SIZE) / 2;
+}
+
+int Game_Map::Parallax::GetY() {
+ return (-panorama.pan_y / TILE_SIZE) / 2;
+}
+
+void Game_Map::Parallax::Initialize(int width, int height) {
+ parallax_width = width;
+ parallax_height = height;
+
+ if (panorama_on_map_init) {
+ SetPositionX(map_info.position_x);
+ SetPositionY(map_info.position_y);
+ }
+
+ if (reset_panorama_x_on_next_init) {
+ ResetPositionX();
+ }
+ if (reset_panorama_y_on_next_init) {
+ ResetPositionY();
+ }
+
+ if (Player::IsRPG2k() && !panorama_on_map_init) {
+ SetPositionX(panorama.pan_x);
+ SetPositionY(panorama.pan_y);
+ }
+
+ panorama_on_map_init = false;
+}
+
+void Game_Map::Parallax::AddPositionX(int off_x) {
+ SetPositionX(panorama.pan_x + off_x);
+}
+
+void Game_Map::Parallax::AddPositionY(int off_y) {
+ SetPositionY(panorama.pan_y + off_y);
+}
+
+void Game_Map::Parallax::SetPositionX(int x) {
+ // FIXME: Fixes a crash with ChangeBG commands in events, but not correct.
+ // Real fix TBD
+ if (parallax_width) {
+ const int w = parallax_width * TILE_SIZE * 2;
+ panorama.pan_x = (x + w) % w;
+ }
+}
+
+void Game_Map::Parallax::SetPositionY(int y) {
+ // FIXME: Fixes a crash with ChangeBG commands in events, but not correct.
+ // Real fix TBD
+ if (parallax_height) {
+ const int h = parallax_height * TILE_SIZE * 2;
+ panorama.pan_y = (y + h) % h;
+ }
+}
+
+void Game_Map::Parallax::ResetPositionX() {
+ Params params = GetParallaxParams();
+
+ if (params.name.empty()) {
+ return;
+ }
+
+ parallax_fake_x = false;
+
+ if (!params.scroll_horz && !LoopHorizontal()) {
+ int pan_screen_width = Player::screen_width;
+ if (Player::game_config.fake_resolution.Get()) {
+ pan_screen_width = SCREEN_TARGET_WIDTH;
+ }
+
+ int tiles_per_screen = pan_screen_width / TILE_SIZE;
+ if (pan_screen_width % TILE_SIZE != 0) {
+ ++tiles_per_screen;
+ }
+
+ if (GetTilesX() > tiles_per_screen && parallax_width > pan_screen_width) {
+ const int w = (GetTilesX() - tiles_per_screen) * TILE_SIZE;
+ const int ph = 2 * std::min(w, parallax_width - pan_screen_width) * map_info.position_x / w;
+ if (Player::IsRPG2k()) {
+ SetPositionX(ph);
+ } else {
+ // 2k3 does not do the (% parallax_width * TILE_SIZE * 2) here
+ panorama.pan_x = ph;
+ }
+ } else {
+ panorama.pan_x = 0;
+ parallax_fake_x = true;
+ }
+ } else {
+ parallax_fake_x = true;
+ }
+}
+
+void Game_Map::Parallax::ResetPositionY() {
+ Params params = GetParallaxParams();
+
+ if (params.name.empty()) {
+ return;
+ }
+
+ parallax_fake_y = false;
+
+ if (!params.scroll_vert && !Game_Map::LoopVertical()) {
+ int pan_screen_height = Player::screen_height;
+ if (Player::game_config.fake_resolution.Get()) {
+ pan_screen_height = SCREEN_TARGET_HEIGHT;
+ }
+
+ int tiles_per_screen = pan_screen_height / TILE_SIZE;
+ if (pan_screen_height % TILE_SIZE != 0) {
+ ++tiles_per_screen;
+ }
+
+ if (GetTilesY() > tiles_per_screen && parallax_height > pan_screen_height) {
+ const int h = (GetTilesY() - tiles_per_screen) * TILE_SIZE;
+ const int pv = 2 * std::min(h, parallax_height - pan_screen_height) * map_info.position_y / h;
+ SetPositionY(pv);
+ } else {
+ panorama.pan_y = 0;
+ parallax_fake_y = true;
+ }
+ } else {
+ parallax_fake_y = true;
+ }
+}
+
+void Game_Map::Parallax::ScrollRight(int distance) {
+ if (!distance) {
+ return;
+ }
+
+ Params params = GetParallaxParams();
+ if (params.name.empty()) {
+ return;
+ }
+
+ if (params.scroll_horz) {
+ AddPositionX(distance);
+ return;
+ }
+
+ if (Game_Map::LoopHorizontal()) {
+ return;
+ }
+
+ ResetPositionX();
+}
+
+void Game_Map::Parallax::ScrollDown(int distance) {
+ if (!distance) {
+ return;
+ }
+
+ Params params = GetParallaxParams();
+ if (params.name.empty()) {
+ return;
+ }
+
+ if (params.scroll_vert) {
+ AddPositionY(distance);
+ return;
+ }
+
+ if (Game_Map::LoopVertical()) {
+ return;
+ }
+
+ ResetPositionY();
+}
+
+void Game_Map::Parallax::Update() {
+ Params params = GetParallaxParams();
+
+ if (params.name.empty())
+ return;
+
+ auto scroll_amt = [](int speed) {
+ return speed < 0 ? (1 << -speed) : -(1 << speed);
+ };
+
+ if (params.scroll_horz
+ && params.scroll_horz_auto
+ && params.scroll_horz_speed != 0) {
+ AddPositionX(scroll_amt(params.scroll_horz_speed));
+ }
+
+ if (params.scroll_vert
+ && params.scroll_vert_auto
+ && params.scroll_vert_speed != 0) {
+ if (parallax_height != 0) {
+ AddPositionY(scroll_amt(params.scroll_vert_speed));
+ }
+ }
+}
+
+void Game_Map::Parallax::ChangeBG(const Params& params) {
+ map_info.parallax_name = params.name;
+ map_info.parallax_horz = params.scroll_horz;
+ map_info.parallax_horz_auto = params.scroll_horz_auto;
+ map_info.parallax_horz_speed = params.scroll_horz_speed;
+ map_info.parallax_vert = params.scroll_vert;
+ map_info.parallax_vert_auto = params.scroll_vert_auto;
+ map_info.parallax_vert_speed = params.scroll_vert_speed;
+
+ reset_panorama_x_on_next_init = !Game_Map::LoopHorizontal() && !map_info.parallax_horz;
+ reset_panorama_y_on_next_init = !Game_Map::LoopVertical() && !map_info.parallax_vert;
+
+ Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
+ if (!scene || !scene->spriteset)
+ return;
+ scene->spriteset->ParallaxUpdated();
+}
+
+void Game_Map::Parallax::ClearChangedBG() {
+ Params params {}; // default Param indicates no override
+ ChangeBG(params);
+}
+
+bool Game_Map::Parallax::FakeXPosition() {
+ return parallax_fake_x;
+}
+
+bool Game_Map::Parallax::FakeYPosition() {
+ return parallax_fake_y;
+}
+
+
+
+
+
+int Game_Map::GetTileID(int x, int y, int layer) {
+
+
+ int tile_index = x + y * GetTilesX();
+ int tile_raw_id = map->lower_layer[tile_index];
+ int tile_id = 0;
+
+ if (tile_raw_id >= BLOCK_E) {
+ tile_id = tile_raw_id - BLOCK_E;
+ tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX;
+
+ }
+ else if (tile_raw_id >= BLOCK_D) {
+ /*tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX;
+ int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE;
+ Output::Debug(" {} {} {}", tile_id, autotile_id, tile_raw_id);*/
+ //return tile_id;
+ /*if (((Passable::Wall) != 0) && (
+ (autotile_id >= 20 && autotile_id <= 23) ||
+ (autotile_id >= 33 && autotile_id <= 37) ||
+ autotile_id == 42 || autotile_id == 43 ||
+ autotile_id == 45 || autotile_id == 46))
+ return autotile_id;*/
+
+ }
+ else if (tile_raw_id >= BLOCK_C) {
+ tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX;
+
+ }
+ else if (map->lower_layer[tile_index] < BLOCK_C) {
+ tile_id = tile_raw_id / BLOCK_B_STRIDE;
+ }
+
+ return tile_id;
+}
+
+
+Game_Map::Mode7TransformResult Game_Map::TransformToMode7(int screen_x, int screen_y) {
+ // This function takes a standard 2D screen coordinate and projects it
+ // into the pseudo-3D Mode 7 space, returning the new on-screen
+ // coordinates and the appropriate zoom/scale factor.
+
+ // Get map properties.
+ const int center_x = Player::screen_width / 2 - 8;
+ const int center_y = Player::screen_height / 2 + 8;
+ float yaw = Game_Map::GetMode7Yaw();
+ int slant = Game_Map::GetMode7Slant();
+ int horizon = Game_Map::GetMode7Horizon();
+ horizon = (horizon * (90 - slant)) / 90;
+ int baseline = center_y + Game_Map::GetMode7Baseline();
+ double scale = Game_Map::GetMode7Scale();
+
+ // Rotate.
+ double angle = (yaw * (2 * M_PI) / 360);
+ int xx = screen_x - center_x;
+ int yy = screen_y - center_y;
+ double cosA = cos(-angle);
+ double sinA = sin(-angle);
+ int rotatedX = (cosA * xx) + (sinA * yy);
+ int rotatedY = (cosA * yy) - (sinA * xx);
+
+ // Transform
+ double iConst = 1 + (slant / (baseline + horizon));
+ double distanceBase = slant * scale / (baseline + horizon);
+ double syBase = distanceBase * 2;
+ double distance = (syBase - rotatedY) / 2;
+
+ double zoom = (iConst - (distance / scale)) * 2.0;
+ int sy = ((slant * scale) / distance) - horizon - (Player::screen_height / 2) - 4;
+ int sx = rotatedX * zoom;
+
+ return {center_x + sx, center_y + sy, zoom};
+}
+
+void Game_Map::SetMode7Horizon(int h) {
+ mode7Horizon = h;
+}
+
+void Game_Map::SetMode7Zoom(int zoom_factor) {
+ // Value is passed as an integer multiplied by 100 for precision
+ mode7Zoom = zoom_factor / 100.0f;
+ if (mode7Zoom < 0.1f) {
+ mode7Zoom = 0.1f;
+ }
+}
+
+
+void Game_Map::SetMode7Background(std::string_view name) {
+ mode7BackgroundName = ToString(name);
+}
+
+std::string Game_Map::GetMode7Background() {
+ return mode7BackgroundName;
+}
+
+void Game_Map::SetMode7FadeWidth(int pixels) {
+ mode7FadeWidth = std::max(1, pixels); // Prevent division by zero
+}
+
+int Game_Map::GetMode7FadeWidth() {
+ return mode7FadeWidth;
+}
+
+void Game_Map::SetMode7Overlay(int slot, std::string_view name, float anchor, int y, float scroll) {
+ if (name.empty()) {
+ mode7SkyLayers.erase(slot);
+ return;
+ }
+ mode7SkyLayers[slot] = { ToString(name), anchor, y, scroll };
+}
+
+void Game_Map::ClearMode7Overlays() {
+ mode7SkyLayers.clear();
+}
+
+const std::map& Game_Map::GetMode7Overlays() {
+ return mode7SkyLayers;
+}
+
+
diff --git a/src/game_map.h b/src/game_map.h
index d7cb29126a..8f1f1e8f7b 100644
--- a/src/game_map.h
+++ b/src/game_map.h
@@ -39,13 +39,25 @@
#include
#include
#include
-#include
+#include
+
+#include "tilemap_layer.h"
class FileRequestAsync;
struct BattleArgs;
// These are in sixteenths of a pixel.
constexpr int SCREEN_TILE_SIZE = 256;
+
+const int INPUT4_VALUES[4] = { 2,6,8,4 };
+// const int INPUT8_VALUES[8] = { 2,6,8,4,1,3,7,9 };
+// const int INPUT8_VALUES[8] = { 7,8,9,6,3,2,1,4 }; // works North/South, but reverses East/West
+// const int INPUT8_VALUES[8] = { 8,9,6,3,2,1,4,7 }; // works North/South, but reverses East/West
+const int INPUT8_VALUES[8] = {1,2,3,6,9,8,7,4};
+
+// 1 = DownLeft, 2 = Down, 3 = DownRight, 4 = Left, 6 = Right, 7 = UpLeft, 8 = Up, 9 = UpRight
+
+
class MapUpdateAsyncContext {
public:
@@ -77,7 +89,13 @@ class MapUpdateAsyncContext {
/**
* Game_Map namespace
*/
-namespace Game_Map {
+namespace Game_Map {
+
+
+ bool WouldCollideWithCharacter(const Game_Character& self, const Game_Character& other, bool self_conflict); // TODO - PIXELMOVE
+
+
+
/**
* Initialize Game_Map.
*/
@@ -364,7 +382,8 @@ namespace Game_Map {
* If IsActive() after return in, will suspend from that point.
* @param is_preupdate Update only common events and map events
*/
- void Update(MapUpdateAsyncContext& actx, bool is_preupdate = false);
+ void Update(MapUpdateAsyncContext& actx, bool is_preupdate = false);
+ void UpdateMode7();
/**
* Gets current map info.
@@ -428,7 +447,62 @@ namespace Game_Map {
/** @return battle encounter steps. */
int GetEncounterSteps();
-
+
+ /** @return If map is set to Mode7 */
+ int GetMoveDirection(int d);
+ int GetGraphicDirection(int d);
+ bool GetIsMode7();
+ void SetIsMode7(bool v);
+ float GetMode7Slant();
+ float GetMode7Yaw();
+ int GetMode7Horizon();
+ int GetMode7Baseline();
+ double GetMode7Scale();
+ void TiltMode7(int v);
+ void RotateMode7(int v);
+ void TiltTowardsMode7(int v, int duration);
+ void RotateTowardsMode7(int v, int duration);
+ void SetMode7Slant(int v);
+ void SetMode7Yaw(int v);
+ void SetMode7Horizon(int h);
+ void SetMode7Zoom(int zoom_factor);
+ void SetMode7Scale(int scale_factor);
+
+ void SetMode7Background(std::string_view name);
+ std::string GetMode7Background();
+
+ void SetMode7FadeWidth(int pixels);
+ int GetMode7FadeWidth();
+
+ struct Mode7SkyLayer {
+ std::string name;
+ float anchor_percent; // 0-100 (Where it sits in the 360 degree loop)
+ int y_offset; // Vertical position relative to horizon
+ float scroll_ratio; // 1.0 = moves with camera, 0.5 = half speed (depth)
+
+
+};
+
+ void SetMode7Overlay(int slot, std::string_view name, float anchor, int y, float scroll);
+ void ClearMode7Overlays();
+ const std::map& GetMode7Overlays();
+
+
+
+
+
+
+ /** Updates flag based on map's name. */
+ void RefreshMode7();
+
+ struct Mode7TransformResult {
+ int screen_x;
+ int screen_y;
+ double zoom;
+ };
+
+ Mode7TransformResult TransformToMode7(int screen_x, int screen_y);
+
/**
* Sets battle encounter steps.
*
@@ -752,6 +826,12 @@ namespace Game_Map {
void SetNeedRefreshForVarChange(int var_id);
void SetNeedRefreshForSwitchChange(std::initializer_list switch_ids);
void SetNeedRefreshForVarChange(std::initializer_list var_ids);
+
+
+ int GetTileID(int x, int y, int layer);
+ TilemapLayer* GetTilemap(int i);
+
+
namespace Parallax {
struct Params {
diff --git a/src/game_player.cpp b/src/game_player.cpp
index 6ffa40082c..72af2439c7 100644
--- a/src/game_player.cpp
+++ b/src/game_player.cpp
@@ -1,938 +1,1362 @@
-/*
- * This file is part of EasyRPG Player.
- *
- * EasyRPG Player is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * EasyRPG Player is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with EasyRPG Player. If not, see .
- */
-
-// Headers
-#include "game_player.h"
-#include "async_handler.h"
-#include "game_actor.h"
-#include "game_map.h"
-#include "game_message.h"
-#include "game_party.h"
-#include "game_system.h"
-#include "game_screen.h"
-#include "game_pictures.h"
-#include "input.h"
-#include "main_data.h"
-#include "options.h"
-#include "player.h"
-#include "util_macro.h"
-#include "game_switches.h"
-#include "output.h"
-#include "rand.h"
-#include "utils.h"
-#include
-#include
-#include "scene_battle.h"
-#include "scene_menu.h"
-#include
-#include
-#include
-#include "scene_gameover.h"
-
-Game_Player::Game_Player(): Game_PlayerBase(Player)
-{
- SetDirection(lcf::rpg::EventPage::Direction_down);
- SetMoveSpeed(4);
- SetAnimationType(lcf::rpg::EventPage::AnimType_non_continuous);
-}
-
-void Game_Player::SetSaveData(lcf::rpg::SavePartyLocation save)
-{
- *data() = std::move(save);
-
- SanitizeData("Party");
-
- // RPG_RT will always reset the hero graphic on loading a save, even if
- // a move route changed the graphic.
- ResetGraphic();
-}
-
-lcf::rpg::SavePartyLocation Game_Player::GetSaveData() const {
- return *data();
-}
-
-Drawable::Z_t Game_Player::GetScreenZ(int x_offset, int y_offset) const {
- // Player is always "same layer as hero".
- // When the Player is on the same Y-coordinate as an event the Player is always rendered first.
- // This is different to events where, when Y is the same, the highest X-coordinate is rendered first.
- // To ensure this, fake a very high X-coordinate of 65535 (all bits set)
- // See base function for full explanation of the bitmask
- return Game_Character::GetScreenZ(x_offset, y_offset) | (0xFFFFu << 16u);
-}
-
-void Game_Player::ReserveTeleport(int map_id, int x, int y, int direction, TeleportTarget::Type tt) {
- teleport_target = TeleportTarget(map_id, x, y, direction, tt);
-
- FileRequestAsync* request = Game_Map::RequestMap(map_id);
- request->Start();
-}
-
-void Game_Player::ReserveTeleport(const lcf::rpg::SaveTarget& target) {
- const auto* target_map_info = &Game_Map::GetMapInfo(target.map_id);
-
- if (target_map_info->type == lcf::rpg::TreeMap::MapType_area) {
- // Area: Obtain the map the area belongs to
- target_map_info = &Game_Map::GetParentMapInfo(*target_map_info);
- }
-
- ReserveTeleport(target_map_info->ID, target.map_x, target.map_y, Down, TeleportTarget::eSkillTeleport);
-
- if (target.switch_on) {
- Main_Data::game_switches->Set(target.switch_id, true);
- Game_Map::SetNeedRefresh(true);
- }
-}
-
-void Game_Player::PerformTeleport() {
- assert(IsPendingTeleport());
- if (!IsPendingTeleport()) {
- return;
- }
-
- if (teleport_target.GetMapId() <= 0) {
- Output::Error("Invalid Teleport map id! mapid={} x={} y={} d={}", teleport_target.GetMapId(),
- teleport_target.GetX(), teleport_target.GetY(), teleport_target.GetDirection());
- }
-
- const auto map_changed = (GetMapId() != teleport_target.GetMapId());
- MoveTo(teleport_target.GetMapId(), teleport_target.GetX(), teleport_target.GetY());
-
-
- if (teleport_target.GetDirection() >= 0) {
- SetDirection(teleport_target.GetDirection());
- UpdateFacing();
- }
-
- if (map_changed && teleport_target.GetType() != TeleportTarget::eAsyncQuickTeleport) {
- Main_Data::game_screen->OnMapChange();
- Main_Data::game_pictures->OnMapChange();
- Game_Map::GetInterpreter().OnMapChange();
- }
-
- ResetTeleportTarget();
-}
-
-void Game_Player::MoveTo(int map_id, int x, int y) {
- const auto map_changed = (GetMapId() != map_id);
-
- Game_Character::MoveTo(map_id, x, y);
- SetTotalEncounterRate(0);
- SetMenuCalling(false);
-
- auto* vehicle = GetVehicle();
- if (vehicle) {
- // RPG_RT doesn't check the aboard flag for this one
- vehicle->MoveTo(map_id, x, y);
- }
-
- if (map_changed) {
- // FIXME: Assert map pre-loaded in cache.
-
- // pan_state does not reset when you change maps.
- data()->pan_speed = lcf::rpg::SavePartyLocation::kPanSpeedDefault;
- data()->pan_finish_x = GetDefaultPanX();
- data()->pan_finish_y = GetDefaultPanY();
- data()->pan_current_x = GetDefaultPanX();
- data()->pan_current_y = GetDefaultPanY();
- maniac_pan_current_x = static_cast(GetDefaultPanX());
- maniac_pan_current_y = static_cast(GetDefaultPanY());
-
- ResetAnimation();
-
- auto map = Game_Map::LoadMapFile(GetMapId());
-
- Game_Map::Setup(std::move(map));
- Game_Map::PlayBgm();
-
- // This Fixes an RPG_RT bug where the jumping flag doesn't get reset
- // if you change maps during a jump
- SetJumping(false);
- } else {
- Game_Map::SetPositionX(GetSpriteX() - GetPanX());
- Game_Map::SetPositionY(GetSpriteY() - GetPanY());
- }
-
- ResetGraphic();
-}
-
-bool Game_Player::MakeWay(int from_x, int from_y, int to_x, int to_y) {
- if (IsAboard()) {
- return GetVehicle()->MakeWay(from_x, from_y, to_x, to_y);
- }
-
- return Game_Character::MakeWay(from_x, from_y, to_x, to_y);
-}
-
-void Game_Player::MoveRouteSetSpriteGraphic(std::string sprite_name, int index) {
- auto* vh = GetVehicle();
- if (vh) {
- vh->MoveRouteSetSpriteGraphic(std::move(sprite_name), index);
- } else {
- Game_Character::MoveRouteSetSpriteGraphic(std::move(sprite_name), index);
- }
-}
-
-void Game_Player::UpdateScroll(int amount, bool was_jumping) {
- if (IsPanLocked()) {
- return;
- }
-
- auto dx = (GetX() * SCREEN_TILE_SIZE) - Game_Map::GetPositionX() - GetPanX();
- auto dy = (GetY() * SCREEN_TILE_SIZE) - Game_Map::GetPositionY() - GetPanY();
-
- const auto w = Game_Map::GetTilesX() * SCREEN_TILE_SIZE;
- const auto h = Game_Map::GetTilesY() * SCREEN_TILE_SIZE;
-
- dx = Utils::PositiveModulo(dx + w / 2, w) - w / 2;
- dy = Utils::PositiveModulo(dy + h / 2, h) - h / 2;
-
- const auto sx = Utils::Signum(dx);
- const auto sy = Utils::Signum(dy);
-
- if (was_jumping) {
- const auto jdx = sx * std::abs(GetX() - GetBeginJumpX());
- const auto jdy = sy * std::abs(GetY() - GetBeginJumpY());
-
- Game_Map::Scroll(amount * jdx, amount * jdy);
-
- if (!IsJumping()) {
- // RPG does this to fix rounding errors?
- const auto x = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionX() / static_cast(SCREEN_TILE_SIZE));
- const auto y = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionY() / static_cast(SCREEN_TILE_SIZE));
-
- // RPG_RT does adjust map position, but not panorama!
- Game_Map::SetPositionX(x, false);
- Game_Map::SetPositionY(y, false);
- }
- return;
- }
-
- int move_sx = 0;
- int move_sy = 0;
- const auto d = GetDirection();
- if (sy < 0 && (d == Up || d == UpRight || d == UpLeft)) {
- move_sy = sy;
- }
- if (sy > 0 && (d == Down || d == DownRight || d == DownLeft)) {
- move_sy = sy;
- }
- if (sx > 0 && (d == Right || d == UpRight || d == DownRight)) {
- move_sx = sx;
- }
- if (sx < 0 && (d == Left || d == UpLeft || d == DownLeft)) {
- move_sx = sx;
- }
-
- Game_Map::Scroll(move_sx * amount, move_sy * amount);
-}
-
-bool Game_Player::UpdateAirship() {
- auto* vehicle = GetVehicle();
-
- // RPG_RT doesn't check vehicle, but we have to as we don't have another way to fetch it.
- // Also in vanilla RPG_RT it's impossible for the hero to fly without the airship.
- if (vehicle && vehicle->IsFlying()) {
- if (vehicle->AnimateAscentDescent()) {
- if (!vehicle->IsFlying()) {
- // If we landed, them disembark
- Main_Data::game_player->SetFlying(vehicle->IsFlying());
- data()->aboard = false;
- SetFacing(Down);
- data()->vehicle = 0;
- SetMoveSpeed(data()->preboard_move_speed);
-
- Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
- }
-
- return true;
- }
- }
- return false;
-}
-
-void Game_Player::UpdateNextMovementAction() {
- if (UpdateAirship()) {
- return;
- }
-
- UpdateMoveRoute(data()->move_route_index, data()->move_route, true);
-
- if (Game_Map::GetInterpreter().IsRunning()) {
- SetMenuCalling(false);
- return;
- }
-
- if(IsPaused() || IsMoveRouteOverwritten() || Game_Message::IsMessageActive()) {
- return;
- }
-
- if (IsEncounterCalling()) {
- SetMenuCalling(false);
- SetEncounterCalling(false);
-
- BattleArgs args;
- if (Game_Map::PrepareEncounter(args)) {
- Scene::instance->SetRequestedScene(Scene_Battle::Create(std::move(args)));
- return;
- }
- }
-
- if (IsMenuCalling()) {
- SetMenuCalling(false);
-
- ResetAnimation();
- Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
- Game_Map::GetInterpreter().RequestMainMenuScene();
- return;
- }
-
- CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false);
-
- if (Game_Map::IsAnyEventStarting()) {
- return;
- }
-
- int move_dir = -1;
- switch (Input::dir4) {
- case 2:
- move_dir = Down;
- break;
- case 4:
- move_dir = Left;
- break;
- case 6:
- move_dir = Right;
- break;
- case 8:
- move_dir = Up;
- break;
- }
- if (move_dir >= 0) {
- SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through);
- Move(move_dir);
- ResetThrough();
- if (IsStopping()) {
- int front_x = Game_Map::XwithDirection(GetX(), GetDirection());
- int front_y = Game_Map::YwithDirection(GetY(), GetDirection());
- CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false);
- }
- }
-
- if (IsStopping()) {
- if (Input::IsTriggered(Input::DECISION)) {
- if (!GetOnOffVehicle()) {
- CheckActionEvent();
- }
- }
- return;
- }
-
- Main_Data::game_party->IncSteps();
- if (Main_Data::game_party->ApplyStateDamage()) {
- Main_Data::game_screen->FlashMapStepDamage();
- }
- UpdateEncounterSteps();
-}
-
-void Game_Player::UpdateMovement(int amount) {
- const bool was_jumping = IsJumping();
-
- Game_Character::UpdateMovement(amount);
-
- UpdateScroll(amount, was_jumping);
-
- if (!IsMoveRouteOverwritten() && IsStopping()) {
- TriggerSet triggers = { lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision };
- CheckEventTriggerHere(triggers, false);
- }
-}
-
-void Game_Player::Update() {
- Game_Character::Update();
-
- if (IsStopping()) {
- if (data()->boarding) {
- // Boarding completed
- data()->aboard = true;
- data()->boarding = false;
- // Note: RPG_RT ignores the lock_facing flag here!
- SetFacing(Left);
-
- auto* vehicle = GetVehicle();
- SetMoveSpeed(vehicle->GetMoveSpeed());
- }
- if (data()->unboarding) {
- // Unboarding completed
- data()->unboarding = false;
- }
- }
-
- auto* vehicle = GetVehicle();
-
- if (IsAboard() && vehicle) {
- vehicle->SyncWithRider(this);
- }
-
- UpdatePan();
-
- // ESC-Menu calling
- if (Main_Data::game_system->GetAllowMenu()
- && !Game_Message::IsMessageActive()
- && !Game_Map::GetInterpreter().IsRunning())
- {
- if (Input::IsTriggered(Input::CANCEL)) {
- SetMenuCalling(true);
- }
- }
-}
-
-bool Game_Player::CheckActionEvent() {
- if (IsFlying()) {
- return false;
- }
-
- bool result = false;
- int front_x = Game_Map::XwithDirection(GetX(), GetDirection());
- int front_y = Game_Map::YwithDirection(GetY(), GetDirection());
-
- result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true);
- result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true);
-
- // Counter tile loop stops only if you talk to an action event.
- bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true);
- // RPG_RT allows maximum of 3 counter tiles
- for (int i = 0; !got_action && i < 3; ++i) {
- if (!Game_Map::IsCounter(front_x, front_y)) {
- break;
- }
-
- front_x = Game_Map::XwithDirection(front_x, GetDirection());
- front_y = Game_Map::YwithDirection(front_y, GetDirection());
-
- got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true);
- }
- return result || got_action;
-}
-
-bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player) {
- if (InAirship()) {
- return false;
- }
-
- bool result = false;
-
- for (auto& ev: Game_Map::GetEvents()) {
- const auto trigger = ev.GetTrigger();
- if (ev.IsActive()
- && ev.GetX() == GetX()
- && ev.GetY() == GetY()
- && ev.GetLayer() != lcf::rpg::EventPage::Layers_same
- && trigger >= 0
- && triggers[trigger]) {
- SetEncounterCalling(false);
- result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player);
- }
- }
- return result;
-}
-
-bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player) {
- if (InAirship()) {
- return false;
- }
- bool result = false;
-
- for (auto& ev : Game_Map::GetEvents()) {
- const auto trigger = ev.GetTrigger();
- if (ev.IsActive()
- && ev.GetX() == x
- && ev.GetY() == y
- && ev.GetLayer() == lcf::rpg::EventPage::Layers_same
- && trigger >= 0
- && triggers[trigger]) {
- SetEncounterCalling(false);
- result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player);
- }
- }
- return result;
-}
-
-void Game_Player::ResetGraphic() {
-
- auto* actor = Main_Data::game_party->GetActor(0);
- if (actor == nullptr) {
- SetSpriteGraphic("", 0);
- SetTransparency(0);
- return;
- }
-
- SetSpriteGraphic(ToString(actor->GetSpriteName()), actor->GetSpriteIndex());
- SetTransparency(actor->GetSpriteTransparency());
-}
-
-bool Game_Player::GetOnOffVehicle() {
- if (IsDirectionDiagonal(GetDirection())) {
- SetDirection(GetFacing());
- }
-
- return IsAboard()
- ? GetOffVehicle()
- : GetOnVehicle();
-}
-
-bool Game_Player::GetOnVehicle() {
- assert(!IsDirectionDiagonal(GetDirection()));
- assert(!IsAboard());
-
- auto* vehicle = Game_Map::GetVehicle(Game_Vehicle::Airship);
-
- if (vehicle->IsInPosition(GetX(), GetY()) && IsStopping() && vehicle->IsStopping()) {
- data()->vehicle = Game_Vehicle::Airship;
- data()->aboard = true;
-
- // Note: RPG_RT ignores the lock_facing flag here!
- SetFacing(Left);
-
- data()->preboard_move_speed = GetMoveSpeed();
- SetMoveSpeed(vehicle->GetMoveSpeed());
- vehicle->StartAscent();
- Main_Data::game_player->SetFlying(vehicle->IsFlying());
- } else {
- const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection());
- const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection());
-
- vehicle = Game_Map::GetVehicle(Game_Vehicle::Ship);
- if (!vehicle->IsInPosition(front_x, front_y)) {
- vehicle = Game_Map::GetVehicle(Game_Vehicle::Boat);
- if (!vehicle->IsInPosition(front_x, front_y)) {
- return false;
- }
- }
-
- if (!Game_Map::CanEmbarkShip(*this, front_x, front_y)) {
- return false;
- }
-
- SetThrough(true);
- Move(GetDirection());
- // FIXME: RPG_RT resets through to move_route_through || not visible?
- ResetThrough();
-
- data()->vehicle = vehicle->GetVehicleType();
- data()->preboard_move_speed = GetMoveSpeed();
- data()->boarding = true;
- }
-
- Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM());
- Main_Data::game_system->BgmPlay(vehicle->GetBGM());
- return true;
-}
-
-bool Game_Player::GetOffVehicle() {
- assert(!IsDirectionDiagonal(GetDirection()));
- assert(IsAboard());
-
- auto* vehicle = GetVehicle();
- if (!vehicle) {
- return false;
- }
-
- if (InAirship()) {
- if (vehicle->IsAscendingOrDescending()) {
- return false;
- }
-
- // Note: RPG_RT ignores the lock_facing flag here!
- SetFacing(Left);
- vehicle->StartDescent();
- return true;
- }
-
- const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection());
- const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection());
-
- if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) {
- return false;
- }
-
- vehicle->SetDefaultDirection();
- data()->aboard = false;
- SetMoveSpeed(data()->preboard_move_speed);
- data()->unboarding = true;
-
- SetThrough(true);
- Move(GetDirection());
- ResetThrough();
-
- data()->vehicle = 0;
- Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
-
- return true;
-}
-
-void Game_Player::ForceGetOffVehicle() {
- if (!IsAboard()) {
- return;
- }
-
- auto* vehicle = GetVehicle();
- vehicle->ForceLand();
- vehicle->SetDefaultDirection();
-
- data()->flying = false;
- data()->aboard = false;
- SetMoveSpeed(data()->preboard_move_speed);
- data()->unboarding = true;
- data()->vehicle = 0;
- Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
-}
-
-bool Game_Player::InVehicle() const {
- return data()->vehicle > 0;
-}
-
-bool Game_Player::InAirship() const {
- return data()->vehicle == Game_Vehicle::Airship;
-}
-
-Game_Vehicle* Game_Player::GetVehicle() const {
- return Game_Map::GetVehicle((Game_Vehicle::Type) data()->vehicle);
-}
-
-bool Game_Player::Move(int dir) {
- if (!IsStopping()) {
- return true;
- }
-
- Game_Character::Move(dir);
- if (IsStopping()) {
- return false;
- }
-
- if (InAirship()) {
- return true;
- }
-
- int terrain_id = Game_Map::GetTerrainTag(GetX(), GetY());
- const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, terrain_id);
- bool red_flash = false;
-
- if (terrain) {
- if (terrain->damage != 0) {
- for (auto hero : Main_Data::game_party->GetActors()) {
- if (terrain->damage < 0 || !hero->PreventsTerrainDamage()) {
- if (terrain->damage > 0) {
- red_flash = true;
- }
- if (terrain->easyrpg_damage_in_percent) {
- int value = std::max(1, std::abs(hero->GetMaxHp() * terrain->damage / 100));
- hero->ChangeHp((terrain->damage > 0 ? -value : value), terrain->easyrpg_damage_can_kill);
- } else {
- hero->ChangeHp(-terrain->damage, terrain->easyrpg_damage_can_kill);
- }
- }
- }
- if (terrain->damage > 0 && terrain->easyrpg_damage_can_kill) {
- if (!Main_Data::game_party->IsAnyActive() && Main_Data::game_party->GetBattlerCount() > 0) {
- Scene::instance->SetRequestedScene(std::make_shared());
- return true;
- }
- }
- }
- if ((!terrain->on_damage_se || red_flash) && Player::IsRPG2k3()) {
- Main_Data::game_system->SePlay(terrain->footstep);
- }
- } else {
- Output::Warning("Player BeginMove: Invalid terrain ID {} at ({}, {})", terrain_id, GetX(), GetY());
- }
-
- if (red_flash) {
- Main_Data::game_screen->FlashMapStepDamage();
- }
-
- return true;
-}
-
-bool Game_Player::IsAboard() const {
- return data()->aboard;
-}
-
-bool Game_Player::IsBoardingOrUnboarding() const {
- return data()->boarding || data()->unboarding;
-}
-
-void Game_Player::UpdateEncounterSteps() {
- if (Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) {
- return;
- }
-
- if(IsFlying()) {
- return;
- }
-
- const auto encounter_steps = Game_Map::GetEncounterSteps();
-
- if (encounter_steps <= 0) {
- SetTotalEncounterRate(0);
- return;
- }
-
- int x = GetX();
- int y = GetY();
-
- const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, Game_Map::GetTerrainTag(x,y));
- if (!terrain) {
- Output::Warning("UpdateEncounterSteps: Invalid terrain at ({}, {})", x, y);
- return;
- }
-
- data()->total_encounter_rate += terrain->encounter_rate;
-
- struct Row {
- int ratio;
- float pmod;
- };
-
-#if 1
- static constexpr Row enc_table[] = {
- { 0, 0.0625},
- { 20, 0.125 },
- { 40, 0.25 },
- { 60, 0.5 },
- { 100, 2.0 },
- { 140, 4.0 },
- { 160, 8.0 },
- { 180, 16.0 },
- { INT_MAX, 16.0 }
- };
-#else
- //Old versions of RM2k used this table.
- //Left here for posterity.
- static constexpr Row enc_table[] = {
- { 0, 0.5 },
- { 20, 2.0 / 3.0 },
- { 50, 5.0 / 6.0 },
- { 100, 6.0 / 5.0 },
- { 200, 3.0 / 2.0 },
- { INT_MAX, 3.0 / 2.0 }
- };
-#endif
- const auto ratio = GetTotalEncounterRate() / encounter_steps;
-
- auto& idx = last_encounter_idx;
- while (ratio > enc_table[idx+1].ratio) {
- ++idx;
- }
- const auto& row = enc_table[idx];
-
- const auto pmod = row.pmod;
- const auto p = (1.0f / float(encounter_steps)) * pmod * (float(terrain->encounter_rate) / 100.0f);
-
- if (!Rand::PercentChance(p)) {
- return;
- }
-
- SetTotalEncounterRate(0);
- SetEncounterCalling(true);
-}
-
-void Game_Player::SetTotalEncounterRate(int rate) {
- last_encounter_idx = 0;
- data()->total_encounter_rate = rate;
-}
-
-int Game_Player::GetDefaultPanX() {
- return static_cast(std::ceil(static_cast