TUTORIAL: Unit with charging system and rechargeable

Contribute, display and discuss rulesets and modpacks for use in Freeciv here.
Post Reply
leo.priori
Posts: 15
Joined: Mon Jan 01, 2024 3:22 am

TUTORIAL: Unit with charging system and rechargeable

Post by leo.priori »

Level needed -
* Ruleset: Basics
* Scipt LUA: none, 0, nadica

Hey guys! Firstly, I want to thank all the developers, especially cazfi and Ignatus who helped me create the units I wanted and also make this tutorial possible.

Based on their idea, it is possible to make several units with different actions.
Their idea is simple, use extras (extra terrain) as counters and flags.
This way it is possible to count how many charges each unit spent.
To do this, the unit must be Unique ("Unique" flag).
This is because it is not possible to associate each unit with an extra.
So if the unit has the "Unique" flag, or if it is possible to create a unit per city, it is then possible to store the counter (unit charges) on the land of its hometown.
So, with a little scripting in Lua, i could make the following units:

Actions in cities with varied effects:
Marshall, Clown, TaxMen, Cleaners, General and Constructor.

Actions on units (using "Heal Unit" and "Heal Unit 2")
Healer, Doctor, Carpenter and Mechanic

Actions on tiles
Archaeologist (just like civ6) (my favorite one)

It is possible to control the effects in cities for several turns.
Different effects in each turn or in a range of turns.
Unfortunately I had to stop because I discovered the limit of 45 flags. So sad :(

So I want to share my script in LUA and how to use with your own units.

How it works:
1) The unit perform the action to a city, unit or a tile
2) If action is successful, the script places an extra in his hometown (pay one charge);
3) If the charges run out, the unit disbands;
4) If the unit is in the city with a specific building for a turn without moving, recharge completely.

Our example (Doctor) will have 5 charges, and can recharge them in a city with Temples.

So, briefly we need:

a) The unit
- Create 1 flag. Example: "Healer";
- Create 1 unit. Example: "The Doctors".

b) The charges
- Create 4 extras to control the charges (5 charges)

c) The effects
- Configure the effects.

d) The game
- Configure unit actions.

e) The script
- Copy, paste and cross your fingers.


To take a ruleset as a example, copy the entire classic folder and the classic.serv file
(usually located in /INSTALLED_FOLDER/share/freeciv/ ) to your own rules folder (usually (In linux) : /home/username/.freeciv/3.1)
I used 3.1 version, and I DONT believe that other previous versions (like 3.0) should work.
This is due to the fact that the "action_finished_unit_unit" function is from version 3.1

Lets begin!

a) The unit

Open the file units.ruleset, put the flag "Healer" in the "flags" property, right below "Bomber".

File: units.ruleset

Code: Select all

flags =
  { "name", "helptxt"
	... 
	... 
	_("Bomber"), _("Bad at attacking Fighters")
	_("Healer")
  }
And to make testing easier, copy the "Explorer" unit and paste it at the end of the file.
Change the "tech_req" property and the "obsolete_by" property to "None".
Add the "Healer" and "Unique" flags in the end of the "flags" property.
Comment the line that contains the "roles" property (add a semicolon at the beginning of the line).

File: units.ruleset

Code: Select all


; ----------------------------------------
;  UNIT The Doctors (copied from Explorer)
; ----------------------------------------

[unit_doctors]
name          = _("The Doctors")
tech_req      = "None"
obsolete_by   = "None"
...
...
flags         = "IgTer", "IgZOC", "NonMil", "HasNoZOC", "Healer", "Unique"
; roles         = "Explorer", "ExplorerStartUnit"  
...
...

; ----------------------------------------
b) The charges

Now let's create the charges for the unit.
In our example, we need 4 new extras to control its charges.
The rule is simple, 5 charges = 4 new extras. If you want 10 charges = 9 new extras

ATTENTION:
The "rule_name" property must start with the same name as the flag.
And end with "_CHARGE_1" or "_CHARGE_2" or "_CHARGE_3", etc...
Open the terrain.ruleset file and place at the end of it:

File: terrain.ruleset

Code: Select all


; ----------------------------------------
; DOCTOR - CHARGES 
; (5 charges => "_CHARGE_1" until "_CHARGE_4" )
; ----------------------------------------
[extra_healer_charges_1]
name         = _("Doctor used charge 1")
rule_name    = "Healer_CHARGE_1"
category     = "Bonus"
graphic      = "extra.ruins"
activity_gfx = "None"
rmact_gfx    = "None"
buildable    = FALSE
helptext     = ""
; ----------------------------------------
[extra_healer_charges_2]
name         = _("Doctor used charge 2")
rule_name    = "Healer_CHARGE_2"
category     = "Bonus"
graphic      = "extra.ruins"
activity_gfx = "None"
rmact_gfx    = "None"
buildable    = FALSE
helptext     = ""
; ----------------------------------------
[extra_healer_charges_3]
name         = _("Doctor used charge 3")
rule_name    = "Healer_CHARGE_3"
category     = "Bonus"
graphic      = "extra.ruins"
activity_gfx = "None"
rmact_gfx    = "None"
buildable    = FALSE
helptext     = ""
; ----------------------------------------
[extra_healer_charges_4]
name         = _("Doctor used charge 4")
rule_name    = "Healer_CHARGE_4"
category     = "Bonus"
graphic      = "extra.ruins"
activity_gfx = "None"
rmact_gfx    = "None"
buildable    = FALSE
helptext     = ""
; ----------------------------------------

c) The effects

Now open the file effects.ruleset.
Copy and paste all of it at the end of the file.
The first two effects are to control the movement after the action is successful.
Both units lose movement after successful action.
The next effect is to stipulate how much the unit will recover.
In this case 50%
And the last one is "User_Effect_1", which controls whether it can recharge

File: effects.ruleset

Code: Select all


; ----------------------------------------
; EFFECTS HEALER
; ----------------------------------------
[effect_action_success_heal_unit_actor]
type  = "Action_Success_Actor_Move_Cost"
value = 65535
reqs  = {
	"type",     "name",      "range", "present"
	"Action",   "Heal Unit", "Local", TRUE
}
; ----------------------------------------
[effect_action_success_heal_unit_target]
type  = "Action_Success_Target_Move_Cost"
value = 65535
reqs  = {
	"type",     "name",      "range", "present"
	"Action",   "Heal Unit", "Local", TRUE
}
; ----------------------------------------
[effect_action_heal_unit_recover]
type  = "Heal_Unit_Pct"
value = -50
reqs  = {
	"type",     "name",      "range", "present"
	"Action",   "Heal Unit", "Local", TRUE
}
; ----------------------------------------
[effect_action_heal_unit_recharge]
type  = "User_Effect_1"
value = 1
reqs  = {
	"type",         "name",            "range",   "present"
	"UnitState",    "MovedThisTurn",   "Local",   FALSE
	"MinMoveFrags", "1",               "Local",   TRUE
	"Extra",        "Healer_CHARGE_1", "Local",   TRUE
	"UnitFlag",     "Healer",          "Local",   TRUE
	"CityTile",     "Center",          "Local",   TRUE
	"Building",     "Temple",          "City",    TRUE
}
; ----------------------------------------


d) The game

Now, in the game.ruleset file, we cannot place the following codes at the end of the file as we did before.
We need to put in specific places that will be detailed below.
This next script should be placed inside the [actions] property.
Add the following line right below the "Keep Moving" action

ui_name_heal_unit = _("%sHEAL THIS UNIT%s")

File: game.ruleset

Code: Select all

...
...
...
; /* TRANS: Regular _Move (100% chance of success). */
ui_name_unit_move = _("%sKeep moving%s")

ui_name_heal_unit = _("%sHEAL THIS UNIT%s")
...
...
...
And a few lines below, add the following lines, right before the action "Sabotage City"

File: game.ruleset

Code: Select all

...
...
...
;
; */ <-- avoid gettext warnings

; ----------------------------------------
; ACTION  HEAL UNIT
; ----------------------------------------
[actionenabler_heal_unit]
action = "Heal Unit"
actor_reqs    =
    {"type",         "name",       "range", "present"
     "UnitFlag",     "Healer",     "Local", TRUE
     "MinMoveFrags", 1,            "Local", TRUE
     "DiplRel",      "Armistice",  "Local", FALSE
     "DiplRel",      "War",        "Local", FALSE
     "DiplRel",      "Cease-fire", "Local", FALSE
     "DiplRel",      "Peace",      "Local", FALSE
     "DiplRel",      "Never met",  "Local", FALSE
    }
target_reqs  =
    {"type",           "name",       "range", "present"
	 "MaxUnitsOnTile", 1,            "Local", TRUE
    }
; ----------------------------------------

[actionenabler_sabotage_city]
action = "Sabotage City"
...
...
...
e) The script

Last but not least, the LUA scripts.
Copy everything and paste it at the end of the script.lua file

File: script.lua

Code: Select all


--------------------------------------------------------------------------------
-- Units Flags and the charging system.

-- Place here the units that will use the charging system and how many charges each one has.
-- Remember! Only units that also have the "Unique" flag.

__flagsWithCharges = {
['Healer'] = 5, -- Unit: The Doctor with 5 charges
-- ['ReduceCrime'] = 3, -- Unit: The Marshall with 3 charges (EXAMPLE)
-- ['Happiness'] = 2, -- Unit: The Clown with 2 charges (ANOTHER EXAMPLE)
}

--------------------------------------------------------------------------------
-- This script is adjusted for actions on units. 
-- If the action is in the city or on the tile, the call is similar to the one below
--------------------------------------------------------------------------------
function action_finished_unit_unit_callback(action, result, actor, target)
	if action:rule_name() == 'Heal Unit' and result == true then
		manage_charges(actor)
	end
end
signal.connect('action_finished_unit_unit', 'action_finished_unit_unit_callback')
--------------------------------------------------------------------------------
function unit_built_callback(unit, city) 
	local nmFlag = getFlagUnitByExtras(unit, __flagsWithCharges)
	if nmFlag ~= '' then
		notify_charges_unit_built(unit, nmFlag)
	end
	return false
end
signal.connect('unit_built', 'unit_built_callback')
--------------------------------------------------------------------------------
function player_phase_begin_callback(player, new_phase) 
	notify_charges_remaining_all_units(player)
end
signal.connect('player_phase_begin', 'player_phase_begin_callback')
--------------------------------------------------------------------------------
function player_alive_phase_end_callback(player) 
	manage_charges_recharge(player)
end
signal.connect('player_alive_phase_end', 'player_alive_phase_end_callback')

--------------------------------------------------------------------------------
-- CHARGING SYSTEM
--------------------------------------------------------------------------------
function manage_charges(actor)

	-- check if the unit use the charging system
	local nmFlag = ''
	nmFlag = getFlagUnitByExtras(actor, __flagsWithCharges)

	-- if does, run the function 'manage_charges_actor'
	if nmFlag ~= '' then
		local disbanded = manage_charges_actor(actor, nmFlag)

		-- if run out charges, disband unit
		if disbanded == true then
			actor:kill('disbanded', actor.owner)
		end
	end

end
--------------------------------------------------------------------------------
function manage_charges_actor(actor, nmFlag)  
	local city = actor:get_homecity()
	local disbanded = false

	if city then
		local nmFlagFinal = ''
		local nmFlagFinalC = ''
		local nmFlagFinalT = ''
		local hasExtraC = false
		local hasExtraT = false
		local totalCharges = __flagsWithCharges[nmFlag]

		nmFlagFinal = nmFlag .. '_CHARGE_'
		nmFlagFinalC = nmFlagFinal .. '1'
		nmFlagFinalT = nmFlagFinal .. totalCharges - 1

		-- Is it the first charge or still rolling?
		hasExtraC = city.tile:has_extra(nmFlagFinalC) 

		-- Is it in the end? (if it is, so it will be used and will run out)
		hasExtraT = city.tile:has_extra(nmFlagFinalT) 


		if hasExtraC then
			if hasExtraT then

				-- end of charges (remove all extras)
				rmvExtrasTile(nmFlagFinal, totalCharges, city.tile)
				notify_charges_run_out(actor)
				disbanded = true

			else

				-- pay one more charge (add one more extra)
				addExtrasTile(nmFlagFinal, totalCharges, city.tile)
				notify_charges_used(actor)

			end
		else
			-- begin the counter (first charge used)
			city.tile:create_extra(nmFlagFinalC)
			notify_charges_first(actor)
		end
		if disbanded == false then

			-- show how many charges left
			notify_charges_show(actor, nmFlag, totalCharges)
		end
	end
	return disbanded
end
--------------------------------------------------------------------------------
function manage_charges_recharge(player)
	local txtNacao = player.nation:name_translation()
	if txtNacao ~= "Animal Kingdom" and 
		txtNacao ~= "Barbarian" and 
		txtNacao ~= "Pirate" then
		local unit
		for unit in player:units_iterate() do

			-- check if the unit use the charging system
			local nmFlag = getFlagUnitByExtras(unit, __flagsWithCharges)
			if nmFlag ~= '' then
				local recharge = 0

				-- Check if the effect 'User_Effect_1' returns POSITIVE (1). 
				-- This means that the unit is in the city with the Temple 
				-- and has not moved this turn
				recharge = effects.unit_bonus(unit, unit.owner, 'User_Effect_1')
				if recharge > 0 then
					local city = unit:get_homecity()
					if city then

						-- remove all the extras from the homecity
						local totalCharges = __flagsWithCharges[nmFlag]
						rmvExtrasTile(nmFlag .. '_CHARGE_', totalCharges, city.tile)
						notify_charges_recharge(unit, nmFlag)
					end
				end
			end
		end
	end
end
--------------------------------------------------------------------------------
function getFlagUnitByExtras(unit, extras)
	local nmFlag
	local total
	local nmFlag_final = ''
	for nmFlag, total in pairs(extras) do
		if unit.utype:has_flag(nmFlag) then
			nmFlag_final = nmFlag
			break
		end
	end
	return nmFlag_final
end
--------------------------------------------------------------------------------
function addExtrasTile(nmFlag, total, tile)
	local pos
	for pos = 1, total do
		if not tile:has_extra(nmFlag .. pos) then
			tile:create_extra(nmFlag .. pos) 
			return pos
		end
	end
end
--------------------------------------------------------------------------------
function rmvExtrasTile(nmFlag, total, tile)
	local pos
	for pos = 1, total do
		if tile:has_extra(nmFlag.. pos) then
			tile:remove_extra(nmFlag .. pos) 
		end
	end
end
--------------------------------------------------------------------------------
function getChargesRemaining(actor, nmFlag, totalCharges)
	local pos = 0
	local total = 0
	local pos_final = 0
	local city = actor:get_homecity()
	if city then
		for pos = totalCharges, 1, -1 do
			local hasExtra = city.tile:has_extra(nmFlag .. '_CHARGE_' .. pos)
			if hasExtra then
				pos_final = pos
				break
			end
		end
	end
	total = totalCharges - pos_final
	return total

end
--------------------------------------------------------------------------------
function notify_charges_remaining_all_units(player)
	local unit
	local city
	local totalCharges 
	if player:is_human() then
		for unit in player:units_iterate() do
			city = unit:get_homecity()
			if city then 
				for nmFlag, totalCharges in pairs(__flagsWithCharges) do
					local useChargeSystem = unit.utype:has_flag(nmFlag)
					if useChargeSystem == true then
						notify_charges_show(unit, nmFlag, totalCharges)
					end
				end
			end
		end
	end
end
--------------------------------------------------------------------------------
function notify_charges_run_out(actor)
	local msg = 'The unit %s exhausted its charges and was disbanded.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text())
end
--------------------------------------------------------------------------------
function notify_charges_used(actor)
	local msg = 'The unit %s used a charge.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text())
end
--------------------------------------------------------------------------------
function notify_charges_first(actor)
	local msg = 'The unit %s has just used its first charge.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text())
end
--------------------------------------------------------------------------------
function notify_charges_last(actor)
	local msg = 'The unit %s has its LAST charge.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text())
end
--------------------------------------------------------------------------------
function notify_charges_remaining(actor, charges)
	local msg = 'The unit %s has %d charge(s) left.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text(), charges)
end
--------------------------------------------------------------------------------
function notify_charges_recharge(actor, nmFlag)
	local msg = 'The unit %s has recharged its charges. Now it has %d charge(s) left.'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text(), 
		__flagsWithCharges[nmFlag])
end
--------------------------------------------------------------------------------
function notify_charges_unit_built(actor, nmFlag)
	local msg = 'The unit %s has just been built. It has %d charge(s).'
	notify.event(actor.owner, actor.tile, E.UNIT_ACTION_ACTOR_SUCCESS, msg, actor:link_text(), 
		__flagsWithCharges[nmFlag])
end
--------------------------------------------------------------------------------
function notify_charges_show(actor, nmFlag, totalCharges)
	local charges = getChargesRemaining(actor, nmFlag, totalCharges)
	if charges ~= totalCharges then 
		if charges == 1 then
			notify_charges_last(actor)
		end
	end	
	if charges ~= 1 then
		notify_charges_remaining(actor, charges)
	end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- EDIT: add the functions below to control when the unit or the city is lost
--------------------------------------------------------------------------------
function unit_lost_callback(unit, player, reason)
	local txtNacao = player.nation:name_translation()
	if txtNacao ~= "Animal Kingdom" and txtNacao ~= "Barbarian" and txtNacao ~= "Pirate" then
		manage_charges_unit_lost(unit)
	end
end
signal.connect('unit_lost', 'unit_lost_callback')
--------------------------------------------------------------------------------
function city_transferred_callback(city, loser, winner, reason)
	manage_charges_city_lost(city)
end
signal.connect('city_transferred', 'city_transferred_callback')
--------------------------------------------------------------------------------
function manage_charges_city_lost(city)
	local nmFlag = ''
	for nmFlag, total in pairs(__flagsWithCharges) do
		if city.tile:has_extra(nmFlag .. '_CHARGE_1') then
			rmvExtrasTile(nmFlag .. '_CHARGE_', __flagsWithCharges[nmFlag], city.tile)
		end
	end
end
--------------------------------------------------------------------------------
function manage_charges_unit_lost(unit)
	local nmFlag = ''
	nmFlag = getFlagUnitByExtras(unit, __flagsWithCharges)
	if nmFlag ~= '' then
		local city = unit:get_homecity()
		if city then
			if city.tile:has_extra(nmFlag .. '_CHARGE_1') then
				rmvExtrasTile(nmFlag .. '_CHARGE_', __flagsWithCharges[nmFlag], city.tile)
			end
		end
	end
end
--------------------------------------------------------------------------------


Oh! About the Archaeologist. The script is easy, After removing the extra(artifact) from the tile, the script creates a building in his hometown "GreatWonder" called "Display Fossil Mesozoic Era" or "Display Weapons 10,000 years old" , etc
So a made my own tileset(copied from hexemplio of course), create a LOT of buildings, and grab a LOT of images from internet about artifacts, fossil, etc. Soooo cool!

I hope this helps someone
See ya
Last edited by leo.priori on Mon Apr 08, 2024 6:07 pm, edited 1 time in total.
leo.priori
Posts: 15
Joined: Mon Jan 01, 2024 3:22 am

Re: TUTORIAL: Unit with charging system and rechargeable

Post by leo.priori »

EDIT:

I noticed that the part when the unit or city is lost was missing.
It is already fixed and the scripts have already been included in the scripts above.

I want to remind you that any suggestions, improvements, tips, please feel free to talk, don't be shy. I'm sure the scripts can be improved for those who don't understand programming.

Question for Forum Administrators. Should I change this post to Contribution?
Post Reply