/*** * * Copyright (c) 1996-2002, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * * This source code contains proprietary and confidential information of * Valve LLC and its suppliers. Access to this code is restricted to * persons who have executed a written SDK license with Valve. Any access, * use or distribution of this code by or to any unlicensed person is illegal. * ****/ /* ===== scripted.cpp ======================================================== */ #include "extdll.h" #include "util.h" #include "cbase.h" #include "monsters.h" #if !defined(ANIMATION_H) #include "animation.h" #endif #if !defined(SAVERESTORE_H) #include "saverestore.h" #endif #include "schedule.h" #include "scripted.h" #include "defaultai.h" /* classname "scripted_sequence" targetname "me" - there can be more than one with the same name, and they act in concert target "the_entity_I_want_to_start_playing" or "class entity_classname" will pick the closest inactive scientist play "name_of_sequence" idle "name of idle sequence to play before starting" donetrigger "whatever" - can be any other triggerable entity such as another sequence, train, door, or a special case like "die" or "remove" moveto - if set the monster first moves to this nodes position range # - only search this far to find the target spawnflags - (stop if blocked, stop if player seen) */ // // Cache user-entity-field values until spawn is called. // void CCineMonster::KeyValue( KeyValueData *pkvd ) { if( FStrEq( pkvd->szKeyName, "m_iszIdle" ) ) { m_iszIdle = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_iszPlay" ) ) { m_iszPlay = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_iszEntity" ) ) { m_iszEntity = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_fMoveTo" ) ) { m_fMoveTo = atoi( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_flRepeat" ) ) { m_flRepeat = atof( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_flRadius" ) ) { m_flRadius = atof( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "m_iFinishSchedule" ) ) { m_iFinishSchedule = atoi( pkvd->szValue ); pkvd->fHandled = TRUE; } else { CBaseMonster::KeyValue( pkvd ); } } TYPEDESCRIPTION CCineMonster::m_SaveData[] = { DEFINE_FIELD( CCineMonster, m_iszIdle, FIELD_STRING ), DEFINE_FIELD( CCineMonster, m_iszPlay, FIELD_STRING ), DEFINE_FIELD( CCineMonster, m_iszEntity, FIELD_STRING ), DEFINE_FIELD( CCineMonster, m_fMoveTo, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_flRepeat, FIELD_FLOAT ), DEFINE_FIELD( CCineMonster, m_flRadius, FIELD_FLOAT ), DEFINE_FIELD( CCineMonster, m_iDelay, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_startTime, FIELD_TIME ), DEFINE_FIELD( CCineMonster, m_saved_movetype, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_saved_solid, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_saved_effects, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_iFinishSchedule, FIELD_INTEGER ), DEFINE_FIELD( CCineMonster, m_interruptable, FIELD_BOOLEAN ), }; IMPLEMENT_SAVERESTORE( CCineMonster, CBaseMonster ) LINK_ENTITY_TO_CLASS( scripted_sequence, CCineMonster ) #define CLASSNAME "scripted_sequence" LINK_ENTITY_TO_CLASS( aiscripted_sequence, CCineAI ) void CCineMonster::Spawn( void ) { // pev->solid = SOLID_TRIGGER; // UTIL_SetSize( pev, Vector( -8, -8, -8 ), Vector( 8, 8, 8 ) ); pev->solid = SOLID_NOT; // REMOVE: The old side-effect #if 0 if( m_iszIdle ) m_fMoveTo = 4; #endif // if no targetname, start now if( FStringNull( pev->targetname ) || !FStringNull( m_iszIdle ) ) { SetThink( &CCineMonster::CineThink ); pev->nextthink = gpGlobals->time + 1.0f; // Wait to be used? if( pev->targetname ) m_startTime = gpGlobals->time + (float)1E6; } if( pev->spawnflags & SF_SCRIPT_NOINTERRUPT ) m_interruptable = FALSE; else m_interruptable = TRUE; } //========================================================= // FCanOverrideState - returns FALSE, scripted sequences // cannot possess entities regardless of state. //========================================================= BOOL CCineMonster::FCanOverrideState( void ) { if( pev->spawnflags & SF_SCRIPT_OVERRIDESTATE ) return TRUE; return FALSE; } //========================================================= // FCanOverrideState - returns true because scripted AI can // possess entities regardless of their state. //========================================================= BOOL CCineAI::FCanOverrideState( void ) { return TRUE; } // // CineStart // void CCineMonster::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { // do I already know who I should use CBaseEntity *pEntity = m_hTargetEnt; CBaseMonster *pTarget = NULL; if( pEntity ) pTarget = pEntity->MyMonsterPointer(); if( pTarget ) { // am I already playing the script? if( pTarget->m_scriptState == SCRIPT_PLAYING ) return; m_startTime = gpGlobals->time + 0.05f; } else { // if not, try finding them SetThink( &CCineMonster::CineThink ); pev->nextthink = gpGlobals->time; } } // This doesn't really make sense since only MOVETYPE_PUSH get 'Blocked' events void CCineMonster::Blocked( CBaseEntity *pOther ) { } void CCineMonster::Touch( CBaseEntity *pOther ) { /* ALERT( at_aiconsole, "Cine Touch\n" ); if( m_pentTarget && OFFSET( pOther->pev ) == OFFSET( m_pentTarget ) ) { CBaseMonster *pTarget = GetClassPtr( (CBaseMonster *)VARS( m_pentTarget ) ); pTarget->m_monsterState == MONSTERSTATE_SCRIPT; } */ } /* entvars_t *pevOther = VARS( gpGlobals->other ); if( !FBitSet( pevOther->flags, FL_MONSTER ) ) { // touched by a non-monster. return; } pevOther->origin.z += 1; if( FBitSet( pevOther->flags, FL_ONGROUND ) ) { // clear the onground so physics don't bitch pevOther->flags -= FL_ONGROUND; } // toss the monster! pevOther->velocity = pev->movedir * pev->speed; pevOther->velocity.z += m_flHeight; pev->solid = SOLID_NOT;// kill the trigger for now !!!UNDONE } */ // // ********** Cinematic DIE ********** // void CCineMonster::Die( void ) { SetThink( &CBaseEntity::SUB_Remove ); } // // ********** Cinematic PAIN ********** // void CCineMonster::Pain( void ) { } // // ********** Cinematic Think ********** // // find a viable entity int CCineMonster::FindEntity( void ) { edict_t *pentTarget; pentTarget = FIND_ENTITY_BY_TARGETNAME( NULL, STRING( m_iszEntity ) ); m_hTargetEnt = NULL; CBaseMonster *pTarget = NULL; while( !FNullEnt( pentTarget ) ) { if( FBitSet( VARS( pentTarget )->flags, FL_MONSTER ) ) { pTarget = GetMonsterPointer( pentTarget ); if( pTarget && pTarget->CanPlaySequence( FCanOverrideState(), SS_INTERRUPT_BY_NAME ) ) { m_hTargetEnt = pTarget; return TRUE; } ALERT( at_console, "Found %s, but can't play!\n", STRING( m_iszEntity ) ); } pentTarget = FIND_ENTITY_BY_TARGETNAME( pentTarget, STRING( m_iszEntity ) ); pTarget = NULL; } if( !pTarget ) { CBaseEntity *pEntity = NULL; while( ( pEntity = UTIL_FindEntityInSphere( pEntity, pev->origin, m_flRadius ) ) != NULL ) { if( FClassnameIs( pEntity->pev, STRING( m_iszEntity ) ) ) { if( FBitSet( pEntity->pev->flags, FL_MONSTER ) ) { pTarget = pEntity->MyMonsterPointer(); if( pTarget && pTarget->CanPlaySequence( FCanOverrideState(), SS_INTERRUPT_IDLE ) ) { m_hTargetEnt = pTarget; return TRUE; } } } } } pTarget = NULL; m_hTargetEnt = NULL; return FALSE; } // make the entity enter a scripted sequence void CCineMonster::PossessEntity( void ) { CBaseEntity *pEntity = m_hTargetEnt; CBaseMonster *pTarget = NULL; if( pEntity ) pTarget = pEntity->MyMonsterPointer(); if( pTarget ) { // FindEntity() just checked this! #if 0 if( !pTarget->CanPlaySequence( FCanOverrideState() ) ) { ALERT( at_aiconsole, "Can't possess entity %s\n", STRING( pTarget->pev->classname ) ); return; } #endif pTarget->m_pGoalEnt = this; pTarget->m_pCine = this; pTarget->m_hTargetEnt = this; m_saved_movetype = pTarget->pev->movetype; m_saved_solid = pTarget->pev->solid; m_saved_effects = pTarget->pev->effects; pTarget->pev->effects |= pev->effects; switch( m_fMoveTo ) { case 0: pTarget->m_scriptState = SCRIPT_WAIT; break; case 1: pTarget->m_scriptState = SCRIPT_WALK_TO_MARK; DelayStart( 1 ); break; case 2: pTarget->m_scriptState = SCRIPT_RUN_TO_MARK; DelayStart( 1 ); break; case 4: UTIL_SetOrigin( pTarget->pev, pev->origin ); pTarget->pev->ideal_yaw = pev->angles.y; pTarget->pev->avelocity = Vector( 0, 0, 0 ); pTarget->pev->velocity = Vector( 0, 0, 0 ); pTarget->pev->effects |= EF_NOINTERP; pTarget->pev->angles.y = pev->angles.y; pTarget->m_scriptState = SCRIPT_WAIT; m_startTime = gpGlobals->time + (float)1E6; // UNDONE: Add a flag to do this so people can fixup physics after teleporting monsters // pTarget->pev->flags &= ~FL_ONGROUND; break; } //ALERT( at_aiconsole, "\"%s\" found and used (INT: %s)\n", STRING( pTarget->pev->targetname ), FBitSet( pev->spawnflags, SF_SCRIPT_NOINTERRUPT )? "No" : "Yes" ); pTarget->m_IdealMonsterState = MONSTERSTATE_SCRIPT; if( m_iszIdle ) { StartSequence( pTarget, m_iszIdle, FALSE ); if( FStrEq( STRING( m_iszIdle ), STRING( m_iszPlay ) ) ) { pTarget->pev->framerate = 0; } } } } // make the entity carry out the scripted sequence instructions, but without // destroying the monster's state. void CCineAI::PossessEntity( void ) { Schedule_t *pNewSchedule; CBaseEntity *pEntity = m_hTargetEnt; CBaseMonster *pTarget = NULL; if( pEntity ) pTarget = pEntity->MyMonsterPointer(); if( pTarget ) { if( !pTarget->CanPlaySequence( FCanOverrideState(), SS_INTERRUPT_AI ) ) { ALERT( at_aiconsole, "(AI)Can't possess entity %s\n", STRING( pTarget->pev->classname ) ); return; } pTarget->m_pGoalEnt = this; pTarget->m_pCine = this; pTarget->m_hTargetEnt = this; m_saved_movetype = pTarget->pev->movetype; m_saved_solid = pTarget->pev->solid; m_saved_effects = pTarget->pev->effects; pTarget->pev->effects |= pev->effects; switch( m_fMoveTo ) { case 0: case 5: pTarget->m_scriptState = SCRIPT_WAIT; break; case 1: pTarget->m_scriptState = SCRIPT_WALK_TO_MARK; break; case 2: pTarget->m_scriptState = SCRIPT_RUN_TO_MARK; break; case 4: // zap the monster instantly to the site of the script entity. UTIL_SetOrigin( pTarget->pev, pev->origin ); pTarget->pev->ideal_yaw = pev->angles.y; pTarget->pev->avelocity = Vector( 0, 0, 0 ); pTarget->pev->velocity = Vector( 0, 0, 0 ); pTarget->pev->effects |= EF_NOINTERP; pTarget->pev->angles.y = pev->angles.y; pTarget->m_scriptState = SCRIPT_WAIT; m_startTime = gpGlobals->time + (float)1E6; // UNDONE: Add a flag to do this so people can fixup physics after teleporting monsters pTarget->pev->flags &= ~FL_ONGROUND; break; default: ALERT( at_aiconsole, "aiscript: invalid Move To Position value!" ); break; } ALERT( at_aiconsole, "\"%s\" found and used\n", STRING( pTarget->pev->targetname ) ); pTarget->m_IdealMonsterState = MONSTERSTATE_SCRIPT; /* if( m_iszIdle ) { StartSequence( pTarget, m_iszIdle, FALSE ); if( FStrEq( STRING( m_iszIdle ), STRING( m_iszPlay ) ) ) { pTarget->pev->framerate = 0; } } */ // Already in a scripted state? if( pTarget->m_MonsterState == MONSTERSTATE_SCRIPT ) { pNewSchedule = pTarget->GetScheduleOfType( SCHED_AISCRIPT ); pTarget->ChangeSchedule( pNewSchedule ); } } } void CCineMonster::CineThink( void ) { if( FindEntity() ) { PossessEntity(); ALERT( at_aiconsole, "script \"%s\" using monster \"%s\"\n", STRING( pev->targetname ), STRING( m_iszEntity ) ); } else { CancelScript(); ALERT( at_aiconsole, "script \"%s\" can't find monster \"%s\"\n", STRING( pev->targetname ), STRING( m_iszEntity ) ); pev->nextthink = gpGlobals->time + 1.0f; } } // lookup a sequence name and setup the target monster to play it BOOL CCineMonster::StartSequence( CBaseMonster *pTarget, int iszSeq, BOOL completeOnEmpty ) { if( !iszSeq && completeOnEmpty ) { SequenceDone( pTarget ); return FALSE; } pTarget->pev->sequence = pTarget->LookupSequence( STRING( iszSeq ) ); if( pTarget->pev->sequence == -1 ) { ALERT( at_error, "%s: unknown scripted sequence \"%s\"\n", STRING( pTarget->pev->targetname ), STRING( iszSeq ) ); pTarget->pev->sequence = 0; // return FALSE; } #if 0 char *s; if( pev->spawnflags & SF_SCRIPT_NOINTERRUPT ) s = "No"; else s = "Yes"; ALERT( at_console, "%s (%s): started \"%s\":INT:%s\n", STRING( pTarget->pev->targetname ), STRING( pTarget->pev->classname ), STRING( iszSeq ), s ); #endif pTarget->pev->frame = 0; pTarget->ResetSequenceInfo( ); return TRUE; } // lookup a sequence name and setup the target monster to play it // overridden for CCineAI because it's ok for them to not have an animation sequence // for the monster to play. For a regular Scripted Sequence, that situation is an error. BOOL CCineAI::StartSequence( CBaseMonster *pTarget, int iszSeq, BOOL completeOnEmpty ) { if( iszSeq == 0 && completeOnEmpty ) { // no sequence was provided. Just let the monster proceed, however, we still have to fire any Sequence target // and remove any non-repeatable CineAI entities here ( because there is code elsewhere that handles those tasks, but // not until the animation sequence is finished. We have to manually take care of these things where there is no sequence. SequenceDone ( pTarget ); return TRUE; } pTarget->pev->sequence = pTarget->LookupSequence( STRING( iszSeq ) ); if( pTarget->pev->sequence == -1 ) { ALERT( at_error, "%s: unknown aiscripted sequence \"%s\"\n", STRING( pTarget->pev->targetname ), STRING( iszSeq ) ); pTarget->pev->sequence = 0; // return FALSE; } pTarget->pev->frame = 0; pTarget->ResetSequenceInfo(); return TRUE; } //========================================================= // SequenceDone - called when a scripted sequence animation // sequence is done playing ( or when an AI Scripted Sequence // doesn't supply an animation sequence to play ). Expects // the CBaseMonster pointer to the monster that the sequence // possesses. //========================================================= void CCineMonster::SequenceDone( CBaseMonster *pMonster ) { //ALERT( at_aiconsole, "Sequence %s finished\n", STRING( m_pCine->m_iszPlay ) ); if( !( pev->spawnflags & SF_SCRIPT_REPEATABLE ) ) { SetThink( &CBaseEntity::SUB_Remove ); pev->nextthink = gpGlobals->time + 0.1f; } // This is done so that another sequence can take over the monster when triggered by the first pMonster->CineCleanup(); FixScriptMonsterSchedule( pMonster ); // This may cause a sequence to attempt to grab this guy NOW, so we have to clear him out // of the existing sequence SUB_UseTargets( NULL, USE_TOGGLE, 0 ); } //========================================================= // When a monster finishes a scripted sequence, we have to // fix up its state and schedule for it to return to a // normal AI monster. // // Scripted sequences just dirty the Schedule and drop the // monster in Idle State. //========================================================= void CCineMonster::FixScriptMonsterSchedule( CBaseMonster *pMonster ) { if( pMonster->m_IdealMonsterState != MONSTERSTATE_DEAD ) pMonster->m_IdealMonsterState = MONSTERSTATE_IDLE; pMonster->ClearSchedule(); } //========================================================= // When a monster finishes a scripted sequence, we have to // fix up its state and schedule for it to return to a // normal AI monster. // // AI Scripted sequences will, depending on what the level // designer selects: // // -Dirty the monster's schedule and drop out of the // sequence in their current state. // // -Select a specific AMBUSH schedule, regardless of state. //========================================================= void CCineAI::FixScriptMonsterSchedule( CBaseMonster *pMonster ) { switch ( m_iFinishSchedule ) { case SCRIPT_FINISHSCHED_DEFAULT: pMonster->ClearSchedule(); break; case SCRIPT_FINISHSCHED_AMBUSH: pMonster->ChangeSchedule( pMonster->GetScheduleOfType( SCHED_AMBUSH ) ); break; default: ALERT( at_aiconsole, "FixScriptMonsterSchedule - no case!\n" ); pMonster->ClearSchedule(); break; } } BOOL CBaseMonster::ExitScriptedSequence() { if( pev->deadflag == DEAD_DYING ) { // is this legal? // BUGBUG -- This doesn't call Killed() m_IdealMonsterState = MONSTERSTATE_DEAD; return FALSE; } if( m_pCine ) { m_pCine->CancelScript(); } return TRUE; } void CCineMonster::AllowInterrupt( BOOL fAllow ) { if( pev->spawnflags & SF_SCRIPT_NOINTERRUPT ) return; m_interruptable = fAllow; } BOOL CCineMonster::CanInterrupt( void ) { if( !m_interruptable ) return FALSE; CBaseEntity *pTarget = m_hTargetEnt; if( pTarget != NULL && pTarget->pev->deadflag == DEAD_NO ) return TRUE; return FALSE; } int CCineMonster::IgnoreConditions( void ) { if( CanInterrupt() ) return 0; return SCRIPT_BREAK_CONDITIONS; } void ScriptEntityCancel( edict_t *pentCine ) { // make sure they are a scripted_sequence if( FClassnameIs( pentCine, CLASSNAME ) ) { CCineMonster *pCineTarget = GetClassPtr( (CCineMonster *)VARS( pentCine ) ); // make sure they have a monster in mind for the script CBaseEntity *pEntity = pCineTarget->m_hTargetEnt; CBaseMonster *pTarget = NULL; if( pEntity ) pTarget = pEntity->MyMonsterPointer(); if( pTarget ) { // make sure their monster is actually playing a script if( pTarget->m_MonsterState == MONSTERSTATE_SCRIPT || pTarget->m_IdealMonsterState == MONSTERSTATE_SCRIPT ) { // tell them do die pTarget->m_scriptState = CCineMonster::SCRIPT_CLEANUP; // do it now pTarget->CineCleanup(); } } } } // find all the cinematic entities with my targetname and stop them from playing void CCineMonster::CancelScript( void ) { ALERT( at_aiconsole, "Cancelling script: %s\n", STRING( m_iszPlay ) ); if( !pev->targetname ) { ScriptEntityCancel( edict() ); return; } edict_t *pentCineTarget = FIND_ENTITY_BY_TARGETNAME( NULL, STRING( pev->targetname ) ); while( !FNullEnt( pentCineTarget ) ) { ScriptEntityCancel( pentCineTarget ); pentCineTarget = FIND_ENTITY_BY_TARGETNAME( pentCineTarget, STRING( pev->targetname ) ); } } // find all the cinematic entities with my targetname and tell them to wait before starting void CCineMonster::DelayStart( int state ) { edict_t *pentCine = FIND_ENTITY_BY_TARGETNAME( NULL, STRING( pev->targetname ) ); while( !FNullEnt( pentCine ) ) { if( FClassnameIs( pentCine, "scripted_sequence" ) ) { CCineMonster *pTarget = GetClassPtr( ( CCineMonster *)VARS( pentCine ) ); if( state ) { pTarget->m_iDelay++; } else { pTarget->m_iDelay--; if( pTarget->m_iDelay <= 0 ) pTarget->m_startTime = gpGlobals->time + 0.05f; } } pentCine = FIND_ENTITY_BY_TARGETNAME( pentCine, STRING( pev->targetname ) ); } } // Find an entity that I'm interested in and precache the sounds he'll need in the sequence. void CCineMonster::Activate( void ) { edict_t *pentTarget; CBaseMonster *pTarget; // The entity name could be a target name or a classname // Check the targetname pentTarget = FIND_ENTITY_BY_TARGETNAME( NULL, STRING( m_iszEntity ) ); pTarget = NULL; while( !pTarget && !FNullEnt( pentTarget ) ) { if( FBitSet( VARS( pentTarget )->flags, FL_MONSTER ) ) { pTarget = GetMonsterPointer( pentTarget ); } pentTarget = FIND_ENTITY_BY_TARGETNAME( pentTarget, STRING( m_iszEntity ) ); } // If no entity with that targetname, check the classname if ( !pTarget ) { pentTarget = FIND_ENTITY_BY_CLASSNAME(NULL, STRING( m_iszEntity ) ); while( !pTarget && !FNullEnt( pentTarget ) ) { pTarget = GetMonsterPointer( pentTarget ); pentTarget = FIND_ENTITY_BY_TARGETNAME(pentTarget, STRING( m_iszEntity ) ); } } // Found a compatible entity if ( pTarget ) { void *pmodel; pmodel = GET_MODEL_PTR( pTarget->edict() ); if( pmodel ) { // Look through the event list for stuff to precache SequencePrecache( pmodel, STRING( m_iszIdle ) ); SequencePrecache( pmodel, STRING( m_iszPlay ) ); } } } BOOL CBaseMonster::CineCleanup() { CCineMonster *pOldCine = m_pCine; // am I linked to a cinematic? if( m_pCine ) { // okay, reset me to what it thought I was before m_pCine->m_hTargetEnt = NULL; pev->movetype = m_pCine->m_saved_movetype; pev->solid = m_pCine->m_saved_solid; pev->effects = m_pCine->m_saved_effects; } else { // arg, punt pev->movetype = MOVETYPE_STEP;// this is evil pev->solid = SOLID_SLIDEBOX; } m_pCine = NULL; m_hTargetEnt = NULL; m_pGoalEnt = NULL; if( pev->deadflag == DEAD_DYING ) { // last frame of death animation? pev->health = 0; pev->framerate = 0.0; pev->solid = SOLID_NOT; SetState( MONSTERSTATE_DEAD ); pev->deadflag = DEAD_DEAD; UTIL_SetSize( pev, pev->mins, Vector( pev->maxs.x, pev->maxs.y, pev->mins.z + 2 ) ); if( pOldCine && FBitSet( pOldCine->pev->spawnflags, SF_SCRIPT_LEAVECORPSE ) ) { SetUse( NULL ); // BUGBUG -- This doesn't call Killed() SetThink( NULL ); // This will probably break some stuff SetTouch( NULL ); } else SUB_StartFadeOut(); // SetThink( &SUB_DoNothing ); // This turns off animation & physics in case their origin ends up stuck in the world or something StopAnimation(); pev->movetype = MOVETYPE_NONE; pev->effects |= EF_NOINTERP; // Don't interpolate either, assume the corpse is positioned in its final resting place return FALSE; } // If we actually played a sequence if( pOldCine && pOldCine->m_iszPlay ) { if( !( pOldCine->pev->spawnflags & SF_SCRIPT_NOSCRIPTMOVEMENT ) ) { // reset position Vector new_origin, new_angle; GetBonePosition( 0, new_origin, new_angle ); // Figure out how far they have moved // We can't really solve this problem because we can't query the movement of the origin relative // to the sequence. We can get the root bone's position as we do here, but there are // cases where the root bone is in a different relative position to the entity's origin // before/after the sequence plays. So we are stuck doing this: // !!!HACKHACK: Float the origin up and drop to floor because some sequences have // irregular motion that can't be properly accounted for. // UNDONE: THIS SHOULD ONLY HAPPEN IF WE ACTUALLY PLAYED THE SEQUENCE. Vector oldOrigin = pev->origin; // UNDONE: ugly hack. Don't move monster if they don't "seem" to move // this really needs to be done with the AX,AY,etc. flags, but that aren't consistantly // being set, so animations that really do move won't be caught. if( ( oldOrigin - new_origin).Length2D() < 8.0f ) new_origin = oldOrigin; pev->origin.x = new_origin.x; pev->origin.y = new_origin.y; pev->origin.z += 1; pev->flags |= FL_ONGROUND; int drop = DROP_TO_FLOOR( ENT( pev ) ); // Origin in solid? Set to org at the end of the sequence if( drop < 0 ) pev->origin = oldOrigin; else if( drop == 0 ) // Hanging in air? { pev->origin.z = new_origin.z; pev->flags &= ~FL_ONGROUND; } // else entity hit floor, leave there // pEntity->pev->origin.z = new_origin.z + 5.0; // damn, got to fix this UTIL_SetOrigin( pev, pev->origin ); pev->effects |= EF_NOINTERP; } // We should have some animation to put these guys in, but for now it's idle. // Due to NOINTERP above, there won't be any blending between this anim & the sequence m_Activity = ACT_RESET; } // set them back into a normal state pev->enemy = NULL; if( pev->health > 0 ) m_IdealMonsterState = MONSTERSTATE_IDLE; // m_previousState; else { // Dropping out because he got killed // Can't call killed() no attacker and weirdness (late gibbing) may result m_IdealMonsterState = MONSTERSTATE_DEAD; SetConditions( bits_COND_LIGHT_DAMAGE ); pev->deadflag = DEAD_DYING; FCheckAITrigger(); pev->deadflag = DEAD_NO; } // SetAnimation( m_MonsterState ); ClearBits( pev->spawnflags, SF_MONSTER_WAIT_FOR_SCRIPT ); return TRUE; } class CScriptedSentence : public CBaseToggle { public: void Spawn( void ); void KeyValue( KeyValueData *pkvd ); void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); void EXPORT FindThink( void ); void EXPORT DelayThink( void ); int ObjectCaps( void ) { return ( CBaseToggle::ObjectCaps() & ~FCAP_ACROSS_TRANSITION ); } virtual int Save( CSave &save ); virtual int Restore( CRestore &restore ); static TYPEDESCRIPTION m_SaveData[]; CBaseToggle *FindEntity( void ); BOOL AcceptableSpeaker( CBaseToggle *pTarget ); BOOL StartSentence( CBaseToggle *pTarget ); private: string_t m_iszSentence; // string index for idle animation string_t m_iszEntity; // entity that is wanted for this sentence float m_flRadius; // range to search float m_flDuration; // How long the sentence lasts float m_flRepeat; // repeat rate float m_flAttenuation; float m_flVolume; BOOL m_active; string_t m_iszListener; // name of entity to look at while talking }; #define SF_SENTENCE_ONCE 0x0001 #define SF_SENTENCE_FOLLOWERS 0x0002 // only say if following player #define SF_SENTENCE_INTERRUPT 0x0004 // force talking except when dead #define SF_SENTENCE_CONCURRENT 0x0008 // allow other people to keep talking TYPEDESCRIPTION CScriptedSentence::m_SaveData[] = { DEFINE_FIELD( CScriptedSentence, m_iszSentence, FIELD_STRING ), DEFINE_FIELD( CScriptedSentence, m_iszEntity, FIELD_STRING ), DEFINE_FIELD( CScriptedSentence, m_flRadius, FIELD_FLOAT ), DEFINE_FIELD( CScriptedSentence, m_flDuration, FIELD_FLOAT ), DEFINE_FIELD( CScriptedSentence, m_flRepeat, FIELD_FLOAT ), DEFINE_FIELD( CScriptedSentence, m_flAttenuation, FIELD_FLOAT ), DEFINE_FIELD( CScriptedSentence, m_flVolume, FIELD_FLOAT ), DEFINE_FIELD( CScriptedSentence, m_active, FIELD_BOOLEAN ), DEFINE_FIELD( CScriptedSentence, m_iszListener, FIELD_STRING ), }; IMPLEMENT_SAVERESTORE( CScriptedSentence, CBaseToggle ) LINK_ENTITY_TO_CLASS( scripted_sentence, CScriptedSentence ) void CScriptedSentence::KeyValue( KeyValueData *pkvd ) { if( FStrEq( pkvd->szKeyName, "sentence" ) ) { m_iszSentence = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "entity" ) ) { m_iszEntity = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "duration" ) ) { m_flDuration = atof( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "radius" ) ) { m_flRadius = atof( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "refire") ) { m_flRepeat = atof( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "attenuation" ) ) { pev->impulse = atoi( pkvd->szValue ); pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "volume" ) ) { m_flVolume = atof( pkvd->szValue ) * 0.1f; pkvd->fHandled = TRUE; } else if( FStrEq( pkvd->szKeyName, "listener" ) ) { m_iszListener = ALLOC_STRING( pkvd->szValue ); pkvd->fHandled = TRUE; } else CBaseToggle::KeyValue( pkvd ); } void CScriptedSentence::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { if( !m_active ) return; //ALERT( at_console, "Firing sentence: %s\n", STRING( m_iszSentence ) ); SetThink( &CScriptedSentence::FindThink ); pev->nextthink = gpGlobals->time; } void CScriptedSentence::Spawn( void ) { pev->solid = SOLID_NOT; m_active = TRUE; // if no targetname, start now if( !pev->targetname ) { SetThink( &CScriptedSentence::FindThink ); pev->nextthink = gpGlobals->time + 1.0f; } switch( pev->impulse ) { case 1: // Medium radius m_flAttenuation = ATTN_STATIC; break; case 2: // Large radius m_flAttenuation = ATTN_NORM; break; case 3: //EVERYWHERE m_flAttenuation = ATTN_NONE; break; default: case 0: // Small radius m_flAttenuation = ATTN_IDLE; break; } pev->impulse = 0; // No volume, use normal if( m_flVolume <= 0.0f ) m_flVolume = 1.0f; } void CScriptedSentence::FindThink( void ) { CBaseToggle *pTarget = FindEntity(); if( pTarget ) { StartSentence( pTarget ); if( pev->spawnflags & SF_SENTENCE_ONCE ) UTIL_Remove( this ); SetThink( &CScriptedSentence::DelayThink ); pev->nextthink = gpGlobals->time + m_flDuration + m_flRepeat; m_active = FALSE; //ALERT( at_console, "%s: found target %s\n", STRING( m_iszSentence ), STRING( m_iszEntity ) ); } else { //ALERT( at_console, "%s: can't find target %s\n", STRING( m_iszSentence ), STRING( m_iszEntity ) ); pev->nextthink = gpGlobals->time + m_flRepeat + 0.5f; } } void CScriptedSentence::DelayThink( void ) { m_active = TRUE; if( !pev->targetname ) pev->nextthink = gpGlobals->time + 0.1f; SetThink( &CScriptedSentence::FindThink ); } BOOL CScriptedSentence::AcceptableSpeaker( CBaseToggle *pTarget ) { CBaseMonster *pMonster; EHANDLE hTarget; if( pTarget ) { hTarget = pTarget->MyMonsterPointer(); if( hTarget != 0 ) { CBaseMonster *pMonster = (CBaseMonster*)( (CBaseEntity*)hTarget ); if( pev->spawnflags & SF_SENTENCE_FOLLOWERS ) { if( pMonster->m_hTargetEnt == 0 || !pMonster->m_hTargetEnt->IsPlayer() ) return FALSE; } BOOL override; if( pev->spawnflags & SF_SENTENCE_INTERRUPT ) override = TRUE; else override = FALSE; if( pMonster->CanPlaySentence( override ) ) return TRUE; } else return pTarget->IsAllowedToSpeak(); } return FALSE; } CBaseToggle *CScriptedSentence::FindEntity( void ) { edict_t *pentTarget; CBaseToggle *pTarget; pentTarget = FIND_ENTITY_BY_TARGETNAME( NULL, STRING( m_iszEntity ) ); pTarget = NULL; while( !FNullEnt( pentTarget ) ) { pTarget = (CBaseToggle*)CBaseEntity::Instance( pentTarget ); if( pTarget != NULL ) { if( AcceptableSpeaker( pTarget ) ) return pTarget; //ALERT( at_console, "%s (%s), not acceptable\n", STRING( pTarget->pev->classname ), STRING( pTarget->pev->targetname ) ); } pentTarget = FIND_ENTITY_BY_TARGETNAME( pentTarget, STRING( m_iszEntity ) ); } CBaseEntity *pEntity = NULL; while( ( pEntity = UTIL_FindEntityInSphere( pEntity, pev->origin, m_flRadius ) ) != NULL ) { if( FClassnameIs( pEntity->pev, STRING( m_iszEntity ) ) ) { if( FBitSet( pEntity->pev->flags, FL_MONSTER ) ) { pTarget = pEntity->MyTogglePointer(); if( AcceptableSpeaker( pTarget ) ) return pTarget; } } } return NULL; } BOOL CScriptedSentence::StartSentence( CBaseToggle *pTarget ) { if( !pTarget ) { ALERT( at_aiconsole, "Not Playing sentence %s\n", STRING( m_iszSentence ) ); return FALSE; } BOOL bConcurrent = FALSE; if( !( pev->spawnflags & SF_SENTENCE_CONCURRENT ) ) bConcurrent = TRUE; CBaseEntity *pListener = NULL; if( !FStringNull( m_iszListener ) ) { float radius = m_flRadius; if( FStrEq( STRING( m_iszListener ), "player" ) ) radius = 4096; // Always find the player pListener = UTIL_FindEntityGeneric( STRING( m_iszListener ), pTarget->pev->origin, radius ); } pTarget->PlayScriptedSentence( STRING( m_iszSentence ), m_flDuration, m_flVolume, m_flAttenuation, bConcurrent, pListener ); ALERT( at_aiconsole, "Playing sentence %s (%.1f)\n", STRING( m_iszSentence ), (double)m_flDuration ); SUB_UseTargets( NULL, USE_TOGGLE, 0 ); return TRUE; } //========================================================= // Furniture - this is the cool comment I cut-and-pasted //========================================================= class CFurniture : public CBaseMonster { public: void Spawn( void ); void Die( void ); int Classify( void ); virtual int ObjectCaps( void ) { return (CBaseMonster::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } }; LINK_ENTITY_TO_CLASS( monster_furniture, CFurniture ) //========================================================= // Furniture is killed //========================================================= void CFurniture::Die( void ) { SetThink( &CBaseEntity::SUB_Remove ); pev->nextthink = gpGlobals->time; } //========================================================= // This used to have something to do with bees flying, but // now it only initializes moving furniture in scripted sequences //========================================================= void CFurniture::Spawn() { PRECACHE_MODEL( STRING( pev->model ) ); SET_MODEL( ENT( pev ), STRING( pev->model ) ); pev->movetype = MOVETYPE_NONE; pev->solid = SOLID_BBOX; pev->health = 80000; pev->takedamage = DAMAGE_AIM; pev->effects = 0; pev->yaw_speed = 0; pev->sequence = 0; pev->frame = 0; //pev->nextthink += 1.0f; //SetThink( &WalkMonsterDelay ); ResetSequenceInfo(); pev->frame = 0; MonsterInit(); } //========================================================= // ID's Furniture as neutral (noone will attack it) //========================================================= int CFurniture::Classify( void ) { return CLASS_NONE; }