q3rally/engine/code/cgame/cg_rally_tools.c
2021-03-24 20:13:01 +00:00

810 lines
20 KiB
C

/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2002-2021 Q3Rally Team (Per Thormann - q3rally@gmail.com)
This file is part of q3rally source code.
q3rally source code 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 2 of the License,
or (at your option) any later version.
q3rally source code 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 q3rally; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
#include "cg_local.h"
void CG_DrawCheckpointLinks(void)
{
int i, j;
centity_t *cents[40];
qboolean checkpointFound;
int numCheckpoints = 0;
vec3_t handle;
// FIXME: max of 40 checkpoints
for (i = 0; i < 40; i++)
{
checkpointFound = qfalse;
for (j = 0; j < MAX_GENTITIES; j++)
{
cents[i] = &cg_entities[j];
if (cents[i]->currentState.eType != ET_CHECKPOINT) continue;
if (cents[i]->currentState.weapon != i+1) continue;
numCheckpoints++;
checkpointFound = qtrue;
/*
if( cents[i]->bezierPos[0] == 0.0f &&
cents[i]->bezierPos[1] == 0.0f &&
cents[i]->bezierPos[2] == 0.0f )
{
VectorCopy( cents[i]->currentState.origin2, cents[i]->bezierPos );
}
*/
break;
}
if( !checkpointFound )
break;
}
if( cg.currentBezierPoint == 0 )
cg.currentBezierPoint = 1;
if( cg.currentBezierPoint > numCheckpoints )
cg.currentBezierPoint = 1;
/*
for (i = 1; i < 40; i++){
checkpointFound = qfalse;
for (j = 0; j < MAX_ENTITIES; j++){
cent = &cg_entities[j];
if (cent->currentState.eType != ET_CHECKPOINT) continue;
if (cent->currentState.weapon != i) continue;
if( cent2 != NULL &&
cent3 != NULL &&
cent2->bezierDir[0] == 0.0f &&
cent2->bezierDir[1] == 0.0f &&
cent2->bezierDir[2] == 0.0f )
{
VectorSubtract( cent3->currentState.origin, cent->currentState.origin, cent->bezierDir );
VectorScale( cent->bezierDir, -0.35f, cent->bezierDir );
// cent->bezierDir[0] = 400.0f;
// Com_Printf( "setting bezier direction %i\n", i );
}
if( cent2 != NULL )
{
// Com_Printf( "DrawLine: %f, %f, %f\n", cent2->currentState.origin[0], cent2->currentState.origin[1], cent2->currentState.origin[2] );
// Com_Printf( " to: %f, %f, %f\n", cent->currentState.origin[0], cent->currentState.origin[1], cent->currentState.origin[2] );
// CG_Draw3DLine( cent2->currentState.origin, cent->currentState.origin, 1.0f, 0.0f, 0.0f, 1.0f );
CG_Draw3DBezierCurve( cent2->currentState.origin, cent2->bezierDir, cent->currentState.origin, cent->bezierDir, 16, 1.0f, 0.0f, 0.0f, 1.0f );
}
cent3 = cent2;
cent2 = cent;
checkpointFound = qtrue;
}
if( !checkpointFound )
break;
}
*/
for (i = 0; i <= numCheckpoints; i++)
{
/*
if( cents[i]->bezierDir[0] == 0.0f &&
cents[i]->bezierDir[1] == 0.0f &&
cents[i]->bezierDir[2] == 0.0f )
{
VectorCopy( cents[i]->currentState.angles2, cents[i]->bezierDir );
// VectorSubtract( cents[(i-1)%numCheckpoints]->bezierPos, cents[(i+1)%numCheckpoints]->bezierPos, cents[i]->bezierDir );
// VectorScale( cents[i]->bezierDir, -0.35f, cents[i]->bezierDir );
}
*/
// Com_Printf( "DrawLine: %f, %f, %f\n", cent2->currentState.origin[0], cent2->currentState.origin[1], cent2->currentState.origin[2] );
// Com_Printf( " to: %f, %f, %f\n", cent->currentState.origin[0], cent->currentState.origin[1], cent->currentState.origin[2] );
// CG_Draw3DLine( cent2->currentState.origin, cent->currentState.origin, 1.0f, 0.0f, 0.0f, 1.0f );
CG_Draw3DBezierCurve( cents[i]->currentState.origin2, cents[i]->currentState.angles2, cents[(i+1)%numCheckpoints]->currentState.origin2, cents[(i+1)%numCheckpoints]->currentState.angles2, 16, 1.0f, 0.0f, 0.0f, 1.0f );
VectorAdd( cents[i]->currentState.origin2, cents[i]->currentState.angles2, handle );
CG_Draw3DLine( cents[i]->currentState.origin2, handle, 0.0f, 0.0f, 1.0f, 1.0f );
}
CG_DrawModel( cents[cg.currentBezierPoint-1]->currentState.origin2, trap_R_RegisterModel( "models/test/sphere01.md3" ) );
}
void CG_Sparks( const vec3_t origin, const vec3_t normal, const vec3_t direction, const float speed )
{
vec3_t velocity;
localEntity_t *le;
refEntity_t *re;
VectorCopy( direction, velocity );
velocity[0] += crandom() * 0.5f;
velocity[1] += crandom() * 0.5f;
velocity[2] += random() * 0.5f;
VectorNormalize( velocity );
VectorScale( velocity, speed + crandom() * 10.0f, velocity );
le = CG_AllocLocalEntity();
re = &le->refEntity;
le->leType = LE_FRAGMENT;
le->leFlags = LEF_SCALE_FADE_OUT;
le->startTime = cg.time;
le->endTime = le->startTime + 600 + random() * 200;
le->fadeInTime = le->startTime + ( le->endTime - le->startTime ) * 0.4f;
le->radius = 2.0f;
VectorCopy( origin, re->origin );
re->shaderTime = cg.time / 1000.0f;
re->reType = RT_SPRITE;
re->rotation = 0;
re->radius = 2.0f;
re->customShader = cgs.media.sparkShader;
re->shaderRGBA[0] = 0xff;
re->shaderRGBA[1] = 0xff;
re->shaderRGBA[2] = 0xff;
re->shaderRGBA[3] = 0x7f;
le->color[0] = 1.0f;
le->color[1] = 1.0f;
le->color[2] = 1.0f;
le->color[3] = 0.5f;
le->pos.trType = TR_GRAVITY;
VectorCopy( origin, le->pos.trBase );
VectorCopy( velocity, le->pos.trDelta );
le->pos.trTime = cg.time;
le->bounceFactor = 0.4f;
le->leBounceSoundType = LEBS_BRASS;
le->leMarkType = LEMT_NONE;
}
qboolean CG_FrictionCalc( const carPoint_t *point, float *sCOF, float *kCOF )
{
// TODO
/*
gentity_t *ent;
int entityList[MAX_GENTITIES];
int numListedEntities;
vec3_t mins, maxs;
int i;
for ( i = 0 ; i < 3 ; i++ ) {
mins[i] = point->r[i] - point->radius;
maxs[i] = point->r[i] + point->radius;
}
numListedEntities = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
for ( i = 0 ; i < numListedEntities ; i++ ) {
ent = &g_entities[entityList[ i ]];
if( ent->s.eType != ET_EVENTS + EV_HAZARD ) continue;
if( ent->s.weapon != HT_OIL ) continue;
*sCOF = CP_OIL_SCOF;
*kCOF = CP_OIL_KCOF;
return qtrue;
}
*/
return qfalse;
}
/*
=================
CG_Hazard
Caused by an EV_HAZARD events
=================
*/
void CG_Hazard( int hazard, vec3_t origin, int radius) {
qhandle_t mod;
qhandle_t mark;
qhandle_t shader;
sfxHandle_t sfx;
// float radius;
float light;
vec3_t lightColor;
vec3_t angles, dir, start, dest;
localEntity_t *le;
qboolean alphaFade;
qboolean isSprite;
int duration;
trace_t tr;
VectorCopy(origin, start);
VectorCopy(origin, dest);
start[2] += 2;
dest[2] -= 8000;
CG_Trace( &tr, start, NULL, NULL, dest, 0, MASK_SOLID );
VectorCopy(tr.plane.normal, dir);
radius *= 16;
if (radius <= 0){
radius = 64;
}
mark = 0;
// radius = 32;
sfx = 0;
mod = 0;
shader = 0;
light = 0;
lightColor[0] = 1;
lightColor[1] = 1;
lightColor[2] = 0;
// set defaults
isSprite = qfalse;
duration = 600;
switch ( hazard )
{
case HT_BIO:
VectorMA(tr.endpos, 0, tr.plane.normal, origin);
// mod = cgs.media.dishFlashModel;
// shader = cgs.media.rocketExplosionShader;
// sfx = cgs.media.sfx_rockexp;
mark = cgs.media.bioMarkShader;
radius *= 1.1;
light = 150;
// isSprite = qtrue;
// duration = 2000;
lightColor[0] = 0;
lightColor[1] = 0.75;
lightColor[2] = 0.0;
break;
case HT_OIL:
VectorMA(tr.endpos, 0, tr.plane.normal, origin);
// mod = cgs.media.dishFlashModel;
// shader = cgs.media.rocketExplosionShader;
// sfx = cgs.media.sfx_rockexp;
mark = cgs.media.oilMarkShader;
// radius = 64;
// light = 150;
// isSprite = qtrue;
// duration = 2000;
// lightColor[0] = 0;
// lightColor[1] = 0.75;
// lightColor[2] = 0.0;
break;
case HT_EXPLOSIVE:
mod = cgs.media.dishFlashModel;
shader = cgs.media.rocketExplosionShader;
sfx = cgs.media.sfx_rockexp;
mark = cgs.media.burnMarkShader;
radius = 64;
light = 300;
isSprite = qtrue;
duration = 2000;
lightColor[0] = 1;
lightColor[1] = 0.75;
lightColor[2] = 0.0;
break;
case HT_FIRE:
VectorMA(tr.endpos, 0, tr.plane.normal, origin);
mod = cgs.media.fireModel;
// mod = trap_R_RegisterModel( "models/rearfire/flametrail.md3" );
// shader = cgs.media.flameShader;
// shader = cgs.media.rocketExplosionShader;
// sfx = cgs.media.sfx_rockexp;
// mark = cgs.media.burnMarkShader;
radius = 64;
light = 100;
isSprite = qtrue;
duration = 10000;
lightColor[0] = 1;
lightColor[1] = 0.75;
lightColor[2] = 0.0;
break;
case HT_POISON:
// mod = cgs.media.dishFlashModel;
// shader = cgs.media.smokePuffShader;
// sfx = cgs.media.sfx_rockexp;
// mark = cgs.media.burnMarkShader;
radius = 64;
// isSprite = qtrue;
duration = 1000;
dir[0] = crandom() * 120.0F;
dir[1] = crandom() * 120.0F;
dir[2] = random() * 40.0F;
CreateSmokeCloudEntity(origin, dir, 200, radius, duration, 1, 1, 1, 1, cgs.media.smokePuffShader);
vectoangles(dir, angles);
angles[YAW] += 120;
AngleVectors(angles, dir, NULL, NULL);
CreateSmokeCloudEntity(origin, dir, 200, radius, duration, 1, 1, 1, 1, cgs.media.smokePuffShader);
angles[YAW] += 120;
AngleVectors(angles, dir, NULL, NULL);
CreateSmokeCloudEntity(origin, dir, 200, radius, duration, 1, 1, 1, 1, cgs.media.smokePuffShader);
break;
case HT_SMOKE:
// mod = cgs.media.dishFlashModel;
// shader = cgs.media.smokePuffShader;
// sfx = cgs.media.sfx_rockexp;
// mark = cgs.media.burnMarkShader;
radius = 96;
// isSprite = qtrue;
duration = 2000;
VectorSet(dir, 0, 0, 1);
CreateSmokeCloudEntity( origin, dir, 30, radius, duration, 0.75, 0.75, 0.75, 1, cgs.media.smokePuffShader );
return;
default:
return;
}
if ( sfx ) {
trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, sfx );
}
//
// create the explosion
//
if ( mod ) {
if (hazard == HT_FIRE){
le = CreateFireEntity( origin, dir, mod, shader, duration );
}
else {
le = CG_MakeExplosion( origin, dir, mod, shader, duration, isSprite );
}
le->light = light;
VectorCopy( lightColor, le->lightColor );
}
//
// impact mark
//
if ( mark ){
alphaFade = (mark == cgs.media.energyMarkShader); // plasma fades alpha, all others fade color
CG_ImpactMark( mark, origin, dir, random()*360, 1,1,1,1, alphaFade, radius, qfalse );
}
}
// *******************************************************
// Drawing Tools
// *******************************************************
/*
======================
CG_TagExists
Returns true if the tag is not in the model.. ie: not at the vec3_origin
======================
*/
/*
qboolean CG_TagExists( const refEntity_t *parent, qhandle_t parentModel, char *tagName ) {
orientation_t lerped;
// lerp the tag
trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame,
1.0 - parent->backlerp, tagName );
if (VectorLength(lerped.origin))
return qtrue;
else
return qfalse;
}
*/
// uses frame 0
qboolean CG_TagExists( qhandle_t parentModel, char *tagName ) {
orientation_t lerped;
// lerp the tag
return trap_R_LerpTag( &lerped, parentModel, 0, 0, 1.0, tagName );
}
/*
======================
CG_GetTagPosition
Returns the position of the specified tag
======================
*/
void CG_GetTagPosition( const refEntity_t *parent, qhandle_t parentModel, char *tagName, vec3_t origin ) {
int i;
orientation_t lerped;
// lerp the tag
trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame,
1.0 - parent->backlerp, tagName );
// FIXME: allow origin offsets along tag?
VectorCopy( parent->origin, origin );
for ( i = 0 ; i < 3 ; i++ ) {
VectorMA( origin, lerped.origin[i], parent->axis[i], origin );
}
}
/*
====================
CreateFireEntity
Creates fire localEntities
====================
*/
localEntity_t *CreateFireEntity( vec3_t origin, vec3_t dir,
qhandle_t hModel, qhandle_t shader,
int msec ) {
localEntity_t *ex;
int offset;
vec3_t newOrigin;
if ( msec <= 0 ) {
CG_Error( "CreateFireEntity: msec = %i", msec );
}
// skew the time a bit so they aren't all in sync
offset = rand() & 63;
ex = CG_AllocLocalEntity();
ex->leType = LE_EXPLOSION;
// randomly rotate sprite orientation
ex->refEntity.rotation = 0;
AxisClear(ex->refEntity.axis);
VectorCopy(origin, newOrigin);
ex->startTime = cg.time - offset;
ex->endTime = ex->startTime + msec;
// bias the time so all shader effects start correctly
ex->refEntity.shaderTime = ex->startTime / 1000.0f;
ex->refEntity.hModel = hModel;
if (shader)
ex->refEntity.customShader = shader;
// set origin
VectorCopy( newOrigin, ex->refEntity.origin );
VectorCopy( newOrigin, ex->refEntity.oldorigin );
ex->color[0] = ex->color[1] = ex->color[2] = 1.0;
return ex;
}
/*
=================
CreateSmokeCloudEntity
Creates special q3r smoke puff entities
=================
*/
void CreateSmokeCloudEntity(vec3_t origin, vec3_t vel, float speed, int radius, int duration, float r, float g, float b, float a, qhandle_t hShader ){
vec3_t velocity;
VectorNormalize2(vel, velocity);
velocity[0] += crandom() * 0.2f;
velocity[1] += crandom() * 0.2f;
velocity[2] += crandom() * 0.2f;
VectorNormalize(velocity);
VectorScale(velocity, speed, velocity);
CG_SmokePuff( origin, velocity,
radius,
r, g, b, a,
duration,
cg.time,
0,
0,
hShader );
}
// *******************************************************
// Team Tools
// *******************************************************
/*
================
TeamCount
Returns number of players on a team
================
*/
team_t TeamCount( int ignoreClientNum, int team ) {
int i;
int count = 0;
for ( i = 0 ; i < cgs.maxclients ; i++ ) {
if ( i == ignoreClientNum ) continue;
if ( !cgs.clientinfo[i].infoValid ) continue;
if ( cgs.clientinfo[i].team == team ) count++;
}
return count;
}
int GetTeamAtRank(int rank){
int i, j, count;
int ranks[4];
int counts[4];
for (i = 0; i < 4; i++){
counts[i] = TeamCount(-1, TEAM_RED + i);
ranks[i] = 0;
}
for (i = 0; i < 4; i++){
if (!counts[i]) continue;
count = 0;
for (j = 0; j < 4; j++){
if (!counts[j]) continue;
if (isRallyRace()){
if (cg.teamTimes[i] > cg.teamTimes[j]) count++;
}
else if (cg.teamScores[i] < cg.teamScores[j]) count++;
}
// Com_Printf("teamTimes for team %i = %i, rank %i\n", i, cg.teamTimes[i], count);
// Com_Printf("teamTimes (%i, %i, %i, %i)\n", cg.teamTimes[0], cg.teamTimes[1], cg.teamTimes[2], cg.teamTimes[3]);
while(count < 4 && ranks[count]) count++; // rank is taken so move to the next one
if (count < 4)
ranks[count] = TEAM_RED + i;
}
if (cgs.gametype == GT_CTF && rank > 2){
return -1;
}
else {
return ranks[rank-1];
}
}
qboolean TiedWinner( void ){
int i, winner;
qboolean tied;
tied = qfalse;
winner = GetTeamAtRank(1) - TEAM_RED;
for (i = 0; i < 4; i++){
if (i == winner) continue;
if (!TeamCount(-1, TEAM_RED + i)) continue;
if ((isRallyRace() && cg.teamTimes[winner] == cg.teamTimes[i])
|| (!isRallyRace() && cg.teamScores[winner] == cg.teamScores[i])){
tied = qtrue;
break;
}
}
return tied;
}
#define MAX_REFLECTION_IMAGE_SIZE 20000
qboolean CG_CopyLevelReflectionImage( const char *filename ){
fileHandle_t imageFile;
int len;
// FIXME: use malloc?
byte data[MAX_REFLECTION_IMAGE_SIZE];
// load image
len = trap_FS_FOpenFile( filename, &imageFile, FS_READ );
if ( !imageFile ){
Com_Printf( S_COLOR_YELLOW "Q3R Warning: Could not open %s to copy for level reflection mapping.\n", filename);
return qfalse;
}
if ( !len ){
Com_Printf( S_COLOR_YELLOW "Q3R Warning: Could not open %s: File with 0 length.\n", filename);
return qfalse;
}
if ( len > MAX_REFLECTION_IMAGE_SIZE){
Com_Printf( S_COLOR_YELLOW "Q3R Warning: Could not open %s: File size exceeds %i bytes.\n", filename, MAX_REFLECTION_IMAGE_SIZE );
return qfalse;
}
trap_FS_Read( data, len, imageFile );
trap_FS_FCloseFile( imageFile );
// save image
trap_FS_FOpenFile( "textures/reflect/reflect.jpg", &imageFile, FS_WRITE );
if ( !imageFile ){
Com_Printf( S_COLOR_YELLOW "Q3R Warning: Could not open %s to write level reflection mapping.\n", "textures/reflect/reflect.jpg");
return qfalse;
}
trap_FS_Write( data, len, imageFile );
trap_FS_FCloseFile( imageFile );
return qtrue;
}
void CG_DrawModel( vec3_t start, qhandle_t model )
{
refEntity_t re;
memset( &re, 0, sizeof( refEntity_t ) );
re.shaderTime = cg.time / 1000.0f;
re.reType = RT_MODEL;
re.renderfx = RF_NOSHADOW;
re.frame = 1;
re.hModel = model;
VectorCopy( start, re.origin );
re.shaderRGBA[0] = 255;
re.shaderRGBA[1] = 255;
re.shaderRGBA[2] = 255;
re.shaderRGBA[3] = 255;
AxisClear( re.axis );
trap_R_AddRefEntityToScene( &re );
}
void CG_Draw3DLine( vec3_t start, vec3_t end, float r, float g, float b, float a )
{
refEntity_t re;
memset( &re, 0, sizeof( refEntity_t ) );
re.shaderTime = cg.time / 1000.0f;
re.reType = RT_RAIL_CORE;
re.renderfx = RF_NOSHADOW;
re.customShader = cgs.media.sparkShader;
re.frame = 1;
VectorCopy( end, re.origin );
VectorCopy( start, re.oldorigin );
re.shaderRGBA[0] = r * 255;
re.shaderRGBA[1] = g * 255;
re.shaderRGBA[2] = b * 255;
re.shaderRGBA[3] = a * 255;
AxisClear( re.axis );
trap_R_AddRefEntityToScene( &re );
}
void CG_GetPointOnCurveBetweenCheckpoints( vec3_t start, vec3_t startHandle, vec3_t end, vec3_t endHandle, float f, vec3_t origin )
{
VectorScale( start, (1-f)*(1-f)*(1-f), origin );
VectorMA( origin, 3*f*(1-f)*(1-f), startHandle, origin );
VectorMA( origin, 3*f*f*(1-f), endHandle, origin );
VectorMA( origin, f*f*f, end, origin );
}
void CG_Draw3DBezierCurve( vec3_t start, vec3_t startDir, vec3_t end, vec3_t endDir, int numDivisions, float r, float g, float b, float a )
{
refEntity_t re;
int i;
vec3_t startHandle, endHandle;
float f;
memset( &re, 0, sizeof( refEntity_t ) );
re.shaderTime = cg.time / 1000.0f;
re.reType = RT_RAIL_CORE;
re.renderfx = RF_NOSHADOW;
re.customShader = cgs.media.sparkShader;
re.frame = 1;
// VectorCopy( end, re.origin );
// VectorCopy( start, re.oldorigin );
re.shaderRGBA[0] = r * 255;
re.shaderRGBA[1] = g * 255;
re.shaderRGBA[2] = b * 255;
re.shaderRGBA[3] = a * 255;
AxisClear( re.axis );
VectorAdd( start, startDir, startHandle );
VectorMA( end, -1, endDir, endHandle );
VectorCopy( start, re.oldorigin );
for( i = 1; i <= numDivisions; i++ )
{
f = i / (float)numDivisions;
CG_GetPointOnCurveBetweenCheckpoints( start, startHandle, end, endHandle, f, re.origin );
// VectorScale( start, (1-f)*(1-f)*(1-f), re.origin );
// VectorMA( re.origin, 3*f*(1-f)*(1-f), startHandle, re.origin );
// VectorMA( re.origin, 3*f*f*(1-f), endHandle, re.origin );
// VectorMA( re.origin, f*f*f, end, re.origin );
trap_R_AddRefEntityToScene( &re );
VectorCopy( re.origin, re.oldorigin );
}
}
// *******************************************************
// Misc Tools
// *******************************************************
float Q3VelocityToRL( float length ) {
if ( cg_metricUnits.integer ){
return length / CP_M_2_QU * 3.6f;
}
else {
return length / CP_FT_2_QU * 3600.0f / 5280.0f;
}
}
float Q3DistanceToRL( float length ) {
if ( cg_metricUnits.integer ){
return length / CP_M_2_QU / 1000.0f;
}
else {
return length / CP_FT_2_QU / 5280.0f;
}
}
qboolean isRallyRace( void ){
return (cgs.gametype == GT_RACING
|| cgs.gametype == GT_RACING_DM
|| cgs.gametype == GT_TEAM_RACING
|| cgs.gametype == GT_TEAM_RACING_DM);
}
qboolean isRallyNonDMRace( void ){
return (cgs.gametype == GT_RACING
|| cgs.gametype == GT_TEAM_RACING);
}
/*
=================
isRaceObserver
=================
*/
qboolean isRaceObserver( int clientNum ){
return (cg_entities[clientNum].finishRaceTime && cg_entities[clientNum].finishRaceTime + RACE_OBSERVER_DELAY < cg.time);
}
qboolean CG_InsideBox( vec3_t mins, vec3_t maxs, vec3_t pos ){
if (pos[0] < mins[0] || pos[0] > maxs[0]) return qfalse;
if (pos[1] < mins[1] || pos[1] > maxs[1]) return qfalse;
if (pos[2] < mins[2] || pos[2] > maxs[2]) return qfalse;
return qtrue;
}