Over the Reich: Single Player - Development Thread

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,801
Hello all,

@Prof. Garfield and I are teaming up once again to focus on the skies over Germany, this time trying to bring Over the Reich into a solid single player experience utilizing the powers of Lua. This thread is for its open development and observation. It's easier for us to work this way and also has the added benefit of maybe helping someone out there with their own creations. More to come!
 
For this project, we're using GitHub to collaborate, so we don't have to worry about who is using what file and remembering what files to pass along to the other person.

Our repository is here, for anyone who wants to follow along. Here's a video explaining the basics of Git, GitHub and GitHub desktop.
 
Here's the current combat model. It does not involve munitions, unlike the previous OTR game.

Air units fight for at least 3 rounds of combat and not more than 10. If (after 3 rounds) a fighter is within 3 hits of the other aircraft of being killed, it has a chance to 'escape' combat. This chance is based on the 'escapeSpeed' and 'pursuitSpeed' of the respective aircraft (in combatParametersOTR.lua). A bomber will always try to escape a fighter, regardless of its damage. (At time of writing, only the Me109G6, SpitfireIX, and A20 can be used in test combat.)

Aircraft can defend friendlies that aren't on their tile, as long as they are within the 'interceptionRange' of the would-be defender. If a defender comes from a different tile, it is transported to the tile being attacked. If the defender successfully defends the tile (i.e. it kills the attacker, forces the attacker to flee, or lasts 10 rounds of combat), it returns to the tile it was originally on. If a unit flees, it is teleported to a tile beyond its interception range, so it can't rejoin that battle again. A fleeing unit won't be teleported onto a tile with more than 2 units on it already.

This combat model is subject to change, and will also be expanded for kinds of combat.
 
Right now I'm working on setTraits, but only just adding in which units are fighters, bombers etc. (I noticed you started a few). One question I have is where will we differentiate the high-alt performers vs. low alt performers? Is that going to be something we adjust in combatParameters eventually? So setTraits is extremely broad and combatParameters is narrow?

I might build out the a/d/hp/fp as well as combat parameters in excel before putting it into lua just so I can take a look at things broadly.
 
Also just on that kind of line -

Is there a need for a trait "bomber destroyer" and "night fighter"? I'm just going to put both as "fighter" for now but we now have dedicated slots for rocket-armed 109s and 190s that made them real pigs compared to Allied fighters so they should have a disadvantage when fighting fighters. Likewise the ones that are night fighters should be terrible by day.

I'll just put everything for fighter right now -- just trying to understand how you're conceptualizing all this and if these sort of nuances are going to be in combatParameters?
 
At the moment, I use traits to determine the 'kind' of combat that will take place. So a "fighter" attacking a "fighter" produces different combat than a "fighter" (or "fighterBomber") attacking a bomber. I don't think that we need to distinguish "night fighters" and "bomber destroyers" in the traits just yet. The things that make them special will probably be in the combatParameters or combatSettings eventually.

I think that "bombers" will need to be separated into "tacticalBombers" and "strategicBombers", which will be able to attack "tacticalTarget" and "strategicTarget" respectively. (Some aircraft might need both of these traits.) This way, we don't have bombers attacking the "wrong" targets.

Traits are the way to go if you want to apply rules based on categories. So, if a combat rule is based only on a category that the unit might or might not be in, a trait is a good way to express that. So, if you want all "nightFighter"s to get +2 combat at night, then the "nightFighter" category is useful. If, however, you want night fighters to be better at night, with the details determined by each fighter individually, then a "nightFighter" trait serves little purpose.

I'm willing to change combat to whatever we need. I just figured it was easier to have something to work with, even if it gets changed later. If nothing else, it let me work out the movement expenditure and "escape" mechanics.
 
I should explain how to add fields to the aircraft entries in the combatParameters table. It has some pretty robust data validation, but I need to explain how to use it.

Near the top of the file, I define the 'combatParameter' data type. This is done using the function
Code:
-- gen.createDataType(dataName,specificKeyTable,generalKeyTable,defaultValueTable,fixedKeyTable) -->
--      newItemFunction(table) --> dataType,
--          creates (and validates) a new instance of the data type
--      isDataTypeFunction(value) --> boolean,
--          creates a function that checks if a value is the dataType
--      dataTypeMetatable
--          the metatable for the data type
A data type created in this way will only have keys and values that we specify ahead of time. These keys and allowable values are specified by these tables: the specificKeyTable, generalKeyTable,defaultValueTable,fixedKeyTable.

The fixedKeyTable is a table that specifies keys in the data type which can't be changed after they are created. {anImmutableValue=true,anotherImmutableValue=true} would mean that the keys anImmutableValue and anotherImmutableValue can't be changed after the data type is created. The combatParameter data type doesn't have any fixed keys at the moment.

The defaultValueTable specifies default values for keys, if values are not provided when the combatParameter is created. At the moment, there are two default values:
Code:
local defaultValueTable = {
    interceptionRange=0,
    attackMoveCost = 255,
}
If interceptionRange is not specified, it is 0, and if the attackMoveCost isn't specified, it is 255.

The keys of the specificKeyTable are allowable keys for a combatParameter. The values of the specificKeyTable are 'valueSpecification' tables (explained below), and they describe the allowable values for their corresponding key. At the moment, here is the specificKeyTable
Code:
local numberSpec = {["number"] = true}
local specificKeyTable = {
    pursuitSpeed = numberSpec,
    escapeSpeed = numberSpec,
    pursuitSpeedLow = numberSpec,
    pursuitSpeedHigh = numberSpec,
    pursuitSpeedNight = numberSpec,
    escapeSpeedLow = numberSpec,
    escapeSpeedHigh = numberSpec,
    escapeSpeedNight = numberSpec,
    interceptionRange = {["number"]={minVal=0, maxVal = maxInterceptionRange, integer=true}},
    attackMoveCost = {["number"]={minVal=0,maxVal=255,integer=true}},
}
All of these keys must have number values. The interceptionRange must be at least 0, and not more than the local variable maxInterceptionRange, and must also be an integer. Similarly, the attackMoveCost must be an integer between 0 and 255.

Since none of these specifications allow for 'nil' values, they must all exist in the combatParameter. pursuitSpeedLow/High/Night and escapeSpeedLow/High/Night are set to pursuitSpeed/escapeSpeed during pre-processing of makeCombatParameter, so they don't actually have to be specified. (Keys with default values also don't have to be specified.)

The generalKeyTable works similarly to the specificKeyTable, but has function(candidateKey)-->boolean as keys instead. If a candidateKey makes a key return true in the generalKeyTable, that key can be in the combatParameter, if it satisfies the corresponding valueSpecification. (I don't think this will be used for combatParameters.)

The key to this is the valueSpecification, which is a

A valueSpecification is a table with the following keys and values:

["nil"] = true or nil
If this key is true, the specified value can be nil.

["boolean"] = true, "true", "false", or nil
If this key is true (boolean value), the specified value can be a boolean.
If this key is "true" (string), then the specified value can be true, but not false.
If this key is "false" (string), then the specified value can be false, but not true.
If this key is nil, the specified value can't be a boolean.

["function"] = true, string, or nil
if this key is true or string, the specified value can be a function.
If string, the string describes the function, e.g. function(unit)-->number. Value specification checks do not check if the function actually matches the description, only that it is a function.
If this key is nil, the specified value can't be a function.

["number"] = true or nil or {minVal=number/nil, maxVal=number/nil, integer=true/nil}
If true, the specified value can be any number. If nil, the specified value can't be a number.
If table, any number must also be larger than the minVal (if specified) and smaller than the maxVal (if specified). If the integer key is true, the value must also be an integer.

["string"] = true or {[validString] = truthy} or nil
If this key is true, any string is allowed.
If it is a table, any string value must be a key in that table, with a truthy (anything other than false/nil) associated value.
If nil, the value can't be a string.

["table"]=string, true, nil, or {[1]=function(table)->true or string, [2]=string}
If the key is a string, any table is accepted, and the string describes the kind of table needed.
If true, any table is accepted, and a generated description will be 'table'.
If the key is a table, the table's value for 1 is a function, which returns true if specified value is an acceptable table, and a string describing the problem if it is not. The value for 2 is a string describing the required table, for generated descriptions/errors.
If nil, the specified value can't be a table.

["userdata"] = {[dataTypeName]=isDataTypeFn} or nil
The keys to this table are strings that describe acceptable userdata, and the values are functions which return true if the specified value matches the type, and false otherwise.
E.g.
{["unitTypeObject"] = civ.isUnitType, ["cityObject"] = civ.isCity}
Allows unitTypeObjects and cityObjects, but not other kinds of userdata.

The combatParameters table is made into a "dataTable" with the following command
Code:
gen.makeDataTable(combatParameters)
--[[A dataTable acts as an ordinary table, but, if desired, you can forbid values from being changed, forbid new key-value pairs from being stored, and forbid trying to access keys with a `nil` value. These features can make debugging easier by causing an error to happen on the line the mistake is made.

The following functions can be used to control the data table's features:
Code:
gen.forbidReplacement(dataTable) --> void
gen.allowReplacement(dataTable) --> void
gen.forbidNewKeys(dataTable) --> void
gen.allowNewKeys(dataTable) --> void
gen.forbidNilValueAccess(dataTable) --> void
gen.allowNilValueAccess(dataTable) --> void
gen.restrictValues(dataTable,isValidValueFn,makeValidValueFn) --> void
The relevant function for us is the last one. (Note: I just wrote this function, so it isn't part of the template yet, but it will be part of my next update. If anyone is impatient to use it, let me know.)

After this line,
Code:
gen.restrictValues(combatParameters,isCombatParameter,makeCombatParameter)
any value added to the combatParameters table will first be checked using the 'isCombatParameter' function. If it is one, then it is accepted into the table. If not, then 'makeCombatParameter(value)' is run on the proposed value.

Since makeCombatParameter will generate errors if there is missing or invalid data (or if a key is misspelled), this should be a fairly robust way of making sure that all the data in the combatParameters table is good data.
 
Is there anything important to know about the 3 rules files?

rules.txt (where I've been making changes)
rules_lst.txt
Rules_extra_info_in_names.txt
 
Also - attackMoveCost = 10,

Is this only if they make an interception or is this also where you are storing the new attack costs period? We no longer have bomb units and the units will attack directly - just curious in the case of bombers if I should be specifying this yet?

I've been setting up combatParameters and making a little progress on it tonight. I thought about using historic speeds and even went to gather some data but after looking at it, I think it'll be much easier to tweak/balance by just using 25+ increments for everything.

Edit -

Do I need to define pursuitSpeed separately if I'm filling out something for pursuitSpeedLow, HIgh, Night? Etc.
 
Is there anything important to know about the 3 rules files?

rules.txt (where I've been making changes)
rules_lst.txt
Rules_extra_info_in_names.txt
rules_lst.txt is a file that has the functionality described here. Basically, a way to get moderately powerful versions of Lua settings without learning Lua.

Rules_extra_info_in_names.txt This is an alternate file that I used as rules.txt when generating object.lua and object.js, giving terrain/unitTypes some more descriptive names.

Also - attackMoveCost = 10,

Is this only if they make an interception or is this also where you are storing the new attack costs period? We no longer have bomb units and the units will attack directly - just curious in the case of bombers if I should be specifying this yet?
This is the movement cost a unit expends when attacking. It was actually a bit of a trick to get it to work. I had to change unit type ranges to 1 at the end of combat sometimes, so the game wouldn't use them up. If the attackMoveCost is omitted, the unit used up all its movement points in the attack. This is probably adequate for bombers, unless you want them to be able to fly away after making the attack. I haven't implemented payloads yet, but that will restrict bomber attacks.

Edit -

Do I need to define pursuitSpeed separately if I'm filling out something for pursuitSpeedLow, HIgh, Night? Etc.
My code doesn't use pursuitSpeed/escapeSpeed anywhere, except as a default when constructing the combatParameter. You can omit it, as long as you change the valueSpecification for those two keys to allow for nil values as well.
Code:
pursuitSpeed = {["number"] = true, ["nil"]=true}
escapeSpeed = {["number"] = true, ["nil"]=true}
 
I finished adding all aircraft to combatParameters and putting in the attack movement speed for all (just using what it was in the last version). I'll use the same reaction range as the last version too when I have a chance to go through it. I figure we might as well go with what we've tested several times. I will then add in the pursuit speeds and such.

Since this isn't going to have a bearing on the few units that actually do carry a special munition (namely the pathfinder unit with window), I figure pathfinders might also bomb targets and basically be really good bombers. I'll bump up their cost accordingly so folks don't only go with them, or we might even have to limit how many can be built.
 
I take it the interception for bombers would be their defensive fire in the old version? I'm going ahead and putting in the ranges for that - if I'm wrong and it needs to be removed it won't be a big deal later.
 
I take it the interception for bombers would be their defensive fire in the old version? I'm going ahead and putting in the ranges for that - if I'm wrong and it needs to be removed it won't be a big deal later.
The interceptionRange lets units defend squares as if they were on that square. So, giving an interceptionRange > 0 to bombers means that they will defend neighbouring squares. We probably don't want that, since the bomber will defend the square for 3 rounds of combat, then try to 'escape' and, if successful in that escape, won't return to its original square.

At the moment, instead of having a fighter 'reaction', we let the fighter defend the weaker unit.

I think for bomber formations we're going to want to increase the defense and/or firepower of the bomber, and, maybe, reduce the chance to 'escape' if it isn't seriously damaged, so it doesn't break formation unnecessarily.
 
OK I'll edit that out then... I guess my concern is making there be a point to a formation in the first place (aside from keeping stack kills in the scenario, which I really think we have to in order to avoid 1 tile super stacks). The defensive fire range kind of did that so you'd get some overlap, but in any event, if this isn't the best module for that, no worries. I'll take them out.
 
From PMs:
I presume the 'key = numberSpec' is a result of copy/pasting and that you are gradually filling the fields in with actual numbers.
Code:
combatParameters[object.uMe109G14.id] = {
   pursuitSpeed = numberSpec,
    escapeSpeed = numberSpec,
    pursuitSpeedLow = numberSpec,
    pursuitSpeedHigh = numberSpec,
    pursuitSpeedNight = numberSpec,
    escapeSpeedLow = numberSpec,
    escapeSpeedHigh = numberSpec,
    escapeSpeedNight = numberSpec,
    interceptionRange = 2,
    attackMoveCost = 11,
}
JPetroski:
Yes - I've finished the interceptionRange and attackMoveCost but I'm still mulling over the others and how to handle it. I do think basic, simple, 25 point increments away from a "base" is probably better than using actual speeds (which are awkward because aircraft speed is reliant on alt, load out, and other factors, and also because they don't vary as much as you'd think). That might be a bit of a project.

I can always change the escape chance calculation to something that makes small differences in speeds more significant, if you would find it easier to use approximations of aircraft speed as a starting point.


I take it the interception for bombers would be their defensive fire in the old version? I'm going ahead and putting in the ranges for that - if I'm wrong and it needs to be removed it won't be a big deal later.

I think for bomber formations we're going to want to increase the defense and/or firepower of the bomber, and, maybe, reduce the chance to 'escape' if it isn't seriously damaged, so it doesn't break formation unnecessarily.
I think I might have a good (and relatively simple) way to handle support from other units.

Each unit has a "combatSupport" rating (or ratings, e.g. combatSupportHigh/Low/Climb/Dive/Night) which are added up when computing attack/defense values. Better aircraft just give more combatSupport. 1 combat support gives +1, the next 2 give another +1, then the next 4 give +1 and so on. With each doubling of 'combatSupport' giving another +1. This way, more aircraft attacking or defending is better, but the doubling effect for the next +1 means it won't get out of hand.

This would also mean that the attacker has to commit a group of aircraft to a particular battle to get combat bonuses, so you're penalised for sending 1 fighter at a time until you win, and then sending the rest to different fights.

OK I'll edit that out then... I guess my concern is making there be a point to a formation in the first place (aside from keeping stack kills in the scenario, which I really think we have to in order to avoid 1 tile super stacks). The defensive fire range kind of did that so you'd get some overlap, but in any event, if this isn't the best module for that, no worries. I'll take them out.
I just realised that with the current "escape" system, stack kills would be pretty hard to come by, since everyone would "escape" one by one.

I think the best way to deal with this is to limit the escape mechanic if units are stacked on the tile. One option would be to extend minimum combat for 1 or 2 rounds for each unit in the stack. A second option would be to lower defense values for units in a stack.

I'm inclined to think option 1 would be better. Stacking units would be like telling them to fly in close formation, so they are less likely to flee and break the formation. What do you think?
 
I think I might have a good (and relatively simple) way to handle support from other units.

Each unit has a "combatSupport" rating (or ratings, e.g. combatSupportHigh/Low/Climb/Dive/Night) which are added up when computing attack/defense values. Better aircraft just give more combatSupport. 1 combat support gives +1, the next 2 give another +1, then the next 4 give +1 and so on. With each doubling of 'combatSupport' giving another +1. This way, more aircraft attacking or defending is better, but the doubling effect for the next +1 means it won't get out of hand.

This would also mean that the attacker has to commit a group of aircraft to a particular battle to get combat bonuses, so you're penalised for sending 1 fighter at a time until you win, and then sending the rest to different fights.
I think this would work great - would give one a reason to form up interceptors and them formation move them towards the target before attacking with the first one as well, which is what would actually have happened.

I think the best way to deal with this is to limit the escape mechanic if units are stacked on the tile. One option would be to extend minimum combat for 1 or 2 rounds for each unit in the stack. A second option would be to lower defense values for units in a stack.

I'm inclined to think option 1 would be better. Stacking units would be like telling them to fly in close formation, so they are less likely to flee and break the formation. What do you think?

I also agree #1 would likely be better. One can't really make wild maneuvers if that close in formation anyway, so escaping wouldn't be as likely.
 
I'm currently working through the rules right now and adding in attack values and it got me thinking - in the old version we allowed multiple attacks by deducting MP every time a button was pressed - this also allowed units (like jets) to skiddadle out of danger after making an attack to represent their speed. Are we just going to use "escape speed" and such to represent this now? I'm assuming yes, but this also means units can't make multiple attacks per turn. I'm OK with this for now but we might need to rebalance how many units show up because I think if each unit is "1 attack only" we're probably going to run into issues - not sure if we make less units overall (probably better for playability) or more.
 
I'm currently working through the rules right now and adding in attack values and it got me thinking - in the old version we allowed multiple attacks by deducting MP every time a button was pressed - this also allowed units (like jets) to skiddadle out of danger after making an attack to represent their speed. Are we just going to use "escape speed" and such to represent this now? I'm assuming yes, but this also means units can't make multiple attacks per turn. I'm OK with this for now but we might need to rebalance how many units show up because I think if each unit is "1 attack only" we're probably going to run into issues - not sure if we make less units overall (probably better for playability) or more.
I've already gone to the trouble of adding a attackMoveCost for attacks, so that an attack by an airplane doesn't use up all its movement points in an attack. (But, by default, all points are used up.) So, we can have aircraft make multiple attacks same way as in the old version, and that was how I expected it to be done. My idea with speed was to give fast aircraft an advantage that wasn't strictly an increase in combat stats. For example, there was a bomber version of the Mosquito where they took off the guns to make it faster and able to outrun anything that could try to attack it. In this version, that aircraft would have no attack and low defense, but would be able to survive several attacks because each one would be short.
 
I pushed the changes to rules today that I made. The umlauts are showing up as weird symbols so I changed them. Not sure what's going on there.

(Take a gander and make sure I didn't screw this up. I note that in the github desktop I can see the changes in green so I think I did it right).

I will need to work on the speeds now.
 
Top Bottom