Jump to content
The website was recently updated, and consequently some things are broken. We will be fixing minor theme bugs in the background over the course of the next few days.

Let's Map: Singleplayer


redsaurus

Recommended Posts

As people might be more likely to make SP maps if there's a more comprehensive guide, I'm going to try and do some kind of step-by-step making a SP map thing, hopefully covering all the stuff that's already been covered in the very good tutorials people have written, and maybe some other things too.

 

The plot's pretty irrelevant, and probably won't fit neatly into the SP campaign (but Jedi Knight: Enhanced would welcome any maps that do!). You'll start on somewhere korriban-ish (maybe korriban), and there will be two levels: an outside one in which you try to get inside, and then an inside one in which you finish the level by duelling some kind of cultist or reborn.

 

I'll begin by doing a very simple pair of maps, and then gradually introduce more complex things to make them more interesting. This is probably not the best way to make a SP map, but might make things clearer. To follow this you should probably know how to make a box map properly and how to use the entity window.

 

If you think that I am saying something wrong or bad, let me know and I'll improve that bit. Any feedback or requests would be very welcome.

 

PART 1: TWO ROOMS ONE BOSS

 

I opened up Radiant and checked that I was in SP mapping mode by going to File->Project Settings (I was!).

 

wiZPWJZ.png

 

I made a quick boxmap and put a sky texture on the walls and ceiling, and sand on the ground. I then added a door frame to lead to the next map, and saved the map as map1.map.

 

7IO948B.png

 

As the sky I chose doesn't emit light, I added a couple of entity suns. An entity sun is just a spotlight (a light entity targeted at info_null entity) with the _sun key set to 1 and the _color key set (you can set the colour by hitting K with the entity selected). The second entity sun was meant to be for "shadows", so was a little bluer, not so bright, and in a different direction.

 

yj3cUIT.png

 

The map always needs a spawn point, so I added an info_player_start and set its angle so that it was facing towards the door.

 

6m3FmXg.png

 

To finish the outside map, I needed to make the door frame actually start the next map, so I created a brush to fill the doorframe textured with textures/system/trigger and turned it into a trigger_once. I ticked the playeronly box in the entity window so that only the player reaching the door frame activates the trigger.

 

j4ivZvC.png

 

I then created a target_level_change with the mapname key set to map2, and targeted the trigger_once at the target_level_change.

 

zrxK2cn.png

 

Finally, I compiled the map by doing one of the Bsp->Q3Map2: (test) compile options (doesn't matter which), and put the produced map1.bsp into a maps folder inside my base folder.

 

Z6RaDCg.png

 

I then created a new map and made another quick boxmap, this time with some more stone-type textures, saving the map as map2.map. Although there weren't any sources for light, I placed some light entities and made them flicker a bit by setting the style key to 1.

 

INvHkuU.png

 

The map again needed a spawn point, so I added an info_player_start with the keep_prev spawn flag set so that any damage from the previous level is carried over.

 

ki9HLgT.png

 

For the boss, I added an NPC_Cultist_Saber with the med spawnflag set so that it used medium style, and set its angle to face the player. The credits should run when the boss is killed, so I set its NPC_target to endscriptrunner - the NPC_target is used when the NPC dies.

 

Oai7Cww.png

 

To run the credits, I created a target_scriptrunner with its ​targetname set to endscriptrunner, its delay set to 1 second (so that the cultist's death is still shown), and its usescript set to kor2/theClosingCredits (this means that the script scripts/kor2/theClosingCredits.ibi is run, which is the script used in the base campaign to show the credits).

 

JrNTl8s.png

 

After compiling the map again and putting the map2.bsp in the maps folder, I opened up SP, typed map map1 into the console, and quickly played the maps:

 

https://www.youtube.com/watch?v=0m0DQLXlLag

 

 

NEXT TIME: haven't decided yet

 

 

Link to post

As people might be more likely to make SP maps if there's a more comprehensive guide, I'm going to try and do some kind of step-by-step making a SP map thing, hopefully covering all the stuff that's already been covered in the very good tutorials people have written, and maybe some other things too.

You sir, are awesome!

Lazarus likes this
Link to post

Although they're meant for JK2, Kengo's cutscene tutorials still apply http://web.archive.org/web/20091022065706/http://geocities.com/kengomaps/tutorials.html. In addition to the stuff in the JKHub tutorials section, lassev's site has some interesting stuff http://www.student.oulu.fi/~lvaarisk/sivut/resources.htm. I also looked at the scripting stuff from http://map-forge.net/wiki/doku.php?id=tutorials:index whilst doing this.

 
You can get BehavED and raven's JA example scripts from http://jkhub.org/files/file/1137-jedi-academy-sdk/ or if you're on a mac you can get IBIze and raven's JA example scripts from http://jkhub.org/files/file/1047-ibize-mac/
 
If you're using BehavED you *might* want to look at another scripting tutorial for a guide on how to use it.
 
I feel like I might be going a bit too quickly over stuff - any feedback is welcome.
 
PART 2: SILENT FILM
 
I decided to add a couple of short cutscenes rather than just spawning the player straight into the maps.
 
First, though, the player must have got to korriban somehow, so I added a Z-95 and a small landing pad.
 
KFB3VzQ.png
 
I could have made the Z-95 a misc_model (to get proper shadows) or a misc_model_static, but instead I decided to make it a func_static so that it would be easier to make it move in the future. The func_static consists of an origin brush (a small cube textured with textures/system/origin) and some clip brushes (textured with textures/system/physics_clip) to make the model solid. I found temporarily making a misc_model of the Z-95 was very helpful for working out where the origin brush should go and how to make the clip brushes.
 
uNfhc8L.png
 
After the brushes were turned into a (single) func_static, I set the model2 key to the Z-95 md3 model, and the modelangles key to 0 180 0 - this is same rotation as setting the angle key to 180 for a misc_model.
 
(sidenote: As func_statics don't cast shadows, I basically followed the http://jkhub.org/tutorials/article/179-high-resolution-shadow-trick/ to give the ship a shadow using a shadow rendered to a plane in blender. This wasn't really necessary though, but I can explain it if anyone wants I suppose.)
 
I decided to have the first cutscene just being the camera looking at the door for a few seconds. This meant writing a new ICARUS script, but a couple of things needed to be set up in the map first.
 
LgLTSr2.png
 
I created a ref_tag at the end of the map with the landing pad, and targeted it towards an info_null in the direction of the door. The location of the ref_tag would be used as the location of the camera, and the angles of the camera in the cutscene would be given by the direction of the line connecting the ref_tag and info_null. I set the targetname of the ref_tag to startcam.
 
c0SMlqY.png
 
In order to run the script that would play the cutscene, I added a target_scriptrunner, and set the usescript to map1/intro. I then targeted the info_player_start at the target_scriptrunner so that the script at scripts/map1/intro.ibi would be run as soon as the map started. With everything in the map done for now, I compiled it and put the bsp into my maps folder.
 
kLE09AB.png
 
I now needed to make that script! As I use a Mac I don't use BehavED and just write the scripts in a text editor, but you should probably write things in BehavED most of the time. If you do choose to write them in a text editor, you can still open them up in BehavED and compile them though. If you're on a Mac, you'll need to use IBIze in Terminal.
 
I created an empty text file called intro.icarus (intro.txt may be more sensible) and placed it in a folder called map1 inside the scripts folder in my base (you might need to create scripts). I typed this stuff in:
 

camera ( /*@CAMERA_COMMANDS*/ ENABLE );
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "startcam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "startcam", ANGLES)$, < 0.000 0.000 0.000 >, 0 );
wait ( 5000 );
camera ( /*@CAMERA_COMMANDS*/ DISABLE );
 
saved, and then compiled the script.
 
The ENABLE and DISABLE commands are pretty self-explanatory - they turn camera mode on and off. The MOVE command here moves the camera to the location given by the ref_tag with targetname set to startcam immediately (the 0 at the end means that it takes 0 ms). The PAN command immediately rotates the camera so that it has the same angles as the ref_tag with targetname set to startcam. The wait command means that camera mode stays on for 5000ms, i.e. 5 seconds.
 
I started up the game with map map1 to check that it worked. It did, but there's the slight problem that the player isn't drawn in cutscenes. 
 
Fortunately, there is an entity spawned by NPC_Player that has the same model and weapon as the player that you can use in cutscenes, so I opened the map up again.
 
I placed an NPC_Player entity a bit behind the info_player_start and set its droptofloor, cinematic and notsolid spawnflags. I then set its npc_targetname to fakeplayer so that the spawned NPC would have its targetname and script_targetname set to fakeplayer, and set its angle to match the player's.
 
IlGtMeK.png
 
I thought it would be interesting for the "player" to walk forwards a bit during the cutscene, so I created a waypoint_navgoal near the info_player_start for the NPC to walk to, and set its targetname to walkspawn.
 
ozDjZME.png
 
That was all the mapping needed, so I opened up intro.icarus again after doing a fresh compile of the map, and made a few changes:
 

camera ( /*@CAMERA_COMMANDS*/ ENABLE );
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "startcam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "startcam", ANGLES)$, < 0.000 0.000 0.000 >, 0 );
 
affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
set ( /*@SET_TYPES*/ "SET_ANIM_HOLDTIME_BOTH", 2000 );
wait ( 2000 );
set ( /*@SET_TYPES*/ "SET_WALKING", /*@BOOL_TYPES*/ "true" );
set ( /*@SET_TYPES*/ "SET_NAVGOAL", "walkspawn" );
}
 
wait ( 5000 );
 
remove ( "fakeplayer" );
 
camera ( /*@CAMERA_COMMANDS*/ DISABLE );
 
The new lines mean that the entity with targetname set to fakeplayer (the "player" NPC) has its animation set to BOTH_STAND9 for 2000ms (2 seconds). After these 2 seconds are up, the "player" NPC then walks towards the waypoint_navgoal with targetname set to walkspawn. Just before the camera is turned off, fakeplayer is removed so that the player doesn't have to worry about a doppelganger.
 
(note: although there are two different wait commands, one waiting 2 seconds and the other 5 seconds, only 5 seconds pass before the camera is turned off - this is because the commands in the fakeplayer affect block are being run on fakeplayer in parallel to the other commands (which are actually being run on the target_scriptrunner) and do not interrupt them.)
 
I compiled the script again, and tested the changes in-game.
 
It was now time to add an intro cutscene to the next map. That meant a couple of changes in Radiant.
 
I added an NPC_Player, placing it next to the info_player_start, and set its spawnflags and npc_targetname just as before. I added the spawnflag cinematic to the NPC_Cultist_Saber, and also gave it the npc_targetname bosscultist so that I could control it from a script.
 
jE22hV9.png
 
I created two ref_tag entities and targeted them at corresponding info_null entities - one from above where the info_player_spawn was facing towards the boss, and the other above where the boss was facing towards the info_player_spawn. I set their targetname keys to bosscam and playercam respectively.
 
fPgGD9c.png3kvGhw3.png
 
Finally, in order to run the script that would play the cutscene, I added another target_scriptrunner, with usescript set to map2/intro2. I then targeted the info_player_start at the target_scriptrunner so that the script at scripts/map2/intro2.ibi would be run as soon as the map started. I did a fresh compile of the map and went back to scripting.
 
d7703pf.png
 
Here's what I wrote:
 

camera ( /*@CAMERA_COMMANDS*/ ENABLE ); // OLD
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "bosscam", ORIGIN)$, 0 ); // OLD
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "bosscam", ANGLES)$, < 0.000 0.000 0.000 >, 0 ); // OLD
 
affect ( "bosscultist", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
wait ( 1000 );
set ( "SET_SABERACTIVE", "true" );
wait ( 1500 );
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_SHOWOFF_STRONG" );
}
 
affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" ); // OLD
set ( /*@SET_TYPES*/ "SET_ANIM_HOLDTIME_BOTH", -1 );
}
 
wait ( 3500 );
 
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "playercam", ORIGIN)$, 3000 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "playercam", ANGLES)$, < 0.000 0.000 0.000 >, 3000 );
 
wait ( 3000 );
 
remove ( "fakeplayer" ); // OLD
 
camera ( /*@CAMERA_COMMANDS*/ DISABLE ); // OLD
 
affect ( "bosscultist", FLUSH )
{
wait ( 200 );
set ( /*@SET_TYPES*/ "SET_BEHAVIOR_STATE", /*@BSTATE_STRINGS*/ "BS_DEFAULT" ); // 
set ( /*@SET_TYPES*/ "SET_LOOK_FOR_ENEMIES", "true" ); // not really necessary due to SET_ENEMY
set ( /*@SET_TYPES*/ "SET_ENEMY", "player" );
}
 
affect ( "player", FLUSH )
{
set ( "SET_SABERACTIVE", "true" );
}
 
What happens here is that the camera immediately moves to the position and angles of the ref_tag with targetname bosscam. fakeplayer has its animation set to BOTH_STAND9 for -1ms (that actually means forever, or at least until told otherwise). bosscultist is told to set its animation to BOTH_STAND9 for 1000ms, then turn on its lightsaber and after 1500ms do the taunt animation BOTH_SHOWOFF_STRONG. The script does nothing for 3500ms after issuing these commands. Note that the commands for bosscultist again run in parallel, so the time from start of script until end of wait is 3500ms and not 6000ms.
 
After this wait, the camera is moved to the position and angles of the ref_tag with targetname playercam. As the last number is 3000 rather than 0, this does not happen immediately, but the camera instead moves and rotates to its new position in 3000ms. The next wait command pauses the script until the camera move is finished.
 
Once the camera has been disabled and fakeplayer removed, the remaining commands turn on the players lightsaber, and make the cultist target the player. It is necessary to set the cultist's behaviour to BS_DEFAULT for it to fight because the cinematic spawnflag was set.
 
I finally compiled the script, and went back in game to check it all worked.
 
NEXT TIME: What would be useful?
Cerez, MagSul, Raz0r and 4 others like this
Link to post

Thank you for another incredibly thorough guide, redsaurus! I'll definitely be trying my hand at it over the weekend. I have to say as well that I really appreciate the time you've taken to write this all out, as I'm sure others do too.

 

 

 

NEXT TIME: What would be useful?

 

I'm sure there's lots more, but spur of the moment this is what comes to mind!

  • Dictating the player model/skin combination for the level.
  • Starting the level with no weapons. (Mine seems to still be holding single_1 when I check noweaps in info_player_start, though cannot use it.)
  • Force activated switches and levers.
  • Brushes that rotate in small segments or only once at specific angles rather than continuously.
  • Datapad objectives, creating them and updating as the level progresses.
Link to post

 

Datapad objectives, creating them and updating as the level progresses.

 

 

The amount of objectives is hard coded. They are simple to change, but ONLY change if you're not wanting any compatibility whatsoever with existing savegames as I read in code comments it changes savegame data. I suppose one could just edit the strings for the existing base JKA objectives though as an easier option. :)

therfiles likes this
Link to post

Yeah, datapad objectives either need code changes or overwriting base things, so I might not cover them immediately. I'll probably do them at the same time as setting the player model properly, the intro scrolling text and proper load screen briefings as they all really require the use of a mod folder. lassev's tutorial works fine for the player model stuff already: http://www.student.oulu.fi/~lvaarisk/tutorials/forcedplamodelv2.htm

Link to post

I love this tutorial. you set up. The fake player actually helped me with my SP camera thing. Could you explain one thing when you make more episodes and work outs, and thats how to add costum dialoge with the characters? I asume that is also thank to behaved.

Link to post

how to add costum dialoge with the characters? I asume that is also thank to behaved.

 

I guess you mean brand new dialogues and subtitles, or simply put in existing dialogues? Either way, basically you have to Affect that specific NPC_targetname, then select sound, and make sure to select CHAN_VOICE as the channel. Then the voice will play as long as the path is correct, and depending on the settings, specific subtitles will show up in cinematic or straight in-game (respectively, g_subtitles 1 and g_subtitles 2, if I'm not mistaken).

If you want to you can create brand new voice subtitles, and you can do that by creating a new .str file in strings/English (or any other language you may prefer). The important thing to remember here is to set up the correct name of the sound file, as shown here:

 

REFERENCE           <file name (without .wav extension, just the name)>

NOTES               "notes"

LANG_ENGLISH        "Subtitles."

 

Besides, the .str file should be named after the specific map the voices are played in.

Lazarus and redsaurus like this
Link to post

The amount of objectives is hard coded. They are simple to change, but ONLY change if you're not wanting any compatibility whatsoever with existing savegames as I read in code comments it changes savegame data. I suppose one could just edit the strings for the existing base JKA objectives though as an easier option. :)

Hmm, does it? Logically speaking, it shouldn't?

Link to post

Let's test out my 3dsMax ROFF2 exporter to add a complex vehicle flight path (or something) to also add a tutorial for scripting a ROF (you can find an old tutorial posted in my ROFF WIP thread... just send me one of your maps as an ASE file aliong with an origin brush inside it.

 

I'm planning on doing rof stuff in a bit, will let you know when I do. I might end up just using the Blender plugin by @@mrwonko as it means I don't have to reboot into Windows, though iirc your exporter also supports the effect and sound features of rof files?

Link to post

Sorry this one is so long.

 
Thanks to @@Ramikad for already covering how to do dialogue, I've mentioned it briefly anyway for completeness.
 
Any feedback is useful (especially bad :P )
 
PART 3: MOTION
 
I thought it would be more interesting if the player had to force push the door open to get to the second level, so I had a quick read up on rotating doors: http://www.student.oulu.fi/~lvaarisk/tutorials/rotadoor_index.htm (the tutorial is actually mirrored on jkhub too). It's a nice tutorial to go through anyway.
 
bVphe9e.png
 
Following lassev's tutorial, I created two (quite big) doors. For each door, I created an origin brush where the door would be rotated around, and then made the door and origin brush into a func_static. I set the f_push spawnflag on the two func_static entities so that they would be activated by force push. I set both doors' soundset key to stonedoor, and set the script_targetname keys to bigdoorl and bigdoorr respectively.
 
n4Wapek.png
 
To rotate the doors a new script would be required, so I created a target_scriptrunner with usescript set to map1/bigdoors. I targeted both of the door func_static entities at the target_scriptrunner, so that when they were force pushed the target_scriptrunner would run the script at scripts/map1/bigdoors.ibi.
 
sx7SfC7.png
 
As the default count value of a target_scriptrunner is 1, the script is only run once, so I didn't have to worry about it accidentally being run again once the doors were open.
 
Now I needed to make the script, so I created bigdoors.icarus and put it in scripts/map1 in my base folder. As the door is only meant to open once and I didn't need all of the stuff from lassev's tutorial it's a pretty short script:
 

affect ( "bigdoorr", FLUSH )
{
rotate ( < 0 -75 0 >, 2500 );
}
 
affect ( "bigdoorl", FLUSH )
{
rotate ( < 0 75 0 >, 2500 );
}
 
It just rotates both doors to the desired angle (±75 degrees) over a period of 2500ms. I compiled the script, did a fresh compile of the map, and tested it out.
 
It worked, but the crosshair force corona effect still showed even when I'd opened the doors. This is fine, especially if the door is going to be reusable, but I decided to fix this. I checked how some of the force stuff was done in kor1_sample.map and went back to Radiant.
 
I unset the f_push spawnflag on the func_static door entities, and also removed their target using Del Key/Pair.
 
af695bQ.png
 
To actually trigger the door opening, I created another func_static entity from a brush textured with textures/system/caulk_nonsolid. I set its f_push spawnflag and targeted it at the target_scriptrunner that would open the doors. I'd noticed that in kor1_sample.map some of the force usable things were set up so that only the player could activate them - that wasn't really necessary here, but I did it just to be safe and so set the npc_targetname key of the new func_static to player. I also set the script_targetname key to dummydoor so that it would be possible to remove the dummy func_static after the player had opened the doors, and stop the force crosshair effect from showing.
 
MPqAZ9F.png
7p8822R.png
 
That meant adding another couple of lines to bigdoors.icarus:
 

affect ( "bigdoorr", FLUSH )
{
rotate ( < 0 -75 0 >, 2500 );
}
 
affect ( "bigdoorl", FLUSH )
{
rotate ( < 0 75 0 >, 2500 );
}
 
affect ( "dummydoor", FLUSH )
{
remove ( "self" );
}
 
After compiling the script and doing a fresh compile of the map, I tested that it still worked.
 
(note: the remove ICARUS command only takes a targetname, *not* a script_targetname, as its argument, so remove( "dummydoor" ); wouldn't work unless the func_static had its targetname set to dummydoor. The affect command instead looks for a script_targetname. If an entity does not have a script_targetname set, then its script_targetname will be the same as its targetname)
 
There was still the slight problem that someone just using the map command to load map1 might not have any levels of force push, and so wouldn't be able to open the door. I changed the intro.icarus script in scripts/map1 to fix that by setting the player's force powers and spawn weapon.
 
That meant adding a few lines to the very top of the script:
 

affect ( "player", /*@AFFECT_TYPE*/ FLUSH )
{
set ( /*@SET_TYPES*/ "SET_SABER_THROW", /*@[member='Force']_LEVELS*/ "2" );
set ( /*@SET_TYPES*/ "SET_SABER_DEFENSE", /*@[member='Force']_LEVELS*/ "2" );
set ( /*@SET_TYPES*/ "SET_SABER_OFFENSE", /*@[member='SaberBlade83']_STYLES*/ "2" );
set ( /*@SET_TYPES*/ "SET_FORCE_HEAL_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_JUMP_LEVEL", /*@[member='Force']_LEVELS*/ "3" );
set ( /*@SET_TYPES*/ "SET_FORCE_SPEED_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_PUSH_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_PULL_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_MINDTRICK_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_GRIP_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_LIGHTNING_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_RAGE_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_PROTECT_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_ABSORB_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_DRAIN_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_SIGHT_LEVEL", /*@[member='Force']_LEVELS*/ "3" );
 
set ( /*@SET_TYPES*/ "SET_WEAPON", /*@[member='weaponx']_NAMES*/ "WP_NONE" ); // This means that the player spawns with no weapons in their hands
}
 
camera ( /*@CAMERA_COMMANDS*/ ENABLE );
etc...
 
recompiling the script, and of course testing.
 
Before moving onto the second level, I decided to make it so that the player's Z-95 actually landed in the cutscene at the start of map1. Fortunately I'd already made it a func_static, so I just needed to set its script_targetname to z95.
 
yP5MpS9.png
 
I also turned the z95 shadow plane into a func_static, consisting of an origin brush and the shadow brush, and set its script_targetname to z95_shadow.
 
JxEIdZR.png
 
It's probably most sensible to have your func_static placed where you want it to start from, but I'd placed it where I wanted it to finish, which meant there would be a little extra work.
 
So that it would be easier to write the script to move the Z-95, I placed a load of ref_tag entities. Firstly, one at the origin brush of the shadow with targetname set to shadow_end and another at the origin brush of the Z-95 with targetname set to z95_end. This would be then end destination for the Z-95 and its shadow. Next, I placed one a few hundred units above the Z-95's origin brush with targetname set to z95_hover - the Z-95 would descend and rotate from z95_hover to z95_end. Finally, I placed two more ref_tag entities offset from z95_hover and shadow_end by a few hundred units in the x direction, and gave them targetname z95_start and shadow_start respectively. The Z-95 would move between these ref_tag entities.
 
l74jWM2.png
 
Since I was placing ref_tag entities, I put in another with targetname set to z95cam that would be the position of the camera view following the Z-95. That was everything needed in terms of entities, so I did a fresh compile of the map and moved onto scripting again.
 
Here's what I wrote in intro.icarus:
 

affect ( "player", /*@AFFECT_TYPE*/ FLUSH )
{
set ( /*@SET_TYPES*/ "SET_SABER_THROW", /*@[member='Force']_LEVELS*/ "2" );
set ( /*@SET_TYPES*/ "SET_SABER_DEFENSE", /*@[member='Force']_LEVELS*/ "2" );
set ( /*@SET_TYPES*/ "SET_SABER_OFFENSE", /*@[member='SaberBlade83']_STYLES*/ "2" );
set ( /*@SET_TYPES*/ "SET_FORCE_HEAL_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_JUMP_LEVEL", /*@[member='Force']_LEVELS*/ "3" );
set ( /*@SET_TYPES*/ "SET_FORCE_SPEED_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_PUSH_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_PULL_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_MINDTRICK_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_GRIP_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_LIGHTNING_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_RAGE_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_PROTECT_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_ABSORB_LEVEL", /*@[member='Force']_LEVELS*/ "1" );
set ( /*@SET_TYPES*/ "SET_FORCE_DRAIN_LEVEL", /*@[member='Force']_LEVELS*/ "0" );
set ( /*@SET_TYPES*/ "SET_FORCE_SIGHT_LEVEL", /*@[member='Force']_LEVELS*/ "3" );
 
set ( /*@SET_TYPES*/ "SET_WEAPON", /*@[member='weaponx']_NAMES*/ "WP_NONE" ); // This means that the player spawns with no weapons in their hands
}
 
camera ( /*@CAMERA_COMMANDS*/ ENABLE );
 
affect ( "z95", FLUSH )
{
set ( "SET_CAMERA_GROUP", "ship" );
move ( $tag( "z95_start", ORIGIN)$, < 0 180 0 >, 0 );
wait ( 100 );
move ( $tag( "z95_hover", ORIGIN)$, < 0 180 0 >, 2000 );
wait ( 2000 );
move ( $tag( "z95_end", ORIGIN)$, < 0 0 0 >, 4500 );
}
 
affect ( "z95_shadow", FLUSH )
{
move ( $tag( "shadow_start", ORIGIN)$, < 0 180 0 >, 0 );
wait ( 100 );
move ( $tag( "shadow_end", ORIGIN)$, < 0 180 0 >, 2000 );
wait ( 2000 );
move ( $tag( "shadow_end", ORIGIN)$, < 0 0 0 >, 4500 );
}
 
affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_INVISIBLE", /*@BOOL_TYPES*/ "true" );
}
 
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "z95cam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ FOLLOW, "ship", 0, 0 );
 
wait ( 6000 );
 
camera ( /*@CAMERA_COMMANDS*/ FADE, < 0 0 0 >, 0, < 0 0 0 >, 1, 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ FOLLOW, "NULL", 0, 0 );
 
wait ( 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ FADE, < 0 0 0 >, 1, < 0 0 0 >, 0, 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "startcam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "startcam", ANGLES)$, < 0.000 0.000 0.000 >, 0 );
 
affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_INVISIBLE", /*@BOOL_TYPES*/ "false" );
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
set ( /*@SET_TYPES*/ "SET_ANIM_HOLDTIME_BOTH", 2000 );
wait ( 2000 );
set ( /*@SET_TYPES*/ "SET_WALKING", /*@BOOL_TYPES*/ "true" );
set ( /*@SET_TYPES*/ "SET_NAVGOAL", "walkspawn" );
}
 
wait ( 5000 );
 
remove ( "fakeplayer" );
 
camera ( /*@CAMERA_COMMANDS*/ DISABLE );
 
It's a bit more complicated now (or at least longer), so to split it up a little:
 

affect ( "z95", FLUSH )
{
set ( "SET_CAMERA_GROUP", "ship" );
move ( $tag( "z95_start", ORIGIN)$, < 0 180 0 >, 0 );
wait ( 100 );
move ( $tag( "z95_hover", ORIGIN)$, < 0 180 0 >, 2000 );
wait ( 2000 );
move ( $tag( "z95_end", ORIGIN)$, < 0 0 0 >, 4500 );
}
 
affect ( "z95_shadow", FLUSH )
{
move ( $tag( "shadow_start", ORIGIN)$, < 0 180 0 >, 0 );
wait ( 100 );
move ( $tag( "shadow_end", ORIGIN)$, < 0 180 0 >, 2000 );
wait ( 2000 );
move ( $tag( "shadow_end", ORIGIN)$, < 0 0 0 >, 4500 );
}
 
First, z95 has its SET_CAMERA_GROUP set to ship. Camera groups are collections of entities that the camera can follow as they move around. The move command specifies the final origin, the final angles and the time taken to move the entity. The z95 and z95_shadow are moved to their correct start positions immediately, z95_start and shadow_start respectively, and rotated 180 degrees from their rotation in the map file. After 100ms, they start moving to their next position over a period of 2000ms - the 100ms delay is just so that they actually do get moved to the start position, and wouldn't be necessary if you'd placed the ship sensibly in the first place. Finally, z95 rotates back to its rotation in the map file and lowers to z95_end, and z95_shadow rotates back to its rotation in the map file. I didn't use the rotate command here for z95_shadow as I found that if I did it rotated the other way to z95!
 

affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_INVISIBLE", /*@BOOL_TYPES*/ "true" );
}
 
I didn't want fakeplayer to show up until after the Z-95 had landed, so I made fakeplayer invisible. Remember that because of how affects work, this command is executed at the same time as z95 and z95_shadow moving to their start positions.
 

camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "z95cam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ FOLLOW, "ship", 0, 0 );
 
wait ( 6000 );
 
This command moves the camera origin to the ref_tag I'd placed with targetname z95cam, and the FOLLOW camera command makes the camera follow any entities in camera group ship. That means that it follows z95 as it moves. There's no need for a PAN command if you're using the FOLLOW command. The wait command gives the Z-95 and its shadow time to move.
 

camera ( /*@CAMERA_COMMANDS*/ FADE, < 0 0 0 >, 0, < 0 0 0 >, 1, 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ FOLLOW, "NULL", 0, 0 );
 
wait ( 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ FADE, < 0 0 0 >, 1, < 0 0 0 >, 0, 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "startcam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "startcam", ANGLES)$, < 0.000 0.000 0.000 >, 0 );
 
The first FADE camera command fades the screen to black: it changes the overlay colour from an initial RGB colour < 0 0 0 > (black) and alpha 0 (invisible!) to a final colour < 0 0 0 > (black) and alpha 1 (fully visible) over 1000ms. The RGB values are between 0 and 1. The new FOLLOW command stops the camera from following the entities camera group ship. After the first fade is complete, the camera fades back in from black at the camera location given by the ref_tag with targetname startcam.

affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_INVISIBLE", /*@BOOL_TYPES*/ "false" );
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
set ( /*@SET_TYPES*/ "SET_ANIM_HOLDTIME_BOTH", 2000 );
wait ( 2000 );
set ( /*@SET_TYPES*/ "SET_WALKING", /*@BOOL_TYPES*/ "true" );
set ( /*@SET_TYPES*/ "SET_NAVGOAL", "walkspawn" );
}
 
wait ( 5000 );
 
remove ( "fakeplayer" );
 
camera ( /*@CAMERA_COMMANDS*/ DISABLE );
 
The only change from last time is that fakeplayer has to be made visible again.
 
I compiled the script, and tested the map out again.
 
I thought that the room for the second level needed to be made a bit more visually interesting, so I added a lava pit round the edge. To stop the cultist from jumping in the lava too much, I placed a brush textured with textures/system/do_not_enter across the surface of the lava, and also put some as barriers around the edge of the pit. I did a fresh compile of the map, and moved onto scripting.
 
4Mhiuiy.png
 
I wanted the cultist to actually say something as it was taunting the player, and to zoom the camera in on the cultist a little. Here's what I wrote in intro2.icarus:
 

affect ( "player", /*@AFFECT_TYPE*/ FLUSH )
{
set ( /*@SET_TYPES*/ "SET_WEAPON", /*@[member='weaponx']_NAMES*/ "WP_SABER" );
}
 
camera ( /*@CAMERA_COMMANDS*/ ENABLE );
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "bosscam", ORIGIN)$, 0 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "bosscam", ANGLES)$, < 0.000 0.000 0.000 >, 0 );
 
affect ( "bosscultist", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
wait ( 3000 );
set ( "SET_SABERACTIVE", "true" );
wait ( 500 );
sound ( /*@CHANNELS*/ CHAN_VOICE, "sound/chars/cultist1/misc/victory3.mp3" );
wait ( 1000 );
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_SHOWOFF_STRONG" );
}
 
affect ( "fakeplayer", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
set ( /*@SET_TYPES*/ "SET_ANIM_HOLDTIME_BOTH", -1 );
}
 
wait ( 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ ZOOM, 40, 2000 );
 
wait ( 5500 );
 
camera ( /*@CAMERA_COMMANDS*/ ZOOM, 80, 1000 );
 
camera ( /*@CAMERA_COMMANDS*/ MOVE, $tag( "playercam", ORIGIN)$, 3000 );
camera ( /*@CAMERA_COMMANDS*/ PAN, $tag( "playercam", ANGLES)$, < 0.000 0.000 0.000 >, 3000 );
 
wait ( 3000 );
 
remove ( "fakeplayer" );
 
camera ( /*@CAMERA_COMMANDS*/ DISABLE );
 
affect ( "bosscultist", FLUSH )
{
wait ( 200 );
set ( /*@SET_TYPES*/ "SET_BEHAVIOR_STATE", /*@BSTATE_STRINGS*/ "BS_DEFAULT" ); // 
set ( /*@SET_TYPES*/ "SET_LOOK_FOR_ENEMIES", "true" ); // not really necessary due to SET_ENEMY
set ( /*@SET_TYPES*/ "SET_ENEMY", "player" );
}
 
affect ( "player", FLUSH )
{
set ( "SET_SABERACTIVE", "true" );
}
 
It's largely the same as before - I made sure that the player started with the lightsaber so that when the saber was activated at the end of the script, everything would work properly even if the player had entered the map with a different weapon active. To highlight the mst important changes:
 

affect ( "bosscultist", FLUSH )
{
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_STAND9" );
wait ( 3000 );
set ( "SET_SABERACTIVE", "true" );
wait ( 500 );
sound ( /*@CHANNELS*/ CHAN_VOICE, "sound/chars/cultist1/misc/victory3.mp3" );
wait ( 1000 );
set ( /*@SET_TYPES*/ "SET_ANIM_BOTH", /*@ANIM_NAMES*/ "BOTH_SHOWOFF_STRONG" );
}
 
The sound command with channel set to CHAN_VOICE means that bosscultist says sound/chars/cultist1/misc/victory3.mp3.
 

camera ( /*@CAMERA_COMMANDS*/ ZOOM, 40, 2000 );
 
wait ( 5500 );
 
camera ( /*@CAMERA_COMMANDS*/ ZOOM, 80, 1000 );
 
The ZOOM camera command changes the fov / zoom of the camera. The first command changes the fov to 40 over 2000ms. The second ZOOM command changes the fov back to 80 (which is the default) over 1000ms.
 
I compiled the script, and tested the map again in-game, but even though I had subtitles turned on for cinematics there weren't any!
 
To solve this, I created an empty text file called map2.str in the English folder in the strings folder in base (you may need to create some of these folders). For other languages you'll need to create map2.str in the corresponding language folder in strings. I edited map2.str so that it looked like:
 

VERSION             "1"
 
REFERENCE           VICTORY3
LANG_ENGLISH        "The galaxy will be ours."
 
ENDMARKER
 
When a sound is played in a script, the game looks for the REFERENCE in mapname.str (so map2.str) corresponding to the filename of the sound without a path or extension - so for sound/chars/cultist1/misc/victory3.mp3 that means victory3. It's not case sensitive, so don't worry about the capitalisation. This is why it is sensible to give more unique names to any sound files that are going to be subtitled.
 
I tested the map again, and the subtitles worked.
 
NEXT TIME: requests welcome, will probably take a bit longer to do and also be shorter  :P
Lazarus, Ramikad, Cerez and 3 others like this
Link to post

I'm planning on doing rof stuff in a bit, will let you know when I do. I might end up just using the Blender plugin by @@mrwonko as it means I don't have to reboot into Windows, though iirc your exporter also supports the effect and sound features of rof files?

My exporter supports sounds and EFX files... I do not think the blender version does that.  Plus I'd be willing to make/generate the ROFF for you-- all I need is your landscape or map in ASE format-- including the the origin brush starting point so I can you that as the origin of the animation.  I can export the ROF with sound and EFX embedded in so those will play when the ROF plays via scripting.  I don't think anyone has really tested it out in-game and I would like to do so...

Link to post

Maybe handy to show how to do  (or set) objectives in order to complete a mission rather walk from begin to end with npc's in the middle. I love btw these tutorials. It gives me more and more fun to work on my sp again ...  especially since you tackle some things I hadn't figured out yet.

Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...