1
0
mirror of https://github.com/veden/Rampant.git synced 2025-01-14 02:23:01 +02:00

fixed visualizer and fixed resource unregister bug

This commit is contained in:
Aaron Veden 2019-10-15 17:32:13 -07:00
parent b032668f74
commit 173858ef14
5 changed files with 190 additions and 189 deletions

View File

@ -6,6 +6,7 @@ Date: 10. 13. 2019
- Squad formation now has a gathering delay to allow all members to make the initial group
Bugfixes:
- Fixed old savegames penalties having nil chunk index
- Fixed that mining a resource by hand unregister all remaining resource on a tile
---------------------------------------------------------------------------------------------------
Version: 0.17.28

View File

@ -595,10 +595,8 @@ local function onBuild(event)
local entity = event.created_entity or event.entity
if (entity.surface.index == natives.activeSurface) then
if (entity.type == "resource") and (entity.force.name == "neutral") then
-- print("registering resource", entity.name)
registerResource(entity, map)
else
-- print("registering entity", entity.name)
accountPlayerEntity(map, entity, natives, true, false)
if natives.safeBuildings then
if natives.safeEntities[entity.type] or natives.safeEntityName[entity.name] then
@ -614,7 +612,9 @@ local function onMine(event)
local surface = entity.surface
if (surface.index == natives.activeSurface) then
if (entity.type == "resource") and (entity.force.name == "neutral") then
unregisterResource(entity, map)
if (entity.amount == 0) then
unregisterResource(entity, map)
end
else
accountPlayerEntity(map, entity, natives, false, false)
end

View File

@ -475,12 +475,12 @@ constants.PATH_FINDER_SHORT_CACHE_SIZE = 25
constants.PATH_FINDER_LONG_REQUEST_RATIO = 5
constants.PATH_FINDER_MIN_STEPS_TO_CHECK_PATH = 1000
constants.MAX_FAILED_BEHAVIORS = 100
constants.MAX_FAILED_BEHAVIORS = 1000
constants.UNIT_GROUP_DISOWN_DISTANCE = 100
constants.UNIT_GROUP_TICK_TOLERANCE = 3600000
constants.UNIT_GROUP_MAX_RADIUS = 25
constants.UNIT_GROUP_MAX_RADIUS = 15
constants.UNIT_GROUP_MAX_SPEED_UP = 2
constants.UNIT_GROUP_MAX_SLOWDOWN = 1.0
constants.UNIT_GROUP_SLOWDOWN_FACTOR = 1.0

362
tests.lua
View File

@ -13,16 +13,16 @@ function tests.pheromoneLevels(size)
local playerChunkX = math.floor(player.position.x / 32) * constants.CHUNK_SIZE
local playerChunkY = math.floor(player.position.y / 32) * constants.CHUNK_SIZE
if not size then
size = 3 * constants.CHUNK_SIZE
size = 3 * constants.CHUNK_SIZE
else
size = size * constants.CHUNK_SIZE
size = size * constants.CHUNK_SIZE
end
print("------")
print(#global.map.processQueue)
print(playerChunkX .. ", " .. playerChunkY)
print("--")
for y=playerChunkY-size, playerChunkY+size,32 do
for x=playerChunkX-size, playerChunkX+size,32 do
for x=playerChunkX-size, playerChunkX+size,32 do
if (global.map[x] ~= nil) then
local chunk = global.map[x][y]
if (chunk ~= nil) then
@ -30,34 +30,34 @@ function tests.pheromoneLevels(size)
for i=1,#chunk do
str = str .. " " .. tostring(i) .. "/" .. tostring(chunk[i])
end
str = str .. " " .. "p/" .. game.surfaces[global.natives.activeSurface].get_pollution(chunk) .. " " .. "n/" .. chunkPropertyUtils.getNestCount(global.map, chunk) .. " " .. "w/" .. chunkPropertyUtils.getWormCount(global.map, chunk) .. " pg/" .. chunkPropertyUtils.getPlayerBaseGenerator(global.map, chunk)
if (chunk.x == playerChunkX) and (chunk.y == playerChunkY) then
print("=============")
print(chunk.x, chunk.y, str)
print("=============")
else
print(chunk.x, chunk.y, str)
end
-- print(str)
print("----")
str = str .. " " .. "p/" .. game.surfaces[global.natives.activeSurface].get_pollution(chunk) .. " " .. "n/" .. chunkPropertyUtils.getNestCount(global.map, chunk) .. " " .. "w/" .. chunkPropertyUtils.getWormCount(global.map, chunk) .. " pg/" .. chunkPropertyUtils.getPlayerBaseGenerator(global.map, chunk)
if (chunk.x == playerChunkX) and (chunk.y == playerChunkY) then
print("=============")
print(chunk.x, chunk.y, str)
print("=============")
else
print(chunk.x, chunk.y, str)
end
-- print(str)
print("----")
end
end
end
print("------------------")
print("------------------")
end
end
function tests.killActiveSquads()
print("--")
for i=1, #global.natives.squads do
local squad = global.natives.squads[i]
if (squad.group.valid) then
local members = squad.group.members
for x=1, #members do
local member = members[x]
local val = member.valid and member.die()
end
end
local squad = global.natives.squads[i]
if (squad.group.valid) then
local members = squad.group.members
for x=1, #members do
local member = members[x]
local val = member.valid and member.die()
end
end
end
end
@ -65,69 +65,69 @@ function tests.activeSquads()
print("-----")
print("Squads", #global.natives.squads)
for i=1, #global.natives.squads do
print("-")
print("-")
local squad = global.natives.squads[i]
local squadHealth = 0
local squadMakeup = {}
local squadHealth = 0
local squadMakeup = {}
if squad.group.valid then
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
print(math.floor(squad.group.position.x * 0.03125), math.floor(squad.group.position.y * 0.03125), squad.status, squad.group.state, #squad.group.members, squad.cycles, squad.group.group_number, squadHealth)
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
print(serpent.dump(squad))
end
end
print("---")
print("pending", #global.natives.pendingAttack)
for i=1, #global.natives.pendingAttack do
print("-")
print("-")
local squad = global.natives.pendingAttack[i]
local squadHealth = 0
local squadMakeup = {}
local squadHealth = 0
local squadMakeup = {}
if squad.group.valid then
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
print(math.floor(squad.group.position.x * 0.03125), math.floor(squad.group.position.y * 0.03125), squad.status, squad.group.state, #squad.group.members, squad.cycles, squadHealth, squad.group.group_number)
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
print(serpent.dump(squad))
end
end
print("---")
print("building", #global.natives.building)
for i=1, #global.natives.building do
print("-")
print("-")
local squad = global.natives.building[i]
local squadHealth = 0
local squadMakeup = {}
local squadHealth = 0
local squadMakeup = {}
if squad.group.valid then
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
for x=1,#squad.group.members do
local member = squad.group.members[x].prototype
if not squadMakeup[member.name] then
squadMakeup[member.name] = 0
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
squadHealth = squadHealth + member.max_health
squadMakeup[member.name] = squadMakeup[member.name] + 1
end
print(math.floor(squad.group.position.x * 0.03125), math.floor(squad.group.position.y * 0.03125), squad.status, squad.group.state, #squad.group.members, squad.cycles, squad.group.group_number, squadHealth)
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
-- print(serpent.dump(squadResistances))
print(serpent.dump(squadMakeup))
print(serpent.dump(squad))
end
end
@ -138,8 +138,8 @@ function tests.entitiesOnPlayerChunk()
local chunkX = math.floor(playerPosition.x * 0.03125) * 32
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
local entities = game.surfaces[global.natives.activeSurface].find_entities_filtered({area={{chunkX, chunkY},
{chunkX + constants.CHUNK_SIZE, chunkY + constants.CHUNK_SIZE}},
force="player"})
{chunkX + constants.CHUNK_SIZE, chunkY + constants.CHUNK_SIZE}},
force="player"})
for i=1, #entities do
print(entities[i].name)
end
@ -151,8 +151,8 @@ function tests.findNearestPlayerEnemy()
local chunkX = math.floor(playerPosition.x * 0.03125) * 32
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
local entity = game.surfaces[global.natives.activeSurface].find_nearest_enemy({position={chunkX, chunkY},
max_distance=constants.CHUNK_SIZE,
force = "enemy"})
max_distance=constants.CHUNK_SIZE,
force = "enemy"})
if (entity ~= nil) then
print(entity.name)
end
@ -180,10 +180,10 @@ function tests.fillableDirtTest()
local chunkX = math.floor(playerPosition.x * 0.03125) * 32
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
game.surfaces[global.natives.activeSurface].set_tiles({{name="fillableDirt", position={chunkX-1, chunkY-1}},
{name="fillableDirt", position={chunkX, chunkY-1}},
{name="fillableDirt", position={chunkX-1, chunkY}},
{name="fillableDirt", position={chunkX, chunkY}}},
false)
{name="fillableDirt", position={chunkX, chunkY-1}},
{name="fillableDirt", position={chunkX-1, chunkY}},
{name="fillableDirt", position={chunkX, chunkY}}},
false)
end
function tests.tunnelTest()
@ -199,7 +199,7 @@ function tests.createEnemy(x,d)
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
local a = {name=x, position={chunkX, chunkY}, force="enemy"}
if d then
a['direction'] = d
a['direction'] = d
end
return game.surfaces[global.natives.activeSurface].create_entity(a)
end
@ -207,13 +207,13 @@ end
function tests.registeredNest(x)
local entity = tests.createEnemy(x)
chunk.registerEnemyBaseStructure(global.map,
entity,
nil)
entity,
nil)
end
function tests.attackOrigin()
local enemy = game.surfaces[global.natives.activeSurface].find_nearest_enemy({position={0,0},
max_distance = 1000})
max_distance = 1000})
if (enemy ~= nil) and enemy.valid then
print(enemy, enemy.unit_number)
enemy.set_command({type=defines.command.attack_area,
@ -234,29 +234,29 @@ end
function tests.gaussianRandomTest()
local result = {}
for x=0,100,1 do
result[x] = 0
result[x] = 0
end
for _=1,10000 do
local s = mathUtils.roundToNearest(mathUtils.gaussianRandomRange(50, 25, 0, 100), 1)
result[s] = result[s] + 1
local s = mathUtils.roundToNearest(mathUtils.gaussianRandomRange(50, 25, 0, 100), 1)
result[s] = result[s] + 1
end
for x=0,100,1 do
print(x, result[x])
print(x, result[x])
end
end
function tests.reveal (size)
local pos = game.player.character.position
game.player.force.chart(game.player.surface,
{{x=-size+pos.x, y=-size+pos.y}, {x=size+pos.x, y=size+pos.y}})
{{x=-size+pos.x, y=-size+pos.y}, {x=size+pos.x, y=size+pos.y}})
end
function tests.baseStats()
local natives = global.natives
print ("x", "y", "distanceThreshold", "tick", "points", "temperament", "temperamentTick", "state", "stateTick", "alignments")
for i=1, #natives.bases do
local base = natives.bases[i]
print(base.x,
local base = natives.bases[i]
print(base.x,
base.y,
base.distanceThreshold,
base.tick,
@ -266,7 +266,7 @@ function tests.baseStats()
base.state,
base.stateTick,
serpent.dump(base.alignment))
print("---")
print("---")
end
end
@ -274,34 +274,34 @@ function tests.clearBases()
local surface = game.surfaces[global.natives.activeSurface]
for x=#global.natives.bases,1,-1 do
local base = global.natives.bases[x]
for c=1,#base.chunks do
local chunk = base.chunks[c]
chunkUtils.clearChunkNests(chunk, surface)
end
local base = global.natives.bases[x]
for c=1,#base.chunks do
local chunk = base.chunks[c]
chunkUtils.clearChunkNests(chunk, surface)
end
base.chunks = {}
base.chunks = {}
if (surface.can_place_entity({name="biter-spawner-powered", position={base.cX * 32, base.cY * 32}})) then
surface.create_entity({name="biter-spawner-powered", position={base.cX * 32, base.cY * 32}})
local slice = math.pi / 12
local pos = 0
for i=1,24 do
if (math.random() < 0.8) then
local distance = mathUtils.roundToNearest(mathUtils.gaussianRandomRange(45, 5, 37, 60), 1)
if (surface.can_place_entity({name="biter-spawner", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})) then
if (math.random() < 0.3) then
surface.create_entity({name="small-worm-turret", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})
else
surface.create_entity({name="biter-spawner", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})
end
end
end
pos = pos + slice
end
else
table.remove(global.natives.bases, x)
end
if (surface.can_place_entity({name="biter-spawner-powered", position={base.cX * 32, base.cY * 32}})) then
surface.create_entity({name="biter-spawner-powered", position={base.cX * 32, base.cY * 32}})
local slice = math.pi / 12
local pos = 0
for i=1,24 do
if (math.random() < 0.8) then
local distance = mathUtils.roundToNearest(mathUtils.gaussianRandomRange(45, 5, 37, 60), 1)
if (surface.can_place_entity({name="biter-spawner", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})) then
if (math.random() < 0.3) then
surface.create_entity({name="small-worm-turret", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})
else
surface.create_entity({name="biter-spawner", position={base.cX * 32 + (distance*math.sin(pos)), base.cY * 32 + (distance*math.cos(pos))}})
end
end
end
pos = pos + slice
end
else
table.remove(global.natives.bases, x)
end
end
end
@ -317,55 +317,55 @@ function tests.showBaseGrid()
local n = {}
for k,v in pairs(global.natives.bases) do
n[v] = k % 4
n[v] = k % 4
end
local chunks = global.map.chunkToBase
for chunk,base in pairs(chunks) do
local pick = n[base]
local color = "concrete"
if base.alignment == constants.BASE_ALIGNMENT_NE then
color = "hazard-concrete-left"
elseif (pick == 1) then
color = "water"
elseif (pick == 2) then
color = "deepwater"
elseif (pick == 3) then
color = "water-green"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
local pick = n[base]
local color = "concrete"
if base.alignment == constants.BASE_ALIGNMENT_NE then
color = "hazard-concrete-left"
elseif (pick == 1) then
color = "water"
elseif (pick == 2) then
color = "deepwater"
elseif (pick == 3) then
color = "water-green"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
end
end
function tests.showMovementGrid()
local chunks = global.map.processQueue
for i=1,#chunks do
local chunk = chunks[i]
local color = "concrete"
if (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_ALL_DIRECTIONS) then
color = "hazard-concrete-left"
elseif (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_NORTH_SOUTH) then
color = "deepwater"
elseif (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_EAST_WEST) then
color = "water-green"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
local chunk = chunks[i]
local color = "concrete"
if (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_ALL_DIRECTIONS) then
color = "hazard-concrete-left"
elseif (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_NORTH_SOUTH) then
color = "concrete"
elseif (chunkPropertyUtils.getPassable(global.map, chunk) == constants.CHUNK_EAST_WEST) then
color = "stone-path"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
end
end
function tests.colorResourcePoints()
local chunks = global.map.processQueue
for i=1,#chunks do
local chunk = chunks[i]
local color = "concrete"
if (chunk[constants.RESOURCE_GENERATOR] ~= 0) and (chunk[constants.NEST_COUNT] ~= 0) then
color = "hazard-concrete-left"
elseif (chunk[constants.RESOURCE_GENERATOR] ~= 0) then
color = "deepwater"
elseif (chunk[constants.NEST_COUNT] ~= 0) then
color = "water-green"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
local chunk = chunks[i]
local color = "concrete"
if (chunk[constants.RESOURCE_GENERATOR] ~= 0) and (chunk[constants.NEST_COUNT] ~= 0) then
color = "hazard-concrete-left"
elseif (chunk[constants.RESOURCE_GENERATOR] ~= 0) then
color = "deepwater"
elseif (chunk[constants.NEST_COUNT] ~= 0) then
color = "stone-path"
end
chunkUtils.colorChunk(chunk.x, chunk.y, color, game.surfaces[global.natives.activeSurface])
end
end
@ -375,7 +375,7 @@ function tests.entityStats(name, d)
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
local a = game.surfaces[global.natives.activeSurface].create_entity({name=name, position={chunkX, chunkY}})
if d then
a['direction'] = d
a['direction'] = d
end
print(serpent.dump(a))
a.destroy()
@ -384,10 +384,10 @@ end
function tests.exportAiState()
local printState = function ()
local chunks = global.map.processQueue
local s = ""
for i=1,#chunks do
local chunk = chunks[i]
local chunks = global.map.processQueue
local s = ""
for i=1,#chunks do
local chunk = chunks[i]
local base = chunkPropertyUtils.getChunkBase(global.map, chunk)
local alignmentCount = 0
@ -399,43 +399,43 @@ function tests.exportAiState()
end
end
s = s .. table.concat({chunk[constants.MOVEMENT_PHEROMONE],
chunk[constants.BASE_PHEROMONE],
chunk[constants.PLAYER_PHEROMONE],
chunk[constants.RESOURCE_PHEROMONE],
chunkPropertyUtils.getPassable(global.map, chunk),
chunk[constants.CHUNK_TICK],
chunkPropertyUtils.getPathRating(global.map, chunk),
chunk.x,
chunk.y,
chunkPropertyUtils.getNestCount(global.map, chunk),
chunkPropertyUtils.getWormCount(global.map, chunk),
chunkPropertyUtils.getRallyTick(global.map, chunk),
chunkPropertyUtils.getRetreatTick(global.map, chunk),
chunkPropertyUtils.getResourceGenerator(global.map, chunk),
chunkPropertyUtils.getPlayerBaseGenerator(global.map, chunk),
chunkPropertyUtils.getDeathGenerator(global.map, chunk),
s = s .. table.concat({chunk[constants.MOVEMENT_PHEROMONE],
chunk[constants.BASE_PHEROMONE],
chunk[constants.PLAYER_PHEROMONE],
chunk[constants.RESOURCE_PHEROMONE],
chunkPropertyUtils.getPassable(global.map, chunk),
chunk[constants.CHUNK_TICK],
chunkPropertyUtils.getPathRating(global.map, chunk),
chunk.x,
chunk.y,
chunkPropertyUtils.getNestCount(global.map, chunk),
chunkPropertyUtils.getWormCount(global.map, chunk),
chunkPropertyUtils.getRallyTick(global.map, chunk),
chunkPropertyUtils.getRetreatTick(global.map, chunk),
chunkPropertyUtils.getResourceGenerator(global.map, chunk),
chunkPropertyUtils.getPlayerBaseGenerator(global.map, chunk),
chunkPropertyUtils.getDeathGenerator(global.map, chunk),
game.surfaces[global.natives.activeSurface].get_pollution(chunk),
chunkPropertyUtils.getNestActiveness(global.map, chunk),
chunkPropertyUtils.getRaidNestActiveness(global.map, chunk),
#chunkPropertyUtils.getSquadsOnChunk(global.map, chunk),
alignmentCount}, ",") .. "\n"
end
game.write_file("rampantState.txt", s, false)
end
game.write_file("rampantState.txt", s, false)
end
return function(interval)
if not interval then
interval = 0
else
interval = tonumber(interval)
end
if not interval then
interval = 0
else
interval = tonumber(interval)
end
printState()
printState()
if (interval > 0) then
script.on_nth_tick(interval, printState)
end
if (interval > 0) then
script.on_nth_tick(interval, printState)
end
end
end
@ -446,16 +446,16 @@ function tests.createEnergyTest(x)
local chunkX = math.floor(playerPosition.x * 0.03125) * 32
local chunkY = math.floor(playerPosition.y * 0.03125) * 32
local entities = game.surfaces[global.natives.activeSurface].find_entities_filtered({area={{chunkX, chunkY},
{chunkX + constants.CHUNK_SIZE, chunkY + constants.CHUNK_SIZE}},
type = "electric-pole",
force="player"})
{chunkX + constants.CHUNK_SIZE, chunkY + constants.CHUNK_SIZE}},
type = "electric-pole",
force="player"})
-- for i=1, #entities do
-- print(entities[i].name)
-- end
local wires
if #entities > 0 then
entity.connect_neighbour(entities[1])
entity.connect_neighbour(entities[1])
end
-- if wires then
@ -474,14 +474,14 @@ function tests.unitGroupBuild()
local group = surface.create_unit_group({position={-32, -32}})
for i=1,10 do
group.add_member(surface.create_entity({name="small-biter", position={-32, -32}}))
group.add_member(surface.create_entity({name="small-biter", position={-32, -32}}))
end
group.set_command({
type = defines.command.build_base,
destination = {-64, -64},
distraction = defines.distraction.by_enemy,
ignore_planner = true
type = defines.command.build_base,
destination = {-64, -64},
distraction = defines.distraction.by_enemy,
ignore_planner = true
})
end

View File

@ -171,12 +171,12 @@
(refresh dc)
(thread (lambda ()
(sync (filesystem-change-evt "/data/games/factorio/script-output/rampantState.txt"))
(showVisual dc (readState "/data/games/factorio/script-output/rampantState.txt"))))))
(sync (filesystem-change-evt "/mnt/gallery/gameFiles/factorio/script-output/rampantState.txt"))
(showVisual dc (readState "/mnt/gallery/gameFiles/factorio/script-output/rampantState.txt"))))))
(define dcMap (first dcs))
(showVisual dcMap (readState "/data/games/factorio/script-output/rampantState.txt"))
(showVisual dcMap (readState "/mnt/gallery/gameFiles/factorio/script-output/rampantState.txt"))
(define (chunkX->screenX x)
(roundTo (* (normalize x minX maxX)