Files
questshock/engine/src/GameSrc/leanmetr.c
T
ml a5ec8db5f7
build / build (push) Failing after 5s
Vendor the Shockolate engine and build it via a Docker image with every
dependency (SDL2, SDL2_mixer, fluidsynth-lite, a MIDI soundfont)
prebuilt, so compiling the engine needs no network access - just the
image and the engine source. Add res/assets/extract_assets.sh to pull
the game's data files out of a purchased GOG installer, and a Makefile
that assembles a runnable dist/ from the two.

Also add `make package`, which builds a redistributable tarball that
omits the proprietary game assets (shipping res/GET_ASSETS.txt instead)
plus license information for both the MIT tooling and the GPLv3 engine,
and a Gitea Actions workflow that builds and publishes it to
dl.ladkau.de on every push.
2026-07-19 15:43:38 +02:00

594 lines
18 KiB
C

/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* $Source: r:/prj/cit/src/RCS/leanmetr.c $
* $Revision: 1.31 $
* $Author: mahk $
* $Date: 1994/11/09 20:45:40 $
*
*/
#include "faketime.h"
#include "citres.h"
#include "physics.h"
#include "player.h"
#include "criterr.h"
#include "froslew.h"
#include "objprop.h"
#include "objsim.h"
#include "tools.h"
#include "wares.h"
#include "canvchek.h"
#include "gamescr.h"
#include "otrip.h"
#include "gr2ss.h"
// -------
// DEFINES
// -------
#define SLOT_EYEMETER_X 141
#define SLOT_EYEMETER_Y 1
#define FULL_EYEMETER_X 10
#define FULL_EYEMETER_Y 1
#define EYEMETER_X() (current_meter_region->abs_x)
#define EYEMETER_Y() (current_meter_region->abs_y)
#define LEANOMETER_XOFF 21
#define LEANOMETER_YOFF 0
#define LEANOMETER_X() (EYEMETER_X() + LEANOMETER_XOFF)
#define LEANOMETER_Y() (EYEMETER_Y() + LEANOMETER_YOFF)
#define EYEMETER_W 19
#define EYEMETER_H 22
#define EYE_LEFT_MARGIN 2
#define DISCRETE_EYE_POSITIONS 3
#define DISCRETE_MIDDLE_H 6
#define MAX_EYE_ANGLE (8 * FIXANG_PI / 18)
#define LEANOMETER_W 23
#define LEANOMETER_H EYEMETER_H
#define NUM_LEAN_BMAPS 9
#define BMAPS_PER_POSTURE 3
extern uchar full_game_3d;
static ubyte discrete_eye_height[DISCRETE_EYE_POSITIONS] = {
3,
10,
18,
};
ubyte hires_eye_height[DISCRETE_EYE_POSITIONS] = {
8,
26,
45,
};
LGPoint shield_offsets[9] = {
{2, 1}, // stand right
{2, 1}, // stand
{2, 1}, // stand left
{2, 1}, // crouch right
{2, 1}, // crouch
{2, 0}, // crouch left
{2, 1}, // prone right
{2, 1}, // prone
{0, 1} // prone left
};
void EDMS_lean_o_meter(physics_handle ph, fix *lean, fix *crouch);
// -------
// GLOBALS
// -------
extern uchar gBioInited;
LGRegion slot_meter_region;
LGRegion fullscrn_meter_region;
LGRegion *current_meter_region = &slot_meter_region;
#define PICK_METER_REGION(full) ((full) ? &fullscrn_meter_region : &slot_meter_region)
ushort lean_bmap_res = 0;
ushort shield_bmap_res = 0;
#define lean_bmap_id(i) (MKREF(lean_bmap_res, i))
#define eye_bmap_r(i) (get_bitmap_from_ref(REF_IMG_bmEyeIconR + (i)))
#define eye_bmap_l(i) (get_bitmap_from_ref(REF_IMG_bmEyeIconL + (i)))
// KLC - lock instead of get #define lean_bmaps(i) (get_bitmap_from_ref(lean_bmap_id(i)))
#define lean_bmaps(i) (lock_bitmap_from_ref(lean_bmap_id(i)))
#define meter_bkgnd() (get_bitmap_from_ref(full_game_3d ? REF_IMG_bmLeanBkgndTransp : REF_IMG_bmLeanBkgnd))
uchar eye_fine_mode = FALSE;
// ---------
// INTERNALS
// ---------
#define MAX_LEAN_BASE (RES_leanMeterRad - RES_leanMeterBase)
void set_base_lean_bmap(uchar shield) {
int v;
ushort baseRes;
// Set the lean bitmaps resource.
v = player_struct.hardwarez[CPTRIP(ENV_HARD_TRIPLE)]; // v will be 0-2.
if (v > MAX_LEAN_BASE)
v = MAX_LEAN_BASE;
baseRes = RES_leanMeterBase + v;
if (baseRes != lean_bmap_res) // If the base lean bitmap has changed:
{
if (lean_bmap_res != 0) // free the old bitmap
{
ResUnlock(lean_bmap_res);
ResDrop(lean_bmap_res);
}
ResLock(baseRes); // Load hi and lock the new bitmap series.
lean_bmap_res = baseRes; // this is our base bitmap now
}
// Set the shield bitmaps resource.
if (shield) {
v = SHIELD_SETTING(player_struct.hardwarez_status[CPTRIP(SHIELD_HARD_TRIPLE)]);
if (player_struct.hardwarez[CPTRIP(SHIELD_HARD_TRIPLE)] == SHIELD_VERSIONS)
v = SHIELD_VERSIONS - 1;
baseRes = RES_leanShield1 + v;
if (baseRes != shield_bmap_res) {
// If the base shield bitmap has changed:
if (shield_bmap_res != 0) {
// free the old bitmap
ResUnlock(shield_bmap_res);
ResDrop(shield_bmap_res);
}
ResLock(baseRes); // Load hi and lock the new bitmap series.
shield_bmap_res = baseRes; // this is our base shield bitmap now
}
} else {
// If shields are now off:
if (shield_bmap_res != 0) {
// If shields were previously on, free up the shield bitmaps.
ResUnlock(shield_bmap_res);
ResDrop(shield_bmap_res);
shield_bmap_res = 0;
}
}
}
#define LEAN_CONST (fix_2pi / 10)
#define LEAN_TO_LEANX(ln) lg_min(100, lg_max(-100, (100 * (ln) / LEAN_CONST)))
#define PLAYER_HGT (fix_make(0, 0xbd00))
#define CROUCH_CONST (2 * PLAYER_HGT / 5)
#define CROUCH_TO_POSTURE(c) lg_min(2, lg_max(0, (2 - lg_max(c, 0) * 2 / CROUCH_CONST)))
// The posture meter output is filtered by a capacitor,
// the constant below is the reciprocal of the
// filter's RC time constant, in units of 1/sec.
#define POSTURE_FILTER_RATE 20
#ifdef BIASED_CAPACITOR
fix compute_filter_weight(ulong deltat) {
int rate = POSTURE_FILTER_RATE;
fix bias = FIX_UNIT * CONTROL_MAX_VAL / (CONTROL_MAX_VAL + 3 * abs(player_struct.controls[CONTROL_YVEL]));
fix weight = fix_make(deltat, 0) * rate >> APPROX_CIT_CYCLE_SHFT;
Spew(DSRC_USER_I_Lean, ("posture filter weight = %q, bias = %q\n", weight, bias));
weight = fix_mul(weight, bias);
return weight;
}
#else
fix compute_filter_weight(ulong deltat) {
int rate = POSTURE_FILTER_RATE;
fix weight = fix_make(deltat, 0) * rate >> APPROX_CIT_CYCLE_SHFT;
return weight;
}
#endif
fix apply_weighted_filter(fix input, fix state, ulong deltat) {
fix weight = compute_filter_weight(deltat);
return fix_div(fix_mul(weight, input) + state, weight + FIX_UNIT);
}
#define INTENDED_HGT (PLAYER_HGT * (NUM_POSTURES - player_struct.posture - 1) / (NUM_POSTURES - 1))
#define STORE_STATE(fval) \
(player_struct.lean_filter_state = (ushort)((fval) > FIX_UNIT ? FIX_UNIT - 1 : fix_frac(fval)))
#define GET_STATE (fix_make(0, player_struct.lean_filter_state));
#define SLAM_DURATION (CIT_CYCLE / 2)
void slam_posture_meter_state(void) {
STORE_STATE(INTENDED_HGT);
player_struct.posture_slam_state = player_struct.game_time + SLAM_DURATION;
}
fix velocity_crouch_filter(fix crouch) {
ubyte posture = player_struct.posture + 1;
fix hgt = INTENDED_HGT;
fix vel = posture * FIX_UNIT *
(abs(player_struct.controls[CONTROL_YVEL] + abs(player_struct.controls[CONTROL_ZVEL]))) /
(2 * CONTROL_MAX_VAL);
return fix_div(fix_mul(hgt, vel) + crouch, vel + FIX_UNIT);
}
void lean_icon(LGPoint *pos, grs_bitmap **icon, int *inum) {
int posture, leanx;
uchar *bp;
// Determine the posture and lean amount for the player currently.
if (!global_fullmap->cyber) {
fix ln, crouch;
fix state = GET_STATE;
EDMS_lean_o_meter(PLAYER_PHYSICS, &ln, &crouch);
crouch = velocity_crouch_filter(crouch);
if (player_struct.game_time > player_struct.posture_slam_state)
state = apply_weighted_filter(crouch, state, player_struct.deltat);
STORE_STATE(state);
posture = CROUCH_TO_POSTURE(state);
leanx = LEAN_TO_LEANX(ln);
} else {
posture = POSTURE_STAND;
leanx = 0;
}
// Calculate the bitmap resource index based on posture and lean.
*inum = posture * BMAPS_PER_POSTURE + ((100 - leanx) * BMAPS_PER_POSTURE / 201);
// Get a pointer to the corresponding lean bitmap.
FrameDesc *f = RefGet(MKREF(lean_bmap_res,*inum));
if (f != NULL) {
f->bm.bits = (uchar *)(f + 1);
*icon = &(f->bm);
} else
DEBUG("%s: No lean resource bitmap!", __FUNCTION__);
// Determine where to draw the bitmap.
pos->y = 53 - (*icon)->h;
pos->x = (46 - (*icon)->w + 1) * abs(leanx) / 600;
if (leanx < 0)
pos->x = -pos->x;
pos->x += LEANOMETER_X() + (25) / 2 - ((*icon)->w + 1) / 2;
pos->y += LEANOMETER_Y() - 31;
}
static void undraw_meter_area(LGRect *r) {
short a, b, c, d;
char saveMode;
int x, y;
STORE_CLIP(a, b, c, d);
ss_safe_set_cliprect(r->ul.x, r->ul.y, r->lr.x, r->lr.y);
x = EYEMETER_X();
y = EYEMETER_Y();
if (is_onscreen())
uiHideMouse(r);
ss_bitmap(meter_bkgnd(), x, y);
if (is_onscreen())
uiShowMouse(r);
RESTORE_CLIP(a, b, c, d);
}
void player_reset_eye(void) { player_struct.eye_pos = eye_mods[1] = 0; }
void player_set_eye(byte eyecntl) {
int theta = MAX_EYE_ANGLE * eyecntl / CONTROL_MAX_VAL;
player_struct.eye_pos = theta;
if (theta < 0)
theta += 2 * FIXANG_PI;
eye_mods[1] = theta;
}
byte player_get_eye(void) {
int theta = eye_mods[1];
if (theta > FIXANG_PI)
theta -= 2 * FIXANG_PI;
return (byte)(theta * CONTROL_MAX_VAL / MAX_EYE_ANGLE);
}
void player_set_eye_fixang(int ang) {
int theta = ang;
if (abs(ang) > MAX_EYE_ANGLE)
theta = (ang < 0) ? -MAX_EYE_ANGLE : MAX_EYE_ANGLE;
player_struct.eye_pos = theta;
if (theta < 0)
theta += 2 * FIXANG_PI;
eye_mods[1] = theta;
}
int player_get_eye_fixang(void) {
int theta = eye_mods[1];
if (theta > FIXANG_PI)
theta -= 2 * FIXANG_PI;
return theta;
}
uchar eye_mouse_handler(uiEvent *ev, LGRegion *r, intptr_t data) {
short x = ev->pos.x - r->abs_x;
short y = ev->pos.y - r->abs_y;
extern uchar hack_takeover;
if (hack_takeover || global_fullmap->cyber)
return FALSE;
if (x < 0 || x >= EYEMETER_W)
return FALSE;
eye_fine_mode = 2 * x > EYEMETER_W;
if (!eye_fine_mode)
y = discrete_eye_height[y * DISCRETE_EYE_POSITIONS / EYEMETER_H];
if (ev->mouse_data.buttons & (1 << MOUSE_LBUTTON)) {
int theta;
if ((ev->mouse_data.action & MOUSE_LDOWN) == 0 && uiLastMouseRegion[MOUSE_LBUTTON] != r)
return FALSE;
if (eye_fine_mode)
theta = -2 * MAX_EYE_ANGLE * (y) / (EYEMETER_H - 1) + MAX_EYE_ANGLE;
else
theta = -FIXANG_PI / 6 * (y * DISCRETE_EYE_POSITIONS / EYEMETER_H - 1);
// ui_mouse_constrain_xy(me->pos.x,r->abs_y,me->pos.x,r->abs_y+EYEMETER_H-1);
player_set_eye_fixang(theta);
physics_set_relax(CONTROL_YZROT, FALSE);
}
if (ev->mouse_data.buttons == 0) {
// mouse_constrain_xy(0,0,grd_cap->w-1,grd_cap->h-1);
}
return TRUE;
}
uchar lean_mouse_handler(uiEvent *ev, LGRegion *r, intptr_t data) {
short x = ev->pos.x - r->abs_x - LEANOMETER_XOFF;
short y = ev->pos.y - r->abs_y - LEANOMETER_YOFF;
if (x < 0 || x >= LEANOMETER_W || global_fullmap->cyber)
return FALSE;
if (ev->mouse_data.buttons & (1 << MOUSE_LBUTTON)) {
short posture = y * 3 / LEANOMETER_H;
short xlean = x * 220 / (LEANOMETER_W - 1) - 110;
if ((ev->mouse_data.action & MOUSE_LDOWN) == 0 && uiLastMouseRegion[MOUSE_LBUTTON] != r)
return FALSE;
if (xlean > 10)
xlean -= 10;
else if (xlean < -10)
xlean += 10;
else
xlean = 0;
if (posture != player_struct.posture)
player_set_posture(posture);
player_set_lean(xlean, player_struct.leany);
physics_set_relax(CONTROL_XZROT, FALSE);
// ui_mouse_constrain_xy(LEANOMETER_X(),LEANOMETER_Y()+posture*LEANOMETER_H/3+1,LEANOMETER_X()+LEANOMETER_W-1,LEANOMETER_Y()+(posture+1)*LEANOMETER_H/3-1);
}
if (ev->mouse_data.buttons == 0) {
// mouse_unconstrain();
}
return TRUE;
}
// ---------
// EXTERNALS
// ---------
#define BAD_REGION_CRITERR 3000
void init_posture_meters(LGRegion *root, uchar fullscreen) {
LGRegion *reg = PICK_METER_REGION(fullscreen);
int id;
LGRect r = {{0, 0}, {LEANOMETER_W + LEANOMETER_XOFF, EYEMETER_H}};
errtype err;
if (fullscreen) {
RECT_MOVE(&r, MakePoint(FULL_EYEMETER_X, FULL_EYEMETER_Y));
} else {
RECT_MOVE(&r, MakePoint(SLOT_EYEMETER_X, SLOT_EYEMETER_Y));
}
err = region_create(root, reg, &r, 0, 0, REG_USER_CONTROLLED | AUTODESTROY_FLAG, NULL, NULL, NULL, NULL);
if (err != OK)
critical_error(BAD_REGION_CRITERR);
uiInstallRegionHandler(reg, UI_EVENT_MOUSE | UI_EVENT_MOUSE_MOVE, eye_mouse_handler, 0, &id);
uiInstallRegionHandler(reg, UI_EVENT_MOUSE | UI_EVENT_MOUSE_MOVE, lean_mouse_handler, 0, &id);
}
void update_lean_meter(uchar force) {
static uchar last_shield = FALSE;
static uchar last_shieldstr = 0;
static int last_lean_icon = -1;
static LGPoint last_lean_pos = {-1, -1};
LGRect r;
LGPoint pos;
short a, b, c, d;
grs_bitmap *icon;
int inum;
bool shield = WareActive(player_struct.hardwarez_status[CPTRIP(SHIELD_HARD_TRIPLE)]) != 0;
// int shieldstr = SHIELD_SETTING(player_struct.hardwarez_status[CPTRIP(SHIELD_HARD_TRIPLE)]);
int shieldstr;
char saveMode;
bool saveBio;
// if(player_struct.hardwarez[CPTRIP(SHIELD_HARD_TRIPLE)]==SHIELD_VERSIONS)
// shieldstr=SHIELD_VERSIONS-1;
current_meter_region = PICK_METER_REGION(full_game_3d);
set_base_lean_bmap(shield);
lean_icon(&pos, &icon, &inum);
if (shield_bmap_res > 0)
shieldstr = shield_bmap_res - RES_leanShield1;
else
shieldstr = 0;
// If this is not a force update, and we're drawing the same lean icon in the same position as last
// time, then do nothing.
if (!force && PointsEqual(pos, last_lean_pos) && MKREF(lean_bmap_res, inum) == last_lean_icon
&& shield == last_shield && shieldstr == last_shieldstr)
return;
STORE_CLIP(a, b, c, d);
ss_safe_set_cliprect(LEANOMETER_X(), LEANOMETER_Y(), LEANOMETER_X() + LEANOMETER_W, LEANOMETER_Y() + LEANOMETER_H);
saveBio = gBioInited; // Turn off biometer while updating the lean meter.
gBioInited = FALSE;
if (force || last_lean_icon != -1) {
RECT_FILL(&r, LEANOMETER_X(), LEANOMETER_Y(), LEANOMETER_X() + 46, LEANOMETER_Y() + 53);
undraw_meter_area(&r);
}
RECT_FILL(&r, pos.x, pos.y, pos.x + icon->w, pos.y + icon->h);
// saveMode = convert_use_mode;
// convert_use_mode = 0;
if (is_onscreen())
uiHideMouse(&r);
ss_bitmap(icon, r.ul.x, r.ul.y);
if (shield) {
grs_bitmap *sbm;
// Get a pointer to the corresponding lean bitmap.
FrameDesc *f = RefGet(MKREF(shield_bmap_res, inum));
if (f != NULL) {
f->bm.bits = (uint8_t *)(f + 1);
sbm = &(f->bm);
} else
DEBUG("%s: No shield resource bitmap!", __FUNCTION__);
// Place shield image with offset
ss_bitmap(sbm, r.ul.x - shield_offsets[inum].x, r.ul.y - shield_offsets[inum].y);
}
gBioInited = saveBio;
if (is_onscreen())
uiShowMouse(&r);
// convert_use_mode = saveMode;
last_lean_pos = pos;
last_lean_icon = MKREF(lean_bmap_res, inum);
last_shield = shield;
last_shieldstr = shieldstr;
RESTORE_CLIP(a, b, c, d);
}
void draw_eye_bitmap(grs_bitmap *eye_bmap, LGPoint pos, int lasty) {
LGRect r;
char saveMode;
current_meter_region = PICK_METER_REGION(full_game_3d);
pos.x += EYEMETER_X();
pos.y += EYEMETER_Y();
r.ul = pos;
r.lr.x = pos.x + eye_bmap->w;
r.ul.y = lasty;
r.lr.y = lasty + eye_bmap->h + 1;
undraw_meter_area(&r);
r.ul.y = pos.y;
r.lr.y = pos.y + eye_bmap->h;
// saveMode = convert_use_mode;
// convert_use_mode = 0;
if (is_onscreen())
uiHideMouse(&r);
ss_bitmap(eye_bmap, r.ul.x, r.ul.y);
if (is_onscreen())
uiShowMouse(&r);
// convert_use_mode = saveMode;
}
#define HIRES_EYEMETER_H 53
void update_eye_meter(uchar force) {
static short last_y = 0;
static short last_ly = 0;
static uchar last_mode = FALSE;
short a, b, c, d;
fix pos = eye_mods[1];
int yang = pos % (2 * FIXANG_PI);
short y;
short lefty;
grs_bitmap *eye_rbmap = eye_bmap_r(!eye_fine_mode);
grs_bitmap *eye_lbmap = eye_bmap_l(eye_fine_mode);
bool saveBio;
if (yang > FIXANG_PI)
yang -= 2 * FIXANG_PI;
y = -(EYEMETER_H * yang / (2 * MAX_EYE_ANGLE));
// Hey, let's take gruesome advantange of the fact that
// booleans are zero or one.
lefty = discrete_eye_height[1 + (yang < 0) - (yang > 0)];
lefty -= (eye_lbmap->h) / 2;
y += EYEMETER_H / 2 - 2 - (eye_rbmap->h / 2) / 2;
if (!force && y == last_y && lefty == last_ly && eye_fine_mode == last_mode)
return;
STORE_CLIP(a, b, c, d);
ss_safe_set_cliprect(EYEMETER_X(), EYEMETER_Y(), EYEMETER_X() + EYEMETER_W, EYEMETER_Y() + EYEMETER_H);
saveBio = gBioInited; // Turn off biometer while updating the eye meter.
gBioInited = FALSE;
if (force) {
LGRect r = {{0, 0}, {46, 53}};
RECT_MOVE(&r, MakePoint(EYEMETER_X(), EYEMETER_Y()));
undraw_meter_area(&r);
}
draw_eye_bitmap(eye_lbmap, MakePoint(2, lefty), last_ly);
draw_eye_bitmap(eye_rbmap, MakePoint(19 - eye_rbmap->w, y), last_y);
gBioInited = saveBio;
RESTORE_CLIP(a, b, c, d);
last_y = y;
last_ly = lefty;
last_mode = eye_fine_mode;
}
void update_meters(uchar force) {
current_meter_region = PICK_METER_REGION(full_game_3d);
update_eye_meter(force);
update_lean_meter(force);
}
void zoom_to_lean_meter(void) {
extern bool DoubleSize;
LGPoint pos;
LGRect start = {{-5, -5}, {+5, +5}};
LGRect end = {{0, 0}, {LEANOMETER_W, LEANOMETER_H}};
current_meter_region = PICK_METER_REGION(full_game_3d);
RECT_MOVE(&end, MakePoint(LEANOMETER_X(), LEANOMETER_Y()));
mouse_get_xy(&pos.x, &pos.y);
if (!DoubleSize)
ss_point_convert(&(pos.x), &(pos.y), TRUE);
RECT_MOVE(&start, pos);
zoom_rect(&start, &end);
}