New features via Lua script

Can you help improve your favourite game? Hardcore C mages, talented artists, and players with any level of experience are welcome!
Ignatus
Elite
Posts: 644
Joined: Mon Nov 06, 2017 12:05 pm
Location: St.Petersburg, Russia
Contact:

Re: New features via Lua script

Post by Ignatus »

Molo_Parko wrote: Sat Nov 25, 2023 9:14 pm AiTribesCityDefender Forces computer-controlled tribes to keep a unit in every city. If the only unit in a city moves out of the city, it is moved back into the city.
What about :movement_disallow() method?
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

^ I use Freeciv 2.6.4 which does not have that feature. It's only available from Freeciv 3.0 and later. I could add it as alternate method when Freeciv version is 3 or more though.

EDIT: Now that I think about it, that might hinder the computer-controlled tribe from choosing a better defender -- since the Warriors can't leave, they might not also build a Phalanx, Pikemen, Musketeer, or Rifleman unit to defend the city when those options are available. The way the current script handles it is to only prevent the -last- unit in a city from leaving, so if a Warriors is in the city, and a Musketeers is built, then the Warriors can leave or be disbanded without triggering the script since the city isn't empty because a better unit is in it.
Last edited by Molo_Parko on Thu Nov 30, 2023 8:21 pm, edited 1 time in total.
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

RailMoveLimit limits land units to maximum 9 tile moves per turn -- even on Railroad.

Code: Select all

--##############################################################################
function Lua_RailMoveLimitRefresh(turn, year)

	-- Clear and re-create the global table of unit RailMoves
	RailMoves=nil RailMoves={}

end
signal.connect("turn_started", "Lua_RailMoveLimitRefresh")


--##############################################################################
function Lua_RailMoveLimit(unit, src_tile, dst_tile)

	-- All land units limited to maximum 9 tiles of movement on Railroad

	if ( RailMoveLimit == nil ) then RailMoveLimit=true end
	if not ( RailMoveLimit ) then return end

	-- Prevents looping forever
	if ( RailMoveLimitInProgress == nil ) then RailMoveLimitInProgress=false end
	if ( RailMoveLimitInProgress ) then return end
	RailMoveLimitInProgress=true

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","RailMoveLimit BEGIN") end

	-- Freeciv 2.6.4 Lua can't determine whether a unit's class is land, sea, or air
	if ( unit.utype.id == find.unit_type("Fighter").id ) 
	or ( unit.utype.id == find.unit_type("Bomber").id ) 
	or ( unit.utype.id == find.unit_type("Helicopter").id ) 
	or ( unit.utype.id == find.unit_type("Stealth Fighter").id ) 
	or ( unit.utype.id == find.unit_type("Stealth Bomber").id ) 
	or ( unit.utype.id == find.unit_type("Cruise Missile").id )
	or ( unit.utype.id == find.unit_type("Nuclear").id )
	or ( unit.utype.id == find.unit_type("AWACS").id ) then 
		return
	end

	if ( RailMoves[(unit).id] == nil ) then RailMoves[(unit).id]=0 end

	if ( (src_tile):has_extra("Railroad") ) 
	and ( (dst_tile):has_extra("Railroad") ) then 

		-- Each rail-to-rail tile movement counts toward 9 total
		RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )

		-- Every 3rd rail-to-rail move should use 1 fragment of unit movement
		if ( RailMoves[(unit).id] % 3 == 0 ) then 
			edit.unit_move(unit,dst_tile,1)
		end

		-- Upper limit
		if ( RailMoves[(unit).id] >= 9 ) then 
			-- Remove all unit movement points
			edit.unit_move(unit,dst_tile,99)
		end

	else 
		-- Every move counts toward 9 maximum moves on Railroad
		RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )
	end

	if ( debugLuaScripts ) then log.error("%s","RailMoveLimit END") end
	RailMoveLimitInProgress=false
end
signal.connect("unit_moved", "Lua_RailMoveLimit")
Attached Scenario file "RandoMap" has only the RailMoveLimit Lua scripts for testing.
Attachments
RandoMap with RailMoveLimit ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(8.89 KiB) Downloaded 242 times
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

ExtrasUpkeep charges each player for tile extras within the radius of the player's cities. I'm still experimenting with the costs (in gold) per tile-extra-type. Here's the current list:
-- Upkeep cost per tile extra
cityExtraCosts={
["Airbase"]=1,
["Buoy"]=.1,
["Fallout"]=0,
["Farmland"]=.3,
["Fortress"]=.6,
["Hut"]=0,
["Irrigation"]=.07,
["Mine"]=.09,
["Oil Well"]=.3,
["Pollution"]=0,
["Railroad"]=.5,
["River"]=0,
["Road"]=.05,
["Ruins"]=-.1
} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)
Image
Image

Code: Select all

--##############################################################################
function Lua_ExtrasUpkeep(turn, year)

	if ( ExtrasUpkeep == nil ) then ExtrasUpkeep=true end
	if not ( ExtrasUpkeep ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","ExtrasUpkeep BEGIN") end

	if ( _G.xSize == nil ) then

		-- declare local variables
		local i, topology, count, field, topoFields

		topology=(server.setting.get("topology"))
		_G.xSize=tonumber(server.setting.get("xsize"))
		_G.ySize=tonumber(server.setting.get("ysize"))
		_G.dispersion=(server.setting.get("dispersion"))
		_G.citymindist=(server.setting.get("citymindist"))
		_G.topo="classic"
		_G.wrapX=false
		_G.wrapY=false
		topoFields={}

		-- Split topology field (from scenario settings) to a table
		local count=1
		for field in string.gmatch(topology, "[^|]+") do
			topoFields[count]=field
			count=(count+1)
		end

		-- Evaluate topology settings
		if ( #topoFields > 0 ) then
			for i=1,#topoFields,1 do
				if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true 
				elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true 
				elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO" 
				elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
			end
		end
	end

	-- Declare global function
	function _G.is_XY_map_location_valid(x,y)
		if ( _G.wrapX and _G.wrapY ) 
		or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) ) 
		or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) ) 
		or ( y >= 0 and y < _G.ySize and x >= 0  and x < _G.xSize ) then
			return true
		else
			return false
		end
	end

	local i, h, v, x, y, player, cityTile, city, tile, label, value, upkeepCostTotal, upkeepCost, payment, previousDebtBalance
	local cityExtras={}
	if ( _G.Debt == nil ) then _G.Debt={} end

	-- Skip turn 0
	if ( turn < 1 ) then 
		if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep Not active yet END") end
		return
	end

	-- Upkeep cost per tile extra
	cityExtraCosts={ 
		["Airbase"]=1,
		["Buoy"]=.1,
		["Fallout"]=0,
		["Farmland"]=.3,
		["Fortress"]=.6,
		["Hut"]=0,
		["Irrigation"]=.07,
		["Mine"]=.05,
		["Oil Well"]=.3,
		["Pollution"]=0,
		["Railroad"]=.03, 
		["River"]=0,
		["Road"]=.01,
		["Ruins"]=-.1
	} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)

	for player in players_iterate() do

		-- Reset some variables for each player
		previousDebtBalance=0
		payment=0
		upkeepCost=0
		upkeepCostTotal=0
		if ( _G.Debt[player] == nil ) then _G.Debt[player]=0 end
		cityExtras={ ["Irrigation"]=0,["Mine"]=0,["Oil Well"]=0,["Pollution"]=0,["Hut"]=0,
		["Farmland"]=0,["Fallout"]=0,["Fortress"]=0,["Airbase"]=0,["Buoy"]=0,["Ruins"]=0,
		["Road"]=0,["Railroad"]=0,["River"]=0 }

		for city in (player):cities_iterate() do
			cityTile=(city).tile
			tile=cityTile
			x=(tile).x
			y=(tile).y

			-- Check 21 tiles in city radius for tile extras
			h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
			v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
			for i=1,#h,1 do
				if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
					tile=find.tile( ( h[i] + x ),( v[i] + y ) )
					for label, value in pairs(cityExtras) do
						if ( (tile):has_extra(label) ) then
							cityExtras[label]=( cityExtras[label] + 1 )
						end
					end
				end
			end
		end

		if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
			log.error("%s","\tCity tile extras upkeep expense for:" .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] )
		end

		for label, value in pairs(cityExtras) do
			if ( cityExtras[label] > 0 ) then
				upkeepCost=( (cityExtras[label]) * cityExtraCosts[label] )
				if ( (player):is_human() ) then
					log.error("%s","\t" .. label .. "\tqty:" .. cityExtras[label] .. " @ " .. cityExtraCosts[label] .. "=" .. upkeepCost )
				end
				upkeepCostTotal=( upkeepCostTotal + upkeepCost )
			end
		end

	-- Add the cost to the player's account
	previousDebtBalance=_G.Debt[player]
	_G.Debt[player]=( upkeepCostTotal + _G.Debt[player] )

	-- If player has gold and debt, deduct gold for debt payment
		if ( (player):gold() > 1 ) and ( _G.Debt[player] > 1 ) then
			if ( (player):gold() > _G.Debt[player] ) then
				payment=math.floor( _G.Debt[player] )
			else
				payment=math.floor( (player):gold() )
			end
			_G.Debt[player]=( _G.Debt[player] - payment )
			-- the next command uses "change by" amount rather than "change to" amount
			edit.change_gold( player, -payment )
		end

		if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
			message="\n\tINVOICE - Turn " .. turn .. "- Tile extra upkeep costs for cities owned by: " .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] .. "\n\t\tPrior balance:\t\t\t" .. previousDebtBalance .. "\n\t\tNew charges:\t\t\t+" .. upkeepCostTotal .. "\n\t\tPayment extracted:\t-" .. payment .. "\n\t\tNew balance:\t\t\t" .. _G.Debt[player] .. " gold owed.\n"
			log.error("%s",message)
    			--notify.event(player, nil, E.SCRIPT,message.."\n\tItemization available in Chat pane.")
		end


	end

	if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep END") end

end
signal.connect("turn_started", "Lua_ExtrasUpkeep")
^ Updated 2023-12-04 -- Set math.floor to ensure that payments extracted are in whole units of gold, since decimals don't actually work!

One issue is that tiles that exist in more than one city's radius are charged against the player once per each city. Instead of specifying cost in gold, it might be interesting to specify it in terms of labor-hours, and then calculate gold per labor hour, while considering government type. So in Despotism, a job that could be done in 1 turn might take 3 turns in Democracy (due to 8 hour workdays) and also might cost 100x as much per labor hour...

Attached is a playable scenario file "RandoMap with ExtrasUpkeep only" for Freeciv 2.6.4 and later for testing the routine, and which contains only this script and no others.
Attachments
RandoMap with ExtrasUpkeep ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(11.24 KiB) Downloaded 236 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:48 pm, edited 5 times in total.
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

DecayingExtras -- extras on tiles which are not currently within any city radius decay over time. This is a companion script to "ExtrasUpkeep" which charges for upkeep of extras within any city radius.

Code: Select all

--##############################################################################
function Lua_DecayingExtras(turn, year)

	if ( DecayingExtras == nil ) then DecayingExtras=true end
	if not ( DecayingExtras ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","DecayingExtras BEGIN") end

	if ( turn == 0 ) and ( _G.xSize == nil ) then

		-- declare local variables
		local i, topology, count, field, topoFields

		topology=(server.setting.get("topology"))
		_G.xSize=tonumber(server.setting.get("xsize"))
		_G.ySize=tonumber(server.setting.get("ysize"))
		_G.dispersion=(server.setting.get("dispersion"))
		_G.citymindist=(server.setting.get("citymindist"))
		_G.topo="classic"
		_G.wrapX=false
		_G.wrapY=false
		topoFields={}

		-- Split topology field (from scenario settings) to a table
		local count=1
		for field in string.gmatch(topology, "[^|]+") do
			topoFields[count]=field
			count=(count+1)
		end

		-- Evaluate topology settings
		if ( #topoFields > 0 ) then
			for i=1,#topoFields,1 do
				if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true 
				elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true 
				elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO" 
				elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
			end
		end
	end

	-- Declare global function
	function _G.is_XY_map_location_valid(x,y)
		if ( _G.wrapX and _G.wrapY ) 
		or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) ) 
		or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) ) 
		or ( y >= 0 and y < _G.ySize and x >= 0  and x < _G.xSize ) then
			return true
		else
			return false
		end
	end

	-- declare local variables
	local i, j, h, v, x, y, ib, jb, tile

	-- The order matters. Railroad before road prevents leaving a tile with an Extra missing a prerequisite
	local decayingExtras={"Railroad", "Road", "Farmland", "Irrigation", "Mine", "Oil Well", "Airbase", "Buoy", "Fortress"}

	-- Scenario setting "borders" defaults to Enabled (in some Fc version?)
	-- Possible values are nil, SEE_INSIDE, EXPAND, ENABLED, DISABLED
	_G.Borders=server.setting.get("borders")
	if ( _G.Borders == nil ) then
		_G.Borders="ENABLED"
	end

	-- Select a random tile within each 5x5 area of the map (non-overlapping)
	for ib=0,(_G.ySize-1),5 do
		for jb=0,(_G.xSize-1),5 do
			x=random(jb,(jb+5))
			y=random(ib,(ib+5))

			if ( x > _G.xSize-1 ) or ( y > _G.ySize-1 ) then
				goto continue_1
			end

			-- Check 21 tiles radius for any city
			h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
			v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
			for i=1,#h,1 do
				if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
					tile=find.tile( ( h[i] + x ),( v[i] + y ) )
					if ( (tile):city() ) then goto continue_1 end
				end
			end

			-- If not within any city radius
			tile=find.tile(x,y)
			for i=1,#decayingExtras,1 do
				if ( (tile):has_extra(decayingExtras[i]) ) then
					-- It should be VERY likely that tile extras outside city radius will fall apart
					if ( random(1,10) > 1 ) then
						-- ^ 90% chance
						-- Remove the first matching extra
						edit.remove_extra(tile,decayingExtras[i])
						goto continue_1
					end
				end
			end
		::continue_1::
		end
	end

	if ( debugLuaScripts ) then log.error("%s","\tDecayingExtras End") end

end
signal.connect("turn_started", "Lua_DecayingExtras")
^ Updated 2023-12-04

The attached playable scenario for Freeciv 2.6.4 or later "RandoMap with DecayingExtras only" has only this script for testing. To test, start the scenario game and click "Turn done" repeatedly while watching areas of the map outside of any city radius.
Attachments
RandoMap with DecayingExtras ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(11.48 KiB) Downloaded 249 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:44 pm, edited 2 times in total.
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

CamelSprint allows a caravan's camel to rest at a distance, and then to sprint forward as many tiles as turns rested, to a target tile. This allows a caravan to reach a foreign city to create a trade-route without being in the tribe's territory at turn end. After the camel sprints forward to the target tile, select the caravan and establish the trade route.

Code: Select all

--##############################################################################
function Lua_CamelSprint(x, y, x2, y2)

	-- This routine is activated manually by user calling it
	CamelSprint=true

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","CamelSprint BEGIN") end

	local i, unit, tile1, tile2, distance

	-- Prepare table for up to 100 camels max
	if ( _G.camels == nil ) then
		_G.camels={}
	end

	tile1=find.tile(x,y)
	tile2=find.tile(x2,y2)
	distance=math.sqrt((tile1):sq_distance(tile2))

	if ( (tile2).terrain:class_name() ~= "Land" ) then
		log.error("%s","\tCamelSprint: Destination tile must be of class Land")
		return	
	end

	log.error("%s","\tCamelSprint: "..tostring(tile1).." "..tostring(tile2).." "..distance)

	if ( (tile1):num_units() > 0 ) then
		for unit in (tile1):units_iterate() do
			if ( (unit).utype == find.unit_type("Caravan") ) then
				i=( #_G.camels + 1 )
				_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(distance-1)}
			end
		end
	end

	if ( debugLuaScripts ) then log.error("%s","CamelSprint END") end
end


--##############################################################################
function Lua_CamelSprintGo(turn, year)

	-- If this routine is disabled then just return
	if ( CamelSprint == nil ) then return end
	if not ( CamelSprint ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","CamelSprintGo BEGIN") end

	local i, unit, tile1, tile2, turns, x, y, x2, y2, removeFromList

	for i=1,#_G.camels,1 do

		if ( _G.camels[i].unit == nil ) then break end

		removeFromList=false

		unit=_G.camels[i].unit
		tile1=_G.camels[i].tile1
		tile2=_G.camels[i].tile2
		turns=_G.camels[i].turns
		log.error("%s","\tCamelSprintGo: unit=" .. tostring(unit) .. " tile1=" .. tostring(tile1) .. " tile2=" .. tostring(tile2) .. " turns=" .. tostring(turns))

		if ( (unit).tile ~= tile1 ) then
			log.error("%s","\tCamelSprintGo CANCELLED: Camel " .. tostring(unit) .. " no longer resting at tile: " .. tostring(tile1) )
			removeFromList=true
		end

		if ( ( turns > 0 ) and ( removeFromList == false ) ) then
			_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(turns-1)}
		else
			edit.unit_move(unit,tile2,0)
			removeFromList=true
		end

		if ( removeFromList ) then
			_G.camels[i]=nil
			_G.camels=(table.pack(table.unpack(_G.camels)))
		end
	end

	if ( debugLuaScripts ) then log.error("%s","CamelSprintGo END") end

end
signal.connect("turn_started", "Lua_CamelSprintGo")
^ Updated 2023-12-05

Image
^ Caravan is 6 tiles from Dire Dawa and wishes to avoid being in the other tribe's territory during any "turn end" to preserve peaceful relations.

Image
^ CamelSprint Lua script called with the x,y values of the Caravan's current tile and the target tile.

Image
^ Caravan rests for 6 turns (Sentry the unit if you like) and then sprints forward to Dire Dawa in a single turn!

Image
^ Select the Caravan within Dire Dawa and establish a trade route.

Image
^ Trade route established without the Caravan being in any foreign tribe's territory at turn end.
Attachments
RandoMap with CamelSprint ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(10.3 KiB) Downloaded 246 times
Last edited by Molo_Parko on Tue Dec 05, 2023 4:51 pm, edited 2 times in total.
Molo_Parko
Hardened
Posts: 158
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

Emulate some commands from newer versions of Freeciv implements some commands from Freeciv 3.2 in Freeciv version 2.4 or later so that those commands work in either version. Currently limited to Freeciv version related stuff.

Code: Select all

--##############################################################################
-- Emulate some commands from newer versions of Freeciv
if ( tonumber(string.sub(fc_version(),17,19)) >= 2.4 ) and ( tonumber(string.sub(fc_version(),17,19)) < 3.2 ) then

	-- Emulate Fc 3.2 "version_string()"
	if ( version_string == nil ) then
		function version_string()
			return string.sub(fc_version(),17)
		end
	end

	-- Emulate Fc 3.2 "name_version()"
	if ( name_version == nil ) then
		function _G.name_version()
			return fc_version()
		end
	end

	-- Emulate Fc 3.2 "comparable_version()"
	if ( comparable_version == nil ) then
		function _G.comparable_version()
			local text, count, field, fields
			fields={}
			text=string.sub(fc_version(),17)
			count=1
			for field in ( string.gmatch(text, "[^.]+") ) do
				fields[count]=field
				count=(count+1)
			end
			if ( #fields > 0 ) then
				return (fields[1].."."..fields[2])
			else
				return nil
			end
		end
	end

	-- Emulate Fc 3.2 "versions_compare()"
	if ( versions_compare == nil ) then
		function _G.versions_compare(v1,v2)
			if ( v1 == v2 ) then return 0
			elseif ( v1 > v2 ) then return -1 -- Totally guessing at what "returns a negative" meant
			else return 1
			end
		end
	end

end
Image
Post Reply