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(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -int Game_Player::GetDefaultPanY() { - return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -void Game_Player::LockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; -} - -void Game_Player::UnlockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; -} - -void Game_Player::StartPan(int direction, int distance, int speed) { - distance *= SCREEN_TILE_SIZE; - - if (direction == PanUp) { - int new_pan = data()->pan_finish_y + distance; - data()->pan_finish_y = new_pan; - } else if (direction == PanRight) { - int new_pan = data()->pan_finish_x - distance; - data()->pan_finish_x = new_pan; - } else if (direction == PanDown) { - int new_pan = data()->pan_finish_y - distance; - data()->pan_finish_y = new_pan; - } else if (direction == PanLeft) { - int new_pan = data()->pan_finish_x + distance; - data()->pan_finish_x = new_pan; - } - - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - // Maniac uses separate horizontal/vertical pan for everything - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { - if (!Player::IsPatchManiac()) { - return; - } - - h *= TILE_SIZE; - v *= TILE_SIZE; - - maniac_pan_current_x = static_cast(data()->pan_current_x); - maniac_pan_current_y = static_cast(data()->pan_current_y); - - int new_pan_x; - int new_pan_y; - - if (relative && centered) { - int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; - int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; - new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; - new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; - } else if (relative) { - new_pan_x = data()->pan_finish_x - h; - new_pan_y = data()->pan_finish_y - v; - } else if (centered) { - new_pan_x = GetSpriteX() + GetDefaultPanX() - h; - new_pan_y = GetSpriteY() + GetDefaultPanY() - v; - } else { - new_pan_x = GetSpriteX() - h; - new_pan_y = GetSpriteY() - v; - } - - double h_speed; - double v_speed; - - if (speed == 0) { - // Instant pan if speed is zero - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); - } else if (interpolated) { - // Interpolate distance by number of frames - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); - } else { - // Multiply speed by 0.001 - h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - } - - data()->pan_finish_x = new_pan_x; - data()->pan_finish_y = new_pan_y; - data()->maniac_horizontal_pan_speed = h_speed; - data()->maniac_vertical_pan_speed = v_speed; -} - -void Game_Player::ResetPan(int speed) { - data()->pan_finish_x = GetDefaultPanX(); - data()->pan_finish_y = GetDefaultPanY(); - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - // Maniac uses separate horizontal/vertical pan for everything - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -int Game_Player::GetPanWait() { - bool is_maniac = Player::IsPatchManiac(); - const auto distance = std::max( - std::abs(data()->pan_current_x - data()->pan_finish_x), - std::abs(data()->pan_current_y - data()->pan_finish_y)); - const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max( - std::abs(data()->maniac_horizontal_pan_speed), - std::abs(data()->maniac_vertical_pan_speed))); - assert(speed > 0); - return distance / speed + (distance % speed != 0); -} - -// Fixes compilation error with OpenOrbis toolchain where std::abs fails to cast to double on values where it should -#if __PS4__ -# define PS4_WORKAROUND (double) -#else -# define PS4_WORKAROUND -#endif -void Game_Player::UpdatePan() { - if (!IsPanActive()) - return; - - const int step = data()->pan_speed; - const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; - const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; - - int dx; - int dy; - - if (Player::IsPatchManiac()) { - const double step_x = data()->maniac_horizontal_pan_speed; - const double step_y = data()->maniac_vertical_pan_speed; - - // Maniac uses doubles for smoother screen scrolling - double dx2 = std::min(step_x, PS4_WORKAROUND std::abs(static_cast(pan_remain_x))); - double dy2 = std::min(step_y, PS4_WORKAROUND std::abs(static_cast(pan_remain_y))); - - dx2 = pan_remain_x >= 0 ? dx2 : -dx2; - dy2 = pan_remain_y >= 0 ? dy2 : -dy2; - - maniac_pan_current_x -= dx2; - maniac_pan_current_y -= dy2; - - // Depending on the position, floor or ceil the value - dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); - dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); - } else { - dx = std::min(step, std::abs(pan_remain_x)); - dy = std::min(step, std::abs(pan_remain_y)); - - dx = pan_remain_x >= 0 ? dx : -dx; - dy = pan_remain_y >= 0 ? dy : -dy; - } - - int screen_x = Game_Map::GetPositionX(); - int screen_y = Game_Map::GetPositionY(); - - Game_Map::AddScreenX(screen_x, dx); - Game_Map::AddScreenY(screen_y, dy); - - // If we hit the edge of the map before pan finishes. - if (dx == 0 && dy == 0) { - return; - } - - Game_Map::Scroll(dx, dy); - - data()->pan_current_x -= dx; - data()->pan_current_y -= dy; -} - -bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { - return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); -} +/* + * 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" +#include "cute_c2.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(); + + +// if (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + real_x = (float)GetX(); + real_y = (float)GetY(); + } // END - PIXELMOVE + +} + +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); + +// UpdateScroll(1, 0); // TODO - PIXELMOVE + + + 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()); + */ + +// if (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + } + else { + Game_Map::SetPositionX(GetSpriteX() - GetPanX()); + Game_Map::SetPositionY(GetSpriteY() - GetPanY()); + } // END PIXELMOVE + + + } + + 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 (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + float dx = real_x * SCREEN_TILE_SIZE - Game_Map::GetPositionX() - (Player::screen_width / 2) * TILE_SIZE + SCREEN_TILE_SIZE / 2; + float dy = real_y * SCREEN_TILE_SIZE - Game_Map::GetPositionY() - (Player::screen_height / 2) * TILE_SIZE + SCREEN_TILE_SIZE; + + Game_Map::Scroll(floor(dx), floor(dy)); + return; + } // END - PIXELMOVE + + + 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() { + + canMove = false; + + if (doomWait > 0) { + doomWait--; + } + + 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); + + + + + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + if (Game_Map::IsAnyEventStarting()) { + return; + } + + canMove = true; + + + 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 (true) { //TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + int dx = Input::IsPressed(Input::RIGHT) - Input::IsPressed(Input::LEFT); + int dy = Input::IsPressed(Input::DOWN) - Input::IsPressed(Input::UP); + + int dir8 = 5 + dx - dy * 3; + +// switch (dir8) { + switch (GetInputDirection()) { // This is the only code from pixelmovement that I had to change - LK + case 2: + move_dir = Down; + break; + case 4: + move_dir = Left; + break; + case 6: + move_dir = Right; + break; + case 8: + move_dir = Up; + break; + case 1: + move_dir = DownLeft; + break; + case 3: + move_dir = DownRight; + break; + case 7: + move_dir = UpLeft; + break; + case 9: + move_dir = UpRight; + break; + } + + +// switch (Input::dir4) { + switch (Input::dir8) { + case 2: + move_dir = Down; + break; + case 4: + move_dir = Left; + break; + case 6: + move_dir = Right; + break; + case 8: + move_dir = Up; + break; + case 1: + move_dir = DownLeft; + break; + case 3: + move_dir = DownRight; + break; + case 7: + move_dir = UpLeft; + break; + case 9: + move_dir = UpRight; + break; + } +} + else { + 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) { + + if (move_dir >= 0 && ((doomMoveType <= 0 || doomMoveType == 2) && doomWait <= 0)) { + + SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); + + if (doomMoveType == 0) { + + static const int turn_speed[] = { 64, 32, 24, 16, 12, 8 }; + static const int move_speed[] = { 16, 8, 6, 4, 3, 2 }; + + if (move_dir == Left) { + if (Input::IsPressed(Input::SHIFT)) { + int d = GetDirection(); + Turn90DegreeLeft(); + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + int left_x = Game_Map::XwithDirection(GetX(), d); + int left_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, left_x, left_y, false); + + SetDirection(d); + + } else if (Player::game_config.allow_pixel_movement.Get()) { + // NEW: Smooth continuous turning for pixel movement + + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + } else { + Turn90DegreeLeft(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + } + } + else if (move_dir == Right) { + + if (Input::IsPressed(Input::SHIFT)) { + int d = GetDirection(); + Turn90DegreeRight(); + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + int right_x = Game_Map::XwithDirection(GetX(), d); + int right_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, right_x, right_y, false); + + SetDirection(d); + } else if (Player::game_config.allow_pixel_movement.Get()) { + // NEW: Smooth continuous turning for pixel movement + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + } else { + Turn90DegreeRight(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1]; + } + + } + else if (move_dir == Up) { + + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + + } + else if (move_dir == Down) { + if (Input::IsPressed(Input::SHIFT) || Player::game_config.allow_pixel_movement.Get() ) { + int d = GetDirection(); + Turn180Degree(); + Move(GetDirection()); + int back_x = Game_Map::XwithDirection(GetX(), d); + int back_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, back_x, back_y, false); + + doomWait = move_speed[GetMoveSpeed() - 1]; + SetDirection(d); + } + + else { + Turn180Degree(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1]; + } + + } + + +// - END: CORRECTED Continuous Movement Logic --- + } else { + Move(move_dir); + + + } + + + ResetThrough(); +// if (IsStopping()) { + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + int self_x = GetX(); + int self_y = GetY(); + int self_dir = GetDirection(); + + int front_id = Game_Map::CheckEvent(front_x, front_y); + + + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); + + + +// } + +// if (IsStopping()) { // This was preventing activating events while moving if (Input::IsTriggered(Input::DECISION)) { + + } +// return; +// } + + +// 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(); +} + + + +int Game_Player::GetInputDirection() { + + return Game_Map::GetMoveDirection(Input::dir8); + return Game_Map::GetMoveDirection(Input::dir4); + // This is the only part of Mode7 I had to change - LK +} + + + +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() { + +// PIXELMOVE: Handle the smooth, multi-frame transition for boarding/unboarding. + if (Player::game_config.allow_pixel_movement.Get() && IsBoardingOrUnboarding()) { + if (data()->boarding) { + Game_Vehicle* vehicle = GetVehicle(); + if (vehicle) { + // Move towards the vehicle's center + SetMoveTowardTarget(vehicle->real_x, vehicle->real_y, false); + if (!UpdateMoveTowardTarget()) { + // Arrived at the vehicle, finalize state + data()->boarding = false; + data()->aboard = true; + SetFacing(Left); // RPG_RT behavior + SetMoveSpeed(vehicle->GetMoveSpeed()); + } + } + } else if (data()->unboarding) { + if (!UpdateMoveTowardTarget()) { + // Arrived at shore, finalize state + data()->unboarding = false; + } + } + // Call base update to handle animations, but skip player input processing + Game_Character::Update(); + // Early return to prevent normal movement logic from running + return; + } + + + Game_Character::Update(); + if (IsStopping()) { + if (!Player::game_config.allow_pixel_movement.Get()) { + 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); + } + + if (Input::IsPressed(Input::PLUS)) { + Game_Map::RotateMode7(200); + } + if (Input::IsPressed(Input::MINUS)) { + Game_Map::RotateMode7(-200); + } + if (Input::IsPressed(Input::N1)) { + Game_Map::TiltMode7(-100); + } + if (Input::IsPressed(Input::N3)) { + Game_Map::TiltMode7(100); + } + if (Input::IsPressed(Input::N5)) { + Game_Map::RotateTowardsMode7(0, 20); + Game_Map::TiltTowardsMode7(6000, 20); + } + + + + } +} + +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()); + + // Check for "Action Key" on events in front of the player (Same as Hero layer) + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + + // Check for "Action Key" on events the player is standing on (Above/Below Hero layers) + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); + + // Counter tile logic + // Counter tile loop stops only if you talk to an action event. + bool got_action = result; + 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); + } + result |= got_action; + + 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); + } + } + +*/ + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + + +// if (true) { + if (Player::game_config.allow_pixel_movement.Get()) { + + c2Circle self; + c2Circle other; + + self.p = c2V(real_x, real_y); // Use the float position + self.r = 0.3f; // See Part 2 below for explanation of this change + other.r = 0.4f; + + for (auto& ev : Game_Map::GetEvents()) { + const auto trigger = ev.GetTrigger(); + other.p = c2V(static_cast(ev.GetX()), static_cast(ev.GetY())); + if (ev.IsActive() + && ev.GetLayer() != lcf::rpg::EventPage::Layers_same // This function is ONLY for different-layer events + && trigger >= 0 + && triggers[trigger] + && c2CircletoCircle(self, other)) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } +} else { // Standard tile-based logic + 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 // This function is ONLY for different-layer events + && trigger >= 0 + && triggers[trigger]) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } + } // END - PIXELMOVE + + + + return result; +} + +// CORRECTED and FINAL CheckEventTriggerThere function +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; + + // *** NEW PIXEL MOVEMENT LOGIC *** + if (Player::game_config.allow_pixel_movement.Get()) { + // Define an "interaction box" in front of the player + c2AABB interaction_box; + const float box_width_half = 0.3f; // A bit less than half a tile wide + const float box_depth = 0.4f; // Extends half a tile forward + const float offset = 0.3f; // Starts slightly in front of the player center + + // Get the player's float position and direction + float px = real_x; + float py = real_y; + int dir = GetDirection(); + + // Position the interaction box based on player's direction + if (dir == Up) { + interaction_box.min = c2V(px - box_width_half, py - offset - box_depth); + interaction_box.max = c2V(px + box_width_half, py - offset); + } else if (dir == Down) { + interaction_box.min = c2V(px - box_width_half, py + offset); + interaction_box.max = c2V(px + box_width_half, py + offset + box_depth); + } else if (dir == Left) { + interaction_box.min = c2V(px - offset - box_depth, py - box_width_half); + interaction_box.max = c2V(px - offset, py + box_width_half); + } else { // Right + interaction_box.min = c2V(px + offset, py - box_width_half); + interaction_box.max = c2V(px + offset + box_depth, py + box_width_half); + } + + for (auto& ev : Game_Map::GetEvents()) { + const auto trigger = ev.GetTrigger(); + + // Only check same-layer events that match the trigger type + if (ev.IsActive() + && ev.GetLayer() == lcf::rpg::EventPage::Layers_same + && trigger >= 0 + && triggers[trigger]) + { + // Check for collision between the event's circle and the player's interaction box + c2Circle event_circle; + event_circle.p = c2V(ev.real_x, ev.real_y); + event_circle.r = 0.4f; // Event's hitbox is a full tile + + if (c2CircletoAABB(event_circle, interaction_box)) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } + } + } else { + // --- Original Tile-Based Logic --- + 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()); + + Output::Debug("player.name: {}", GetSpriteName()); // TODO - PIXELMOVE + +} + +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; + 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; + } + + // MODIFIED: + if (Player::game_config.allow_pixel_movement.Get()) { + // Force player to vehicle's position. + // RPG_RT vehicles are 1x1, so we snap to the center of the vehicle's tile. + real_x = vehicle->real_x; + real_y = vehicle->real_y; + SetX(static_cast(round(real_x))); + SetY(static_cast(round(real_y))); + + // ADDED: Force camera update to new position instantly + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + + // Instant boarding, no need for boarding flag update logic in Update() + data()->boarding = true; // if this is false, the player just moves onto the water tile! We could use this for a swimming effector surfing effect possibly. + } else { + SetThrough(true); + Move(GetDirection()); + ResetThrough(); + data()->boarding = true; + } + + data()->vehicle = vehicle->GetVehicleType(); + data()->preboard_move_speed = GetMoveSpeed(); + } + + 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; + } + 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; + + // MODIFIED: + if (Player::game_config.allow_pixel_movement.Get()) { + // Manually calculate target coordinate (16px = 1.0f tile unit) + // and force position update to avoid SetThrough/Move overloading issues. + float dest_x = real_x; + float dest_y = real_y; + + switch (GetDirection()) { + case Up: dest_y -= 1.0f; break; + case Down: dest_y += 1.0f; break; + case Left: dest_x -= 1.0f; break; + case Right: dest_x += 1.0f; break; + } + + // Handle Map Looping for the destination + if (Game_Map::LoopHorizontal()) { + float width = static_cast(Game_Map::GetTilesX()); + if (dest_x < 0) dest_x += width; + else if (dest_x >= width) dest_x -= width; + } + + if (Game_Map::LoopVertical()) { + float height = static_cast(Game_Map::GetTilesY()); + if (dest_y < 0) dest_y += height; + else if (dest_y >= height) dest_y -= height; + } + + // Apply Position + real_x = dest_x; + real_y = dest_y; + SetX(static_cast(round(real_x))); + SetY(static_cast(round(real_y))); + + // ADDED: Force camera update to new position instantly + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + + // Since we moved instantly, we are done unboarding. + data()->unboarding = false; + } else { + 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; + }; + + 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 } + }; + 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(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +int Game_Player::GetDefaultPanY() { + return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +void Game_Player::LockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; +} + +void Game_Player::UnlockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; +} + +void Game_Player::StartPan(int direction, int distance, int speed) { + distance *= SCREEN_TILE_SIZE; + + if (direction == PanUp) data()->pan_finish_y += distance; + else if (direction == PanRight) data()->pan_finish_x -= distance; + else if (direction == PanDown) data()->pan_finish_y -= distance; + else if (direction == PanLeft) data()->pan_finish_x += distance; + + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { + if (!Player::IsPatchManiac()) { + return; + } + + h *= TILE_SIZE; + v *= TILE_SIZE; + + maniac_pan_current_x = static_cast(data()->pan_current_x); + maniac_pan_current_y = static_cast(data()->pan_current_y); + + int new_pan_x, new_pan_y; + + if (relative && centered) { + int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; + int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; + new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; + new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; + } else if (relative) { + new_pan_x = data()->pan_finish_x - h; + new_pan_y = data()->pan_finish_y - v; + } else if (centered) { + new_pan_x = GetSpriteX() + GetDefaultPanX() - h; + new_pan_y = GetSpriteY() + GetDefaultPanY() - v; + } else { + new_pan_x = GetSpriteX() - h; + new_pan_y = GetSpriteY() - v; + } + + double h_speed, v_speed; + + if (speed == 0) { + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); + } else if (interpolated) { + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); + } else { + h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + } + + data()->pan_finish_x = new_pan_x; + data()->pan_finish_y = new_pan_y; + data()->maniac_horizontal_pan_speed = h_speed; + data()->maniac_vertical_pan_speed = v_speed; +} + +void Game_Player::ResetPan(int speed) { + data()->pan_finish_x = GetDefaultPanX(); + data()->pan_finish_y = GetDefaultPanY(); + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +int Game_Player::GetPanWait() { + bool is_maniac = Player::IsPatchManiac(); + const auto distance = std::max(std::abs(data()->pan_current_x - data()->pan_finish_x), std::abs(data()->pan_current_y - data()->pan_finish_y)); + const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max(std::abs(data()->maniac_horizontal_pan_speed), std::abs(data()->maniac_vertical_pan_speed))); + assert(speed > 0); + return distance / speed + (distance % speed != 0); +} + +void Game_Player::UpdatePan() { + if (!IsPanActive()) return; + + const int step = data()->pan_speed; + const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; + const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; + + int dx, dy; + + if (Player::IsPatchManiac()) { + const double step_x = data()->maniac_horizontal_pan_speed; + const double step_y = data()->maniac_vertical_pan_speed; + double dx2 = std::min(step_x, std::abs(static_cast(pan_remain_x))); + double dy2 = std::min(step_y, std::abs(static_cast(pan_remain_y))); + dx2 = pan_remain_x >= 0 ? dx2 : -dx2; + dy2 = pan_remain_y >= 0 ? dy2 : -dy2; + maniac_pan_current_x -= dx2; + maniac_pan_current_y -= dy2; + dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); + dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); + } else { + dx = std::min(step, std::abs(pan_remain_x)); + dy = std::min(step, std::abs(pan_remain_y)); + dx = pan_remain_x >= 0 ? dx : -dx; + dy = pan_remain_y >= 0 ? dy : -dy; + } + + int screen_x = Game_Map::GetPositionX(); + int screen_y = Game_Map::GetPositionY(); + + Game_Map::AddScreenX(screen_x, dx); + Game_Map::AddScreenY(screen_y, dy); + + if (dx == 0 && dy == 0) return; + + Game_Map::Scroll(dx, dy); + + data()->pan_current_x -= dx; + data()->pan_current_y -= dy; +} + +bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { + return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); +} diff --git a/src/game_player.h b/src/game_player.h index 9174e1f231..175c4213b3 100644 --- a/src/game_player.h +++ b/src/game_player.h @@ -51,7 +51,8 @@ class Game_Player : public Game_PlayerBase { bool IsVisible() const override; bool MakeWay(int from_x, int from_y, int to_x, int to_y) override; void UpdateNextMovementAction() override; - void UpdateMovement(int amount) override; + void UpdateMovement(int amount) override; + int GetInputDirection(); void MoveRouteSetSpriteGraphic(std::string sprite_name, int index) override; bool Move(int dir) override; /** @} */ @@ -155,16 +156,39 @@ class Game_Player : public Game_PlayerBase { bool IsMapCompatibleWithSave(int map_save_count) const; bool IsDatabaseCompatibleWithSave(int database_save_count) const; - void UpdateSaveCounts(int db_save_count, int map_save_count); -private: - using TriggerSet = lcf::FlagSet; + void UpdateSaveCounts(int db_save_count, int map_save_count); + +// private: + + bool canMove = false; + + float GetRealX() const { return real_x; } + float GetRealY() const { return real_y; } + + int doomMoveType = -1; + + float angle = 0.0f; + // Move things here + bool CheckActionEvent(); + + + using TriggerSet = lcf::FlagSet; + +// bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key); +// bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key); + + bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player = true); + bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player = true); + +private: + + void UpdateScroll(int amount, bool was_jumping); void UpdatePan(); void UpdateEncounterSteps(); - bool CheckActionEvent(); - bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player = true); - bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player = true); + // bool CheckActionEvent(); + bool GetOnVehicle(); bool GetOffVehicle(); bool UpdateAirship(); diff --git a/src/game_vehicle.cpp b/src/game_vehicle.cpp index 187e271ce4..43f1a3f8ef 100644 --- a/src/game_vehicle.cpp +++ b/src/game_vehicle.cpp @@ -65,7 +65,14 @@ Game_Vehicle::Game_Vehicle(Type type) SetY(lcf::Data::treemap.start.airship_y); SetMoveSpeed(lcf::rpg::EventPage::MoveSpeed_double); break; - } + } + + +// if (true) { // TODO - PIXELMOVE + real_x = (float)GetX(); + real_y = (float)GetY(); +// } // END PIXELMOVE + } void Game_Vehicle::SetSaveData(lcf::rpg::SaveVehicleLocation save) { @@ -128,7 +135,15 @@ void Game_Vehicle::SyncWithRider(const Game_Character* rider) { SetY(rider->GetY()); SetDirection(rider->GetDirection()); SetFacing(rider->GetFacing()); - SetRemainingStep(rider->GetRemainingStep()); + SetRemainingStep(rider->GetRemainingStep()); + +// if (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()) { + real_x = rider->real_x; + real_y = rider->real_y; + } // END - PIXELMOVE + + // RPG_RT doesn't copy jumping chunks @@ -146,9 +161,18 @@ int Game_Vehicle::GetAltitude() const { else return SCREEN_TILE_SIZE / (SCREEN_TILE_SIZE / TILE_SIZE); } + +// int Game_Vehicle::GetYOffset() const { +// return Game_Character::GetYOffset(); +// } int Game_Vehicle::GetScreenY(bool apply_jump) const { - return Game_Character::GetScreenY(apply_jump) - GetAltitude(); +// return Game_Character::GetScreenY(apply_jump) - GetAltitude(); + if (apply_jump) { + return Game_Character::GetScreenY(apply_jump) - GetAltitude(); + } + return Game_Character::GetScreenY(apply_jump); + } bool Game_Vehicle::CanLand() const { diff --git a/src/game_vehicle.h b/src/game_vehicle.h index f5b85c1602..067db4875e 100644 --- a/src/game_vehicle.h +++ b/src/game_vehicle.h @@ -71,7 +71,8 @@ class Game_Vehicle : public Game_VehicleBase { bool IsInUse() const; bool IsAboard() const; void SyncWithRider(const Game_Character* rider); - bool AnimateAscentDescent(); + bool AnimateAscentDescent(); + int GetYOffset() const; int GetScreenY(bool apply_jump = true) const override; bool CanLand() const; void StartAscent(); From fd98e7faed21fcf33ee303389c6ff29aa761d083 Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:50:24 -0600 Subject: [PATCH 3/4] Converted CRLF to LF Ran everything through dos2unix --- src/cute_c2.h | 32 +- src/game_character.cpp | 3748 +++++++++++++------------- src/game_character.h | 52 +- src/game_config_game.cpp | 18 +- src/game_event.cpp | 46 +- src/game_event.h | 4 +- src/game_interpreter.cpp | 4 +- src/game_map.cpp | 5494 +++++++++++++++++++------------------- src/game_map.h | 148 +- src/game_player.cpp | 2724 +++++++++---------- src/game_player.h | 34 +- src/game_vehicle.cpp | 32 +- src/game_vehicle.h | 2 +- 13 files changed, 6169 insertions(+), 6169 deletions(-) diff --git a/src/cute_c2.h b/src/cute_c2.h index be06fc9ee6..3d99a09168 100644 --- a/src/cute_c2.h +++ b/src/cute_c2.h @@ -1,19 +1,19 @@ -/* - -* 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 . +/* + +* 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. diff --git a/src/game_character.cpp b/src/game_character.cpp index 7348551f1b..1e7eb38f4a 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -1,1874 +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 . - */ - -#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); - } -} +/* + * 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_character.h b/src/game_character.h index 3c5ed6da66..c7b3bea184 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -28,15 +28,15 @@ #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; @@ -50,23 +50,23 @@ class Game_Character { 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; + 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; - + float GetStepSize() const; + // END - PIXELMOVE - + using AnimType = lcf::rpg::EventPage::AnimType; @@ -617,7 +617,7 @@ 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); /** @@ -946,9 +946,9 @@ class Game_Character { DownLeft, UpLeft }; - + float Epsilon = pow(256, -2); //TODO - PIXELMOVE - + static bool IsDirectionDiagonal(int d); @@ -960,10 +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); @@ -984,18 +984,18 @@ 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; + +// 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 @@ -1042,14 +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 f9d9ca38e8..7cd8cbae71 100644 --- a/src/game_config_game.cpp +++ b/src/game_config_game.cpp @@ -69,11 +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, 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) { @@ -239,10 +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 (allow_pixel_movement.FromIni(ini)) { + patch_override = true; + } if (RuntimePatches::ParseFromIni(ini)) { patch_override = true; diff --git a/src/game_event.cpp b/src/game_event.cpp index abf20c5796..985dd210eb 100644 --- a/src/game_event.cpp +++ b/src/game_event.cpp @@ -33,7 +33,7 @@ #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) : @@ -44,17 +44,17 @@ Game_Event::Game_Event(int map_id, const lcf::rpg::Event* event) : SetMapId(map_id); SetX(event->x); SetY(event->y); - -// if (true) { + +// 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(); - + + RefreshPage(); + Output::Debug("event[{}].name: {}", data()->ID, GetSpriteName()); //TODO - PIXELMOVE } @@ -103,9 +103,9 @@ void Game_Event::SetSaveData(lcf::rpg::SaveMapEvent save) if (has_state) { interpreter->SetState(state); } - } - -// if (true) { + } + +// if (true) { if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE real_x = (float)GetX(); real_y = (float)GetY(); @@ -487,13 +487,13 @@ void Game_Event::MoveTypeRandom() { SetStopCount(Rand::GetRandomNumber(0, GetMaxStopCount())); return; } - + /* Move(GetDirection()); -*/ - +*/ + // if (true) { // TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()){ + if (Player::game_config.allow_pixel_movement.Get()){ c2v target = c2V( round(real_x + GetDxFromDirection(GetDirection())), round(real_y + GetDyFromDirection(GetDirection())) @@ -505,7 +505,7 @@ void Game_Event::MoveTypeRandom() { Move(GetDirection()); } // END - PIXELMOVE - + if (IsStopping()) { if (IsWaitingForegroundExecution() || (GetStopCount() >= GetMaxStopCount() + 60)) { @@ -570,8 +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); @@ -589,10 +589,10 @@ void Game_Event::MoveTypeTowardsOrAwayPlayer(bool towards) { } Move(dir); - */ - - -// if (true) { + */ + + +// if (true) { if (Player::game_config.allow_pixel_movement.Get()){ //TODO - PIXELMOVE int dir = 0; int draw = 0; @@ -651,9 +651,9 @@ void Game_Event::MoveTypeTowardsOrAwayPlayer(bool towards) { 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 60e288f01c..09943d274a 100644 --- a/src/game_event.h +++ b/src/game_event.h @@ -46,8 +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_interpreter.cpp b/src/game_interpreter.cpp index cfe56c0fb8..0f8c70130e 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -992,8 +992,8 @@ void Game_Interpreter::SetupChoices(const std::vector& choices, int } pm.SetChoiceContinuation([this, indent](int choice_result) { - if (IsRunning()) { - SetSubcommandIndex(indent, choice_result); + if (IsRunning()) { + SetSubcommandIndex(indent, choice_result); } return AsyncOp(); }); diff --git a/src/game_map.cpp b/src/game_map.cpp index c44c0d20c0..8618c0e117 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -1,2747 +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 - -#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; -} - - +/* + * 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 8f1f1e8f7b..9f414eb2f7 100644 --- a/src/game_map.h +++ b/src/game_map.h @@ -39,8 +39,8 @@ #include #include #include -#include - +#include + #include "tilemap_layer.h" class FileRequestAsync; @@ -48,16 +48,16 @@ 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 - +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: @@ -89,12 +89,12 @@ 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. @@ -382,7 +382,7 @@ 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(); /** @@ -447,62 +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); - + + /** @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. * @@ -826,12 +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 72af2439c7..33220f20ec 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -1,1362 +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" -#include "cute_c2.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(); - - -// if (true) { // TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()){ - real_x = (float)GetX(); - real_y = (float)GetY(); - } // END - PIXELMOVE - -} - -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); - -// UpdateScroll(1, 0); // TODO - PIXELMOVE - - - 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()); - */ - -// if (true) { // TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()){ - Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); - Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); - } - else { - Game_Map::SetPositionX(GetSpriteX() - GetPanX()); - Game_Map::SetPositionY(GetSpriteY() - GetPanY()); - } // END PIXELMOVE - - - } - - 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 (true) { // TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()){ - float dx = real_x * SCREEN_TILE_SIZE - Game_Map::GetPositionX() - (Player::screen_width / 2) * TILE_SIZE + SCREEN_TILE_SIZE / 2; - float dy = real_y * SCREEN_TILE_SIZE - Game_Map::GetPositionY() - (Player::screen_height / 2) * TILE_SIZE + SCREEN_TILE_SIZE; - - Game_Map::Scroll(floor(dx), floor(dy)); - return; - } // END - PIXELMOVE - - - 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() { - - canMove = false; - - if (doomWait > 0) { - doomWait--; - } - - 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); - - - - - CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); - - - if (Game_Map::IsAnyEventStarting()) { - return; - } - - canMove = true; - - - 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 (true) { //TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()){ - int dx = Input::IsPressed(Input::RIGHT) - Input::IsPressed(Input::LEFT); - int dy = Input::IsPressed(Input::DOWN) - Input::IsPressed(Input::UP); - - int dir8 = 5 + dx - dy * 3; - -// switch (dir8) { - switch (GetInputDirection()) { // This is the only code from pixelmovement that I had to change - LK - case 2: - move_dir = Down; - break; - case 4: - move_dir = Left; - break; - case 6: - move_dir = Right; - break; - case 8: - move_dir = Up; - break; - case 1: - move_dir = DownLeft; - break; - case 3: - move_dir = DownRight; - break; - case 7: - move_dir = UpLeft; - break; - case 9: - move_dir = UpRight; - break; - } - - -// switch (Input::dir4) { - switch (Input::dir8) { - case 2: - move_dir = Down; - break; - case 4: - move_dir = Left; - break; - case 6: - move_dir = Right; - break; - case 8: - move_dir = Up; - break; - case 1: - move_dir = DownLeft; - break; - case 3: - move_dir = DownRight; - break; - case 7: - move_dir = UpLeft; - break; - case 9: - move_dir = UpRight; - break; - } -} - else { - 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) { - - if (move_dir >= 0 && ((doomMoveType <= 0 || doomMoveType == 2) && doomWait <= 0)) { - - SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); - - if (doomMoveType == 0) { - - static const int turn_speed[] = { 64, 32, 24, 16, 12, 8 }; - static const int move_speed[] = { 16, 8, 6, 4, 3, 2 }; - - if (move_dir == Left) { - if (Input::IsPressed(Input::SHIFT)) { - int d = GetDirection(); - Turn90DegreeLeft(); - Move(GetDirection()); - doomWait = move_speed[GetMoveSpeed() - 1]; - int left_x = Game_Map::XwithDirection(GetX(), d); - int left_y = Game_Map::YwithDirection(GetY(), d); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, left_x, left_y, false); - - SetDirection(d); - - } else if (Player::game_config.allow_pixel_movement.Get()) { - // NEW: Smooth continuous turning for pixel movement - - SetFacing(GetDirection()); - doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); - - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); - CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); - - - } else { - Turn90DegreeLeft(); - SetFacing(GetDirection()); - doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); - } - } - else if (move_dir == Right) { - - if (Input::IsPressed(Input::SHIFT)) { - int d = GetDirection(); - Turn90DegreeRight(); - Move(GetDirection()); - doomWait = move_speed[GetMoveSpeed() - 1]; - int right_x = Game_Map::XwithDirection(GetX(), d); - int right_y = Game_Map::YwithDirection(GetY(), d); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, right_x, right_y, false); - - SetDirection(d); - } else if (Player::game_config.allow_pixel_movement.Get()) { - // NEW: Smooth continuous turning for pixel movement - SetFacing(GetDirection()); - doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); - - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); - CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); - - - } else { - Turn90DegreeRight(); - SetFacing(GetDirection()); - doomWait = turn_speed[GetMoveSpeed() - 1]; - } - - } - else if (move_dir == Up) { - - Move(GetDirection()); - doomWait = move_speed[GetMoveSpeed() - 1]; - - } - else if (move_dir == Down) { - if (Input::IsPressed(Input::SHIFT) || Player::game_config.allow_pixel_movement.Get() ) { - int d = GetDirection(); - Turn180Degree(); - Move(GetDirection()); - int back_x = Game_Map::XwithDirection(GetX(), d); - int back_y = Game_Map::YwithDirection(GetY(), d); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, back_x, back_y, false); - - doomWait = move_speed[GetMoveSpeed() - 1]; - SetDirection(d); - } - - else { - Turn180Degree(); - SetFacing(GetDirection()); - doomWait = turn_speed[GetMoveSpeed() - 1]; - } - - } - - -// - END: CORRECTED Continuous Movement Logic --- - } else { - Move(move_dir); - - - } - - - ResetThrough(); -// if (IsStopping()) { - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - int self_x = GetX(); - int self_y = GetY(); - int self_dir = GetDirection(); - - int front_id = Game_Map::CheckEvent(front_x, front_y); - - - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); - - - -// } - -// if (IsStopping()) { // This was preventing activating events while moving if (Input::IsTriggered(Input::DECISION)) { - - } -// return; -// } - - -// 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(); -} - - - -int Game_Player::GetInputDirection() { - - return Game_Map::GetMoveDirection(Input::dir8); - return Game_Map::GetMoveDirection(Input::dir4); - // This is the only part of Mode7 I had to change - LK -} - - - -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() { - -// PIXELMOVE: Handle the smooth, multi-frame transition for boarding/unboarding. - if (Player::game_config.allow_pixel_movement.Get() && IsBoardingOrUnboarding()) { - if (data()->boarding) { - Game_Vehicle* vehicle = GetVehicle(); - if (vehicle) { - // Move towards the vehicle's center - SetMoveTowardTarget(vehicle->real_x, vehicle->real_y, false); - if (!UpdateMoveTowardTarget()) { - // Arrived at the vehicle, finalize state - data()->boarding = false; - data()->aboard = true; - SetFacing(Left); // RPG_RT behavior - SetMoveSpeed(vehicle->GetMoveSpeed()); - } - } - } else if (data()->unboarding) { - if (!UpdateMoveTowardTarget()) { - // Arrived at shore, finalize state - data()->unboarding = false; - } - } - // Call base update to handle animations, but skip player input processing - Game_Character::Update(); - // Early return to prevent normal movement logic from running - return; - } - - - Game_Character::Update(); - if (IsStopping()) { - if (!Player::game_config.allow_pixel_movement.Get()) { - 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); - } - - if (Input::IsPressed(Input::PLUS)) { - Game_Map::RotateMode7(200); - } - if (Input::IsPressed(Input::MINUS)) { - Game_Map::RotateMode7(-200); - } - if (Input::IsPressed(Input::N1)) { - Game_Map::TiltMode7(-100); - } - if (Input::IsPressed(Input::N3)) { - Game_Map::TiltMode7(100); - } - if (Input::IsPressed(Input::N5)) { - Game_Map::RotateTowardsMode7(0, 20); - Game_Map::TiltTowardsMode7(6000, 20); - } - - - - } -} - -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()); - - // Check for "Action Key" on events in front of the player (Same as Hero layer) - result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); - - // Check for "Action Key" on events the player is standing on (Above/Below Hero layers) - result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); - - // Counter tile logic - // Counter tile loop stops only if you talk to an action event. - bool got_action = result; - 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); - } - result |= got_action; - - 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); - } - } - -*/ - - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - - -// if (true) { - if (Player::game_config.allow_pixel_movement.Get()) { - - c2Circle self; - c2Circle other; - - self.p = c2V(real_x, real_y); // Use the float position - self.r = 0.3f; // See Part 2 below for explanation of this change - other.r = 0.4f; - - for (auto& ev : Game_Map::GetEvents()) { - const auto trigger = ev.GetTrigger(); - other.p = c2V(static_cast(ev.GetX()), static_cast(ev.GetY())); - if (ev.IsActive() - && ev.GetLayer() != lcf::rpg::EventPage::Layers_same // This function is ONLY for different-layer events - && trigger >= 0 - && triggers[trigger] - && c2CircletoCircle(self, other)) { - SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); - } - } -} else { // Standard tile-based logic - 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 // This function is ONLY for different-layer events - && trigger >= 0 - && triggers[trigger]) { - SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); - } - } - } // END - PIXELMOVE - - - - return result; -} - -// CORRECTED and FINAL CheckEventTriggerThere function -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; - - // *** NEW PIXEL MOVEMENT LOGIC *** - if (Player::game_config.allow_pixel_movement.Get()) { - // Define an "interaction box" in front of the player - c2AABB interaction_box; - const float box_width_half = 0.3f; // A bit less than half a tile wide - const float box_depth = 0.4f; // Extends half a tile forward - const float offset = 0.3f; // Starts slightly in front of the player center - - // Get the player's float position and direction - float px = real_x; - float py = real_y; - int dir = GetDirection(); - - // Position the interaction box based on player's direction - if (dir == Up) { - interaction_box.min = c2V(px - box_width_half, py - offset - box_depth); - interaction_box.max = c2V(px + box_width_half, py - offset); - } else if (dir == Down) { - interaction_box.min = c2V(px - box_width_half, py + offset); - interaction_box.max = c2V(px + box_width_half, py + offset + box_depth); - } else if (dir == Left) { - interaction_box.min = c2V(px - offset - box_depth, py - box_width_half); - interaction_box.max = c2V(px - offset, py + box_width_half); - } else { // Right - interaction_box.min = c2V(px + offset, py - box_width_half); - interaction_box.max = c2V(px + offset + box_depth, py + box_width_half); - } - - for (auto& ev : Game_Map::GetEvents()) { - const auto trigger = ev.GetTrigger(); - - // Only check same-layer events that match the trigger type - if (ev.IsActive() - && ev.GetLayer() == lcf::rpg::EventPage::Layers_same - && trigger >= 0 - && triggers[trigger]) - { - // Check for collision between the event's circle and the player's interaction box - c2Circle event_circle; - event_circle.p = c2V(ev.real_x, ev.real_y); - event_circle.r = 0.4f; // Event's hitbox is a full tile - - if (c2CircletoAABB(event_circle, interaction_box)) { - SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); - } - } - } - } else { - // --- Original Tile-Based Logic --- - 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()); - - Output::Debug("player.name: {}", GetSpriteName()); // TODO - PIXELMOVE - -} - -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; - 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; - } - - // MODIFIED: - if (Player::game_config.allow_pixel_movement.Get()) { - // Force player to vehicle's position. - // RPG_RT vehicles are 1x1, so we snap to the center of the vehicle's tile. - real_x = vehicle->real_x; - real_y = vehicle->real_y; - SetX(static_cast(round(real_x))); - SetY(static_cast(round(real_y))); - - // ADDED: Force camera update to new position instantly - Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); - Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); - - // Instant boarding, no need for boarding flag update logic in Update() - data()->boarding = true; // if this is false, the player just moves onto the water tile! We could use this for a swimming effector surfing effect possibly. - } else { - SetThrough(true); - Move(GetDirection()); - ResetThrough(); - data()->boarding = true; - } - - data()->vehicle = vehicle->GetVehicleType(); - data()->preboard_move_speed = GetMoveSpeed(); - } - - 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; - } - 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; - - // MODIFIED: - if (Player::game_config.allow_pixel_movement.Get()) { - // Manually calculate target coordinate (16px = 1.0f tile unit) - // and force position update to avoid SetThrough/Move overloading issues. - float dest_x = real_x; - float dest_y = real_y; - - switch (GetDirection()) { - case Up: dest_y -= 1.0f; break; - case Down: dest_y += 1.0f; break; - case Left: dest_x -= 1.0f; break; - case Right: dest_x += 1.0f; break; - } - - // Handle Map Looping for the destination - if (Game_Map::LoopHorizontal()) { - float width = static_cast(Game_Map::GetTilesX()); - if (dest_x < 0) dest_x += width; - else if (dest_x >= width) dest_x -= width; - } - - if (Game_Map::LoopVertical()) { - float height = static_cast(Game_Map::GetTilesY()); - if (dest_y < 0) dest_y += height; - else if (dest_y >= height) dest_y -= height; - } - - // Apply Position - real_x = dest_x; - real_y = dest_y; - SetX(static_cast(round(real_x))); - SetY(static_cast(round(real_y))); - - // ADDED: Force camera update to new position instantly - Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); - Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); - - // Since we moved instantly, we are done unboarding. - data()->unboarding = false; - } else { - 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; - }; - - 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 } - }; - 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(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -int Game_Player::GetDefaultPanY() { - return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -void Game_Player::LockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; -} - -void Game_Player::UnlockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; -} - -void Game_Player::StartPan(int direction, int distance, int speed) { - distance *= SCREEN_TILE_SIZE; - - if (direction == PanUp) data()->pan_finish_y += distance; - else if (direction == PanRight) data()->pan_finish_x -= distance; - else if (direction == PanDown) data()->pan_finish_y -= distance; - else if (direction == PanLeft) data()->pan_finish_x += distance; - - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { - if (!Player::IsPatchManiac()) { - return; - } - - h *= TILE_SIZE; - v *= TILE_SIZE; - - maniac_pan_current_x = static_cast(data()->pan_current_x); - maniac_pan_current_y = static_cast(data()->pan_current_y); - - int new_pan_x, new_pan_y; - - if (relative && centered) { - int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; - int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; - new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; - new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; - } else if (relative) { - new_pan_x = data()->pan_finish_x - h; - new_pan_y = data()->pan_finish_y - v; - } else if (centered) { - new_pan_x = GetSpriteX() + GetDefaultPanX() - h; - new_pan_y = GetSpriteY() + GetDefaultPanY() - v; - } else { - new_pan_x = GetSpriteX() - h; - new_pan_y = GetSpriteY() - v; - } - - double h_speed, v_speed; - - if (speed == 0) { - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); - } else if (interpolated) { - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); - } else { - h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - } - - data()->pan_finish_x = new_pan_x; - data()->pan_finish_y = new_pan_y; - data()->maniac_horizontal_pan_speed = h_speed; - data()->maniac_vertical_pan_speed = v_speed; -} - -void Game_Player::ResetPan(int speed) { - data()->pan_finish_x = GetDefaultPanX(); - data()->pan_finish_y = GetDefaultPanY(); - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -int Game_Player::GetPanWait() { - bool is_maniac = Player::IsPatchManiac(); - const auto distance = std::max(std::abs(data()->pan_current_x - data()->pan_finish_x), std::abs(data()->pan_current_y - data()->pan_finish_y)); - const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max(std::abs(data()->maniac_horizontal_pan_speed), std::abs(data()->maniac_vertical_pan_speed))); - assert(speed > 0); - return distance / speed + (distance % speed != 0); -} - -void Game_Player::UpdatePan() { - if (!IsPanActive()) return; - - const int step = data()->pan_speed; - const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; - const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; - - int dx, dy; - - if (Player::IsPatchManiac()) { - const double step_x = data()->maniac_horizontal_pan_speed; - const double step_y = data()->maniac_vertical_pan_speed; - double dx2 = std::min(step_x, std::abs(static_cast(pan_remain_x))); - double dy2 = std::min(step_y, std::abs(static_cast(pan_remain_y))); - dx2 = pan_remain_x >= 0 ? dx2 : -dx2; - dy2 = pan_remain_y >= 0 ? dy2 : -dy2; - maniac_pan_current_x -= dx2; - maniac_pan_current_y -= dy2; - dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); - dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); - } else { - dx = std::min(step, std::abs(pan_remain_x)); - dy = std::min(step, std::abs(pan_remain_y)); - dx = pan_remain_x >= 0 ? dx : -dx; - dy = pan_remain_y >= 0 ? dy : -dy; - } - - int screen_x = Game_Map::GetPositionX(); - int screen_y = Game_Map::GetPositionY(); - - Game_Map::AddScreenX(screen_x, dx); - Game_Map::AddScreenY(screen_y, dy); - - if (dx == 0 && dy == 0) return; - - Game_Map::Scroll(dx, dy); - - data()->pan_current_x -= dx; - data()->pan_current_y -= dy; -} - -bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { - return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); -} +/* + * 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" +#include "cute_c2.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(); + + +// if (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + real_x = (float)GetX(); + real_y = (float)GetY(); + } // END - PIXELMOVE + +} + +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); + +// UpdateScroll(1, 0); // TODO - PIXELMOVE + + + 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()); + */ + +// if (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + } + else { + Game_Map::SetPositionX(GetSpriteX() - GetPanX()); + Game_Map::SetPositionY(GetSpriteY() - GetPanY()); + } // END PIXELMOVE + + + } + + 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 (true) { // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + float dx = real_x * SCREEN_TILE_SIZE - Game_Map::GetPositionX() - (Player::screen_width / 2) * TILE_SIZE + SCREEN_TILE_SIZE / 2; + float dy = real_y * SCREEN_TILE_SIZE - Game_Map::GetPositionY() - (Player::screen_height / 2) * TILE_SIZE + SCREEN_TILE_SIZE; + + Game_Map::Scroll(floor(dx), floor(dy)); + return; + } // END - PIXELMOVE + + + 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() { + + canMove = false; + + if (doomWait > 0) { + doomWait--; + } + + 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); + + + + + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + if (Game_Map::IsAnyEventStarting()) { + return; + } + + canMove = true; + + + 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 (true) { //TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ + int dx = Input::IsPressed(Input::RIGHT) - Input::IsPressed(Input::LEFT); + int dy = Input::IsPressed(Input::DOWN) - Input::IsPressed(Input::UP); + + int dir8 = 5 + dx - dy * 3; + +// switch (dir8) { + switch (GetInputDirection()) { // This is the only code from pixelmovement that I had to change - LK + case 2: + move_dir = Down; + break; + case 4: + move_dir = Left; + break; + case 6: + move_dir = Right; + break; + case 8: + move_dir = Up; + break; + case 1: + move_dir = DownLeft; + break; + case 3: + move_dir = DownRight; + break; + case 7: + move_dir = UpLeft; + break; + case 9: + move_dir = UpRight; + break; + } + + +// switch (Input::dir4) { + switch (Input::dir8) { + case 2: + move_dir = Down; + break; + case 4: + move_dir = Left; + break; + case 6: + move_dir = Right; + break; + case 8: + move_dir = Up; + break; + case 1: + move_dir = DownLeft; + break; + case 3: + move_dir = DownRight; + break; + case 7: + move_dir = UpLeft; + break; + case 9: + move_dir = UpRight; + break; + } +} + else { + 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) { + + if (move_dir >= 0 && ((doomMoveType <= 0 || doomMoveType == 2) && doomWait <= 0)) { + + SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); + + if (doomMoveType == 0) { + + static const int turn_speed[] = { 64, 32, 24, 16, 12, 8 }; + static const int move_speed[] = { 16, 8, 6, 4, 3, 2 }; + + if (move_dir == Left) { + if (Input::IsPressed(Input::SHIFT)) { + int d = GetDirection(); + Turn90DegreeLeft(); + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + int left_x = Game_Map::XwithDirection(GetX(), d); + int left_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, left_x, left_y, false); + + SetDirection(d); + + } else if (Player::game_config.allow_pixel_movement.Get()) { + // NEW: Smooth continuous turning for pixel movement + + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + } else { + Turn90DegreeLeft(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + } + } + else if (move_dir == Right) { + + if (Input::IsPressed(Input::SHIFT)) { + int d = GetDirection(); + Turn90DegreeRight(); + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + int right_x = Game_Map::XwithDirection(GetX(), d); + int right_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, right_x, right_y, false); + + SetDirection(d); + } else if (Player::game_config.allow_pixel_movement.Get()) { + // NEW: Smooth continuous turning for pixel movement + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1];//1 << (1 + GetMoveSpeed()); + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched}, front_x, front_y, false); + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + + } else { + Turn90DegreeRight(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1]; + } + + } + else if (move_dir == Up) { + + Move(GetDirection()); + doomWait = move_speed[GetMoveSpeed() - 1]; + + } + else if (move_dir == Down) { + if (Input::IsPressed(Input::SHIFT) || Player::game_config.allow_pixel_movement.Get() ) { + int d = GetDirection(); + Turn180Degree(); + Move(GetDirection()); + int back_x = Game_Map::XwithDirection(GetX(), d); + int back_y = Game_Map::YwithDirection(GetY(), d); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, back_x, back_y, false); + + doomWait = move_speed[GetMoveSpeed() - 1]; + SetDirection(d); + } + + else { + Turn180Degree(); + SetFacing(GetDirection()); + doomWait = turn_speed[GetMoveSpeed() - 1]; + } + + } + + +// - END: CORRECTED Continuous Movement Logic --- + } else { + Move(move_dir); + + + } + + + ResetThrough(); +// if (IsStopping()) { + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + int self_x = GetX(); + int self_y = GetY(); + int self_dir = GetDirection(); + + int front_id = Game_Map::CheckEvent(front_x, front_y); + + + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); + + + +// } + +// if (IsStopping()) { // This was preventing activating events while moving if (Input::IsTriggered(Input::DECISION)) { + + } +// return; +// } + + +// 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(); +} + + + +int Game_Player::GetInputDirection() { + + return Game_Map::GetMoveDirection(Input::dir8); + return Game_Map::GetMoveDirection(Input::dir4); + // This is the only part of Mode7 I had to change - LK +} + + + +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() { + +// PIXELMOVE: Handle the smooth, multi-frame transition for boarding/unboarding. + if (Player::game_config.allow_pixel_movement.Get() && IsBoardingOrUnboarding()) { + if (data()->boarding) { + Game_Vehicle* vehicle = GetVehicle(); + if (vehicle) { + // Move towards the vehicle's center + SetMoveTowardTarget(vehicle->real_x, vehicle->real_y, false); + if (!UpdateMoveTowardTarget()) { + // Arrived at the vehicle, finalize state + data()->boarding = false; + data()->aboard = true; + SetFacing(Left); // RPG_RT behavior + SetMoveSpeed(vehicle->GetMoveSpeed()); + } + } + } else if (data()->unboarding) { + if (!UpdateMoveTowardTarget()) { + // Arrived at shore, finalize state + data()->unboarding = false; + } + } + // Call base update to handle animations, but skip player input processing + Game_Character::Update(); + // Early return to prevent normal movement logic from running + return; + } + + + Game_Character::Update(); + if (IsStopping()) { + if (!Player::game_config.allow_pixel_movement.Get()) { + 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); + } + + if (Input::IsPressed(Input::PLUS)) { + Game_Map::RotateMode7(200); + } + if (Input::IsPressed(Input::MINUS)) { + Game_Map::RotateMode7(-200); + } + if (Input::IsPressed(Input::N1)) { + Game_Map::TiltMode7(-100); + } + if (Input::IsPressed(Input::N3)) { + Game_Map::TiltMode7(100); + } + if (Input::IsPressed(Input::N5)) { + Game_Map::RotateTowardsMode7(0, 20); + Game_Map::TiltTowardsMode7(6000, 20); + } + + + + } +} + +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()); + + // Check for "Action Key" on events in front of the player (Same as Hero layer) + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + + // Check for "Action Key" on events the player is standing on (Above/Below Hero layers) + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); + + // Counter tile logic + // Counter tile loop stops only if you talk to an action event. + bool got_action = result; + 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); + } + result |= got_action; + + 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); + } + } + +*/ + + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + + +// if (true) { + if (Player::game_config.allow_pixel_movement.Get()) { + + c2Circle self; + c2Circle other; + + self.p = c2V(real_x, real_y); // Use the float position + self.r = 0.3f; // See Part 2 below for explanation of this change + other.r = 0.4f; + + for (auto& ev : Game_Map::GetEvents()) { + const auto trigger = ev.GetTrigger(); + other.p = c2V(static_cast(ev.GetX()), static_cast(ev.GetY())); + if (ev.IsActive() + && ev.GetLayer() != lcf::rpg::EventPage::Layers_same // This function is ONLY for different-layer events + && trigger >= 0 + && triggers[trigger] + && c2CircletoCircle(self, other)) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } +} else { // Standard tile-based logic + 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 // This function is ONLY for different-layer events + && trigger >= 0 + && triggers[trigger]) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } + } // END - PIXELMOVE + + + + return result; +} + +// CORRECTED and FINAL CheckEventTriggerThere function +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; + + // *** NEW PIXEL MOVEMENT LOGIC *** + if (Player::game_config.allow_pixel_movement.Get()) { + // Define an "interaction box" in front of the player + c2AABB interaction_box; + const float box_width_half = 0.3f; // A bit less than half a tile wide + const float box_depth = 0.4f; // Extends half a tile forward + const float offset = 0.3f; // Starts slightly in front of the player center + + // Get the player's float position and direction + float px = real_x; + float py = real_y; + int dir = GetDirection(); + + // Position the interaction box based on player's direction + if (dir == Up) { + interaction_box.min = c2V(px - box_width_half, py - offset - box_depth); + interaction_box.max = c2V(px + box_width_half, py - offset); + } else if (dir == Down) { + interaction_box.min = c2V(px - box_width_half, py + offset); + interaction_box.max = c2V(px + box_width_half, py + offset + box_depth); + } else if (dir == Left) { + interaction_box.min = c2V(px - offset - box_depth, py - box_width_half); + interaction_box.max = c2V(px - offset, py + box_width_half); + } else { // Right + interaction_box.min = c2V(px + offset, py - box_width_half); + interaction_box.max = c2V(px + offset + box_depth, py + box_width_half); + } + + for (auto& ev : Game_Map::GetEvents()) { + const auto trigger = ev.GetTrigger(); + + // Only check same-layer events that match the trigger type + if (ev.IsActive() + && ev.GetLayer() == lcf::rpg::EventPage::Layers_same + && trigger >= 0 + && triggers[trigger]) + { + // Check for collision between the event's circle and the player's interaction box + c2Circle event_circle; + event_circle.p = c2V(ev.real_x, ev.real_y); + event_circle.r = 0.4f; // Event's hitbox is a full tile + + if (c2CircletoAABB(event_circle, interaction_box)) { + SetEncounterCalling(false); + result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + } + } + } + } else { + // --- Original Tile-Based Logic --- + 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()); + + Output::Debug("player.name: {}", GetSpriteName()); // TODO - PIXELMOVE + +} + +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; + 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; + } + + // MODIFIED: + if (Player::game_config.allow_pixel_movement.Get()) { + // Force player to vehicle's position. + // RPG_RT vehicles are 1x1, so we snap to the center of the vehicle's tile. + real_x = vehicle->real_x; + real_y = vehicle->real_y; + SetX(static_cast(round(real_x))); + SetY(static_cast(round(real_y))); + + // ADDED: Force camera update to new position instantly + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + + // Instant boarding, no need for boarding flag update logic in Update() + data()->boarding = true; // if this is false, the player just moves onto the water tile! We could use this for a swimming effector surfing effect possibly. + } else { + SetThrough(true); + Move(GetDirection()); + ResetThrough(); + data()->boarding = true; + } + + data()->vehicle = vehicle->GetVehicleType(); + data()->preboard_move_speed = GetMoveSpeed(); + } + + 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; + } + 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; + + // MODIFIED: + if (Player::game_config.allow_pixel_movement.Get()) { + // Manually calculate target coordinate (16px = 1.0f tile unit) + // and force position update to avoid SetThrough/Move overloading issues. + float dest_x = real_x; + float dest_y = real_y; + + switch (GetDirection()) { + case Up: dest_y -= 1.0f; break; + case Down: dest_y += 1.0f; break; + case Left: dest_x -= 1.0f; break; + case Right: dest_x += 1.0f; break; + } + + // Handle Map Looping for the destination + if (Game_Map::LoopHorizontal()) { + float width = static_cast(Game_Map::GetTilesX()); + if (dest_x < 0) dest_x += width; + else if (dest_x >= width) dest_x -= width; + } + + if (Game_Map::LoopVertical()) { + float height = static_cast(Game_Map::GetTilesY()); + if (dest_y < 0) dest_y += height; + else if (dest_y >= height) dest_y -= height; + } + + // Apply Position + real_x = dest_x; + real_y = dest_y; + SetX(static_cast(round(real_x))); + SetY(static_cast(round(real_y))); + + // ADDED: Force camera update to new position instantly + Game_Map::SetPositionX(real_x * SCREEN_TILE_SIZE - SCREEN_TILE_SIZE / 2 - GetPanX()); + Game_Map::SetPositionY(real_y * SCREEN_TILE_SIZE + SCREEN_TILE_SIZE / 2 - GetPanY()); + + // Since we moved instantly, we are done unboarding. + data()->unboarding = false; + } else { + 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; + }; + + 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 } + }; + 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(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +int Game_Player::GetDefaultPanY() { + return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +void Game_Player::LockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; +} + +void Game_Player::UnlockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; +} + +void Game_Player::StartPan(int direction, int distance, int speed) { + distance *= SCREEN_TILE_SIZE; + + if (direction == PanUp) data()->pan_finish_y += distance; + else if (direction == PanRight) data()->pan_finish_x -= distance; + else if (direction == PanDown) data()->pan_finish_y -= distance; + else if (direction == PanLeft) data()->pan_finish_x += distance; + + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { + if (!Player::IsPatchManiac()) { + return; + } + + h *= TILE_SIZE; + v *= TILE_SIZE; + + maniac_pan_current_x = static_cast(data()->pan_current_x); + maniac_pan_current_y = static_cast(data()->pan_current_y); + + int new_pan_x, new_pan_y; + + if (relative && centered) { + int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; + int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; + new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; + new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; + } else if (relative) { + new_pan_x = data()->pan_finish_x - h; + new_pan_y = data()->pan_finish_y - v; + } else if (centered) { + new_pan_x = GetSpriteX() + GetDefaultPanX() - h; + new_pan_y = GetSpriteY() + GetDefaultPanY() - v; + } else { + new_pan_x = GetSpriteX() - h; + new_pan_y = GetSpriteY() - v; + } + + double h_speed, v_speed; + + if (speed == 0) { + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); + } else if (interpolated) { + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); + } else { + h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + } + + data()->pan_finish_x = new_pan_x; + data()->pan_finish_y = new_pan_y; + data()->maniac_horizontal_pan_speed = h_speed; + data()->maniac_vertical_pan_speed = v_speed; +} + +void Game_Player::ResetPan(int speed) { + data()->pan_finish_x = GetDefaultPanX(); + data()->pan_finish_y = GetDefaultPanY(); + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +int Game_Player::GetPanWait() { + bool is_maniac = Player::IsPatchManiac(); + const auto distance = std::max(std::abs(data()->pan_current_x - data()->pan_finish_x), std::abs(data()->pan_current_y - data()->pan_finish_y)); + const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max(std::abs(data()->maniac_horizontal_pan_speed), std::abs(data()->maniac_vertical_pan_speed))); + assert(speed > 0); + return distance / speed + (distance % speed != 0); +} + +void Game_Player::UpdatePan() { + if (!IsPanActive()) return; + + const int step = data()->pan_speed; + const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; + const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; + + int dx, dy; + + if (Player::IsPatchManiac()) { + const double step_x = data()->maniac_horizontal_pan_speed; + const double step_y = data()->maniac_vertical_pan_speed; + double dx2 = std::min(step_x, std::abs(static_cast(pan_remain_x))); + double dy2 = std::min(step_y, std::abs(static_cast(pan_remain_y))); + dx2 = pan_remain_x >= 0 ? dx2 : -dx2; + dy2 = pan_remain_y >= 0 ? dy2 : -dy2; + maniac_pan_current_x -= dx2; + maniac_pan_current_y -= dy2; + dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); + dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); + } else { + dx = std::min(step, std::abs(pan_remain_x)); + dy = std::min(step, std::abs(pan_remain_y)); + dx = pan_remain_x >= 0 ? dx : -dx; + dy = pan_remain_y >= 0 ? dy : -dy; + } + + int screen_x = Game_Map::GetPositionX(); + int screen_y = Game_Map::GetPositionY(); + + Game_Map::AddScreenX(screen_x, dx); + Game_Map::AddScreenY(screen_y, dy); + + if (dx == 0 && dy == 0) return; + + Game_Map::Scroll(dx, dy); + + data()->pan_current_x -= dx; + data()->pan_current_y -= dy; +} + +bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { + return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); +} diff --git a/src/game_player.h b/src/game_player.h index 175c4213b3..c35eeb8e2d 100644 --- a/src/game_player.h +++ b/src/game_player.h @@ -51,7 +51,7 @@ class Game_Player : public Game_PlayerBase { bool IsVisible() const override; bool MakeWay(int from_x, int from_y, int to_x, int to_y) override; void UpdateNextMovementAction() override; - void UpdateMovement(int amount) override; + void UpdateMovement(int amount) override; int GetInputDirection(); void MoveRouteSetSpriteGraphic(std::string sprite_name, int index) override; bool Move(int dir) override; @@ -156,32 +156,32 @@ class Game_Player : public Game_PlayerBase { bool IsMapCompatibleWithSave(int map_save_count) const; bool IsDatabaseCompatibleWithSave(int database_save_count) const; - void UpdateSaveCounts(int db_save_count, int map_save_count); + void UpdateSaveCounts(int db_save_count, int map_save_count); -// private: - - bool canMove = false; - - float GetRealX() const { return real_x; } - float GetRealY() const { return real_y; } +// private: + + bool canMove = false; + + float GetRealX() const { return real_x; } + float GetRealY() const { return real_y; } int doomMoveType = -1; - float angle = 0.0f; + float angle = 0.0f; // Move things here - bool CheckActionEvent(); - + bool CheckActionEvent(); + + + using TriggerSet = lcf::FlagSet; - using TriggerSet = lcf::FlagSet; - // bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key); // bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key); - + bool CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player = true); - bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player = true); + bool CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player = true); + +private: -private: - void UpdateScroll(int amount, bool was_jumping); diff --git a/src/game_vehicle.cpp b/src/game_vehicle.cpp index 43f1a3f8ef..764122bdc5 100644 --- a/src/game_vehicle.cpp +++ b/src/game_vehicle.cpp @@ -65,10 +65,10 @@ Game_Vehicle::Game_Vehicle(Type type) SetY(lcf::Data::treemap.start.airship_y); SetMoveSpeed(lcf::rpg::EventPage::MoveSpeed_double); break; - } - + } + -// if (true) { // TODO - PIXELMOVE +// if (true) { // TODO - PIXELMOVE real_x = (float)GetX(); real_y = (float)GetY(); // } // END PIXELMOVE @@ -135,10 +135,10 @@ void Game_Vehicle::SyncWithRider(const Game_Character* rider) { SetY(rider->GetY()); SetDirection(rider->GetDirection()); SetFacing(rider->GetFacing()); - SetRemainingStep(rider->GetRemainingStep()); - + SetRemainingStep(rider->GetRemainingStep()); + // if (true) { // TODO - PIXELMOVE - if (Player::game_config.allow_pixel_movement.Get()) { + if (Player::game_config.allow_pixel_movement.Get()) { real_x = rider->real_x; real_y = rider->real_y; } // END - PIXELMOVE @@ -161,18 +161,18 @@ int Game_Vehicle::GetAltitude() const { else return SCREEN_TILE_SIZE / (SCREEN_TILE_SIZE / TILE_SIZE); } - -// int Game_Vehicle::GetYOffset() const { -// return Game_Character::GetYOffset(); -// } + +// int Game_Vehicle::GetYOffset() const { +// return Game_Character::GetYOffset(); +// } int Game_Vehicle::GetScreenY(bool apply_jump) const { -// return Game_Character::GetScreenY(apply_jump) - GetAltitude(); - if (apply_jump) { - return Game_Character::GetScreenY(apply_jump) - GetAltitude(); - } - return Game_Character::GetScreenY(apply_jump); - +// return Game_Character::GetScreenY(apply_jump) - GetAltitude(); + if (apply_jump) { + return Game_Character::GetScreenY(apply_jump) - GetAltitude(); + } + return Game_Character::GetScreenY(apply_jump); + } bool Game_Vehicle::CanLand() const { diff --git a/src/game_vehicle.h b/src/game_vehicle.h index 067db4875e..2cda1ac9a1 100644 --- a/src/game_vehicle.h +++ b/src/game_vehicle.h @@ -71,7 +71,7 @@ class Game_Vehicle : public Game_VehicleBase { bool IsInUse() const; bool IsAboard() const; void SyncWithRider(const Game_Character* rider); - bool AnimateAscentDescent(); + bool AnimateAscentDescent(); int GetYOffset() const; int GetScreenY(bool apply_jump = true) const override; bool CanLand() const; From d5fdc63ecd39b9967871e78599bd0af178cf3df6 Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Sat, 2 May 2026 14:06:29 -0600 Subject: [PATCH 4/4] Update game_character.cpp Hopefully this fixes the whitespace issue. I removed all the //TODO PIXELMOVE placeholders as well. --- src/game_character.cpp | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index 1e7eb38f4a..ca39f23b13 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -74,12 +74,12 @@ void Game_Character::SanitizeMoveRoute(std::string_view name, const lcf::rpg::Mo void Game_Character::MoveTo(int map_id, int x, int y) { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ 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. @@ -193,9 +193,9 @@ void Game_Character::Update() { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ UpdateMoveTowardTarget(); - } // END - PIXELMOVE + } if (IsStopping()) { @@ -315,9 +315,9 @@ void Game_Character::UpdateFlash() { 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 + if (true && is_moving_toward_target && !current_route.skippable) { return; - } // END - PIXELMOVE + } @@ -398,7 +398,6 @@ void Game_Character::UpdateMoveRoute(int32_t& current_index, const lcf::rpg::Mov */ -// 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; @@ -407,8 +406,7 @@ void Game_Character::UpdateMoveRoute(int32_t& current_index, const lcf::rpg::Mov 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 + else if (Player::game_config.allow_pixel_movement.Get()){ float vx = (float)GetDxFromDirection(GetDirection()); float vy = (float)GetDyFromDirection(GetDirection()); c2v target; @@ -429,7 +427,7 @@ void Game_Character::UpdateMoveRoute(int32_t& current_index, const lcf::rpg::Mov } else { Move(GetDirection()); - } // END - PIXELMOV + } static const int move_speed[] = { 16, 8, 6, 4, 3, 2 }; @@ -668,7 +666,7 @@ bool Game_Character::MoveVector(c2v vector) { return MoveVector(vector.x, vector.y); } -bool Game_Character::MoveVector(float vx, float vy) { // TODO - PIXELMOVE +bool Game_Character::MoveVector(float vx, float vy) { // if (abs(vx) <= Epsilon && abs(vy) <= Epsilon) { // return false; // } @@ -940,7 +938,7 @@ bool Game_Character::MoveVector(float vx, float vy) { // TODO - PIXELMOVE bool Game_Character::Move(int dir) { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){// TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ SetDirection(dir); c2v vector = c2V(GetDxFromDirection(dir), GetDyFromDirection(dir)); float step_size = GetStepSize(); @@ -1277,12 +1275,12 @@ bool Game_Character::Jump(int x, int y) { SetJumping(true); SetRemainingStep(SCREEN_TILE_SIZE); - /* if (true) { // TODO - PIXELMOVE + /* if (true) { // SetDirection(GetDirection()); - c2v vector = c2V(GetDxFromDirection(GetDirection()), GetDyFromDirection(GetDirection())); + c2v vector = c2V(GetDxFromDirection(dir), GetDyFromDirection(dir)); // c2v vector = c2V(real_x - begin_x, real_y - begin_y); float length = c2Len(vector); c2v vectorNorm = c2Div(vector, length); @@ -1325,7 +1323,7 @@ bool Game_Character::Jump(int x, int y) { int Game_Character::GetDistanceXfromCharacter(const Game_Character& target) const { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ float sx = real_x - Main_Data::game_player->real_x; @@ -1338,7 +1336,7 @@ int Game_Character::GetDistanceXfromCharacter(const Game_Character& target) cons } } return round(sx * SCREEN_TILE_SIZE); - } //END - PIXELMOVE + } int sx = GetX() - target.GetX(); @@ -1356,7 +1354,7 @@ int Game_Character::GetDistanceXfromCharacter(const Game_Character& target) cons int Game_Character::GetDistanceYfromCharacter(const Game_Character& target) const { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXELMOVE + if (Player::game_config.allow_pixel_movement.Get()){ float sy = real_y - Main_Data::game_player->real_y; if (Game_Map::LoopVertical()) { @@ -1368,7 +1366,7 @@ int Game_Character::GetDistanceYfromCharacter(const Game_Character& target) cons } } return round(sy * SCREEN_TILE_SIZE); - } // END - PIXELMOVE + } @@ -1720,9 +1718,9 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { int Game_Character::GetSpriteX() const { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXEL MOVE + if (Player::game_config.allow_pixel_movement.Get()){ return round(real_x * SCREEN_TILE_SIZE); - } // END - PIXELMOVE + } int x = GetX() * SCREEN_TILE_SIZE; @@ -1743,9 +1741,9 @@ int Game_Character::GetSpriteX() const { int Game_Character::GetSpriteY() const { // if (true) { - if (Player::game_config.allow_pixel_movement.Get()){ // TODO - PIXEL MOVE + if (Player::game_config.allow_pixel_movement.Get()){ return round(real_x * SCREEN_TILE_SIZE); - } // END - PIXELMOVE + } int y = GetY() * SCREEN_TILE_SIZE;