1 /*
2 ===========================================================================
3 Copyright (C) 2000-2006 Tim Angus
4
5 This file is part of Tremulous.
6
7 Tremulous is free software; you can redistribute it
8 and/or modify it under the terms of the GNU General Public License as
9 published by the Free Software Foundation; either version 2 of the License,
10 or (at your option) any later version.
11
12 Tremulous is distributed in the hope that it will be
13 useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with Tremulous; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20 ===========================================================================
21 */
22
23 // cg_tutorial.c -- the tutorial system
24
25 #include "cg_local.h"
26
27 typedef struct
28 {
29 char *command;
30 char *humanName;
31 keyNum_t keys[ 2 ];
32 } bind_t;
33
34 static bind_t bindings[ ] =
35 {
36 { "+button2", "Activate Upgrade", { -1, -1 } },
37 { "+speed", "Run/Walk", { -1, -1 } },
38 { "boost", "Sprint", { -1, -1 } },
39 { "+moveup", "Jump", { -1, -1 } },
40 { "+movedown", "Crouch", { -1, -1 } },
41 { "+zoom", "ZoomView", { -1, -1 } },
42 { "+attack", "Primary Attack", { -1, -1 } },
43 { "+button5", "Secondary Attack", { -1, -1 } },
44 { "reload", "Reload", { -1, -1 } },
45 { "buy ammo", "Buy Ammo", { -1, -1 } },
46 { "itemact medkit", "Use Medkit", { -1, -1 } },
47 { "+button7", "Use Structure/Evolve", { -1, -1 } },
48 { "deconstruct", "Deconstruct Structure", { -1, -1 } },
49 { "weapprev", "Previous Upgrade", { -1, -1 } },
50 { "weapnext", "Next Upgrade", { -1, -1 } }
51 };
52
53 static const int numBindings = sizeof( bindings ) / sizeof( bind_t );
54
55 /*
56 =================
57 CG_GetBindings
58 =================
59 */
CG_GetBindings(void)60 static void CG_GetBindings( void )
61 {
62 int i, j, numKeys;
63 char buffer[ MAX_STRING_CHARS ];
64
65 for( i = 0; i < numBindings; i++ )
66 {
67 bindings[ i ].keys[ 0 ] = bindings[ i ].keys[ 1 ] = K_NONE;
68 numKeys = 0;
69
70 for( j = 0; j < K_LAST_KEY; j++ )
71 {
72 trap_Key_GetBindingBuf( j, buffer, MAX_STRING_CHARS );
73
74 if( buffer[ 0 ] == 0 )
75 continue;
76
77 if( !Q_stricmp( buffer, bindings[ i ].command ) )
78 {
79 bindings[ i ].keys[ numKeys++ ] = j;
80
81 if( numKeys > 1 )
82 break;
83 }
84 }
85 }
86 }
87
88 /*
89 ===============
90 CG_KeyNameForCommand
91 ===============
92 */
CG_KeyNameForCommand(const char * command)93 static const char *CG_KeyNameForCommand( const char *command )
94 {
95 int i, j;
96 static char buffer[ MAX_STRING_CHARS ];
97 int firstKeyLength;
98
99 buffer[ 0 ] = '\0';
100
101 for( i = 0; i < numBindings; i++ )
102 {
103 if( !Q_stricmp( command, bindings[ i ].command ) )
104 {
105 if( bindings[ i ].keys[ 0 ] != K_NONE )
106 {
107 trap_Key_KeynumToStringBuf( bindings[ i ].keys[ 0 ],
108 buffer, MAX_STRING_CHARS );
109 firstKeyLength = strlen( buffer );
110
111 for( j = 0; j < firstKeyLength; j++ )
112 buffer[ j ] = toupper( buffer[ j ] );
113
114 if( bindings[ i ].keys[ 1 ] != K_NONE )
115 {
116 Q_strcat( buffer, MAX_STRING_CHARS, " or " );
117 trap_Key_KeynumToStringBuf( bindings[ i ].keys[ 1 ],
118 buffer + strlen( buffer ), MAX_STRING_CHARS - strlen( buffer ) );
119
120 for( j = firstKeyLength + 4; j < strlen( buffer ); j++ )
121 buffer[ j ] = toupper( buffer[ j ] );
122 }
123 }
124 else
125 {
126 Q_strncpyz( buffer, va( "\"%s\"", bindings[ i ].humanName ),
127 MAX_STRING_CHARS );
128 Q_strcat( buffer, MAX_STRING_CHARS, " (unbound)" );
129 }
130
131 return buffer;
132 }
133 }
134
135 return "";
136 }
137
138 #define MAX_TUTORIAL_TEXT 4096
139
140 /*
141 ===============
142 CG_BuildableInRange
143 ===============
144 */
CG_BuildableInRange(playerState_t * ps)145 static qboolean CG_BuildableInRange( playerState_t *ps )
146 {
147 vec3_t view, point;
148 trace_t trace;
149 entityState_t *es;
150
151 AngleVectors( cg.refdefViewAngles, view, NULL, NULL );
152 VectorMA( cg.refdef.vieworg, 64, view, point );
153 CG_Trace( &trace, cg.refdef.vieworg, NULL, NULL,
154 point, ps->clientNum, MASK_SHOT );
155
156 es = &cg_entities[ trace.entityNum ].currentState;
157
158 if( es->eType == ET_BUILDABLE &&
159 ps->stats[ STAT_PTEAM ] == BG_FindTeamForBuildable( es->modelindex ) )
160 return qtrue;
161 else
162 return qfalse;
163 }
164
165 /*
166 ===============
167 CG_AlienBuilderText
168 ===============
169 */
CG_AlienBuilderText(char * text,playerState_t * ps)170 static void CG_AlienBuilderText( char *text, playerState_t *ps )
171 {
172 buildable_t buildable = ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT;
173
174 if( buildable > BA_NONE )
175 {
176 Q_strcat( text, MAX_TUTORIAL_TEXT,
177 va( "Press %s to place the %s\n",
178 CG_KeyNameForCommand( "+attack" ),
179 BG_FindHumanNameForBuildable( buildable ) ) );
180
181 Q_strcat( text, MAX_TUTORIAL_TEXT,
182 va( "Press %s to cancel placing the %s\n",
183 CG_KeyNameForCommand( "+button5" ),
184 BG_FindHumanNameForBuildable( buildable ) ) );
185 }
186 else
187 {
188 Q_strcat( text, MAX_TUTORIAL_TEXT,
189 va( "Press %s to build a structure\n",
190 CG_KeyNameForCommand( "+attack" ) ) );
191
192 if( CG_BuildableInRange( ps ) )
193 {
194 Q_strcat( text, MAX_TUTORIAL_TEXT,
195 va( "Press %s to destroy this structure\n",
196 CG_KeyNameForCommand( "deconstruct" ) ) );
197 }
198 }
199
200 if( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG )
201 {
202 if( ( ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) == BA_NONE )
203 {
204 Q_strcat( text, MAX_TUTORIAL_TEXT,
205 va( "Press %s to swipe\n",
206 CG_KeyNameForCommand( "+button5" ) ) );
207 }
208
209 Q_strcat( text, MAX_TUTORIAL_TEXT,
210 va( "Press %s to launch a projectile\n",
211 CG_KeyNameForCommand( "+button2" ) ) );
212
213 Q_strcat( text, MAX_TUTORIAL_TEXT,
214 va( "Press %s to walk on walls\n",
215 CG_KeyNameForCommand( "+movedown" ) ) );
216 }
217 }
218
219 /*
220 ===============
221 CG_AlienLevel0Text
222 ===============
223 */
CG_AlienLevel0Text(char * text,playerState_t * ps)224 static void CG_AlienLevel0Text( char *text, playerState_t *ps )
225 {
226 Q_strcat( text, MAX_TUTORIAL_TEXT,
227 "Touch a human to damage it\n" );
228
229 Q_strcat( text, MAX_TUTORIAL_TEXT,
230 va( "Press %s to walk on walls\n",
231 CG_KeyNameForCommand( "+movedown" ) ) );
232 }
233
234 /*
235 ===============
236 CG_AlienLevel1Text
237 ===============
238 */
CG_AlienLevel1Text(char * text,playerState_t * ps)239 static void CG_AlienLevel1Text( char *text, playerState_t *ps )
240 {
241 Q_strcat( text, MAX_TUTORIAL_TEXT,
242 "Touch a human to grab it\n" );
243
244 Q_strcat( text, MAX_TUTORIAL_TEXT,
245 va( "Press %s to swipe\n",
246 CG_KeyNameForCommand( "+attack" ) ) );
247
248 if( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_LEVEL1_UPG )
249 {
250 Q_strcat( text, MAX_TUTORIAL_TEXT,
251 va( "Press %s to spray poisonous gas\n",
252 CG_KeyNameForCommand( "+button5" ) ) );
253 }
254
255 Q_strcat( text, MAX_TUTORIAL_TEXT,
256 va( "Press %s to walk on walls\n",
257 CG_KeyNameForCommand( "+movedown" ) ) );
258 }
259
260 /*
261 ===============
262 CG_AlienLevel2Text
263 ===============
264 */
CG_AlienLevel2Text(char * text,playerState_t * ps)265 static void CG_AlienLevel2Text( char *text, playerState_t *ps )
266 {
267 Q_strcat( text, MAX_TUTORIAL_TEXT,
268 va( "Press %s to bite\n",
269 CG_KeyNameForCommand( "+attack" ) ) );
270
271 if( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_LEVEL2_UPG )
272 {
273 Q_strcat( text, MAX_TUTORIAL_TEXT,
274 va( "Press %s to invoke an electrical attack\n",
275 CG_KeyNameForCommand( "+button5" ) ) );
276 }
277
278 Q_strcat( text, MAX_TUTORIAL_TEXT,
279 va( "Hold down %s then touch a wall to wall jump\n",
280 CG_KeyNameForCommand( "+moveup" ) ) );
281 }
282
283 /*
284 ===============
285 CG_AlienLevel3Text
286 ===============
287 */
CG_AlienLevel3Text(char * text,playerState_t * ps)288 static void CG_AlienLevel3Text( char *text, playerState_t *ps )
289 {
290 Q_strcat( text, MAX_TUTORIAL_TEXT,
291 va( "Press %s to bite\n",
292 CG_KeyNameForCommand( "+attack" ) ) );
293
294 if( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_LEVEL3_UPG )
295 {
296 Q_strcat( text, MAX_TUTORIAL_TEXT,
297 va( "Press %s to launch a projectile\n",
298 CG_KeyNameForCommand( "+button2" ) ) );
299 }
300
301 Q_strcat( text, MAX_TUTORIAL_TEXT,
302 va( "Hold down and release %s to pounce\n",
303 CG_KeyNameForCommand( "+button5" ) ) );
304 }
305
306 /*
307 ===============
308 CG_AlienLevel4Text
309 ===============
310 */
CG_AlienLevel4Text(char * text,playerState_t * ps)311 static void CG_AlienLevel4Text( char *text, playerState_t *ps )
312 {
313 Q_strcat( text, MAX_TUTORIAL_TEXT,
314 va( "Press %s to swipe\n",
315 CG_KeyNameForCommand( "+attack" ) ) );
316
317 Q_strcat( text, MAX_TUTORIAL_TEXT,
318 va( "Hold down and release %s to charge\n",
319 CG_KeyNameForCommand( "+button5" ) ) );
320 }
321
322 /*
323 ===============
324 CG_HumanCkitText
325 ===============
326 */
CG_HumanCkitText(char * text,playerState_t * ps)327 static void CG_HumanCkitText( char *text, playerState_t *ps )
328 {
329 buildable_t buildable = ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT;
330
331 if( buildable > BA_NONE )
332 {
333 Q_strcat( text, MAX_TUTORIAL_TEXT,
334 va( "Press %s to place the %s\n",
335 CG_KeyNameForCommand( "+attack" ),
336 BG_FindHumanNameForBuildable( buildable ) ) );
337
338 Q_strcat( text, MAX_TUTORIAL_TEXT,
339 va( "Press %s to cancel placing the %s\n",
340 CG_KeyNameForCommand( "+button5" ),
341 BG_FindHumanNameForBuildable( buildable ) ) );
342 }
343 else
344 {
345 Q_strcat( text, MAX_TUTORIAL_TEXT,
346 va( "Press %s to build a structure\n",
347 CG_KeyNameForCommand( "+attack" ) ) );
348
349 if( CG_BuildableInRange( ps ) )
350 {
351 Q_strcat( text, MAX_TUTORIAL_TEXT,
352 va( "Press %s to destroy this structure\n",
353 CG_KeyNameForCommand( "deconstruct" ) ) );
354 }
355 }
356 }
357
358 /*
359 ===============
360 CG_HumanText
361 ===============
362 */
CG_HumanText(char * text,playerState_t * ps)363 static void CG_HumanText( char *text, playerState_t *ps )
364 {
365 char *name;
366 int ammo, clips;
367 upgrade_t upgrade = UP_NONE;
368
369 if( cg.weaponSelect <= 32 )
370 name = cg_weapons[ cg.weaponSelect ].humanName;
371 else if( cg.weaponSelect > 32 )
372 {
373 name = cg_upgrades[ cg.weaponSelect - 32 ].humanName;
374 upgrade = cg.weaponSelect - 32;
375 }
376
377 BG_UnpackAmmoArray( ps->weapon, ps->ammo, ps->powerups, &ammo, &clips );
378
379 if( !ammo && !clips && !BG_FindInfinteAmmoForWeapon( ps->weapon ) )
380 {
381 //no ammo
382 switch( ps->weapon )
383 {
384 case WP_MACHINEGUN:
385 case WP_CHAINGUN:
386 case WP_SHOTGUN:
387 case WP_FLAMER:
388 Q_strcat( text, MAX_TUTORIAL_TEXT,
389 va( "Find an Armoury and press %s for more ammo\n",
390 CG_KeyNameForCommand( "buy ammo" ) ) );
391 break;
392
393 case WP_LAS_GUN:
394 case WP_PULSE_RIFLE:
395 case WP_MASS_DRIVER:
396 case WP_LUCIFER_CANNON:
397 Q_strcat( text, MAX_TUTORIAL_TEXT,
398 va( "Find a Reactor or Repeater and press %s for more ammo\n",
399 CG_KeyNameForCommand( "buy ammo" ) ) );
400 break;
401
402 default:
403 break;
404 }
405 }
406 else
407 {
408 switch( ps->weapon )
409 {
410 case WP_BLASTER:
411 case WP_MACHINEGUN:
412 case WP_SHOTGUN:
413 case WP_LAS_GUN:
414 case WP_CHAINGUN:
415 case WP_PULSE_RIFLE:
416 case WP_FLAMER:
417 Q_strcat( text, MAX_TUTORIAL_TEXT,
418 va( "Press %s to fire the %s\n",
419 CG_KeyNameForCommand( "+attack" ),
420 BG_FindHumanNameForWeapon( ps->weapon ) ) );
421 break;
422
423 case WP_MASS_DRIVER:
424 Q_strcat( text, MAX_TUTORIAL_TEXT,
425 va( "Press %s to fire the %s\n",
426 CG_KeyNameForCommand( "+attack" ),
427 BG_FindHumanNameForWeapon( ps->weapon ) ) );
428
429 Q_strcat( text, MAX_TUTORIAL_TEXT,
430 va( "Hold %s to zoom\n",
431 CG_KeyNameForCommand( "+zoom" ) ) );
432 break;
433
434 case WP_PAIN_SAW:
435 Q_strcat( text, MAX_TUTORIAL_TEXT,
436 va( "Hold %s to activate the %s\n",
437 CG_KeyNameForCommand( "+attack" ),
438 BG_FindHumanNameForWeapon( ps->weapon ) ) );
439 break;
440
441 case WP_LUCIFER_CANNON:
442 Q_strcat( text, MAX_TUTORIAL_TEXT,
443 va( "Hold and release %s to fire a charged shot\n",
444 CG_KeyNameForCommand( "+attack" ) ) );
445
446 Q_strcat( text, MAX_TUTORIAL_TEXT,
447 va( "Press %s to fire the %s\n",
448 CG_KeyNameForCommand( "+button5" ),
449 BG_FindHumanNameForWeapon( ps->weapon ) ) );
450 break;
451
452 case WP_HBUILD:
453 case WP_HBUILD2:
454 CG_HumanCkitText( text, ps );
455 break;
456
457 default:
458 break;
459 }
460 }
461
462 Q_strcat( text, MAX_TUTORIAL_TEXT,
463 va( "Press %s and ",
464 CG_KeyNameForCommand( "weapprev" ) ) );
465 Q_strcat( text, MAX_TUTORIAL_TEXT,
466 va( "%s to select an upgrade\n",
467 CG_KeyNameForCommand( "weapnext" ) ) );
468
469 if( upgrade == UP_NONE ||
470 ( upgrade > UP_NONE && BG_FindUsableForUpgrade( upgrade ) ) )
471 {
472 Q_strcat( text, MAX_TUTORIAL_TEXT,
473 va( "Press %s to use the %s\n",
474 CG_KeyNameForCommand( "+button2" ),
475 name ) );
476 }
477
478 if( ps->stats[ STAT_HEALTH ] <= 35 &&
479 BG_InventoryContainsUpgrade( UP_MEDKIT, ps->stats ) )
480 {
481 Q_strcat( text, MAX_TUTORIAL_TEXT,
482 va( "Press %s to use your %s\n",
483 CG_KeyNameForCommand( "itemact medkit" ),
484 BG_FindHumanNameForUpgrade( UP_MEDKIT ) ) );
485 }
486
487 Q_strcat( text, MAX_TUTORIAL_TEXT,
488 va( "Press %s to use a structure\n",
489 CG_KeyNameForCommand( "+button7" ) ) );
490 }
491
492 /*
493 ===============
494 CG_SpectatorText
495 ===============
496 */
CG_SpectatorText(char * text,playerState_t * ps)497 static void CG_SpectatorText( char *text, playerState_t *ps )
498 {
499 if( ps->pm_flags & PMF_FOLLOW )
500 {
501 Q_strcat( text, MAX_TUTORIAL_TEXT,
502 va( "Press %s to return to free spectator mode\n",
503 CG_KeyNameForCommand( "+button2" ) ) );
504
505 if( CG_PlayerCount( ) > 1 )
506 {
507 Q_strcat( text, MAX_TUTORIAL_TEXT,
508 va( "Press %s or ",
509 CG_KeyNameForCommand( "weapprev" ) ) );
510 Q_strcat( text, MAX_TUTORIAL_TEXT,
511 va( "%s to change player\n",
512 CG_KeyNameForCommand( "weapnext" ) ) );
513 }
514 }
515 else if( ps->pm_type == PM_SPECTATOR )
516 {
517 Q_strcat( text, MAX_TUTORIAL_TEXT,
518 va( "Press %s to join a team\n",
519 CG_KeyNameForCommand( "+attack" ) ) );
520
521 if( CG_PlayerCount( ) > 0 )
522 {
523 Q_strcat( text, MAX_TUTORIAL_TEXT,
524 va( "Press %s to enter spectator follow mode\n",
525 CG_KeyNameForCommand( "+button2" ) ) );
526 }
527 }
528 else
529 {
530 Q_strcat( text, MAX_TUTORIAL_TEXT,
531 va( "Press %s to spawn\n",
532 CG_KeyNameForCommand( "+attack" ) ) );
533 }
534 }
535
536 /*
537 ===============
538 CG_TutorialText
539
540 Returns context help for the current class/weapon
541 ===============
542 */
CG_TutorialText(void)543 const char *CG_TutorialText( void )
544 {
545 playerState_t *ps;
546 static char text[ MAX_TUTORIAL_TEXT ];
547
548 CG_GetBindings( );
549
550 text[ 0 ] = '\0';
551 ps = &cg.snap->ps;
552
553 if( !cg.intermissionStarted && !cg.demoPlayback )
554 {
555 if( ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR ||
556 ps->pm_flags & PMF_FOLLOW )
557 {
558 CG_SpectatorText( text, ps );
559 }
560 else if( ps->stats[ STAT_HEALTH ] > 0 )
561 {
562 switch( ps->stats[ STAT_PCLASS ] )
563 {
564 case PCL_ALIEN_BUILDER0:
565 case PCL_ALIEN_BUILDER0_UPG:
566 CG_AlienBuilderText( text, ps );
567 break;
568
569 case PCL_ALIEN_LEVEL0:
570 CG_AlienLevel0Text( text, ps );
571 break;
572
573 case PCL_ALIEN_LEVEL1:
574 case PCL_ALIEN_LEVEL1_UPG:
575 CG_AlienLevel1Text( text, ps );
576 break;
577
578 case PCL_ALIEN_LEVEL2:
579 case PCL_ALIEN_LEVEL2_UPG:
580 CG_AlienLevel2Text( text, ps );
581 break;
582
583 case PCL_ALIEN_LEVEL3:
584 case PCL_ALIEN_LEVEL3_UPG:
585 CG_AlienLevel3Text( text, ps );
586 break;
587
588 case PCL_ALIEN_LEVEL4:
589 CG_AlienLevel4Text( text, ps );
590 break;
591
592 case PCL_HUMAN:
593 CG_HumanText( text, ps );
594 break;
595
596 default:
597 break;
598 }
599
600 if( ps->stats[ STAT_PTEAM ] == PTE_ALIENS &&
601 BG_UpgradeClassAvailable( ps ) )
602 {
603 Q_strcat( text, MAX_TUTORIAL_TEXT,
604 va( "Press %s to evolve\n",
605 CG_KeyNameForCommand( "+button7" ) ) );
606 }
607 }
608
609 Q_strcat( text, MAX_TUTORIAL_TEXT, "Press ESC for the menu" );
610 }
611
612 return text;
613 }
614