Should I use Classes to do this?

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
HighlightKing
Newbie
Posts: 10
Joined: Fri Feb 23, 2018 2:38 pm
Contact:

Should I use Classes to do this?

#1 Post by HighlightKing » Thu Apr 28, 2022 9:06 am

Hey there!

Sorry for the vague title, but it really is kind of difficult to explain.
So this is what I'd like to happen: Let's say I have three cards, - kind of like in a deck building game - a light attack, a heavy attack, and a card that makes the enemy miss a turn.
We also have two enemies, with different properties. One has armor, the other doesn't. One is agile, the other isn't.

Is it possible to define the enemies and the cards as "Objects" or "Classes", give them properties, and then "let things play out", if say I chose an enemy, and then clicked a card to attack it?
I don't have a lot of experience with Classes, I've mostly achieved things by setting values and using '"If" statements. You could do almost anything with "If" statements, however, very quickly things can turn incredibly convoluted, and a lot problems arise, some might be just simply insurmountable using that - very primitive, I know - method.

So I guess my question is, is it doable? To make Classes (Or Objects, if that's a thing) interact, based on predetermined properties, and then yield the expected results.
Let me just close with an example:
I click/attack Enemy #2, who has the properties of being agile, and wears armor. Then I choose the card I'm attacking with, say a Light Attack. The Light Attack has the properties of 25% chance of extra piercing damage, but NOT against armored enemies, and if used against an enemy who has the "agile" property, there is a 25% chance of a miss. But if I used it against Enemy #1 instead, who doesn't have the "agile" property, it would hit, 100%.

Could this be done in some way? To have all these properties check against each other, and yield the correct/expected result?

I hope I've managed to explain what I'm looking for at least a little bit. If someone could confirm if it's doable, perhaps even give a little hint as to how, it'd be greatly appreciated. Feel free to go into details as much as you'd like! :)

Thank you for reading!

User avatar
RicharDann
Veteran
Posts: 284
Joined: Thu Aug 31, 2017 11:47 am
Contact:

Re: Should I use Classes to do this?

#2 Post by RicharDann » Thu Apr 28, 2022 3:32 pm

Short answer: yes, this is easily doable and objects and classes are a good way to go, if not the best. Though I say "easily", at first these concepts may be difficult to wrap your head around, but once you understand them it's quite straightforward.

You'll want to learn about python objects, classes, and also functions. For your example, you'll need a Card class and an Enemy class, which will serve as the base for each of your individual skills and enemy entities. Functions on the other hand will let you do the math and other operations with your objects, like checking if a skill connects or if the target is armored or not.

In the cookbook section of this forum there are a few tutorials on how to incorporate object oriented programming into Ren'Py, and already made battle system examples you can check out.
The most important step is always the next one.

HighlightKing
Newbie
Posts: 10
Joined: Fri Feb 23, 2018 2:38 pm
Contact:

Re: Should I use Classes to do this?

#3 Post by HighlightKing » Fri Apr 29, 2022 6:31 am

Hey there, RicharDann!

First off, thank you so much for your reply!

So I made these few lines of code:

Code: Select all

init python:
    class Card:
        def __init__(self, card_damage, card_type, card_image="", card_image_hover=""):
            self.card_damage = card_damage
            self.card_type = card_type
            self.card_image = card_image
            self.card_image_hover = card_image_hover

    class Enemy:
        def __init__(self, enemy_health, enemy_type, enemy_attacks, enemy_image="", enemy_image_hover=""):
            self.enemy_health = enemy_health
            self.enemy_type = enemy_type
            self.enemy_size = enemy_size
            self.enemy_attacks = enemy_attacks
            self.enemy_image = enemy_image
            self.enemy_image_hover = enemy_image_hover
Let's say these attributes, for starter, could kind of cover what I described in my original post.
So now I - hypothetically - can have clickable cards and enemies on the screen.
The part that I don't really understand, is how to make them interact. I've seen Classes be used for items and Inventory, when in the script, using $ variables you could buy them, etc. But how would all this work in a scene, where you essentially have to click on a card, then Enemy 1's turn, then click another card, then Enemy 2's turn... That I don't really get, unfortunately.

Again, though, thank you for your reply, and if you don't have the time, or just don't want to keep having this conversation, I totally understand. :)

User avatar
RicharDann
Veteran
Posts: 284
Joined: Thu Aug 31, 2017 11:47 am
Contact:

Re: Should I use Classes to do this?

#4 Post by RicharDann » Fri Apr 29, 2022 10:22 am

That's a good start for your classes!

I don't have access to Ren'Py at the moment to provide a working example but I'll try to explain (briefly) how the process goes and what you should look into.

To make things interact with the player, in Ren'Py you need to use screens. These let you create an User Interface, wich is the buttons, bars, and other screen elements. These buttons, paired with screen actions, let the player interact with the game and control things like which card they select and which enemy.

But just having the UI is not enough, you need to have some way to control the battle flow and how the game reacts to the player's actions. For that, you can use labels. With these you can make the game jump to specific parts of the code, call screens for the player to interact with or basically execute any kind of code, for example if the player eliminates all enemies, you would go to the "WIN" label, and proceed with the game, but if you lose you would go to the "LOSE" label, and end the game.

You will also need to make use of variables, lists, and other kinds of python built-in objects and functions (like list comprehension, while and for loops) to organize and control your code.

The combination of all these things (variables, classes, objects, functions, labels, and screens) is what makes the game work.

Now I realize this is a lot of information to process, so I suggest you slowly test things out as you go. Make a few test objects and screens using the examples provided in the documentation and the Ren'Py Tutorial game (also very good I recommend you follow through it).

And feel free to ask questions if you get stuck, that's what this forum is for :)
The most important step is always the next one.

HighlightKing
Newbie
Posts: 10
Joined: Fri Feb 23, 2018 2:38 pm
Contact:

Re: Should I use Classes to do this?

#5 Post by HighlightKing » Fri Apr 29, 2022 11:34 am

Thanks for the reply! Super appreciated! However... :)

It's my bad, I should've been clearer. I know all about screens, labels, variables and such. In fact, as I've been trying to build this fight system, I've been using all the things you mentioned, aside from Classes and Objects.

But this feels like "brute-forcing" things. Because there is just so much "if this is true, then that happens, but at the same time if X is true for Y, then Z does this, but not if A is B or C, etc...". And I was wondering if it was possible to create self contained elements (Classes) with all the possible variables, and then let them unfold.

So to solve the problem of 25 million things pointing to all sorts of labels and screen, - some of these issues are pretty janky to solve, at least for me - I was hoping find out how to create a more universal and self contained way to deal with them.

So lets say I have a "Light Attack" card. I set up its properties for the "Card Class". How would an imagebutton's Action call/reference it?
Because if it needs to point to a label in the script that has that "Light Attack Card" Class reference, then how does it differ from just having something like this:

Code: Select all

label light_attack_card:
$ enemy_health -= 5
if armor == True:
    $ enemy_health -= 2
I'm sorry if I'm not explaining all this well enough, it really would be easier to do this as an actual conversation. I'm at a very weird place with RenPy, where I can - again - brute-force my way through issues, but I know that someone who ACTUALLY knows how to use the engine would find FAR more efficient ways to do things.

And once again, I appreciate your replies very much!

philat
Eileen-Class Veteran
Posts: 1853
Joined: Wed Dec 04, 2013 12:33 pm
Contact:

Re: Should I use Classes to do this?

#6 Post by philat » Fri Apr 29, 2022 12:24 pm

Each class should have a generic "use" method that can refer to the various instances you would need to calculate the outcome. E.g. very barebones example below: you would use Function(light_attack.play, enemyunit) to trigger the play method of a light_attack Card instance with a reference to a Unit instance. The light attack or heavy attack instances can be initialized with different amounts, different units can have different armor stats, etc, but it's plug and play if you set it up right.

Code: Select all

class Card():

    def __init__(self, amount):
        self.amount = amount

   def play(self, target):
        target.hp + target.armor -= self.amount
 
class Unit():
    def __init__(self, hp, armor):
        self.hp = hp
        self.armor = armor

User avatar
Ocelot
Eileen-Class Veteran
Posts: 1883
Joined: Tue Aug 23, 2016 10:35 am
Github: MiiNiPaa
Discord: MiiNiPaa#4384
Contact:

Re: Should I use Classes to do this?

#7 Post by Ocelot » Fri Apr 29, 2022 1:25 pm

Classes can be as complex, as you want. philat example is a simple one, but it is likely to be enough for your game. If you are making complex card game, on the level of MTG, for example, you would need more complex classes.

Does card applies its effect, or the owner of the card? Or maybe the enemy resoves effects of card? Or some kind of battle manager resolves everything? Abilities, are they simply tags which are taken into account during calculations or are full-fledged classes with own code? Do only characters have abilities or cards can have them too? How interactions between abilities should be handled?

Answers to all these questions will influence your design. For example this is roughtly how card use was handled in one of my (non-RenPy) projects:

Code: Select all

class BattleManager:
    def resolve_turn(self, owner, target, card):
        attack_result = [ [AttacktResult.PENDING], [ ] ]
        attack_result = owner.on_play(card, target, attack_result )
        attack_result = card.on_play(owner, target, attack_result )
        attack_result = target.on_defense(card, owner, attack_result )

       attack_result[1].extend( card.calculate_damage(owner, target) )
        
       attack_result = owner.before_give_damage(card, target, attack_result )
       attack_result = card.before_give_damage(owner, target, attack_result )
       attack_result = target.before_take_damage(card, owner, attack_result )

       attack_result = target.take_damage(card, owner, attack_result)
       
       owner.after_give_damage(card, target, attack_result )
       card.after_give_damage(owner, target, attack_result )
       target.after_take_damage(card, owner, attack_result )

       return attack_result

# Example how before_take_damage was handle in characters
class Combatant:
    def before_take_damage(self, card, owner, attack_result):
        for ability in self.passive_abilities:
            attack_result = ability.before_take_damage(card, owner, attack_result)
        for effect in self.active_effects:
            attack_result = effect.before_take_damage(card, owner, attack_result)
        return attack_result

# Example of ability definition. It turns electric attacks into healing.
class ElectricPowered(PassiveAbilityBase):
    def before_take_damage(self, card, owner, attack_result):
        damage = attack_result[1]
        for instance in damage:
            if instance.type == DamageType.ELECTRIC:
                instance.type = DamageType.HEAL
You probably do not need anything near this complex, but is is an example, how these things can be.
< < insert Rick Cook quote here > >

User avatar
RicharDann
Veteran
Posts: 284
Joined: Thu Aug 31, 2017 11:47 am
Contact:

Re: Should I use Classes to do this?

#8 Post by RicharDann » Fri Apr 29, 2022 1:37 pm

HighlightKing wrote:
Fri Apr 29, 2022 11:34 am
I was wondering if it was possible to create self contained elements (Classes) with all the possible variables, and then let them unfold.

So to solve the problem of 25 million things pointing to all sorts of labels and screen, - some of these issues are pretty janky to solve, at least for me - I was hoping find out how to create a more universal and self contained way to deal with them.
That's what functions are for. As previous commenters explained, they let you reuse code without having to repeat it multiple times and also allow you to group different parts in logical blocks of code.

Here's a quick example, fly typed but should give you a rough idea of what we mean:

Code: Select all

init python:
    
    # Some basic classes
    class Card():
        def __init__(self, name="", damage=1):
            self.name = name
            self.damage = damage
            
        def __repr__(self):
            return self.name
            

    class Enemy():
        def __init__(self, name="", health=10):
            self.name = name
            self.health = health
          
        def __repr__(self):
            return self.name   

    # This is a function that's executed when a card is played. For simplicity this one just displays some text as dialogue
    def play_card(card, target):

        # This is a renpy function that just displays dialogue on screen
        renpy.say( narrator, "Player used {} to target {}".format(card, target) )
     

# ------ Variables ------     
#Here we define/create a new card instance
default light_attack = Card(name="Light Attack", damage=10)

# An enemy instance
default mob = Enemy(name="Mob", health=10)

#This list will hold current enemies that the player is facing
default enemy_group = []

# This list will hold the cards the player can use
default card_deck = []

# This variable will be used to store the player's chosen card
default selected_card = None

# This variable will be used to store the player's chosen enemy
default selected_enemy = None

# ------ Screens ------
# A simple screen to show the player some basic stats for the enemy
screen hud():

    frame:
        has vbox
        label "Enemies"
        for e in enemy_group:
            text "{} - Health: {}".format(e.name, e.health)

# This screen contains a list of buttons that represent the player's cards
screen card_select():

    frame:
        xalign .5
        yalign .5
        has vbox 
        label "Select a Card"
        
        for c in card_deck:
            # this button executes two actions when clicked. it sets the variable to the player's chosen card, and hides the screen
            textbutton "{}".format(c.name) action [SetVariable("selected_card", c), Return()]
    
# This screen lets the player select an enemy
screen target_select():

    frame:
        xalign .5
        yalign .5        
        has vbox 
        label "Select an Enemy"

        for e in enemy_group:
            textbutton "{}".format(e.name) action [SetVariable("selected_enemy", e), Return()]


# ------ Labels ------
label start:

    $ card_deck = [light_attack] #we setup the player's cards
    
    $ enemy_group = [mob] #setup the enemies

    call battle_label # call the label where battle takes place
    
    return

# this label can be reused for whenever a fight needs to happen
label battle_label:

    show screen hud() # the hud screen is shown, with "show" statement so it doesn't pause gameplay
    
    # the card_select screen is called, with "call" statement so gameplay is paused 
    # until the player chooses a card. The screen hides automatically when he/she does.
    call screen card_select() 
    
    # The player selects an enemy to use the card in.
    call screen target_select()
    
    # With the card and enemy set, we execute a function that will make the battle operation take place
    $ play_card(selected_card, selected_enemy)

    # Once operations are done, we return to the previous state
    return
The most important step is always the next one.

HighlightKing
Newbie
Posts: 10
Joined: Fri Feb 23, 2018 2:38 pm
Contact:

Re: Should I use Classes to do this?

#9 Post by HighlightKing » Fri Apr 29, 2022 2:26 pm

Thank you everyone, and thank you RicharDann, this did clear a few things up that I hadn't really got before.
As per Ocelot's question as far as how complex I'd want it to be; fairly complex. I thought I'd give an actual example of how it would look like.
The economy of this example might not make sense, but the flow and how different attributes would look like, is what I have in mind.

Combat starts, you are now on the combat screen. This fight has two enemies.
On the screen, you see two clickable enemies as PNG imagebuttons.
You also see your deck of cards, that - for the sake of this example - has 3 cards in it, also as clickable imagebuttons.
1) sword_light_attack: deals 5 damage, is a blade attack
2) mace_heavy_attack: deals 10 damage, is a blunt attack
3) a combo_attack: deals 5 damage to the selected enemy, and 3 damage to one other random enemy, if there is at least one. (In this example there is one.)

There is an "enemy health counter" for the two enemies separately, and you have the player's health counter.

You start the fight by selecting an enemy, and then selecting a card (you can only select a card after selecting an enemy).
Once you picked a card, it disappears, until you run out of all the cards, after which you get back the whole deck again. This can happen an unlimited number of times, as the fight goes on.
(On a side note, hovering over the card is a separate "hovered" PNG, with additional details as to what the card does.)
After clicking the card, the selected enemy is hit, with a small pop up of the damage dealt.

Now it's Enemy_1's turn. Enemy_1 has the following properties:
1) has 100 hit-points
2) is armored, meaning it reduces blade attack damage by 3
3) has a light_attack for 3 damage and a heavy attack for 8 damage; has a 3 to 1 probability that he uses a light_attack vs a heavy_attack

After the enemy's turn, there is a pop up screen. It is the "Counter screen".
On this screen you have two counter cards: a Dodge Card and a Counter Slash card.
If you pick the Dodge card, you avoid the damage.
If you pick the Counter Slash card, you take the damage, but you also inflict 3 points of damage on the enemy that hit you.
And you also have the option to just continue, aka take the hit, without picking any counter card.
These cards also disappear after use, and only come back after your regular deck comes back, aka after you've used up all your regular deck cards. (After which, again they come back.)

After that, it's Enemy_2's turn. Enemy_2 has the following properties:
1) has 100 hit-points
2) has a light_attack for 2 damage and a heavy attack for 6 damage; has a 2 to 1 probability that he uses a light_attack vs a heavy_attack

And the cycle repeats.
This is what it would look like. I thought perhaps detailing it like this would put things in perspective a little better.

Again, thank you to everyone who has taken their time to respond. Much appreciated! :)

Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot], _ticlock_