Post by Sorzo on May 8, 2022 6:24:04 GMT -8
The following is a repost of a tutorial written by The Bigfoot on the old innomenpro Sunrider forums. As that site has been down for some time and will likely remain so indefinitely, with its contents now only accessible via web archives such as Wayback Machine, I have taken it upon myself to copy Bigfoot's excellent guide here for the sake of preservation and accessibility.
Please note that, unfortunately, the example files attached to the original post are no longer available.
EDIT: I reached out to Bigfoot on Discord and he was graciously willing to look for and send the old files he still had lying around. One is for dynamic combat animations in MoA, while the other is a larger modding framework for LD with full documentation. Both are linked below.
---
Tutorial: Annotated Story mod tutorial
As there is very little information on how to write a mod and incorporate it into the game, I thought I would write this to help anyone who wanted to try it out. This was written at the same time I coded the mod so it should be a good example of how to structure writing a mod as well as the actual coding.
It is written so that no Python needs to be known, but introduces some basic code that will be invaluable for story mods.
The mod we will make is about Kayto and Ava watching a news broadcast about the fall of Cera.
The finished mod will include:
1. Seamless inclusion of the mod into the game
2. A completely stupid plot
3. Kayto and Ava talking
4. Changing sprites and making backgrounds
5. A battle with a modified ship
6. A new minor character
7. A very important choice
8. A new planet!
9. A working store item
10. NO alterations to script or existing files in the game
Attached, there are some rpy files that could be useful. They are designed to enhance modifiability and increase mod compatibility. These non-standard functions are written in red. They are designed to work with 7.2 and using them will possibly break the mod when there are updates, but I will do my utmost to re-code them for each new version.
Ok...
Requirements
The Sunrider game is obvious and the only other requirement is a simple text editor (you can use almost any but I highly recommend Notepad++).
Setup
What is RenPy and how does modding work?
Renpy is the engine for Sunrider and for our purposes is a very flexible, fantastically complex interpreter. When it is run it scans all the rpy files in its directory and acts on them. As rpy files are simple txt files, editing them is very easy.
The majority of what you will write is written in RenPy language which is very similar to Python crossed with English. Even without knowing python, you can code a great deal.
Before we type anything, you need to know basic formatting. RenPy code is made out of blocks, these can be labels or code segments but a bock is always started with a colon and text in that block is indented 4 spaces. Text should not exist outside a block unless commented out.
Renpy is the engine for Sunrider and for our purposes is a very flexible, fantastically complex interpreter. When it is run it scans all the rpy files in its directory and acts on them. As rpy files are simple txt files, editing them is very easy.
The majority of what you will write is written in RenPy language which is very similar to Python crossed with English. Even without knowing python, you can code a great deal.
Before we type anything, you need to know basic formatting. RenPy code is made out of blocks, these can be labels or code segments but a bock is always started with a colon and text in that block is indented 4 spaces. Text should not exist outside a block unless commented out.
label Text:
This is a block
text is indented by 4 spaces
init:
An init block is run when Sunrider is launched or a save is loaded
python:
A python block is comprised entirely of python code
init python:
This python block is run at the same time as an init block but can only contain python code.
In addition, unless your text editor is set up to convert tabs, don’t use them as they will choke RenPy up.
To start, you need to navigate to the Sunrider directory and under game, create a txt file. Rename it as Newsmod.rpy (if you don’t have file extensions visible then open it and save as Newsmod.rpy and use the all files format).
Open it up and then select whatever program you want to modify it in.
Now we can start coding. It is best if you type in all the code in code blocks rather than copy/pasting from this tutorial as this will help you familiarize yourself with its structure and make it easier to write your own.
Basic mod building
Labels and Launching
label News: # The start of the news block
ava "Captain! The Alliance is broadcasting a report on the PACT advance and the attack on Cera!" # defined speaker
"Kayto" "They have to start mobilizing soon, put it on the main screen." # Named text
Right, there are four things to look at here.
1. label News: This is the start of the News block and all code that follows it and is indented belongs to that block. When the indent is removed then that block is ended.
2. ava "text" This is text spoken by Ava. ava is defined in the core game files, you will see how to define your own later.
3. "Kayto" "text" This is an alternative way to write text, the first doublequote is the speaker, the second is the text. To have no speaker, just use "text".
4. # Is a comment. Any text to the right of it is ignored by the program so it is useful for documentation of your own code or to remove some temporarily. This is the single exception to breaking indentation.
Now that we have a few lines of text, we can run the mod. Always do this when you try something new and try to follow the code at the same time as you test the mod.
The problem is we have no way to call our label. There are three native ways to integrate the mod into the game without modifying core files and these will come later but for now we will run it manually. There is an additional way to call mods added in the Functions zip.
At the start of the mod, add this:
init python: # init blocks run at the start of the game or on loading
config.developer = True # Enables the dev console
def Newsmod(): # Defines a function
renpy.jump("News") # Python version of jump
This init block will run when Sunrider is launched and enable the dev console, to access it, start a new game or load a save and press Shift + O. Then jump to the label News by typing Newsmod() into the console.
The Dev console is very useful as it lets you call functions, jump to points in the story and reload without closing and opening Sunrider. (Shift + R)
--------- Results Breakdown ---------
1. The mod showed us some text but no images
2. The mod kept its background image
3. After the mod ran through its lines it returned to Main Menu
There were no images because they need to be displayed by the show command (which we will cover next). By the same token, the background stayed the same. If you set a background and then jump to an original scene, that scene may not have had to change the background before so you need to do it yourself at the end of your label.
After the mod ran then it returned to Main Menu because after you jump to a label, the game reads down through the text, if it runs out then it returns to menu. This is very bad and you should ALWAYS direct code at the end of a label, even if the label you want is directly below in your file.
To move to a label, use jump lablename, or if your label was called, use return to bounce back to the label it was called from.
Now lets work on images...
Backgrounds and Sprites!
Adding Images
label News: # The start of the News Block.
show bg bridge # show background bridge
show ava uniform altneutral angry with fade: # with fade is how it enters
xpos 0.5 # X axis location for middle of screen
ava "Captain! The Alliance is broadcasting a report on the PACT advance and attack on Cera!" # defined speaker
kay "They have to start mobilizing soon, put it on the main screen." # kay is the shorthand for kayto speaking
"Ava turns to the screen" # With no speaker quotes, this is just placed on the screen.
show ava uniform altneutral angry with move: # moves rather than places, angry because she normally is!
xpos 0.3
ava "It looks like their showing our escape..."
return
The major difference between show and scene is that scene wipes all images on that layer, making it useful but not mandatory for backgrounds
show on the other hand just displays the image, making it perfect for sprites
Transformations are added to the image name to control how it appears, and the xpos is the x position on the screen (0 to 1). There are more ways to display an image but it is best to look them up in the RenPy documentation. Remember, showing an image uses a block for position code! That means you need a : and to indent the xpos code.
Show:
Syntax:
show imagename with transformation # Basic show image.
show imagename with transformation: # Lets you choose where on the x axis it is.
xpos x
scene imagename # Displays a scene, wipeing all previous images.
for example;
show ava uniform altneutral angry with fade:
xpos 0.5
-or-
scene bridge
Hide:
Hide imagename
#just like show, you can use transforms but you don't need to be so explicit as with show. hide ava will work just as well as hide ava uniform altneutral angry
##Transformations list##
with fade: - Fades in or out
with dissolve: - Dissolves in or out
with pixellate: - pixilates for .5 seconds
with move: moves existing sprite
More at http://www.renpy.org/doc/html/transitions.html#transitions
To find character sprites, look in Sunrider/Game/Character and in most cases just copy the filename and remove the underscores, most of them are already defined.
If you want to change ava from angry to happy, just show her with the new sprite and it will change her, there’s no need to hide her first.
---------Breakdown---------
show is a very easy keyword, just remember that if you are moving labels you may need to change a background before you do!
---------Breakdown---------
Now lets get into the meat of a story mod... Choices
Story-changing choices!
There is nothing more thrilling than life or death choices, they are the meat of a good mod, its just a shame you arn't writing one!
Ok so follow the drill and edit your code to...:
Well... That was just thrilling. Controlling trivial things like that makes this feel almost like a stat management game! Fortunately after we go through the code we can move on to something genuinely exciting.
You can use menu: either as the start of a menu block or as an actual label. The difference is that you can jump directly to the choice if you make it a label. This helps if you want to show or hide options by variable.
This creates compact code that is easy to read and options which cannot be reused.
Ok so follow the drill and edit your code to...:
ava "It looks like their showing our escape..."
menu: # Menu block, everything is indented.
ava "Do you want any popcorn?" # Is not a new block so displays normally
"Yes": # [u]Is[/u] a new block so this is a choice
kay "Watching us almost get killed does make me hungry..." # Indented
jump Popcorn # Jump to label Popcorn
"No":
kay "No thanks, just a diet coke."
jump Nopopcorn
label Popcorn:
"Ava passes you the popcorn and you settle into your seats as the lights dim"
jump setupmissionnews
label Nopopcorn:
"Ava shrugs and passes you the bottle. You had better watch those expensive command consoles!"
"As you settle back into your chairs, the lights start to dim in preparation"
jump setupmissionnews
Well... That was just thrilling. Controlling trivial things like that makes this feel almost like a stat management game! Fortunately after we go through the code we can move on to something genuinely exciting.
You can use menu: either as the start of a menu block or as an actual label. The difference is that you can jump directly to the choice if you make it a label. This helps if you want to show or hide options by variable.
# For this more complex example, we will improve code already in the game
# From the end of beach_episode where you choose who to talk to.
# The game currently has a choice for all the girls and reduces a counter before rewriting the menu for each of them.
# This means wasted code and people can be picked twice.
# This menu will use variables to track who has been spoken to and remove them from the choices.
#Original code:
$ beachtalk = 3
menu:
"Asaga":
jump beachasaga
"Chigara":
jump beachchigara
"Ava":
jump beachava
"Icari and Kryska":
jump beachicarikryska
"Sola":
jump beachsola
"Claude":
jump beachclaude
label beachasaga:
$ beachtalk -= 1
$ affection_asaga += 1
#asaga words
#retyped menu
Instead we will do
$ ava, asa, chi, ica, sol, cla = 0,0,0,0,0 # defines all variables at 0
$ bt = 3
menu beachmenu:
kay "(Now, what should I do?)"
"Talk to Asaga" if asa == 0 and bt > 0: # if asa equals 0 and bt is greater than 0 return true.
$asa = 1 # disables this choice.
$beachtalk -= 1 # beachtalk = beachtalk -1
jump beachasaga
"Help Ava with the Barbecue" if beachtalk == 0:
jump afterbeachtalk
label beachasaga
text
jump beachmenu
# This creates a loop that sends you to the label for the character you pick, In this case Asaga and then disables the choice.
# After the beach talk, you are bounced back to the menu.
# When you have spoken to three people then you are given only one choice, to help Ava and the story continues.
This creates compact code that is easy to read and options which cannot be reused.
Setting Variables
Ok, we are near the end of the Basic section, the last thing that is important to a story mod is setting variables.
Variables are vital to track story paths, manipulate menus and pretty much everything interactive. This will not use code for the module but it is still important to learn.
Setting a variable:
Variables are trivial to set and easy to use, as seen in the menu:
That’s all there is to it. Its very simple and with use it will become natural. The only thing to remember is that when python code is used outside a python block, you need to add a $ before it. like $ E = T * X
Now take a break and let it all soak in. If you were observant you might have realized after the menu, we are programming a battle!
Variables are vital to track story paths, manipulate menus and pretty much everything interactive. This will not use code for the module but it is still important to learn.
Setting a variable:
#There are a number of different variable types, python sorts them for you so all you need to do is supply the information.
# To assign a variable, use name = value.
# Values can be text, numbers, lists of values and a number of advanced things like tuples and dictionaries or other values.
The value type is determined by how the value is added, for example
String = "Hello!" # a string is a string of characters, either letters or numbers. they always have quotation marks to distinguish them from variables
integer = 1 # an integer is a whole number, it has no special container characters
float = 1.1 # as integer but not limited to a whole number.
list = [value1, value2] # a list of values
variable = T # variable becomes the value of T
You can use almost any name for a value such as chi or bt in the previous section.
Variables are trivial to set and easy to use, as seen in the menu:
T = 1
If T == 1: # if true T is equal to 1, then:
T += 1 # T is equal to T + 1 (T becomes 2)
#This was used in the menu example to make choices appear and disappear. They do however need to be set before they can be mentioned in the code, otherwise there is an error.
#When you use variables, python sees the content so if you say:
X = 5
T = 2
E = T * X
#python sees
X = 5
T = 2
E = 5 * 2
That’s all there is to it. Its very simple and with use it will become natural. The only thing to remember is that when python code is used outside a python block, you need to add a $ before it. like $ E = T * X
Now take a break and let it all soak in. If you were observant you might have realized after the menu, we are programming a battle!
Battle Making
Starting a battle!
Does it feel good to be back to your crummy little mod? I hope so because its about to get awesome!
Battles at first seem quite complex so I have split it into three parts. First of all, lets look at the setup code:
Well that was short and disappointing!
Effectively it hides the message bar and sets BM.mission to 'news'. This is important because of how battles work and you should always be careful how you name your missions
Then a label called missionnewsinit is called, call means the label. is jumped to and when it is returned it returns to the point it was called.
Then we jump to the battle_start label, this isn’t one we set so we just leave it alone.
Note: If you use a string for the battle ID ($BM.mission = 'whatever') then the formation phase is skipped. If you want your mission to have formation placing then use an integer that is above 12 like ($BM.mission = 67).
The next segment is better I promise.
Battles at first seem quite complex so I have split it into three parts. First of all, lets look at the setup code:
jump setupmissionnews
label setupmissionnews:
window hide # Hides message bar
$ BM.mission = 'news' # tells Battle manager mission name
call missionnewsinit # sets mission variables
jump battle_start # starts the battle
Well that was short and disappointing!
Effectively it hides the message bar and sets BM.mission to 'news'. This is important because of how battles work and you should always be careful how you name your missions
Then a label called missionnewsinit is called, call means the label. is jumped to and when it is returned it returns to the point it was called.
Then we jump to the battle_start label, this isn’t one we set so we just leave it alone.
Note: If you use a string for the battle ID ($BM.mission = 'whatever') then the formation phase is skipped. If you want your mission to have formation placing then use an integer that is above 12 like ($BM.mission = 67).
The next segment is better I promise.
The Shipyard
In the missionxinit section we set up the enemy ships, place our own on the map and make any cover or set music. Making your ships placeable will come later.
Here is the code, remember don’t be lazy!
Ok then, lets work through this.
The label moves immediately into a python block, but that’s not really important here, lets break it up.
The first three commands are nothing of import, you can just use them time and again without needing to change them.
The second set are placing your own ships. You are running a python function embedded within your ship objects (don’t worry if you don’t understand, its not important right now) and telling it to use the co-ordinates you give it.
sunrider.set_location(1, 1) will set the sunrider to the top left corner.
The ship location doesn’t change when the battle is won so even if the ship is commented out, if you have already met it, it will spawn there. To not use the ship in a battle, set location to None (no quotes, this is a keyword).
For a better way to remove the ship from battle, then remove it from player_ships and BM.ships. This will be dealt with later.
Code: [Select]
Again these two are inconsequential, just where the battlemap view starts.
The first line in the code tells the game to make and place a MissileFrigate at 13, 5. Its very easy to use and in the second file attached, there is a list of names you can use for it.
create_ship() is the function name, it is called by having brackets on the end.
MissileFrigate() is the ship object, the brackets are important. You can find a list in the library rpy.
[PactFrigateMissile()] # This is a list of weapons that are assigned to the ship. If not used then it uses whatever default is assigned to it. Again the () is important.
The second block is making another ship but modifying its attributes. It uses enemy_ships[-1] to access the last ship in that list (this is the last as it was most recently created and all enemy ships go in this list).
These commands make cover as found when you first meet Sola.
Code: [Select]
The last commands set the music on each turn and the return command bounces us back to setup as this label was started by call lablename. Just note that they intentionally out of line from the python block to show that the python code needs to have $ in front.
Now we have only one more segment to cover and we can start the battle!
Here is the code, remember don’t be lazy!
jump battle_start # starts the battle
label missionnewsinit:
python: # /
zoomlevel = 1 #| This code is used in every mission init
enemy_ships = [] #| It just cleans the battlefield
destroyed_ships = [] # \
sunrider.set_location(1,1) # Comment out ships that you haven’t met yet!
#blackjack.set_location(2,2) # These are coordinates to place the ship
#liberty.set_location(3,3)
#phoenix.location = None # Set location to None to use in battle, there is a better way to do this but its simple for now
#bianca.set_location(5,5)
#seraphim.set_location(6,6)
#paladin.set_location(7,7)
BM.xadj.value = 872 # set battle view, doesn’t really need to be changed ever
BM.yadj.value = 370
create_ship(MissileFrigate(),(13,5)) # This creates a ship with default armament
create_ship(MissileFrigate(),(15,5),[PactFrigateMissile()]) # Creates a modifed ship
enemy_ships[-1].max_en=80
enemy_ships[-1].boss = False
enemy_ships[-1].Faction ='PACT'
enemy_ships[-1].name = 'Damaged Missile Frigate'
enemy_ships[-1].max_hp = 100
enemy_ships[-1].hp = 30
create_cover((8,4)) # create cover
create_cover((1,1))
$ PlayerTurnMusic = "music/Titan.ogg" # set music
$ EnemyTurnMusic = "music/Dusty_Universe.ogg"
return #goes back to setupmissionnews where it was called
Ok then, lets work through this.
The label moves immediately into a python block, but that’s not really important here, lets break it up.
zoomlevel = 1 #| This code is used in every mission init
enemy_ships = [] #| It just cleans the battlefield
destroyed_ships = [] # \
The first three commands are nothing of import, you can just use them time and again without needing to change them.
sunrider.set_location(1,1) # Comment out ships that you haven’t met yet!
#blackjack.set_location(2,2) # These are coordinates to place the ship
#liberty.set_location(3,3)
#phoenix.set_location(4,4) # Set location to None to not use in battle
#bianca.set_location(5,5)
#seraphim.set_location(6,6)
#paladin.set_location(7,7)
The second set are placing your own ships. You are running a python function embedded within your ship objects (don’t worry if you don’t understand, its not important right now) and telling it to use the co-ordinates you give it.
sunrider.set_location(1, 1) will set the sunrider to the top left corner.
The ship location doesn’t change when the battle is won so even if the ship is commented out, if you have already met it, it will spawn there. To not use the ship in a battle, set location to None (no quotes, this is a keyword).
For a better way to remove the ship from battle, then remove it from player_ships and BM.ships. This will be dealt with later.
Code: [Select]
BM.xadj.value = 872 # set battle view, doesn’t really need to be changed ever
BM.yadj.value = 370
Again these two are inconsequential, just where the battlemap view starts.
create_ship(MissileFrigate(),(13,5)) # This creates a ship with default armament
create_ship(MissileFrigate(),(13,5),[PactFrigateMissile()]) # modify a ship
enemy_ships[-1].max_en=100
enemy_ships[-1].boss = False
enemy_ships[-1].Faction ='PACT'
enemy_ships[-1].name = 'Damaged Missile Frigate'
enemy_ships[-1].max_hp = 100
enemy_ships[-1].hp = 30
The first line in the code tells the game to make and place a MissileFrigate at 13, 5. Its very easy to use and in the second file attached, there is a list of names you can use for it.
create_ship() is the function name, it is called by having brackets on the end.
MissileFrigate() is the ship object, the brackets are important. You can find a list in the library rpy.
[PactFrigateMissile()] # This is a list of weapons that are assigned to the ship. If not used then it uses whatever default is assigned to it. Again the () is important.
The second block is making another ship but modifying its attributes. It uses enemy_ships[-1] to access the last ship in that list (this is the last as it was most recently created and all enemy ships go in this list).
create_cover((8,4)) # create cover at 8, 4
create_cover((5,4))
These commands make cover as found when you first meet Sola.
Code: [Select]
$ PlayerTurnMusic = "music/Titan.ogg" # set music
$ EnemyTurnMusic = "music/Dusty_Universe.ogg"
return #goes back to setupmissionnews where it was called
The last commands set the music on each turn and the return command bounces us back to setup as this label was started by call lablename. Just note that they intentionally out of line from the python block to show that the python code needs to have $ in front.
Now we have only one more segment to cover and we can start the battle!
Begin the Fight!
label missionnews: # This is the actual mission, very simple
$ BM.battle() # Calls your turn and waits for a response
if BM.battlemode == True: #When set to false battle ends
jump missionnews #Loop Back
else:
jump after_missionnews
Ok first note that missionnews was not called by us. This is why we need to be consistent with the naming on our labels, otherwise BAD THINGS happen.
This is just a basic loop, whenever an action occurs, it runs and if the battle is over, it jumps to after_missionnews, otherwise it loops again.
If you want to script the battle, then enter it between the $BM.battle() and if BM.battlemode == True: lines. This will be dealt with in depth later. For now, we have very little interaction with this code.
The final thing to note is the jump if battlemode isn’t True, that is where you go after the battle. The victory screen comes before then.
Now just add this to the end of the code...
label after_missionnews:
"Newsperson" "So at least some of the Cera military are known to have survived the PACT assault"
kay "So... At least someone in Cera may know we survived"
ava "But PACT are more than likely to come after us now we're in the public view."
jump dispatch # shipmap
And you are ready to launch your mod!
Do so now to check it all works, have a break and then head on to Integrating the Mod. If it doesn't work then just check the code against mine and try to find any spelling mistakes. The error screen on RenPy is quite good at pointing out where the problems are.
Complete Battle Code
label setupmissionnews:
window hide
$ BM.mission = 'news'
call missionnewsinit
jump battle_start
label missionnewsinit:
python: # /
zoomlevel = 1 #| This code is used in every mission init
enemy_ships = [] #|
destroyed_ships = [] # \
sunrider.set_location(1,1) #comment out ships that you haven't met yet
#blackjack.set_location(2,2) # These are coordinates to place the ship
#liberty.set_location(3,3)
#phoenix.set_location(4,4) #set location to None to not use in battle
#bianca.set_location(5,5)
#seraphim.set_location(6,6)
#paladin.set_location(7,7)
BM.xadj.value = 872 # set battle view
BM.yadj.value = 370
create_ship(MissileFrigate(),(13,5)) # create a ship
create_ship(MissileFrigate(),(15,5),[PactFrigateMissile()]) # modify a ship
enemy_ships[-1].max_en=80
enemy_ships[-1].boss = False
enemy_ships[-1].Faction ='PACT'
enemy_ships[-1].name = 'Damaged Missile Frigate'
enemy_ships[-1].max_hp = 100
enemy_ships[-1].hp = 30
create_cover((8,4)) # create cover
create_cover((5,4))
$ PlayerTurnMusic = "music/Titan.ogg" # set music
$ EnemyTurnMusic = "music/Dusty_Universe.ogg"
return #goes back to setupmissionnews where it was called
label missionnews: # This is the actual mission, very simple
$ BM.battle() #Continues battle
if BM.battlemode == True: #When set to false battle ends
jump missionnews #Loop Back
else:
jump after_missionnews
Hooking into the game.
How?
We already know to get to a label you use a jump command. However it is a bad idea to alter the core files as these will be wiped whenever the game is updated and could clash with other mods.
Without being able to alter the core scripts, you need to look at init blocks.
These aren’t called by the script but automatically run whenever the game launches so you can use them to sidestep into the game through one of three (four) ways.
1. You can Hijack a label
2. You can make your own Planet.
3. You can make an item in the Store.
4. You can add a talk button to the Sunrider map.
Hijacking a label. is good for minor inserts or edits to the story, it is useful to redirect a jump or make text/code changes. This is entirely seamless but has the potential to cause mod conflicts (although the chances are low and there are methods to make this very unlikely.
Making your own planet is quite simple and although there is a lot of code, especially if you use it to show a transit screen, I have made a rpy file that lets you just plug in names of labels and map images that is suited to most cutscenes. The downside to using only this method is a planet will appear on the galaxy map and the player will have no idea why, or even miss it entirely.
Making an item for the store is a nice blend of the two but it is flawed as a method of including your own content. It is extremely adaptive as when someone buys an item you can trigger any number of functions, variable changes and jump to labels but it is also hard for the player to see what will happen and hard to alert them to it. It is best used as an enabler such as setting a talk on the ship map to active or revealing a planet on the galaxy map. This may require a lot of python (if you make your own events) or as little as setting a variable.
Adding a chat button to the ship map is the most unobtrusive method of adding mod content but it also can be hard to 'aim' and requires basic understanding of python. This should really be used as a do-once option rather than a more persistent planet.
So for pure story edits, use hijacks. For additions to the story, a combination of hijacks, store items, planets and chat buttons works very well.
In the following segments, there are examples of each method, however they will need to be adjusted to fit your mod. Everything you need to change is something you will have seen. Try and get each method to work and then combine them
so you use all three (or four) at once.
If you cant do it then have a look at the finished mod code at the end of the post and see if you can find how it was done, then try and do it without copy-pasting. Feel free to ask questions as I am sure many people on the forum will be happy to help.
Without being able to alter the core scripts, you need to look at init blocks.
These aren’t called by the script but automatically run whenever the game launches so you can use them to sidestep into the game through one of three (four) ways.
1. You can Hijack a label
2. You can make your own Planet.
3. You can make an item in the Store.
4. You can add a talk button to the Sunrider map.
Hijacking a label. is good for minor inserts or edits to the story, it is useful to redirect a jump or make text/code changes. This is entirely seamless but has the potential to cause mod conflicts (although the chances are low and there are methods to make this very unlikely.
Making your own planet is quite simple and although there is a lot of code, especially if you use it to show a transit screen, I have made a rpy file that lets you just plug in names of labels and map images that is suited to most cutscenes. The downside to using only this method is a planet will appear on the galaxy map and the player will have no idea why, or even miss it entirely.
Making an item for the store is a nice blend of the two but it is flawed as a method of including your own content. It is extremely adaptive as when someone buys an item you can trigger any number of functions, variable changes and jump to labels but it is also hard for the player to see what will happen and hard to alert them to it. It is best used as an enabler such as setting a talk on the ship map to active or revealing a planet on the galaxy map. This may require a lot of python (if you make your own events) or as little as setting a variable.
Adding a chat button to the ship map is the most unobtrusive method of adding mod content but it also can be hard to 'aim' and requires basic understanding of python. This should really be used as a do-once option rather than a more persistent planet.
So for pure story edits, use hijacks. For additions to the story, a combination of hijacks, store items, planets and chat buttons works very well.
In the following segments, there are examples of each method, however they will need to be adjusted to fit your mod. Everything you need to change is something you will have seen. Try and get each method to work and then combine them
so you use all three (or four) at once.
If you cant do it then have a look at the finished mod code at the end of the post and see if you can find how it was done, then try and do it without copy-pasting. Feel free to ask questions as I am sure many people on the forum will be happy to help.
Setting Attributes
Before going further, attributes will need to be covered.
Unfortunately as init blocks are ran as the game starts, they will overwrite standard variables, therefore resetting your mod. Object Attributes however are a more substantial type of variable. They are available throughout the mod (even in functions without being declared global) and are easy to check for.
Attributes can be any of the variable types including functions and are referenced by object.attribute. These are what we were tweaking in the battle init section.
RenPy games natively have a store object so assigning variables to that is a convenient thing to do.
You would add it like this.
Of course what variables and objects you need to store depends on how you intend to implement the mod and variables.
Unfortunately as init blocks are ran as the game starts, they will overwrite standard variables, therefore resetting your mod. Object Attributes however are a more substantial type of variable. They are available throughout the mod (even in functions without being declared global) and are easy to check for.
Attributes can be any of the variable types including functions and are referenced by object.attribute. These are what we were tweaking in the battle init section.
RenPy games natively have a store object so assigning variables to that is a convenient thing to do.
You would add it like this.
python init:
if not hasattr(store,'variablename'): # This triggers if the attribute has not been set
store.variablename = value # sets the attribute
#This space can also be used to create objects like planets or store items
Of course what variables and objects you need to store depends on how you intend to implement the mod and variables.
Label Hijacking
You can use a RenPy config code to replace a label with your own like this.
Its exactly that simple.
Example:
init python:
config.label_overrides['original label'] = 'your label'
Its exactly that simple.
Example:
init python:
config.label_overrides['checkformissions'] = 'checkformissionsmod'
label checkformissions:
#text
kay "Well, more money for us. I can't complain about smashing up pirates for some quick credits. What about the other one?"
ava "The last mission could be more complicated."
ava "The Union has lost a number of transport ships recently, they were long-range tug ships that were intended to move small asteroids and carry them through warp jumps. These ships, had an advanced homing beacon on-board and have been tracked to deep space, where they appear to be moving under engines only."
# text
$ knowaboutmod = True
jump afterbeachcarry
Planetary Construction
A planet is simply an object with certain attributes, much like a ship. If it is detected when the galaxy map opens, it is placed on there and when clicked it jumps to a label.
This can be anything from a text label. to a mission selection or briefing.
MODPLANET is the name of the planet on the galaxy map
"News" is the label. it loads on clicking
x + y are coordinates for the map
"condition is If true, planet is visible"
Put simply, "condition" checks everything in the quotes, and if it returns True, the planet is displayed.
This can be anything from a text label. to a mission selection or briefing.
init python:
Planet("MODPLANET", "News", x, y, "condition")
MODPLANET is the name of the planet on the galaxy map
"News" is the label. it loads on clicking
x + y are coordinates for the map
"condition is If true, planet is visible"
Put simply, "condition" checks everything in the quotes, and if it returns True, the planet is displayed.
Example Code:
Knowmod = 1
TalkedAva = True
TalkedChigara = False
Planet("EXAMPLE", "Label", x, y, "Knowmod == 1") # if Knowmod is 1, planet is visible.
Planet("EXAMPLE", "Label", x, y, "TalkedAva== True") # if TalkedAva is True, planet is visible.
Planet("EXAMPLE", "Label", x, y, "True") # Planet is always visible
Planet("EXAMPLE", "Label", x, y, "TalkedAva == True and TalkedChigara == True") # Because TalkedChigara is False, even though TalkedAva is True, planet is not useable.
#You can use the variable name on its own if it is set to True
Planet("EXAMPLE", "Label", x, y, "TalkedAva and TalkedChigara") # is the same as the previous example.
At the moment this is quite unimpressive, clicking the planet just sends you to the News label and starts the mod.
I have created a quick method of making the sunrider jump screen but if you don't want to use the rpy files (although this one will not break with updates), you can still set it manually.
If you want to have missions spawning there then instead of directing it straight to News, do the following code:
(Manual)
Mission Selection Screen Code:
init python: # create the planet
Planet("ModPlanet", "Modplanetwarp", 1370, 450, "True")
# Planet("PLANET NAME", "LABEL", X, Y, "CONDITION")
label Modplanetwarp: # This is the label in the planet object. RENAME THIS
$ map_back = "Modplanetback" #plug in the label to go back (below)
if modmission == 0: # set a variable if you want the mission(s) to be available, you can have three missions visible at once but it dosen't line up well.
$ galaxymission1 = True
$ mission1 = "Newswarpto" #Mission Label (Where you want to go if its clicked)
$ mission1_name = "Memory: Escape from Cera."
else: # If the variable isent matched then these missions are used (all set to false atm)
$ galaxymission1 = False
$ mission1 = None
$ mission1_name = None
jump showmodplanet
There are a number of things here that you need to replace to let this work for your own mod.
1. Use your own label name
2. Use your own map_back (explained in the next code block)
3. Use your own mission variable
4. Set your mission name and mission label
Show Planet Code:
#This is triggered when you click the planet on the galaxy map
label showmodplanet:
scene bg black
show galaxymap:
zoom 1 alpha 1
parallel:
ease 0.5 alpha 0
parallel:
ease 1 xpos -13710 ypos -5190 zoom 10
show "Map/Farport.jpg": # *** This is the planet image.
zoom 0.0268041237113402
xpos 1071 ypos 519 alpha 0
parallel:
ease 1.1 alpha 1
parallel:
ease 1 zoom 1 xpos 0 ypos -430
pause 1
show "Map/farport_info.png": #*** This is the infobox (It can be skipped if you want)
xpos 1098 ypos 200
call screen map_travelto
with dissolve
label planetback: # this is used to return from the planet map to the galaxy map
hide "Map/farport_info.png" # *** This is the infobox
scene bg black
show "Map/Farport.jpg": # *** This is the planet image.
zoom 1
xpos 1370 ypos 450 alpha 1
parallel:
ease 0.5 alpha 0
parallel:
ease 1 zoom 0.0268041237113402 xpos 1490 ypos 725
show galaxymap:
xpos -14900 ypos -7250 zoom 10 alpha 0
parallel:
ease 1.1 alpha 1
parallel:
ease 1 xpos 0 ypos 0 zoom 1
pause 1
call screen galaxymap_buttons
Right then... With this you need your own custom labelnames and you need to replace the planet image and the infobox image. These are marked in the code block with ***
Other than that its very simple and can largely be left alone.
Warpout Code:
With this, all you need to do is replace the label with the one for your planet, replace the planet image (again marked by ***) and replace the jump at the end with your mod's label.
And its done, the Sunrider now warps to the planet when the mission is clicked and you can start your scene.
This is quite substantial code and it is quite bulky to put in a mod, especially if you have more than one planet to deal with. Fortunately a lot of the code is just left alone and dosen't change between planets.
In the Functions.zip there is a Rpy called Transit that lets you do all of this with under 20 lines of code, not including planet definition.
To use this:
1. Create one label per planet defining its image and infobox (modbg2 and modinfo2)
2. Create one label per mission setting modjumplabel to its label
3. Set mission variables
The other bits of code are tucked away in transit.rpy
label Newswarpto:
$ Random = renpy.random.randint(1,9) # sets random integer for below text.
if Random == 1:
scene space back1
if Random == 2:
scene space back2
if Random == 3:
scene space back3
if Random == 4:
scene space back4
if Random == 5:
scene space back5
if Random == 6:
scene space back6
if Random == 7:
scene space back7
if Random == 8:
scene space back8
if Random == 9:
scene space back9
show sunrider_warpout_standard:
xpos 700 ypos 350
with dissolve
pause 1.0
play sound "Sound/large_warpout.ogg"
show sunrider_warpout_standard_flash:
xpos 426 ypos 0 alpha 0
linear 0.1 alpha 1
linear 0.1 alpha 0
show sunrider_warpout_standard out:
xpos 700 ypos 350
ease 0.2 xpos 200 ypos 300 zoom 0
pause 1.0
show "Map/Farport.jpg": # *** Insert planet image here
ypos 0
ease 1.5 ypos -120
with dissolve
pause 1
show sunrider_warpout_standard out:
xpos 2300 ypos 1200 zoom 2
ease 0.2 xpos 1000 ypos 500 zoom 0.5
pause 0.2
play sound "Sound/large_warpout.ogg"
show cg_legionwarpin_missilefrigate_warpflash:
zoom 1.5 xpos 1550 ypos 750
show sunrider_warpout_standard
pause 2.0
jump News
With this, all you need to do is replace the label with the one for your planet, replace the planet image (again marked by ***) and replace the jump at the end with your mod's label.
And its done, the Sunrider now warps to the planet when the mission is clicked and you can start your scene.
This is quite substantial code and it is quite bulky to put in a mod, especially if you have more than one planet to deal with. Fortunately a lot of the code is just left alone and dosen't change between planets.
In the Functions.zip there is a Rpy called Transit that lets you do all of this with under 20 lines of code, not including planet definition.
#The select mission part is very similar to the previous code.
Planet("ModPlanet", "Modplanetwarp", 1370, 450, "True")
# Planet("PLANET NAME", "LABEL", X, Y, "CONDITION")
label Modplanetwarp: # Have one of these for each planet
image modbg2 = "Map/Farport.jpg"
image modinfo2 = "Map/farport_info.png"
label missionselectionlabel: #Make a custom label for your planet
$ map_back = "planetback" # No need to touch this
if modmission == 0:
$ galaxymission1 = True
$ mission1 = "Missionselected" #Label to jump to for mission, make one per mission.
$ mission1_name = "Memory: Escape from Cera." #mission name
else: # If the variable isn't matched then these missions are used (all set to false atm)
$ galaxymission1 = False
$ mission1 = None
$ mission1_name = None
jump showmodplanet
label Missionselected:
$ modjumplabel = "News" # This is the label for the mod content.
jump transit
To use this:
1. Create one label per planet defining its image and infobox (modbg2 and modinfo2)
2. Create one label per mission setting modjumplabel to its label
3. Set mission variables
The other bits of code are tucked away in transit.rpy
Store method
The store has been set up to automatically add storeobjects found in store.mod_items. This is a godsend, look at this code!
init python:
class Activeatemod(StoreItem): # needs to be a unique name
def __init__(self):
StoreItem.__init__(self)
self.id = 'Activatemod' # needs to be unique, I normally use the class name
self.display_name = "Click to play" # name in store
self.cost = 2000 #cost
self.tooltip = "Clicking me does something"
self.visibility_condition = "Test == 1" # (Visible if Test is equal to 1) If something is true or it is equal to True, then this is visible.
#This can be set to a false value in the buy function so it is not re-buyable.
def buy(self): #buy function
global Test # This needs to be here so that the Test variable updates
Test = 0 # This would let you make the item invisible
store.variablename = value # update an object attribute
Event() # This is triggered when the object is brought
if not 'Activeatemod' in store.mod_items: store.mod_items.append(Activeatemod)
# This line adds the item into the mod_items list if its not in there already.
When it is brought, the buy function triggers and activates all the things indented under it.
This can therefore be used to trigger something... like a planet appearing by buying a map.
So to modify Sunrider's Missiles to not take up ammo but increase energy cost, you could put:
def buy():
sunrider.weapons[2].ammo_use = 0 # 3rd weapon in sunrider.weapons list
sunrider.weapons[2].energy_use = 50
The store is an incredibly flexible and usefull tool, an example of modifying a weapon is below
Example of modifying a weapon:
init python:
class ModifyGG(StoreItem): # unique class ID
def __init__(self):
StoreItem.__init__(self) # same as class ID
self.id = ModifyGG
self.visibility_condition = 'bianca.weapon[1].energy_use == 60 and bianca.max_en >= 120' # visible if bianca max energy is greater than or equal to 120 and Bianca's 2nd weapon (gravity gun) has an energy cost of 60.
self.display_name = 'UPRADE: GRAV GUN'
self.cost = 800
self.tooltip = 'This upgrade uses Bianca's upgraded reactor to channel excess power into her gravity gun'
self.variable_name = None
self.max_amt = 0
def buy(self):
bianca.weapon[1].energy_use = 40 # sets the energy_use attribute of weapon 2 in bianca.weapons to 40, thus removing itself from the store as its visibility condition is now not met.
store.mod_items.append(ModifyGG) # adds the item to the store.
ChatButtons
This is a modification of the dispatch label via a label hijack that is auto activated when the M52-54.rpy file is in the Sunrider/game folder.
It lets you add chat buttons to the sunrider map when certain conditions are met and is ideal for introducing stories or letting the player talk to people at will.
Ok... This can look complicated (and it can be extremely complicated) but it is fairly simple when broken down into bits.
chat_labels.append([]) # This adds an entry into the chat_labels list. it is a list as you can tell by the []
'condition' # This works the same as the planet visibility and mission visibility. (See below)
'Char' # This is the same shorthand as the text code we saw in the first few lines. It tells you what button to show and the variable to modify
'Location' # This is the location on the map to display the button
'Label' # This is the label to jump to.
The Locations are:
'captainsloft', 'sickbay', 'messhall', 'bridge', 'engineering', 'lab', 'hangar'
Conditions:
This is a bit tricky... When working out the conditions then you want:
1: Timing variables (These can be your own, or they could be mission**_complete and such.
2: Character variables (so you don't overwrite any existing buttons, think ica_location == None)
3: Stopping variables (A variable set when you use the label so it isn't refreshed constantly)
It lets you add chat buttons to the sunrider map when certain conditions are met and is ideal for introducing stories or letting the player talk to people at will.
init python:
chat_labels.append(['Condition','char','location','label'])
#Example:
chat_labels.append(['mission11_complete and ica_location == None and chi_location == None and Boostertalk == False','ica','lab','Rebuildbooster'])
Ok... This can look complicated (and it can be extremely complicated) but it is fairly simple when broken down into bits.
chat_labels.append([]) # This adds an entry into the chat_labels list. it is a list as you can tell by the []
'condition' # This works the same as the planet visibility and mission visibility. (See below)
'Char' # This is the same shorthand as the text code we saw in the first few lines. It tells you what button to show and the variable to modify
'Location' # This is the location on the map to display the button
'Label' # This is the label to jump to.
The Locations are:
'captainsloft', 'sickbay', 'messhall', 'bridge', 'engineering', 'lab', 'hangar'
Conditions:
This is a bit tricky... When working out the conditions then you want:
1: Timing variables (These can be your own, or they could be mission**_complete and such.
2: Character variables (so you don't overwrite any existing buttons, think ica_location == None)
3: Stopping variables (A variable set when you use the label so it isn't refreshed constantly)
The Ending
Well... If you have managed to read through this entire tutorial I am deeply impressed and if you actually managed to piece the mod together and it helped make a mod, I am ecstatic!
The complete mod code is here: (although it doesn’t have a label. hijack because I don’t know where you would be in the story and it doesn't have a chatbutton code)
The complete mod code is here: (although it doesn’t have a label. hijack because I don’t know where you would be in the story and it doesn't have a chatbutton code)
init python: # init blocks run at the start of the game or on loading
def Newsmod(): # Defines a function
renpy.jump("News") # Python version of jump
class Map(StoreItem): # needs to be a uniuqe name
def __init__(self):
StoreItem.__init__(self)
self.id = 'ModMap' # needs to be uniuqe, I normaly use the class
self.display_name = "Mystry Map" # name in store
self.cost = 0 #cost
self.tooltip = "Clicking me enables the News planet"
self.visibility_condition = 'store.Hasmap == False'
def buy(self):
store.Hasmap = True # Hasmap is a attribute of the store object
if not hasattr(store,'Hasmap'): # This sets Hasmap to False if it dosn't exist
store.Hasmap = False
modmission = 0
store.mod_items.append(Map) # This puts the Map item in store.mod_items
Planet("ModPlanet", "planetwarp1", 1370, 450, "store.Hasmap == True")
label planetwarp1:
image modbg2 = "Map/Farport.jpg"
image modinfo2 = "Map/farport_info.png"
$ modjumplabel = "News"
jump missionselectionlabel
label missionselectionlabel: # You can call this whatever you want
$ map_back = "planetback" #plug in the label to go back (below)
if modmission == 0: # set a variable if you want the mission(s) to be available, you can have three missions visible at once but it dosent line up well
$ galaxymission1 = True #/ and is not suited to use with flexicode atm
$ mission1 = "transit" # Jump to label for mission
$ mission1_name = "Memory: Escape from Cera." #mission name
else: # If the variable isent matched then these missions are used (all set to false atm)
$ galaxymission1 = False
$ mission1 = None
$ mission1_name = None
jump showmodplanet
label showmodplanet:
scene bg black
show galaxymap:
zoom 1 alpha 1
parallel:
ease 0.5 alpha 0
parallel:
ease 1 xpos -13710 ypos -5190 zoom 10
show modbg2:
zoom 0.0268041237113402
xpos 1071 ypos 519 alpha 0
parallel:
ease 1.1 alpha 1
parallel:
ease 1 zoom 1 xpos 0 ypos -430
pause 1
show modinfo2:
xpos 1098 ypos 200
call screen map_travelto
with dissolve
label planetback: # this is used to return from the planet map to the galaxy map
hide modinfo
scene bg black
show modbg2:
zoom 1
xpos 1370 ypos 450 alpha 1
parallel:
ease 0.5 alpha 0
parallel:
ease 1 zoom 0.0268041237113402 xpos 1490 ypos 725
show galaxymap:
xpos -14900 ypos -7250 zoom 10 alpha 0
parallel:
ease 1.1 alpha 1
parallel:
ease 1 xpos 0 ypos 0 zoom 1
pause 1
call screen galaxymap_buttons
label transit:
$ Random = renpy.random.randint(1,9) # sets random integer for below text.
if Random == 1:
scene space back1
if Random == 2:
scene space back2
if Random == 3:
scene space back3
if Random == 4:
scene space back4
if Random == 5:
scene space back5
if Random == 6:
scene space back6
if Random == 7:
scene space back7
if Random == 8:
scene space back8
if Random == 9:
scene space back9
show sunrider_warpout_standard:
xpos 700 ypos 350
with dissolve
pause 1.0
play sound "Sound/large_warpout.ogg"
show sunrider_warpout_standard_flash:
xpos 426 ypos 0 alpha 0
linear 0.1 alpha 1
linear 0.1 alpha 0
show sunrider_warpout_standard out:
xpos 700 ypos 350
ease 0.2 xpos 200 ypos 300 zoom 0
pause 1.0
scene modbg2:
ypos 0
ease 1.5 ypos -120
with dissolve
pause 1
show sunrider_warpout_standard out:
xpos 2300 ypos 1200 zoom 2
ease 0.2 xpos 1000 ypos 500 zoom 0.5
pause 0.2
play sound "Sound/large_warpout.ogg"
show cg_legionwarpin_missilefrigate_warpflash:
zoom 1.5 xpos 1550 ypos 750
show sunrider_warpout_standard
pause 2.0
$renpy.jump(modjumplabel)
label News: # The start of the News Block.
$Modmission = 1
show bg bridge
show ava uniform altneutral angry with fade:
xpos 0.5
ava "Captain! The Alliance is broadcasting a report on the PACT advance and attack on Cera!" # defined speaker
kay "They have to start mobilising soon, put it on the main screen."
"" "Ava turns to the screen"
show ava uniform altneutral angry:
ava "It looks like their showing our escape..."
menu: # Menu block, everything is indented.
ava "Do you want any popcorn?" # Is not a new block so displays normally
"Yes": # Is a new block so this is a choice
kay "Watching us almost get killed does make me hungery..." # Indented
jump Popcorn # Jump to label Popcorn
"No":
kay "No thanks, just a diet coke."
jump Nopopcorn
label Popcorn:
"Ava passes you the popcorn and you settle into your seats as the lights dim"
jump setupmission22
label Nopopcorn:
"Ava shrugs and passes you the bottle. You had better watch those expensive command consoles!"
"As you settle back into your chairs, the lights start to dim in preparation"
jump setupmission22
label setupmission22:
window hide
$ BM.mission = '22'
call mission22init
jump battle_start
label mission22init:
python:
zoomlevel = 1
enemy_ships = []
destroyed_ships = []
BM.xadj.value = 872
BM.yadj.value = 370
create_ship(MissileFrigate(),(11, 7))
create_ship(PhoenixBoaster(),(11, 6))
#sunrider.set_location(7, 7)
$ PlayerTurnMusic = "music/Titan.ogg"
$ EnemyTurnMusic = "music/Dusty_Universe.ogg"
return
label mission22:
$ BM.battle()
if BM.battlemode == True:
jump missionnews
else:
jump aftermission22
label after_mission22:
"Newsperson" "So at least some of the Cera military are known to have suvived the PACT assault"
kay "So... At least someone in Cera may know we survived"
ava "But PACT are more than likely to come after us now we're in the public view."
jump dispatch
Good Luck and Happy Modding
If there's anything someone thinks should be added, let me know!
If there's anything someone thinks should be added, let me know!
Advanced Stuff:
Battle Modding:
Battle Modding:
Understanding Battles:
The entire player turn is a loop that continues until the player looses or it is ended by clicking the end turn button. At that point, it goes through the enemy_turn code before returning back to the player code.
This means when you play the battle, every click you make starts the loop again.
Therefore we need some way to distinguish between turns.
This means when you play the battle, every click you make starts the loop again.
Therefore we need some way to distinguish between turns.
Turn Gates:
Basic Battle Code:
label Mission99:
$ BM.battle()
if BM.battlemode == True:
jump mission99
else:
jump aftermission99
If you add $turncount = 0 to the init label before the mission you can then do this:
Turn Gate Code:
Turn Gate Code:
label mission99:
if turncount != BM.turn_count:
$ turncount = BM.turn_count
if turncount == 1:
do stuff
python:
if len(BM.ships) > shipcount: shipcount = len(BM.ships)
if len(BM.ships) < shipcount:
shipcount = len(BM.ships)
renpy.say("Ava", "Ship destroyed")
$ BM.battle()
if BM.battlemode == True:
jump mission99
else:
jump aftermission99
Anything in the turn-gate gets run once per turn, you can seperate turns by adding a check for what turn it is.
Anything outside of the turn-gate, like the second code block is run with every click of the mouse. This is good for checking the state of the battle and responding to it. In this case, Ava makes a comment about a ship being destroyed when the number of entries in BM.ships is less than it was.
(Assume shipcount is set to 0 or something in the battle-init label)
The M52-53._7.2_1.1.rpy file in functions zip allows you to have a function that runs at the start of every turn of missions. This is most useful for global mods.
Showing images in battle:
Whilst there is no real difference in speaking in a battle (with the exception of speaking whilst in a python block), to display an image then you need to include a small bit of extra code.
Basically whenever you want an image to appear or dissapear in a battle, you need to specify onlayer screens.
label mission99:
if turncount != BM.turn_count:
$ turncount = BM.turn_count
show kryska plugsuit alt neutral frown onlayer screens with dissolve
kry "I AM SPEAKING IN SPACE"
hide kryska onlayer screens with dissolve
Basically whenever you want an image to appear or dissapear in a battle, you need to specify onlayer screens.
Speaking and showing images in Python
This is all possible inside a python block, it is however slightly different.
There are two ways to do this, either useing the shorthand as normal or useing the renpy.say function. For most purposes you can use the shorthand (as it is actually a function) and just need to make a slight change in format.
This is by far the easiet way to talk in a python block.
Shorthand Code:
Note that you need to include brackets because this is a function, and either single or double quotes to tell python that this is a string. Other than that it is very similar to the standard shorthand text we covered in the first two mins of this tutorial.
The other method is not normally requried but it can be usefull if you dont want to define a speaker and want a one-off character. It is usefull to know as it helps you understand renpy.equivilents with code you have already used.
Functions are basicly small snippits of code that are recyclable, in short we have been useing disguised functions whenever we make a character speak. This is what they look like under the skin!
(have a look at: www.renpy.org/doc/html/statement_equivalents.html for more)
Renpy Say Equivalent Code:
Its very easy to use this, but you must make sure you have the quotes.
Unlike displaying text, images can look quite complex to display. It follows the same patterns but there are extra variables and to get it to fade, you need to add another command.
This show command is harder to place, the image name can be either the physical location of the image, or it can be a defined image. the at_list variable is used for positioning and transforms. (see: www.renpy.org/doc/html/transforms.html)
These can be created but they are still a slightly clunky way to place an image.
Example Code:
This sets the image to appear at topleft (look at the link for the complete list) but also includes a new statement. If you don't use an at_list then it defaults to center.
renpy.with_statement(transition) is used when you want to add a transition to an image changing, think of it as the with dissolve we used earlier.
Complete Example Code:
This completed example checks the turn number, then if its the 2nd turn, shows ava shouting at the pilot, changing position and hides her, then a cover is made slightly away from the sunrider location.
Speaking:
There are two ways to do this, either useing the shorthand as normal or useing the renpy.say function. For most purposes you can use the shorthand (as it is actually a function) and just need to make a slight change in format.
Shorthand:
This is by far the easiet way to talk in a python block.
Shorthand Code:
python:
ava("... Idiot")
Note that you need to include brackets because this is a function, and either single or double quotes to tell python that this is a string. Other than that it is very similar to the standard shorthand text we covered in the first two mins of this tutorial.
The other method is not normally requried but it can be usefull if you dont want to define a speaker and want a one-off character. It is usefull to know as it helps you understand renpy.equivilents with code you have already used.
Renpy say equivalent:
Functions are basicly small snippits of code that are recyclable, in short we have been useing disguised functions whenever we make a character speak. This is what they look like under the skin!
(have a look at: www.renpy.org/doc/html/statement_equivalents.html for more)
Renpy Say Equivalent Code:
python:
renpy.say("character","words") # This will display the name character saying words
renpy.say("kryska","stuff")
renpy.say(ava,"idiot") # The shorthands work as well, but why you would use them if their already set up, I'm not sure.
Its very easy to use this, but you must make sure you have the quotes.
Displaying images in python blocks:
Unlike displaying text, images can look quite complex to display. It follows the same patterns but there are extra variables and to get it to fade, you need to add another command.
python:
renpy.show('imagename', at_list = [], layer = 'screens')
renpy.hide('image', layer = 'screens')
This show command is harder to place, the image name can be either the physical location of the image, or it can be a defined image. the at_list variable is used for positioning and transforms. (see: www.renpy.org/doc/html/transforms.html)
These can be created but they are still a slightly clunky way to place an image.
Example Code:
python:
renpy.show('kryska plugshit altneutral frown' at_list = [topleft], layer = 'screens')
renpy.with_statement(dissolve)
renpy.hide('kryska', layer = 'screens')
renpy.with_statement(wipedown)
This sets the image to appear at topleft (look at the link for the complete list) but also includes a new statement. If you don't use an at_list then it defaults to center.
renpy.with_statement(transition) is used when you want to add a transition to an image changing, think of it as the with dissolve we used earlier.
Complete Example Code:
label mission99:
python:
if turncount != BM.turn_count:
turncount = BM.turn_count
if turncount == 2:
renpy.show("ava uniform alt order mouthopen", at_list = [topleft], layer = 'screens')
renpy.with_statement(wipeup)
ava("Watch out for that ship!")
renpy.show('ava uniform alt neutral mad', atlist = [topleft], layer = 'screens')
renpy.say("Ava", "You need to pay attention!")
renpy.hide('ava',layer = 'screens')
renpy.with_statement(wipedown)
create_cover((sunrider.location[0]+1,sunrider.location[1]))
This completed example checks the turn number, then if its the 2nd turn, shows ava shouting at the pilot, changing position and hides her, then a cover is made slightly away from the sunrider location.
Battle Maps
Placing ships can be hard to visualize, and doing so while testing for balance can be frustrating so the Toolkit function in functions.zip, when dropped in Sunrider/game will create a small + button on the top of the skirmish map.
When you have placed the ships you want, then click the + sign and it will make a file with a basic battle printout that you can use to test your battle for balancing.
As of the moment it doesn't make covers and it doesn't separate player ships into its own paragraph but at some point I will update it to do so.
When you have placed the ships you want, then click the + sign and it will make a file with a basic battle printout that you can use to test your battle for balancing.
As of the moment it doesn't make covers and it doesn't separate player ships into its own paragraph but at some point I will update it to do so.
Example Output Code:
label setupmission**:
window hide
$ BM.mission = '**'
call mission**init
jump battle_start
label mission**init:
python:
zoomlevel = 1
enemy_ships = []
destroyed_ships = []
BM.xadj.value = 872
BM.yadj.value = 370
create_ship(PactMook(),(8, 3))
create_ship(PactMook(),(10, 3))
seraphim.set_location(2, 4)
blackjack.set_location(6, 4)
create_ship(PactMook(),(8, 4))
create_ship(PactAssaultCarrier(),(16, 4))
bianca.set_location(5, 5)
create_ship(MissileFrigate(),(12, 5))
create_ship(PactAssaultCarrier(),(16, 5))
liberty.set_location(4, 6)
sunrider.set_location(5, 6)
phoenix.set_location(7, 6)
create_ship(PactMook(),(8, 6))
paladin.set_location(5, 7)
create_ship(MissileFrigate(),(10, 7))
create_ship(MissileFrigate(),(11, 7))
create_ship(PactBattleship(),(13, 8))
$ PlayerTurnMusic = "music/Titan.ogg"
$ EnemyTurnMusic = "music/Dusty_Universe.ogg"
return
label mission**:
$ BM.battle()
if BM.battlemode == True:
jump mission**
else:
jump aftermission**
Just change the ** to your mission variable and you can drop it right into the battle. This will not pick up non-core player units like mercenaries. This will likely save you hours of work, but as a side effect it also enables all enemy_ships for the skirmish placement and SHOULD NOT be included with your module
Ending Battles
Battles are won when BM.you_win() is called. This can be by destroying a ship flagged as Boss, destroying all enemy ships or by code directly calling BM.you_win()
Battles are lost by calling BM.you_loose(), this is called when a critical ship is killed or all player ships are destroyed.
As modifying these will affect the entire game, not just a specific mod. I have created replacement functions that will let you schedule functions for either winning or losing and run these either with or instead of the originals.
To use this then include the three M52 files from functions.zip in your mod.
Include the following code in an init block after -10:
Usage Code:
When using this remember that to maintain flexibility, if you do not direct the function out of the battle, it will carry on to either the loss or victory screen.
To escape this then use something like this:
Escape Battle Code:
Battles are lost by calling BM.you_loose(), this is called when a critical ship is killed or all player ships are destroyed.
As modifying these will affect the entire game, not just a specific mod. I have created replacement functions that will let you schedule functions for either winning or losing and run these either with or instead of the originals.
To use this then include the three M52 files from functions.zip in your mod.
Include the following code in an init block after -10:
Usage Code:
init python:
init_win_funcs.append([BM MISSION TO USE, FUNCTION TO USE])
#or
init_loss_funcs.append([BM MISSION TO USE, FUNCTION TO USE])
#Example:
init_loss_funcs.append([60,crashships])
When using this remember that to maintain flexibility, if you do not direct the function out of the battle, it will carry on to either the loss or victory screen.
To escape this then use something like this:
Escape Battle Code:
init python:
def function():
#do stuff
clean_battle_exit() # this resets the battle map for the next fight
renpy.jump('labelname')
Programmed Battle phases
This may be useful to coders for understanding how to implement complex things into battles, but remember if done without returning them to their original states, modifying this will cause other battles to be changed as well.
Battle Start:
This is a label that is called, it is used to set the battle up and to store variables for restarting a battle.
label battle_start:
play music PlayerTurnMusic # starts up the music
python:
BM.battlestart.player_ships = store.player_ships[:] #|
for ship in BM.battlestart.player_ships: #|
ship.battlestart_location = ship.location #|
BM.battlestart.enemy_ships = deepcopy(store.enemy_ships) #| stores variables
BM.battlestart.covers = deepcopy(BM.covers) #|
BM.battlestart.sunrider_rockets = sunrider.rockets #|
BM.battlestart.sunrider_repair_drones = sunrider.repair_drones #|
BM.battlestart.cmd = BM.cmd #|
BM.stopAI = False
BM.order_used = False
BM.enemy_vanguard_path = []
BM.player_vanguard_path = []
BM.active_strategy = [None,0]
renpy.take_screenshot() #| Autosaves
renpy.save('beginturn') #|
if BM.show_tooltips:
renpy.show_screen('tooltips')
# BM.xadj.value = 872
# BM.yadj.value = 370
for ship in player_ships: #|
ship.hp = ship.max_hp #| Refreshes player ships
ship.en = ship.max_en #|
# renpy.show_screen('mousefollow')
store.zoomlevel = 0.65
BM.show_grid = False
sort_ship_list()
BM.start() #| Starts the battle loop
return
BM.start() (L950 classes.rpy):
This is the bit of code that checks if the formation picker is used and then shows the screens. BM.editableformations is the function that chooses and could be modified, but its of limited interest for modding.
def start(self):
self.battle_log_insert(['system'], "-------------BATTLE START-------------")
BM.player_ai = False
battlemode() #stop scrollback and set BM.battlemode = True
update_stats() #used to update some attributes like armour and shields
renpy.show_screen('battle_screen')
#new formation feature (only after mission 12 for now)
if self.editableformations(): # if BM.mission > 12 and not string
self.phase = 'formation'
for ship in player_ships:
if ship.location != None:
set_cell_available(ship.location)
ship.location = None
renpy.show_screen('player_unit_pool_collapsed')
renpy.show_screen('player_unit_pool')
BM.selectedmode = False #failsafe
BM.targetmode = False
BM.selected = None #the selected unit doesn't show up in the pool
self.formation_phase()
else:
self.jumptomission()
formation_phase is the handling function of placing ships and there's nothing really to mod there either.
jumptomission() just jumps you to your mission label. This is why you should always use the mission format given earlier in the tutorial.
The mission label scrolls through your code and then hits BM.battle where it starts to loop until you end your turn.
BM.battle() (981 classes.rpy):
This is getting more interesting! This function checks what the player clicked and then sends a value to BM.dispatch_handler(). It then checks for loss or win and if either of those returns true, they run the you_win and you_lose functions.
After that it bounces you back to your mission label.
After that it bounces you back to your mission label.
def battle(self):
for ship in player_ships:
if not hasattr(ship, 'blbl'): #| If they don't have the attr blbl
ship.blbl = ship.lbl #| sets blbl to lbl for display
if self.player_ai: #|
self.player_AI() #| player can be set to AI, could well
self.toggle_player_ai() #| be some interesting mod stuff here...
self.result = 'endturn' #| NPC controlled ships independent of players?
else:
#battle_screen should be shown, and ui.interact waits for your input. 'result' stores the value return from the Return actionable in the screen
self.result = ui.interact()
if store.Difficulty < self.lowest_difficulty:
self.lowest_difficulty = store.Difficulty
self.just_moved = False #this sets it so you can no longer take back your move
renpy.hide_screen('game_over_gimmick') #disables the screensaver gimmick
if self.stopAI and sunrider.hp < 0: #some failsafe checking. stopAI functions like an emergency stop for AI code
renpy.jump('sunrider_destroyed')
if hasattr(store,'mochi'):
if hasattr(mochi,'hp'):
if mochi.hp < 0 and mochi in player_ships:
renpy.jump('sunrider_destroyed')
#sanity check
for ship in self.ships:
if ship.hp <= 0:
destroyed_ships.append(ship)
if ship in player_ships:
player_ships.remove(ship)
if ship in enemy_ships:
enemy_ships.remove(ship)
if ship in self.ships:
self.ships.remove(ship)
self.dispatch_handler(self.result)()
self.check_for_loss()
self.check_for_win()
return
Dispatches (classes 187+):
The dispatch_handler function checks for the result of ui.interact in the previous function and then works out what to do with it.
This is done by checking it against the dispatcher returns list in classes.rpy lines 95-129. When it matches up the response then it knows what to do. Dispatcher locations are classes.rpy, lines 200-615.
These would be ideal bits to mod but (sorry to say this again you should remember these will affect all battles). It should be possible to add your own dispatchers and dispatch triggers. look in the custom screens rpy file to see more. There is unfortunately too much code to include it here.
This is done by checking it against the dispatcher returns list in classes.rpy lines 95-129. When it matches up the response then it knows what to do. Dispatcher locations are classes.rpy, lines 200-615.
These would be ideal bits to mod but (sorry to say this again you should remember these will affect all battles). It should be possible to add your own dispatchers and dispatch triggers. look in the custom screens rpy file to see more. There is unfortunately too much code to include it here.
End Player Turn:
This is done by clicking a displayable set in custom screens, it causes the battle_end_turn function to run which in turn calls the BM.end_player_turn function after clearing the selected ship.
Most of what this function does is set the AI to work. It ticks the turn_count up a bit and resets ship flak
Using M52-53 then you can set a function to run at the end of the player turn (so pretty much on the enemy turn) if the BM.mission matches the requirements.
end_player_turn (clases line 1054) Code:
Most of what this function does is set the AI to work. It ticks the turn_count up a bit and resets ship flak
Using M52-53 then you can set a function to run at the end of the player turn (so pretty much on the enemy turn) if the BM.mission matches the requirements.
end_player_turn (clases line 1054) Code:
def end_player_turn(self):
self.battle_log_insert(['system'], "---------Player turn end---------")
self.battle_log_trimm()
renpy.hide_screen('commands')
self.selected = None #some sanity checking
self.target = None
self.moving = False
self.selectedmode = False
self.targetingmode = False
self.active_weapon = None
self.weaponhover = None
self.turn_count += 1
renpy.music.play(EnemyTurnMusic)
renpy.call_in_new_context('endofturn')
for ship in self.ships:
ship.flak_effectiveness = 100
self.enemy_AI() #call the AI to take over
self.battle_log_insert(['system'], "---------{0} turn end---------".format(self.phase))
self.battle_log_trimm()
##I have NO idea why this dumb workaround is needed, but the destroy() method -somehow- doesn't want to jump to this label sometimes.
if sunrider.hp < 0:
renpy.jump('sunrider_destroyed')
for ship in self.ships:
ship.flak_effectiveness = 100
ship.getting_curse = False #failsafes
ship.getting_buff = False
for ship in player_ships:
ship.en = ship.max_en * (100 + ship.modifiers['energy regen'][0] ) / 100
if ship.en < 0: ship.en = 0
self.active_weapon = None
self.targetingmode = False
self.target = None
self.selected = None
self.selectedmode = False
self.order_used = False
self.moving = False
#run the end of turn callbacks
if BM.end_turn_callbacks != []:
for callback in BM.end_turn_callbacks:
callback()
if self.battlemode:
renpy.music.play(PlayerTurnMusic)
renpy.call_in_new_context('endofturn')
renpy.take_screenshot()
# I've sometimes been getting this error for some silly reason:
# WindowsError: [Error 183] Cannot create a file when that file already exists
# may just be me, but to be safe I'll put a catch here
try:
renpy.save('beginturn')
except:
pass
At the end of the enemy's turn it jumps to endofturn label and updates buffs/curses. Now, finally the game makes its way back to the player turn and starts looping through BM.battle/dispatchers and the mission label.
Failed Missions:
If you fail then you are booted to the tryagain label, this resets certain variables stored by the mission at the start and offers you the chance to load a game. If you choose to restart then you are bounced back to battle_start.
The m52 file lets you store variables and run functions at battle-start and changes the tryagain label to Try2 so you can reset other variables, not just the ones auto-reset in this label.
The m52 file lets you store variables and run functions at battle-start and changes the tryagain label to Try2 so you can reset other variables, not just the ones auto-reset in this label.
label tryagain:
hide badend
$ clean_battle_exit(True)
python:
store.battle1_check1 = False
store.battle2_check1 = False
store.battle2_check2 = False
store.battle_check1 = False
i = 1
while True:
if hasattr(store, 'check{}'.format(i)):
setattr(store, 'check{}'.format(i), False)
i += 1
else:
break
try:
store.destroyed_ships = []
store.player_ships = BM.battlestart.player_ships
for ship in store.player_ships:
ship.missiles = ship.max_missiles
ship.location = ship.battlestart_location
sunrider.rockets = BM.battlestart.sunrider_rockets
sunrider.repair_drones = BM.battlestart.sunrider_repair_drones
BM.cmd = BM.battlestart.cmd
BM.turn_count = 1
store.enemy_ships = BM.battlestart.enemy_ships
for ship in store.enemy_ships:
if isinstance(ship, Havoc):
store.havoc = ship
BM.covers = BM.battlestart.covers
for cover in BM.covers:
cover.hp = cover.max_hp
except:
renpy.jump('tryagain_old')
BM.ships = []
for ship in store.player_ships:
BM.ships.append(ship)
for ship in store.enemy_ships:
BM.ships.append(ship)
BM.grid = []
for a in range(GRID_SIZE[0]):
BM.grid.append([False]*GRID_SIZE[1])
for ship in BM.ships:
if ship.location == None:
continue
x, y = ship.location
BM.grid[x - 1][y - 1] = True
jump battle_start
return
If you Win:
When you win, battle_end is called. This ends the battle loop and resets BM variables for selected ships. It then shows the victory screen and displays ships destroyed in the battle_end function. This would be simple to mod for missions where it wasn't so clear if victory had been achieved, you wanted to modify money or you wanted to put your own spin on captured ships.
def battle_end(self, lost = False):
"""ending the battle - reset values for next battle"""
self.battlemode = False #this ends the battle loop
if self.selected != None: self.unselect_ship(self.selected)
self.targetingmode = False
self.vanguardtarget = False
self.weaponhover = None
self.hovered = None
BM.enemy_vanguard_path = []
renpy.hide_screen('tooltips')
BM.phase = 'Player'
if store.Difficulty < self.lowest_difficulty:
self.lowest_difficulty = store.Difficulty
if not lost:
#show the victory screen
renpy.music.stop()
renpy.music.play('Music/Posthumus_Regium_Finale.ogg', loop = False)
renpy.hide_screen('commands')
self.draggable = False
renpy.show_screen('victory')
renpy.pause(3.0)
renpy.hide_screen('victory')
store.repair_cost = 0
store.total_money = 0
store.boss_killed = False
store.surrender_bonus = 0
for ship in destroyed_ships:
if ship.faction == 'Player':
store.repair_cost += int(ship.max_hp * 0.2)
else:
if ship.boss: store.boss_killed = True #check if a boss was killed
store.total_money += ship.money_reward
if store.boss_killed:
for ship in enemy_ships:
if ship.hp > 0:
store.surrender_bonus += ship.money_reward / 2
for ship in player_ships:
store.repair_cost += int((ship.max_hp - ship.hp)*0.1)
store.net_gain = int(store.total_money + store.surrender_bonus - store.repair_cost)
#SPACE WHALE TAX!
if store.Difficulty == 5:
store.net_gain *= 0.8
self.money += int(net_gain)
#Captain and higher difficulties reduce the total CP you get per battle.
difficulty_penalty = store.Difficulty - 1
if difficulty_penalty < 0: difficulty_penalty = 0
self.cmd += int((net_gain*10)/(BM.turn_count+difficulty_penalty))
renpy.show_screen('victory2')
renpy.pause(1)
renpy.hide_screen('victory2')
self.draggable = True
self.turn_count = 1
self.active_strategy = [None,0]
self.ships = []
self.selectedmode = False
self.battle_log = []
renpy.hide_screen("battle_log")
VNmode() #return to visual novel mode. this mostly just restored scrolling rollback
for ship in destroyed_ships:
if self.mission == 'skirmish' or (ship.faction == 'Player' and not ship.mercenary):
player_ships.append(ship)
self.ships.append(ship)
for ship in player_ships:
self.ships.append(ship)
for ship in player_ships:
ship.en = ship.max_en
ship.hp = ship.max_hp
ship.hate = 100
ship.total_damage = 0
ship.total_missile_damage = 0
ship.total_kinetic_damage = 0
ship.total_energy_damage = 0
ship.missiles = ship.max_missiles
ship.location = None #this helps if you add new ships but don't know the current location of the existing ones.
for modifier in ship.modifiers:
ship.modifiers[modifier] = [0,0]
#reset the entire grid to empty and BM.ships with only the player_ships list
clean_grid()
self.covers = []
renpy.hide_screen('battle_screen')
renpy.hide_screen('commands')
renpy.block_rollback()