diff --git a/.tinyci b/.tinyci index b97ba3a5f0..1a531096be 100644 --- a/.tinyci +++ b/.tinyci @@ -4,6 +4,8 @@ enabled = True [build-linux] directory = _build_native commands = + git -C ../../SeriousProton checkout ECS + git -C ../../SeriousProton pull rm -rf script_reference.html cmake .. -G Ninja -DSERIOUS_PROTON_DIR=../../SeriousProton -DCMAKE_BUILD_TYPE=RelWithDebInfo ninja -j 10 package diff --git a/CMakeLists.txt b/CMakeLists.txt index 44d719d767..52232ba2b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ if(APPLE) message(STATUS "Mac deployment target is: $ENV{MACOSX_DEPLOYMENT_TARGET}") endif() -cmake_minimum_required(VERSION 3.8 FATAL_ERROR) +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) @@ -212,36 +212,35 @@ set(GUI_LIB_SOURCES ) set(MAIN_SOURCES + src/init/config.h + src/init/config.cpp + src/init/resources.h + src/init/resources.cpp + src/init/displaywindows.h + src/init/displaywindows.cpp + src/init/ecs.h + src/init/ecs.cpp + src/main.cpp src/threatLevelEstimate.cpp src/preferenceManager.cpp - src/pathPlanner.cpp src/epsilonServer.cpp src/particleEffect.cpp src/httpScriptAccess.cpp - src/modelInfo.cpp src/packResourceProvider.cpp - src/scienceDatabase.cpp - src/commsScriptInterface.cpp - src/modelData.cpp src/gameGlobalInfo.cpp src/GMActions.cpp src/script.cpp src/playerInfo.cpp src/gameStateLogger.cpp - src/shipTemplate.cpp - src/beamTemplate.cpp src/missileWeaponData.cpp - src/factionInfo.cpp src/mesh.cpp src/scenarioInfo.cpp - src/repairCrew.cpp - src/GMScriptCallback.cpp - src/GMMessage.cpp src/tutorialGame.cpp - src/scriptDataStorage.cpp src/shaderRegistry.cpp src/glObjects.cpp + src/stdinLuaConsole.h + src/stdinLuaConsole.cpp src/menus/joinServerMenu.cpp src/menus/serverBrowseMenu.cpp src/menus/mainMenus.cpp @@ -325,33 +324,175 @@ set(MAIN_SOURCES src/screenComponents/miniGame.cpp src/screenComponents/mineSweeper.cpp src/screenComponents/impulseSound.cpp - src/spaceObjects/missiles/missileWeapon.cpp - src/spaceObjects/missiles/EMPMissile.cpp - src/spaceObjects/missiles/homingMissile.cpp - src/spaceObjects/missiles/hvli.cpp - src/spaceObjects/missiles/nuke.cpp - src/spaceObjects/spaceStation.cpp - src/spaceObjects/spaceship.cpp - src/spaceObjects/wormHole.cpp - src/spaceObjects/spaceObject.cpp - src/spaceObjects/nebula.cpp - src/spaceObjects/explosionEffect.cpp - src/spaceObjects/cpuShip.cpp - src/spaceObjects/asteroid.cpp - src/spaceObjects/mine.cpp - src/spaceObjects/blackHole.cpp - src/spaceObjects/playerSpaceship.cpp - src/spaceObjects/beamEffect.cpp - src/spaceObjects/electricExplosionEffect.cpp - src/spaceObjects/supplyDrop.cpp - src/spaceObjects/warpJammer.cpp - src/spaceObjects/scanProbe.cpp - src/spaceObjects/artifact.cpp - src/spaceObjects/shipTemplateBasedObject.cpp - src/spaceObjects/planet.cpp - src/spaceObjects/zone.cpp - src/spaceObjects/spaceshipParts/beamWeapon.cpp - src/spaceObjects/spaceshipParts/weaponTube.cpp + src/components/faction.h + src/components/faction.cpp + src/components/radar.h + src/components/hull.h + src/components/rendering.h + src/components/rendering.cpp + src/components/docking.h + src/components/coolant.h + src/components/shipsystem.h + src/components/shipsystem.cpp + src/components/impulse.h + src/components/maneuveringthrusters.h + src/components/warpdrive.h + src/components/jumpdrive.h + src/components/reactor.h + src/components/shields.h + src/components/shields.cpp + src/components/beamweapon.h + src/components/beamweapon.cpp + src/components/target.h + src/components/missiletubes.h + src/components/selfdestruct.h + src/components/player.h + src/components/player.cpp + src/components/pickup.h + src/components/spin.h + src/components/rendering.h + src/components/name.h + src/components/customshipfunction.h + src/components/shiplog.h + src/components/shiplog.cpp + src/components/comms.h + src/components/scanning.h + src/components/scanning.cpp + src/components/internalrooms.h + src/components/internalrooms.cpp + src/components/probe.h + src/components/hacking.h + src/components/ai.h + src/components/radarblock.h + src/components/gravity.h + src/components/avoidobject.h + src/components/sfx.h + src/components/missile.h + src/components/database.h + src/components/zone.h + src/components/zone.cpp + src/components/moveto.h + src/components/lifetime.h + src/systems/ai.h + src/systems/ai.cpp + src/systems/docking.h + src/systems/docking.cpp + src/systems/shipsystemssystem.h + src/systems/shipsystemssystem.cpp + src/systems/coolantsystem.h + src/systems/coolantsystem.cpp + src/systems/energysystem.h + src/systems/energysystem.cpp + src/systems/impulse.h + src/systems/impulse.cpp + src/systems/maneuvering.h + src/systems/maneuvering.cpp + src/systems/beamweapon.h + src/systems/beamweapon.cpp + src/systems/missilesystem.h + src/systems/missilesystem.cpp + src/systems/warpsystem.h + src/systems/warpsystem.cpp + src/systems/jumpsystem.h + src/systems/jumpsystem.cpp + src/systems/shieldsystem.h + src/systems/shieldsystem.cpp + src/systems/damage.h + src/systems/damage.cpp + src/systems/selfdestruct.h + src/systems/selfdestruct.cpp + src/systems/pickup.h + src/systems/pickup.cpp + src/systems/basicmovement.h + src/systems/basicmovement.cpp + src/systems/gravity.h + src/systems/gravity.cpp + src/systems/comms.h + src/systems/comms.cpp + src/systems/radarblock.h + src/systems/radarblock.cpp + src/systems/internalcrew.h + src/systems/internalcrew.cpp + src/systems/pathfinding.h + src/systems/pathfinding.cpp + src/systems/rendering.h + src/systems/rendering.cpp + src/systems/scanning.h + src/systems/scanning.cpp + src/systems/planet.h + src/systems/planet.cpp + src/systems/player.h + src/systems/player.cpp + src/systems/radar.h + src/systems/radar.cpp + src/systems/zone.h + src/systems/zone.cpp + src/multiplayer/beamweapon.h + src/multiplayer/beamweapon.cpp + src/multiplayer/shields.h + src/multiplayer/shields.cpp + src/multiplayer/faction.h + src/multiplayer/faction.cpp + src/multiplayer/radar.h + src/multiplayer/radar.cpp + src/multiplayer/comms.h + src/multiplayer/comms.cpp + src/multiplayer/probe.h + src/multiplayer/probe.cpp + src/multiplayer/orbit.h + src/multiplayer/orbit.cpp + src/multiplayer/radarblock.h + src/multiplayer/radarblock.cpp + src/multiplayer/player.h + src/multiplayer/player.cpp + src/multiplayer/name.h + src/multiplayer/name.cpp + src/multiplayer/impulse.h + src/multiplayer/impulse.cpp + src/multiplayer/warp.h + src/multiplayer/warp.cpp + src/multiplayer/jumpdrive.h + src/multiplayer/jumpdrive.cpp + src/multiplayer/docking.h + src/multiplayer/docking.cpp + src/multiplayer/coolant.h + src/multiplayer/coolant.cpp + src/multiplayer/reactor.h + src/multiplayer/reactor.cpp + src/multiplayer/rendering.h + src/multiplayer/rendering.cpp + src/multiplayer/selfdestruct.h + src/multiplayer/selfdestruct.cpp + src/multiplayer/hull.h + src/multiplayer/hull.cpp + src/multiplayer/target.h + src/multiplayer/target.cpp + src/multiplayer/database.h + src/multiplayer/database.cpp + src/multiplayer/maneuveringthrusters.h + src/multiplayer/maneuveringthrusters.cpp + src/multiplayer/hacking.h + src/multiplayer/hacking.cpp + src/multiplayer/customshipfunction.h + src/multiplayer/customshipfunction.cpp + src/multiplayer/gravity.h + src/multiplayer/gravity.cpp + src/multiplayer/scanning.h + src/multiplayer/scanning.cpp + src/multiplayer/missile.h + src/multiplayer/missile.cpp + src/multiplayer/missiletubes.h + src/multiplayer/missiletubes.cpp + src/multiplayer/spin.h + src/multiplayer/spin.cpp + src/multiplayer/moveto.h + src/multiplayer/moveto.cpp + src/multiplayer/internalrooms.h + src/multiplayer/internalrooms.cpp + src/multiplayer/shiplog.h + src/multiplayer/shiplog.cpp + src/multiplayer/zone.h + src/multiplayer/zone.cpp src/ai/fighterAI.cpp src/ai/ai.cpp src/ai/aiFactory.cpp @@ -366,23 +507,25 @@ set(MAIN_SOURCES src/hardware/devices/uDMXDevice.cpp src/hardware/devices/virtualOutputDevice.cpp src/hardware/devices/philipsHueDevice.cpp + src/script/components.cpp + src/script/dataStorage.h + src/script/dataStorage.cpp + src/script/gm.h + src/script/gm.cpp + src/script/scriptRandom.h + src/script/scriptRandom.cpp src/ai/aiFactory.h src/ai/ai.h src/ai/evasionAI.h src/ai/fighterAI.h src/ai/missileVolleyAI.h - src/beamTemplate.h - src/commsScriptInterface.h src/epsilonServer.h - src/factionInfo.h src/featureDefs.h src/gameGlobalInfo.h src/gameStateLogger.h src/glObjects.h src/GMActions.h - src/GMMessage.h - src/GMScriptCallback.h src/hardware/devices/dmx512SerialDevice.h src/hardware/devices/enttecDMXProDevice.h src/hardware/devices/philipsHueDevice.h @@ -409,16 +552,12 @@ set(MAIN_SOURCES src/menus/luaConsole.h src/mesh.h src/missileWeaponData.h - src/modelData.h - src/modelInfo.h src/packResourceProvider.h src/particleEffect.h - src/pathPlanner.h + src/crewPosition.h src/playerInfo.h src/preferenceManager.h - src/repairCrew.h src/scenarioInfo.h - src/scienceDatabase.h src/screenComponents/aimLock.h src/screenComponents/alertLevelButton.h src/screenComponents/alertOverlay.h @@ -494,36 +633,11 @@ set(MAIN_SOURCES src/screens/windowScreen.h src/script.h src/shaderRegistry.h - src/shipTemplate.h - src/spaceObjects/artifact.h - src/spaceObjects/asteroid.h - src/spaceObjects/beamEffect.h - src/spaceObjects/blackHole.h - src/spaceObjects/cpuShip.h - src/spaceObjects/electricExplosionEffect.h - src/spaceObjects/explosionEffect.h - src/spaceObjects/mine.h - src/spaceObjects/missiles/EMPMissile.h - src/spaceObjects/missiles/homingMissile.h - src/spaceObjects/missiles/hvli.h - src/spaceObjects/missiles/missileWeapon.h - src/spaceObjects/missiles/nuke.h - src/spaceObjects/nebula.h - src/spaceObjects/planet.h - src/spaceObjects/playerSpaceship.h - src/spaceObjects/scanProbe.h - src/spaceObjects/shipTemplateBasedObject.h - src/spaceObjects/spaceObject.h - src/spaceObjects/spaceship.h - src/spaceObjects/spaceshipParts/beamWeapon.h - src/spaceObjects/spaceshipParts/weaponTube.h - src/spaceObjects/spaceStation.h - src/spaceObjects/supplyDrop.h - src/spaceObjects/warpJammer.h - src/spaceObjects/wormHole.h - src/spaceObjects/zone.h src/threatLevelEstimate.h src/tutorialGame.h + src/script/components.h + src/script/vector.h + src/script/enum.h ) if (WITH_DISCORD) @@ -620,12 +734,12 @@ if(WITH_DISCORD) install(PROGRAMS "$" DESTINATION "${discord_install_prefix}/plugins") endif() -find_package(PythonInterp) -if(PYTHONINTERP_FOUND) +find_package(Python3 COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) set(SCRIPT_REFERENCE_HTML "${PROJECT_BINARY_DIR}/script_reference.html") add_custom_command( OUTPUT "${SCRIPT_REFERENCE_HTML}" - COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/compile_script_docs.py "${SCRIPT_REFERENCE_HTML}" + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/script_docs/main.py "${SCRIPT_REFERENCE_HTML}" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Building script reference documentation.") add_custom_target(script_reference ALL DEPENDS "${SCRIPT_REFERENCE_HTML}") diff --git a/artemis_mission_convert.py b/artemis_mission_convert.py deleted file mode 100644 index a252b8ca76..0000000000 --- a/artemis_mission_convert.py +++ /dev/null @@ -1,496 +0,0 @@ -from glob import glob -import json -import xml.etree.ElementTree -import os.path -import traceback -import sys -import fnmatch -import tokenize -import io - -class UnknownArtemisTagError(Exception): - def __init__(self, node): - super().__init__('%s: %s' % (node.tag, node.attrib)) - -def convertString(s): - return s.replace('\n', '\\n').replace('\'', '\\\'').replace('"', '\\"').replace('^', '\\n').strip() - -def convertFloat(f): - try: - return str(float(eval(str(f), {}, {}))) - except NameError: - pass - result = '(' - for token in tokenize.tokenize(io.BytesIO(str(f).encode('UTF-8')).readline): - if token.type == tokenize.ENCODING or token.type == tokenize.ENDMARKER: - pass - elif token.type == tokenize.NAME: - result += 'variable_%s' % (convertName(token.string)) - elif token.type == tokenize.OP: - result += '%s' % (token.string) - elif token.type == tokenize.NUMBER: - result += '%s' % (token.string) - else: - raise ValueError(token) - result += ')' - return result - -def convertPosition(x, z): - return convertFloat('20000-(%s)' % (x)), convertFloat('(%s)-100000' % (z)) - -def convertName(name): - return '%s' % (name.replace(' ', '_').replace('-', '_').replace('*', 'X').replace('.', '__')) - -def convertRaceKeys(node, default=None): - keys = node.get('raceKeys', default) - keys = keys.lower().split(' ') - if 'biomech' in keys: - return "Ghosts" - elif 'friendly' in keys: - return "Human Navy" - elif 'enemy' in keys: - return "Kraylor" - elif 'neutral' in keys: - return "Independent" - raise UnknownArtemisTagError(node) - -def convertComparator(node): - comparator = node.get('comparator').lower() - if comparator == "equals" or comparator == "=": - return "==" - elif comparator == "not" or comparator == "!=": - return "~=" - elif comparator == "greater" or comparator == ">": - return ">" - elif comparator == "less" or comparator == "<": - return "<" - elif comparator == "greater_equal" or comparator == "<=": - return "<=" - elif comparator == "less_equal" or comparator == ">=": - return ">=" - raise UnknownArtemisTagError(node) - -def convertSystemName(node): - system = node.get('systemType') - if system == 'systemBeam': - return 'beamweapons' - elif system == 'systemTorpedo': - return 'missilesystem' - elif system == 'systemTactical': # Sensors, we map it to reactor, as we don't have sensor power/damage - return 'reactor' - elif system == 'systemTurning': - return 'maneuver' - elif system == 'systemImpulse': - return 'impulse' - elif system == 'systemWarp': - return 'warp' - elif system == 'systemFrontShield': - return 'frontshield' - elif system == 'systemBackShield': - return 'rearshield' - - raise UnknownArtemisTagError(node) - -class Event: - def __init__(self, main_node): - self._valid = True - self._body = [] - self._conditions = [] - self._warnings = [] - self._done = {} - self._ai_info = {} - - for node in main_node: - if node.tag == 'big_message': - message = convertString(node.get('title', '')) - if node.get('subtitle1') is not None: - message += '\\n%s' % (convertString(node.get('subtitle1'))) - if node.get('subtitle2') is not None: - message += '\\n%s' % (convertString(node.get('subtitle2'))) - self._body.append('globalMessage("%s");' % (message)); - elif node.tag == 'incoming_comms_text': - self._body.append('temp_transmission_object:setCallSign("%s"):sendCommsMessage(getPlayerShip(-1), "%s")' % (convertString(node.get('from')), convertString(node.text))); - elif node.tag == 'warning_popup_message': - self.warning('Ignore', node) - elif node.tag == 'start_getting_keypresses_from': - self.warning('Ignore', node) - elif node.tag == 'end_getting_keypresses_from': - self.warning('Ignore', node) - elif node.tag == 'set_damcon_members': - self.warning('Ignore', node) - elif node.tag == 'incoming_message': - self.warning('Ignore', node) - elif node.tag == 'set_difficulty_level': - self.warning('Ignore', node) - elif node.tag == 'log': - self._body.append('print("%s")' % (convertString(node.get('text')))); - - elif node.tag == 'set_skybox_index': - pass #We don't have other skyboxes. So ignore this command. - elif node.tag == 'create': - self.parseCreate(node) - elif node.tag == 'clear_ai': - name = convertName(node.get('name')) - if name not in self._ai_info: - self._ai_info[name] = {} - self._ai_info[name]['CLEAR'] = True - elif node.tag == 'add_ai': - if node.get('name') is None: - self.warning('Ignore', node) - else: - name = convertName(node.get('name')) - if name not in self._ai_info: - self._ai_info[name] = {} - self._ai_info[name][node.get('type').upper()] = node.attrib - elif node.tag == 'set_object_property': - name = convertName(node.get('name')) - property = node.get('property') - self._body.append('if %s ~= nil and %s:isValid() then' % (name, name)) - if property == 'positionX': - self._body.append(' local x, y = %s:getPosition()' % (name)) - x, y = convertPosition(node.get('value'), 0) - self._body.append(' %s:setPosition(%s, y)' % (name, x)) - elif property == 'positionY': - pass - elif property == 'positionZ': - self._body.append(' local x, y = %s:getPosition()' % (name)) - x, y = convertPosition(0, node.get('value')) - self._body.append(' %s:setPosition(x, %s)' % (name, y)) - elif property == 'shieldStateFront': - self._body.append(' %s:setFrontShield(%f)' % (name, float(node.get('value')))) - elif property == 'shieldStateBack': - self._body.append(' %s:setRearShield(%f)' % (name, float(node.get('value')))) - elif property == 'shieldMaxStateFront': - self._body.append(' %s:setFrontShieldMax(%f)' % (name, float(node.get('value')))) - elif property == 'shieldMaxStateBack': - self._body.append(' %s:setRearShieldMax(%f)' % (name, float(node.get('value')))) - elif property == 'systemDamageBeam': - self._body.append(' %s:setSystemHealth("beamweapons", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageTorpedo': - self._body.append(' %s:setSystemHealth("missilesystem", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageTactical': - self.warning('Reactor instead of sensors', node) - self._body.append(' %s:setSystemHealth("reactor", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageTurning': - self._body.append(' %s:setSystemHealth("maneuver", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageImpulse': - self._body.append(' %s:setSystemHealth("impulse", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageWarp': - self._body.append(' %s:setSystemHealth("warp", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - self._body.append(' %s:setSystemHealth("jumpdrive", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageFrontShield': - self._body.append(' %s:setSystemHealth("frontshield", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'systemDamageBackShield': - self._body.append(' %s:setSystemHealth("rearshield", %f)' % (name, 1.0 - float(node.get('value')) / 100.0)) - elif property == 'willAcceptCommsOrders': - self.warning('Ignore', node) - elif property == 'eliteAIType': - self.warning('Ignore', node) - elif property == 'eliteAbilityBits': - bits = int(node.get('value')) - if (bits & 8) or (bits & 64): - self._body.append(' %s:setJumpDrive(True)' % (name)) - if bits & 32: - self._body.append(' %s:setWarpDrive(True)' % (name)) - else: - self.warning('Ignore', node) - #raise UnknownArtemisTagError(node) - self._body.append('end') - elif node.tag == 'set_fleet_property': - self.warning('Ignore', node) - elif node.tag == 'set_timer': - self._body.append('timers["%s"] = %f' % (convertName(node.get('name')), float(node.get('seconds')))) - elif node.tag == 'set_variable': - if node.get('randomIntHigh') is not None: - self._body.append('variable_%s = random(%d, %d) --Should be random int...' % (convertName(node.get('name')), int(node.get('randomIntLow')), int(node.get('randomIntHigh')))) - elif node.get('randomFloatHigh') is not None: - self._body.append('variable_%s = random(%d, %d)' % (convertName(node.get('name')), float(node.get('randomFloatLow')), int(node.get('randomFloatHigh')))) - else: - self._body.append('variable_%s = %s' % (convertName(node.get('name')), convertFloat(node.get('value')))) - elif node.tag == 'set_ship_text': - self.warning('Ignore', node) - elif node.tag == 'set_relative_position': - self._body.append('tmp_x, tmp_y = %s:getPosition()' % (convertName(node.get('name1')))); - self._body.append('tmp_x2, tmp_y2 = vectorFromAngle(%s:getRotation() + %f, %f)' % (convertName(node.get('name1')), float(node.get('angle')), float(node.get('distance')))); - self._body.append('%s:setPosition(x, y);' % (convertName(node.get('name2')))); - elif node.tag == 'end_mission': - self._body.append('victory("Independent")') - elif node.tag == 'set_player_grid_damage': - if convertSystemName(node) == 'warp': - self._body.append('getPlayerShip(-1):setSystemHealth("%s", %f)' % ('jumpdrive', 1.0 - float(node.get('value')) * 2.0)) - self._body.append('getPlayerShip(-1):setSystemHealth("%s", %f)' % (convertSystemName(node), 1.0 - float(node.get('value')) * 2.0)) - elif node.tag == 'destroy': - name = convertName(node.get('name')) - self._body.append('if %s ~= nil and %s:isValid() then %s:destroy() end' % (name, name, name)) - elif node.tag == 'destroy_near': - obj_type = node.get('type') - if obj_type == 'nebulas': - obj_type = 'Nebula' - elif obj_type == 'asteroids': - obj_type = 'Asteroid' - elif obj_type == 'mines': - obj_type = 'Mine' - else: - raise UnknownArtemisTagError(node) - if node.get('name'): - name = convertName(node.get('name')) - self._body.append('if %s ~= nil and %s:isValid() then' % (name, name)) - self._body.append(' for _, obj in ipairs(%s:getObjectsInRange(%f)) do' % (name, float(node.get('radius')))) - self._body.append(' if obj.typeName == "%s" then obj:destroy() end' % (obj_type)) - self._body.append(' end') - self._body.append('end') - else: - x, y = convertPosition(node.get('centerX', 0), node.get('centerZ', 0)) - r = float(node.get('radius')) - self._body.append('for _, obj in ipairs(getObjectsInRadius(%s, %s, %f)) do' % (x, y, float(node.get('radius')))) - self._body.append(' if obj.typeName == "%s" then obj:destroy() end' % (obj_type)) - self._body.append('end') - - elif node.tag == 'if_gm_key': - self._conditions.append('0') # gm key triggers are never run. - self.warning('Ignore', node) - elif node.tag == 'if_client_key': - self._conditions.append('0') - self.warning('Ignore', node) - elif node.tag == 'if_variable': - self._conditions.append("variable_%s %s (%s)" % (convertName(node.get("name")), convertComparator(node), node.get("value"))) - elif node.tag == 'if_timer_finished': - self._conditions.append('(timers["%s"] ~= nil and timers["%s"] < 0.0)' % (convertName(node.get("name")), convertName(node.get("name")))) - elif node.tag == 'if_outside_box': - x1, y1 = convertPosition(node.get('leastX'), node.get('leastZ')) - x2, y2 = convertPosition(node.get('mostX'), node.get('mostZ')) - self._conditions.append('ifOutsideBox(%s, %s, %s, %s, %s)' % (convertName(node.get("name")), x1, y1, x2, y2)) - elif node.tag == 'if_inside_box': - x1, y1 = convertPosition(node.get('leastX'), node.get('leastZ')) - x2, y2 = convertPosition(node.get('mostX'), node.get('mostZ')) - self._conditions.append('ifInsideBox(%s, %s, %s, %s, %s)' % (convertName(node.get("name")), x1, y1, x2, y2)) - elif node.tag == 'if_inside_sphere': - x1, y1 = convertPosition(node.get('centerX'), node.get('centerZ')) - r = float(node.get('radius')) - self._conditions.append('ifInsideSphere(%s, %s, %s, %f)' % (convertName(node.get("name")), x1, y1, r)) - elif node.tag == 'if_outside_sphere': - x1, y1 = convertPosition(node.get('centerX'), node.get('centerZ')) - r = float(node.get('radius')) - self._conditions.append('ifOutsideSphere(%s, %s, %s, %f)' % (convertName(node.get("name")), x1, y1, r)) - elif node.tag == 'if_docked': - self._conditions.append('ifdocked(%s)' % (convertName(node.get("name")))) - elif node.tag == 'if_fleet_count': - self._conditions.append('countFleet(%d) %s %f' % (int(node.get('fleetnumber', 0)), convertComparator(node), float(node.get('value')))) - elif node.tag == 'if_distance': - self._conditions.append('(%s ~= nil and %s ~= nil and %s:isValid() and %s:isValid() and distance(%s, %s) %s %f)' % (convertName(node.get('name1')), convertName(node.get('name2')), convertName(node.get('name1')), convertName(node.get('name2')), convertName(node.get('name1')), convertName(node.get('name2')), convertComparator(node), float(node.get('value')))) - elif node.tag == 'if_exists': - self._conditions.append('(%s ~= nil and %s:isValid())' % (convertName(node.get('name')), convertName(node.get('name')))) - elif node.tag == 'if_not_exists': - self._conditions.append('(%s == nil or not %s:isValid())' % (convertName(node.get('name')), convertName(node.get('name')))) - elif node.tag == 'if_player_is_targeting': - self._conditions.append('(%s ~= nil and %s:isValid() and getPlayerShip(-1):getTarget() == %s)' % (convertName(node.get('name')), convertName(node.get('name')), convertName(node.get('name')))) - else: - raise UnknownArtemisTagError(node) - self.warning('Ignore', node) - - # Convert the AI statements to EE AI. - for name, ai in self._ai_info.items(): - ai_list = sorted(list(ai.keys())) - if ai_list == ['ATTACK'] or ai_list == ['ATTACK', 'ELITE_AI'] or ai_list == ['ATTACK', 'CLEAR'] or ai_list == ['ATTACK', 'CHASE_NEUTRAL']: - self._body.append('%s:orderAttack(%s)' % (name, convertName(ai['ATTACK']['targetName']))) - elif ai_list == ['POINT_THROTTLE'] or ai_list == ['FOLLOW_COMMS_ORDERS', 'POINT_THROTTLE'] or ai_list == ['CHASE_PLAYER', 'POINT_THROTTLE']: - x, y = convertPosition(ai['POINT_THROTTLE']['value1'], ai['POINT_THROTTLE']['value3']) - self._body.append('%s:orderFlyTowards(%s, %s)' % (name, x, y)) - elif ai_list == ['CLEAR']: - self._body.append('%s:orderIdle()' % (name)) - elif ai_list == ['ELITE_AI']: - pass - else: - self.warning('Unknown AI: %s: %s' % (name, ai)) - - def parseCreate(self, node): - if node.get('use_gm_position') is not None: - return - create_type = node.get('type') - if create_type == 'player': - name = convertName(node.get('name')) - x, y = convertPosition(node.get('x'), node.get('z')) - self._body.append('%s = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Player Cruiser"):setCallSign("%s"):setPosition(%s, %s)' % (name, node.get('name'), x, y)) - elif create_type == 'neutral': - name = convertName(node.get('name')) - x, y = convertPosition(node.get('x'), node.get('z')) - self._body.append('%s = CpuShip():setTemplate("Tug"):setCallSign("%s"):setFaction("%s"):setPosition(%s, %s):orderRoaming()' % (name, node.get('name'), convertRaceKeys(node, 'neutral'), x, y)) - self.addToFleet(name, node) - elif create_type == 'enemy': - name = convertName(node.get('name', 'temp_enemy_name')) - x, y = convertPosition(node.get('x'), node.get('z')) - self._body.append('%s = CpuShip():setTemplate("Cruiser"):setCallSign("%s"):setFaction("%s"):setPosition(%s, %s):orderRoaming()' % (name, node.get('name'), convertRaceKeys(node, 'enemy'), x, y)) - self.addToFleet(name, node) - - self.addToFleet(name, node, 0) # Add every enemy ship to fleet 0 - elif create_type == 'station': - name = convertName(node.get('name')) - x, y = convertPosition(node.get('x'), node.get('z')) - self._body.append('%s = SpaceStation():setTemplate("Small Station"):setCallSign("%s"):setFaction("%s"):setPosition(%s, %s)' % (name, node.get('name'), convertRaceKeys(node, 'friendly'), x, y)) - elif create_type == 'blackHole': - name = convertName(node.get('name', 'temp_blackhole_name')) - x, y = convertPosition(node.get('x'), node.get('z')) - self._body.append('%s = BlackHole():setPosition(%s, %s)' % (name, x, y)) - elif create_type == 'whale': - self.warning('Ignore', node) - elif create_type == 'monster': - self.warning('Ignore', node) - elif create_type == 'genericMesh': - self.warning('Ignore', node) - elif create_type == 'anomaly': - # Using a supply drop instead of an anomaly - output = "" - if node.get('name') is not None: - name = convertName(node.get('name')) - output = "%s = " % (name) - x, y = convertPosition(node.get('x'), node.get('z')) - output += 'SupplyDrop():setFaction("Human Navy"):setPosition(%s, %s):setEnergy(500):setWeaponStorage("Nuke", 0):setWeaponStorage("Homing", 0):setWeaponStorage("Mine", 0):setWeaponStorage("EMP", 0)' % (x, y) - self._body.append(output) - elif create_type == 'asteroids': - self.parseCreateCount('Asteroid()', node) - elif create_type == 'mines': - self.parseCreateCount('Mine()', node) - elif create_type == 'nebulas': - node.set('count', '(%s + 24) / 25' % convertFloat(node.get('count'))) - if node.get('randomRange') is not None: - node.set('randomRange', '%s - 2500' % convertFloat(node.get('randomRange'))) - self.parseCreateCount('Nebula()', node) - else: - raise UnknownArtemisTagError(node) - - def parseCreateCount(self, object_create_script, node): - count = convertFloat(node.get('count')) - x, y = convertPosition(node.get('startX', 0), node.get('startZ', 0)) - self._body.append('tmp_count = %s' % (count)) - self._body.append('for tmp_counter=1,tmp_count do') - if node.get('radius') is not None: - radius = convertFloat(node.get('radius')) - start_angle = float(node.get('startAngle', 0)) - 90 - end_angle = float(node.get('endAngle', 360)) - 90 - self._body.append(' tmp_x, tmp_y = vectorFromAngle(%s + (%s - %s) * (tmp_counter - 1) / tmp_count, %s)' % (start_angle, end_angle, start_angle, radius)) - self._body.append(' tmp_x, tmp_y = tmp_x + %s, tmp_y + %s' % (x, y)) - else: - x2, y2 = convertPosition(node.get('endX'), node.get('endZ')) - self._body.append(' tmp_x = %s + (%s - %s) * (tmp_counter - 1) / tmp_count' % (x, x2, x)) - self._body.append(' tmp_y = %s + (%s - %s) * (tmp_counter - 1) / tmp_count' % (y, y2, y)) - if node.get('randomRange') is not None: - random_range = convertFloat(node.get('randomRange', 0)) - self._body.append(' tmp_x2, tmp_y2 = vectorFromAngle(random(0, 360), random(0, %s))' % (random_range)) - self._body.append(' tmp_x, tmp_y = tmp_x + tmp_x2, tmp_y + tmp_y2') - self._body.append(' %s:setPosition(tmp_x, tmp_y)' % (object_create_script)) - self._body.append('end') - - def addToFleet(self, name, node, fleetnumber=-1): - if fleetnumber != -1: - fleetnumber = int(node.get('fleetnumber', -1)) - if fleetnumber > 0: - if 'fleet_check_%d' % (fleetnumber) not in self._done: - self._done['fleet_check_%d' % (fleetnumber)] = True - self._body.append('if fleet[%d] == nil then fleet[%d] = {} end' % (fleetnumber, fleetnumber)) - self._body.append('table.insert(fleet[%d], %s)' % (fleetnumber, name)) - - def warning(self, *args): - message = '' - for arg in args: - if isinstance(arg, str): - message += arg + ' ' - elif isinstance(arg, xml.etree.ElementTree.Element): - message += '<' + arg.tag + '> ' + str(arg.attrib) + ' ' - if arg.text is not None: - message += convertString(arg.text) - else: - message += str(arg) - self._body.append('--WARNING: %s' % (message)) - self._warnings.append(args) - - def getBody(self, indent=1): - body = '' - for line in self._body: - body += (' ' * indent) + line + '\n'; - return body - - def getCondition(self): - return ' and '.join(self._conditions) - - def getWarnings(self): - return self._warnings - - def isValid(self): - return self._valid - -class Converter: - def __init__(self, filename): - self._data = xml.etree.ElementTree.XML(open(filename, 'rb').read().replace(b'"<="', b'"<="').replace(b'"<"', b'"<"').replace(b'">="', b'">="').replace(b'">"', b'">"')) - - self._events = [] - self._start_event = Event(self._data.find("start")) - for node in self._data.findall("event"): - self._events.append(Event(node)) - - def export(self, name, filename): - f = open(filename, "w") - f.write('-- Name: %s\n' % (name)) - f.write('-- Description: Converted Artemis mission\n') - warnings = [] - for line in open("artemis_mission_convert_template.lua", "r"): - if line.strip() == '###START###': - f.write(self._start_event.getBody()) - warnings += self._start_event.getWarnings() - elif line.strip() == '###EVENTS###': - for event in self._events: - if not event.isValid(): - continue - if event.getCondition() != "": - f.write(" if %s then\n" % event.getCondition()) - f.write(event.getBody(2)) - warnings += event.getWarnings() - f.write(" end\n") - else: - f.write(event.getBody(1)) - warnings += event.getWarnings() - else: - f.write(line) - print('Written: %s with %d warnings' % (filename, len(warnings))) - warning_types = {} - for warning in warnings: - for item in warning: - if isinstance(item, xml.etree.ElementTree.Element): - if item.tag not in warning_types: - warning_types[item.tag] = 0 - warning_types[item.tag] += 1 - for key, count in warning_types.items(): - print("Warning: %s %dx" % (key, count)) - return len(warning_types) == 0 - -if __name__ == "__main__": - count = 0 - success = 0 - for arg in sys.argv[1:]: - if os.path.isfile(arg): - filename = arg - count += 1 - print("========================================================"); - print("Converting: ", filename); - try: - c = Converter(filename) - name = os.path.splitext(os.path.basename(filename))[0].replace("MISS_", "") - c.export(name, "scripts/scenario_99_%s.lua" % (name)) - success += 1 - except: - traceback.print_exc() - for root, dirnames, filenames in os.walk(arg): - for filename in fnmatch.filter(filenames, '*.xml'): - filename = os.path.join(root, filename) - count += 1 - print("========================================================"); - print("Converting: ", filename); - try: - c = Converter(filename) - name = os.path.splitext(os.path.basename(filename))[0].replace("MISS_", "") - if c.export(name, "scripts/scenario_99_%s.lua" % (name)): - sys.exit(1) - success += 1 - except: - traceback.print_exc() - sys.exit(1) - print("Converted %d of the %d scripts" % (success, count)) diff --git a/cmake/android.toolchain b/cmake/android.toolchain index c57776287a..7cdcdba250 100644 --- a/cmake/android.toolchain +++ b/cmake/android.toolchain @@ -191,3 +191,4 @@ endmacro() include("${ANDROID_SDK_PATH}/ndk/${ANDROID_NDK_VERSION}/build/cmake/android.toolchain.cmake") +add_definitions(-DGLM_FORCE_CXX17=1) \ No newline at end of file diff --git a/cmake/mingw.toolchain b/cmake/mingw.toolchain index 36a8bc3532..78d993883b 100644 --- a/cmake/mingw.toolchain +++ b/cmake/mingw.toolchain @@ -1,8 +1,8 @@ set(CMAKE_SYSTEM_NAME Windows) -set(CMAKE_C_COMPILER i686-w64-mingw32-gcc-posix) -set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++-posix) -set(CMAKE_RC_COMPILER i686-w64-mingw32-windres) +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) set(CMAKE_CXX_FLAGS_RELEASE_INIT "-flto") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "-flto") @@ -16,7 +16,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) -set(MING_DLL_PATH /usr/lib/gcc/i686-w64-mingw32/4.8/) +set(MING_DLL_PATH /usr/lib/gcc/x86_64-w64-mingw32/4.8/) function(execute_process_ex) execute_process(${ARGV} RESULT_VARIABLE RES) @@ -32,15 +32,15 @@ if(NOT SDL2_ROOT) if(NOT IS_DIRECTORY "${WIN32_BASE}/SDL2-2.0.16") file(DOWNLOAD http://libsdl.org/release/SDL2-devel-2.0.16-mingw.tar.gz "${WIN32_BASE}/SDL2.tar.gz" SHOW_PROGRESS EXPECTED_HASH SHA256=2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f) execute_process_ex(COMMAND ${CMAKE_COMMAND} -E tar xzvf SDL2.tar.gz WORKING_DIRECTORY "${WIN32_BASE}") - execute_process_ex(COMMAND sed -i "s|set(prefix .*)|set(prefix \"${WIN32_BASE}/SDL2-2.0.16/i686-w64-mingw32/\")|g" ${WIN32_BASE}/SDL2-2.0.16/i686-w64-mingw32/lib/cmake/SDL2/sdl2-config.cmake) + execute_process_ex(COMMAND sed -i "s|set(prefix .*)|set(prefix \"${WIN32_BASE}/SDL2-2.0.16/x86_64-w64-mingw32/\")|g" ${WIN32_BASE}/SDL2-2.0.16/x86_64-w64-mingw32/lib/cmake/SDL2/sdl2-config.cmake) endif() - set(SDL2_ROOT "${WIN32_BASE}/SDL2-2.0.16/i686-w64-mingw32/") + set(SDL2_ROOT "${WIN32_BASE}/SDL2-2.0.16/x86_64-w64-mingw32/") option(ENABLE_CRASH_LOGGER "" ON) set(DRMINGW_ROOT "${WIN32_BASE}/drmingw-0.8.2-win32") if(NOT IS_DIRECTORY "${DRMINGW_ROOT}") - file(DOWNLOAD https://github.com/jrfonseca/drmingw/releases/download/0.8.2/drmingw-0.8.2-win32.7z ${WIN32_BASE}/drmingw.7z SHOW_PROGRESS) + file(DOWNLOAD https://github.com/jrfonseca/drmingw/releases/download/0.8.2/drmingw-0.8.2-win64.7z ${WIN32_BASE}/drmingw.7z SHOW_PROGRESS) execute_process_ex( COMMAND ${CMAKE_COMMAND} -E tar "x" "drmingw.7z" WORKING_DIRECTORY ${WIN32_BASE}) diff --git a/compile_script_docs.py b/compile_script_docs.py index 7d1c957450..6c053546ca 100644 --- a/compile_script_docs.py +++ b/compile_script_docs.py @@ -295,6 +295,11 @@ def readScriptDefinitions(self): current_class = ScriptClass(res.group(1).strip()) current_class.description = description self._definitions.append(current_class) + res = re.search("REGISTER_SCRIPT_CLASS_NAMED\(([^\)]*), \"([^\"]*)\"\)", line) + if res != None: + current_class = ScriptClass(res.group(2).strip()) + current_class.description = description + self._definitions.append(current_class) res = re.search("REGISTER_SCRIPT_CLASS_NO_CREATE\(([^\)]*)\)", line) if res != None: current_class = ScriptClass(res.group(1).strip()) @@ -307,6 +312,12 @@ def readScriptDefinitions(self): current_class.parent_name = res.group(2).strip() current_class.description = description self._definitions.append(current_class) + res = re.search("REGISTER_SCRIPT_SUBCLASS_NAMED\(([^,]*),([^\)]*), \"([^\"]*)\"\)", line) + if res != None: + current_class = ScriptClass(res.group(3).strip()) + current_class.parent_name = res.group(2).strip() + current_class.description = description + self._definitions.append(current_class) res = re.search( "REGISTER_SCRIPT_SUBCLASS_NO_CREATE\(([^,]*),([^\)]*)\)", line ) diff --git a/resources/gui/default.theme.txt b/resources/gui/default.theme.txt index 5b89d89fe3..495e63667f 100644 --- a/resources/gui/default.theme.txt +++ b/resources/gui/default.theme.txt @@ -55,6 +55,17 @@ image.focus: gui/widget/TextEntryBackground.focused.png } } + + [luaconsole] { + size: 12 + font: gui/fonts/RobotoMono-SemiBold.ttf + [luaconsole.log.back] { + } + [luaconsole.entry.back] { + image: gui/widget/TextEntryBackground.png + image.focus: gui/widget/TextEntryBackground.focused.png + } + } [luaconsole] { size: 12 diff --git a/resources/gui/fonts/LICENSE b/resources/gui/fonts/BebasNeue-LICENSE similarity index 100% rename from resources/gui/fonts/LICENSE rename to resources/gui/fonts/BebasNeue-LICENSE diff --git a/resources/gui/fonts/BigShouldersDisplay-LICENSE b/resources/gui/fonts/BigShouldersDisplay-LICENSE new file mode 100644 index 0000000000..d6d8ec3560 --- /dev/null +++ b/resources/gui/fonts/BigShouldersDisplay-LICENSE @@ -0,0 +1,41 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting--in part or in whole--any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/gui/fonts/RobotoMono-LICENSE b/resources/gui/fonts/RobotoMono-LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/resources/gui/fonts/RobotoMono-LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/radar/blackHole.png b/resources/radar/blackHole.png index 0db2ed820f..c1ad9957d8 100644 Binary files a/resources/radar/blackHole.png and b/resources/radar/blackHole.png differ diff --git a/script_docs/README.md b/script_docs/README.md new file mode 100644 index 0000000000..23f42c8991 --- /dev/null +++ b/script_docs/README.md @@ -0,0 +1 @@ +Python scripts to generate the HTML documentation for the scripting API. \ No newline at end of file diff --git a/script_docs/main.py b/script_docs/main.py new file mode 100644 index 0000000000..3fae204aae --- /dev/null +++ b/script_docs/main.py @@ -0,0 +1,51 @@ +import os +import re +import warnings +import argparse +import html +from scriptfunctiondb import ScriptFunctionDatabase + + +class ReferenceBuilder: + def __init__(self, output_filename): + self.db = ScriptFunctionDatabase(os.path.join(os.path.dirname(__file__), "..", "scripts")) + self.db.read("api/all.lua") + self.output_file = open(output_filename, "wt") + + def process_block(self, data, tag, params): + if tag == "foreach": + for func in self.db.filter(params): + self.output_file.write(data.replace("{{name}}", html.escape(func.name)).replace("{{doc}}", html.escape("\n".join(func.doc)))) + else: + print(tag, params) + + def process(self): + input_file = open(os.path.join(os.path.dirname(__file__), "template.html"), "rt").read() + tags = list(re.finditer(r"{{([a-z]+) *([^{]*)}}", input_file)) + start = 0 + start_tag = None + for m in tags: + tag = m.group(1) + if tag == "foreach": + self.output_file.write(input_file[start:m.start()]) + start = m.end() + start_tag = m + elif tag == "end": + params = start_tag.group(2) + params = {p.partition("=")[0]: p.partition("=")[2] for p in params.split()} + self.process_block(input_file[start:m.start()], start_tag.group(1), params) + start = m.end() + start_tag = None + self.output_file.write(input_file[start:]) + + +def main(args=None): + parser = argparse.ArgumentParser() + parser.add_argument("output") + args = parser.parse_args(args) + + rb = ReferenceBuilder(args.output) + rb.process() + +if __name__ == "__main__": + main() diff --git a/script_docs/scriptfunctiondb.py b/script_docs/scriptfunctiondb.py new file mode 100644 index 0000000000..249c4f66a5 --- /dev/null +++ b/script_docs/scriptfunctiondb.py @@ -0,0 +1,64 @@ +import os +import re + + +class ScriptFunction: + def __init__(self, name, from_file, doc, params): + self.name = name + self.doc = [] + self.params = params + self.metadata = {} + for doc_line in doc: + if doc_line.startswith("@"): + key, _, value = doc_line.partition(" ") + self.metadata[key[1:].strip()] = value.strip() + else: + self.doc.append(doc_line) + + def check_filters(self, filters): + for k, v in filters.items(): + if k not in self.metadata: + return False + if self.metadata[k] != v: + return False + return True + + def __repr__(self): + return f"<{self.name}({', '.join(self.params)})>" + + +class ScriptFunctionDatabase: + def __init__(self, base_path): + self.__base_path = base_path + self.__functions = {} + + def filter(self, filters): + for name, func in self.__functions.items(): + if func.check_filters(filters): + yield func + + def read(self, filename: str): + print(f"Reading {filename}") + doc_str = [] + for line in open(os.path.join(self.__base_path, filename), "rt"): + require = re.match(r'require\("([^"]+)"\)', line) + if require: + self.read(require.group(1)) + if line.startswith("---"): + doc_str.append(line[3:].strip()) + if line.strip() == "end": + doc_str = [] + func = re.match(r'function\s+([^\()]+)\(([^\))]*)\)', line) + if func: + if func.group(1) in self.__functions: + print(f"WARNING: Duplicate function: {func.group(1)} {filename} : {self.__functions[func.group(1)]}") + if not func.group(1).startswith("__") and doc_str: + params = [p.strip() for p in func.group(2).split(",") if p.strip() != ""] + self.__functions[func.group(1)] = ScriptFunction(func.group(1), filename, doc_str, params) + + def dump(self): + print(f"### Dumping {len(self.__functions)} functions ###") + for k, v in self.__functions.items(): + print(k, v) + print(" " + "\n ".join(v.doc)) + diff --git a/script_docs/template.html b/script_docs/template.html new file mode 100644 index 0000000000..8aa271d37a --- /dev/null +++ b/script_docs/template.html @@ -0,0 +1,133 @@ + + + + +EmptyEpsilon - Scripting documentation + + + + + + + +
+

EmptyEpsilon Scripting Reference

+

This is the EmptyEpsilon script reference for this version of EmptyEpsilon.

+

By no means this is a guide to help you scripting, you should check EmptyEpsilon website for the guide on scripting. As well as check the already existing scenario and ship data files on how to get started.

+
+ +
+

Some of the types in the parameters:

+ +
+ +
+

Objects

+ +
+ +{{foreach type=creation}} +
+

{{name}}

+
{{doc}}
+
+{{end}} + + + + diff --git a/scripts/api/all.lua b/scripts/api/all.lua new file mode 100644 index 0000000000..78fd72f9f6 --- /dev/null +++ b/scripts/api/all.lua @@ -0,0 +1,26 @@ +require("api/modelData.lua") +require("api/shipTemplate.lua") +require("api/entity/spaceobject.lua") +require("api/entity/factioninfo.lua") +require("api/entity/shiptemplatebasedobject.lua") +require("api/entity/spaceship.lua") +require("api/entity/playerspaceship.lua") +require("api/entity/cpuship.lua") +require("api/entity/spacestation.lua") +require("api/entity/warpjammer.lua") + +require("api/entity/artifact.lua") +require("api/entity/asteroid.lua") +require("api/entity/mine.lua") +require("api/entity/nebula.lua") +require("api/entity/blackhole.lua") +require("api/entity/planet.lua") +require("api/entity/sciencedatabase.lua") +require("api/entity/multiuse.lua") +require("api/entity/scanprobe.lua") +require("api/entity/supplydrop.lua") +require("api/entity/zone.lua") +require("api/entity/beameffect.lua") + +require("api/gm.lua") +require("api/callsign.lua") diff --git a/scripts/api/callsign.lua b/scripts/api/callsign.lua new file mode 100644 index 0000000000..977234c6ab --- /dev/null +++ b/scripts/api/callsign.lua @@ -0,0 +1,31 @@ + +__random_callsign_index = 1 +__random_callsign_prefix_length = 0 +__random_callsign_prefix_pool = {} +function generateRandomCallSign(prefix) + if prefix == nil then + if __random_callsign_prefix_length == 0 then + for i=1,26 do + table.insert(__random_callsign_prefix_pool, string.char(i+64)) + end + __random_callsign_prefix_length = 1 + end + for i=1,__random_callsign_prefix_length do + if #__random_callsign_prefix_pool < 1 then + for i=1,26 do + table.insert(__random_callsign_prefix_pool, string.char(i+64)) + end + __random_callsign_prefix_length = __random_callsign_prefix_length + 1 + end + local prefix_index = math.random(1,#__random_callsign_prefix_pool) + prefix = "" + prefix = prefix .. __random_callsign_prefix_pool[prefix_index] + table.remove(__random_callsign_prefix_pool, prefix_index) + end + end + __random_callsign_index = __random_callsign_index + irandom(1, 3) + if __random_callsign_index > 999 then + __random_callsign_index = __random_callsign_index - 999 + end + return string.format("%s%i", prefix, __random_callsign_index) +end diff --git a/scripts/api/entity/artifact.lua b/scripts/api/entity/artifact.lua new file mode 100644 index 0000000000..7881fa4cc5 --- /dev/null +++ b/scripts/api/entity/artifact.lua @@ -0,0 +1,124 @@ +--- An Artifact is a configurable SpaceObject that can interact with other objects via collisions or scripting. +--- Use this to define arbitrary objects or collectible pickups in scenario scripts. +--- Example: artifact = Artifact():setModel("artifact6"):setSpin(0.5) +--- @type creation +function Artifact() + local e = createEntity() + e.components.transform = {rotation=random(0, 360)} + + local model_number = irandom(1, 8) + e.components.mesh_render = { + mesh="mesh/Artifact" .. model_number .. ".obj", + texture="texture/electric_sphere_texture.png", + scale=3.0, + } + e.components.radar_trace = { + icon="radar/blip.png", + radius=120.0, + rotate=false, + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets the 3D model used for this artifact, by its ModelData name. +--- ModelData is defined in scripts/model_data.lua. +--- Defaults to a ModelData whose name starts with "artifact" and ends with a random number between 1 and 8. +--- Example: artifact:setModel("artifact6") +function Entity:setModel(model_name) + for k, v in pairs(__model_data[model_name]) do + if string.sub(1, 2) ~= "__" then + self.components[k] = table.deepcopy(v) + end + end + return self +end +--- Immediately destroys this artifact with a visual explosion. +--- Example: artifact:explode() -- artifact is destroyed +function Entity:explode() + local e = ExplosionEffect() + e:setSize(120) + local x, y = self:getPosition() + e:setPosition(x, y) + self:destroy() + return self +end +--- Defines whether this artifact can be picked up via collision. +--- The artifact is destroyed upon being picked up. +--- Defaults to false. +--- Example: artifact:allowPickup(true) +function Entity:allowPickup(allow) + if allow then + self.components.pickup = {} + else + self.components.pickup = nil + end + return self +end +--- Defines a function to call every tick when a SpaceObject is colliding with the artifact. +--- Passes the artifact and colliding SpaceObject to the called function. +--- Example: artifact:onCollision(function(artifact, collider) print("Collision occurred") end) +function Entity:onCollision(callback) + self.components.collision_callback = {player=false, callback} + return self +end +--- Defines a function to call every tick when a PlayerSpaceship is colliding with the artifact. +--- Passes the artifact and colliding PlayerSpaceship to the called function. +--- Example: artifact:onCollision(function(artifact, player) print("Collision occurred") end) +function Entity:onPlayerCollision(callback) + self.components.collision_callback = {player=true, callback} + return self +end +--- Defines a function to call once when a PlayerSpaceship collides with the artifact and allowPickup is enabled. +--- Passes the artifact and colliding PlayerSpaceship to the called function. +--- Example: artifact:onPickUp(function(artifact, player) print("Artifact retrieved") end) + --- Defines a function to call when a SpaceShip collides with the supply drop. +--- Passes the supply drop and the colliding ship (if it's a PlayerSpaceship) to the function. +--- Example: supply_drop:onPickUp(function(drop,ship) print("Supply drop picked up") end) +function Entity:onPickUp(callback) + self.components.pickup = {callback = callback} + return self +end +--- Alias of Artifact:onPickUp(). +function Entity:onPickup(callback) + return self:onPickUp(callback) +end +--- Defines whether the artifact rotates, and if so at what rotational velocity. (unit?) +--- For reference, normal asteroids spin at a rate between 0.1 and 0.8. +--- Example: artifact:setSpin(0.5) +function Entity:setSpin(spin) + if spin ~= 0.0 then + self.spin = {rate=spin} + else + self.spin = nil + end + return self +end +--- Sets the radar trace image for this artifact. +--- Optional. Defaults to "blip.png". +--- Valid values are filenames to PNG files relative to resources/radar/. +--- Example: artifact:setRadarTraceIcon("arrow.png") -- displays an arrow instead of a blip for this artifact +function Entity:setRadarTraceIcon(icon) + if self.radar_trace then self.radar_trace.icon = "radar/" .. icon end + return self +end +--- Scales the radar trace for this artifact. +--- A value of 0 restores standard autoscaling relative to the artifact's radius. +--- Set to 1 to mimic ship traces. +--- Example: artifact:setRadarTraceScale(0.7) +function Entity:setRadarTraceScale(scale) + if self.radar_trace then + self.radar_trace.min_size = scale * 32 + self.radar_trace.max_size = scale * 32 + end + return self +end +--- Sets the color of this artifact's radar trace. +--- Optional. Defaults to solid white (255,255,255) +--- Example: artifact:setRadarTraceColor(255,200,100) -- mimics an asteroid +function Entity:setRadarTraceColor(r, g, b) + if self.radar_trace then + self.radar_trace.color = {r, g, b} + end + return self +end diff --git a/scripts/api/entity/asteroid.lua b/scripts/api/entity/asteroid.lua new file mode 100644 index 0000000000..9a843c3246 --- /dev/null +++ b/scripts/api/entity/asteroid.lua @@ -0,0 +1,78 @@ +--- An Asteroid is an inert piece of space terrain. +--- Upon collision with another SpaceObject, it deals damage and is destroyed. +--- It has a default rotation speed, random z-offset, and model, and AI behaviors attempt to avoid hitting them. +--- To create a customizable object with more complex actions upon collisions, use an Artifact or SupplyDrop. +--- For a purely decorative asteroid positioned outside of the movement plane, use a VisualAsteroid. +--- Example: asteroid = Asteroid():setSize(150):setPosition(1000,2000) +--- @type creation +function Asteroid() + local z = random(-50, 50) + local size = random(110, 130) + + local model_number = irandom(1, 10) + local e = createEntity() + e.components = { + transform = {rotation=random(0, 360)}, + radar_signature = {gravity=0.05}, + mesh_render = { + mesh="Astroid_" .. model_number .. ".model", + mesh_offset={0, 0, z}, + texture="Astroid_" .. model_number .. "_d.png", + specular_texture="Astroid_" .. model_number .. "_s.png", + scale=size, + }, + physics = {type="Sensor", size=size}, + radar_trace = { + icon="radar/blip.png", + radius=size, + color={255, 200, 100, 255}, + rotate=false, + }, + spin={rate=random(0.1, 0.8)}, + avoid_object={range=size*2}, + explode_on_touch={damage_at_center=35, damage_at_edge=35,blast_range=size}, + } + return e +end + +function VisualAsteroid() + local z = random(300, 800); + if random(0, 100) < 50 then z = -z end + local size = random(110, 130) + local e = createEntity() + local model_number = irandom(1, 10) + e.components = { + transform = {rotation=random(0, 360)}, + radar_signature = {gravity=0.05}, + + mesh_render = { + mesh="Astroid_" .. model_number .. ".model", + mesh_offset={0, 0, z}, + texture="Astroid_" .. model_number .. "_d.png", + specular_texture="Astroid_" .. model_number .. "_s.png", + scale=size, + }, + spin={rate=random(0.1, 0.8)}, + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets this Asteroid's radius. +--- Defaults to a random value between 110 and 130. +--- Example: asteroid:setSize(150) +function Entity:setSize(radius) + local comp = self.components + if comp.physics then comp.physics.size=radius end + if comp.mesh_render then comp.mesh_render.scale=radius end + if comp.avoid_object then comp.avoid_object.range=radius*2 end + return self +end +--- Returns this Asteroid's radius. +--- Example: asteroid:getSize() +function Entity:getSize() + local comp = self.components + if comp.physics then return comp.physics.size end + if comp.mesh_render then return comp.mesh_render.scale end + return 100.0 +end diff --git a/scripts/api/entity/beameffect.lua b/scripts/api/entity/beameffect.lua new file mode 100644 index 0000000000..3d89a1a58e --- /dev/null +++ b/scripts/api/entity/beameffect.lua @@ -0,0 +1,68 @@ +--- A BeamEffect is a beam weapon firing audio/visual effect that fades after its duration expires. +--- This is a cosmetic effect and does not deal damage on its own. +--- Example: beamfx = BeamEffect():setSource(player,0,0,0):setTarget(enemy,0,0,0) +function BeamEffect() + local e = createEntity() + e.components = { + transform = {}, + beam_effect = {beam_texture = "texture/beam_orange.png"}, + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets the BeamEffect's origin SpaceObject. +--- Requires a 3D x/y/z vector positional offset relative to the object's origin point. +--- Example: beamfx:setSource(source, 0,0,0) +function Entity:setSource(source, x, y, z) + if self.components.beam_effect then + self.components.beam_effect.source = source + self.components.beam_effect.source_offset = {x, y, z} + end + return self +end +--- Sets the BeamEffect's texture. +--- Valid values are filenames of PNG files relative to the resources/ directory. +--- Defaults to "texture/beam_orange.png". +--- Example: beamfx:setTexture("beam_blue.png") +function Entity:setTexture(texture) + if self.components.beam_effect then + self.components.beam_effect.beam_texture = texture + end + return self +end +--- Sets the BeamEffect's sound effect. +--- Valid values are filenames of WAV files relative to the resources/ directory. +--- Defaults to "sfx/laser_fire.wav". +--- Example: beamfx:setBeamFireSound("sfx/hvli_fire.wav") +function Entity:setBeamFireSound() + --TODO + return self +end +--- Sets the magnitude of the BeamEffect's sound effect. +--- Defaults to 1.0. +--- Larger values are louder and can be heard from larger distances. +--- This value also affects the sound effect's pitch. +--- Example: beamfx:setBeamFireSoundPower(0.5) +function Entity:setBeamFireSoundPower(level) + --TODO + return self +end +--- Sets the BeamEffect's duration, in seconds. +--- Defaults to 1.0. +--- Example: beamfx:setDuration(1.5) +function Entity:setDuration(duration) + if self.components.beam_effect then + self.components.beam_effect.lifetime = duration + end + return self +end +--- Defines whether the BeamEffect generates an impact ring on the target end. +--- Defaults to true. +--- Example: beamfx:setRing(false) +function Entity:setRing(enabled) + if self.components.beam_effect then + self.components.beam_effect.fire_ring = enabled + end + return self +end diff --git a/scripts/api/entity/blackhole.lua b/scripts/api/entity/blackhole.lua new file mode 100644 index 0000000000..1f73a24a72 --- /dev/null +++ b/scripts/api/entity/blackhole.lua @@ -0,0 +1,63 @@ +--- A BlackHole is a piece of space terrain that pulls all nearby SpaceObjects within a 5U radius, including otherwise immobile objects like SpaceStations, toward its center. +--- A SpaceObject capable of taking damage is dealt an increasing amount of damage as it approaches the BlackHole's center. +--- Upon reaching the center, any SpaceObject is instantly destroyed even if it's otherwise incapable of taking damage. +--- AI behaviors avoid BlackHoles by a 2U margin. +--- In 3D space, a BlackHole resembles a black sphere with blue horizon. +--- Example: black_hole = BlackHole():setPosition(1000,2000) +--- @type creation +function BlackHole() + local e = createEntity() + e.components = { + transform = {}, + never_radar_blocked = {}, + gravity = {range=5000, damage=true}, + avoid_object = {range=7000}, + radar_signature = {gravity=0.9}, + radar_trace = {icon="radar/blackHole.png", min_size=0, max_size = 2048, radius=5000}, + billboard_render = {texture="blackHole3d.png", size=5000} + } + return e +end + + +--- A WormHole is a piece of space terrain that pulls all nearby SpaceObjects within a 5U radius, including otherwise immobile objects like SpaceStations, toward its center. +--- Any SpaceObject that reaches its center is teleported to another point in space. +--- AI behaviors avoid WormHoles by a 2U margin. +--- Example: wormhole = WormHole():setPosition(1000,1000):setTargetPosition(10000,10000) +function WormHole() + local e = createEntity() + local radius = 2500 + e.components = { + transform = {}, + never_radar_blocked = {}, + gravity = {range=radius, damage=false}, + avoid_object = {range=radius*1.2}, + radar_signature = {gravity=0.9}, + radar_trace = {icon="radar/wormhole.png", min_size=0, max_size = 2048, radius=radius}, + billboard_render = {texture="wormHole3d.png", size=5000} + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets the target teleportation coordinates for SpaceObjects that pass through the center of this WormHole. +--- Example: wormhole:setTargetPosition(10000,10000) +function Entity:setTargetPosition(x, y) + if self.components.gravity then self.components.gravity.wormhole_target = {x, y} end + return self +end +--- Returns the target teleportation coordinates for SpaceObjects that pass through the center of this WormHole. +--- Example: wormhole:getTargetPosition() +function Entity:getTargetPosition() + if self.components.gravity then return self.components.gravity.wormhole_target end + return nil +end +--- Defines a function to call when this WormHole teleports a SpaceObject. +--- Passes the WormHole object and the teleported SpaceObject. +--- Example: +--- -- Outputs teleportation details to the console window and logging file +--- wormhole:onTeleportation(function(this_wormhole,teleported_object) print(teleported_object:getCallSign() .. " teleported to " .. this_wormhole:getTargetPosition()) end) +function Entity:onTeleportation(callback) + if self.components.gravity then self.components.gravity.on_teleportation = callback end + return self +end diff --git a/scripts/api/entity/cpuship.lua b/scripts/api/entity/cpuship.lua new file mode 100644 index 0000000000..42ac8d090c --- /dev/null +++ b/scripts/api/entity/cpuship.lua @@ -0,0 +1,147 @@ +local Entity = getLuaEntityFunctionTable() +__default_cpu_ship_faction = "Kraylor" + +--- A CpuShip is an AI-controlled SpaceShip. +--- The AI can be assigned an order (be idle, roam freely, defend location, etc.) and a combat behavior state (attack at close or long range, be evasive). +--- AI behaviors are defined in ai.cpp and other files in src/ai/. +--- CpuShip:order... functions also broadcast their orders over friendly comms. +--- Autonomous combat AI orders use the CpuShip's short- and long-range radar ranges to acquire targets, which can be affected by nebulae. +--- They also rank prospective targets by their type, distance, and capabilities. +--- Example: +--- -- Place a Fighter-class Human Navy CpuShip, order it to roam, and if it engages in combat it will fight evasively +--- ship = CpuShip():setTemplate("Fighter"):setPosition(10000,3000):setFaction("Human Navy"):orderRoaming():setAI("evasive"):setScanned(true) +--- @type creation +function CpuShip() + local e = createEntity() + e.components = { + transform = {rotation=random(0, 360)}, + ai_controller = {new_name="default", orders="roaming"}, + scan_state = {allow_simple_scan=true}, + callsign = {callsign=generateRandomCallSign()}, + } + e:setFaction(__default_cpu_ship_faction) + return e +end + +--- Sets the default combat AI state for this CpuShip. +--- Combat AI states determine the AI's combat tactics and responses. +--- They're distinct from orders, which determine the ship's active objectives and are defined by CpuShip:order...() functions. +--- Combat AI state can be set per CpuShip, defined in the ShipTemplate, or left to "default". +--- Valid combat AI states are: +--- - "default" directly pursues enemies at beam range while making opportunistic missile attacks +--- - "evasion" maintains distance from enemy weapons and evades attacks +--- - "fighter" prefers strafing maneuvers and attacks briefly at close range while passing +--- - "missilevolley" prefers lining up missile attacks from long range +--- Example: ship:setAI("fighter") +function Entity:setAI(ai_name) + if self.components.ai_controller then self.components.ai_controller.new_name = ai_name end + return self +end +--- Orders this CpuShip to stay at its current position and do nothing. +--- Idle CpuShips don't target or attack nearby enemies. +--- Example: ship:orderIdle() +function Entity:orderIdle() + if self.components.ai_controller then self.components.ai_controller.orders = "idle" end + return self +end +--- Orders this CpuShip to roam and engage at will, without a specific target. +--- A Roaming ship can acquire hostile targets within its long-range radar range, and prefers the best hostile target within 2U of its short-range radar range. +--- If this ship has weapon tubes but lacks beam weapons and is out of weapons stock, it attempts to Retreat to a weapons restock target within long-range radar range. +--- Example: ship:orderRoaming() +function Entity:orderRoaming() + if self.components.ai_controller then self.components.ai_controller = {orders = "roaming", order_target_location={0, 0}} end + return self +end +function Entity:orderRoamingAt(x, y) + if self.components.ai_controller then self.components.ai_controller = {orders = "roaming", order_target_location={x, y}} end + return self +end +--- Orders this CpuShip to move toward the given SpaceObject and dock, restock weapons, and repair its hull. +--- If the SpaceObject is a dockable ShipTemplateBasedObject, this ship moves directly toward it and docks with it as soon as possible. +--- If not, this ship moves toward the best weapons restocking target within relay range (double its long-range radar range). +--- If this ship still can't find a restocking target, or it is fully repaired and re-stocked, this ship reverts to Roaming orders. +--- Example: ship:orderRetreat(base) -- retreat to the SpaceObject `base` +function Entity:orderRetreat(target) + if self.components.ai_controller then self.components.ai_controller = {orders = "retreat", order_target=target} end + return self +end +--- Orders this CpuShip to stay at its current position and attack nearby hostiles. +--- This ship will rotate to face a target and fires missiles within 4.5U if it has any, but won't move, roam, or patrol. +--- Example: ship:orderStandGround() +function Entity:orderStandGround() + if self.components.ai_controller then self.components.ai_controller = {orders = "stand ground"} end + return self +end +--- Orders this CpuShip to move to the given coordinates, patrol within a 1.5U radius, and attack any hostiles that move within 2U of its short-range radar range. +--- If a targeted hostile moves more than 3U out of this ship's short-range radar range, this ship drops the target and resumes defending its position. +--- Example: ship:orderDefendLocation(500, 1000) -- defend the space near these coordinates +function Entity:orderDefendLocation(x, y) + if self.components.ai_controller then self.components.ai_controller = {orders = "defend location", order_target_location={x, y}} end + return self +end +--- Orders this CpuShip to maintain a 2U escort distance from the given SpaceObject and attack nearby hostiles. +--- If a targeted hostile moves more than 3U out of this ship's short-range radar range, this ship drops the target and resumes escorting. +--- If the SpaceObject being defended is destroyed, this ship reverts to Roaming orders. +--- Example: ship:orderDefendTarget(base) -- defend the space near the SpaceObject `base` +function Entity:orderDefendTarget(target) + if self.components.ai_controller then self.components.ai_controller = {orders = "defend target", order_target=target} end + return self +end +--- Orders this CpuShip to fly toward the given SpaceObject and follow it from the given offset distance. +--- This ship also targets anything its given SpaceObject targets. +--- If the SpaceObject being followed is destroyed, this ship reverts to Roaming orders. +--- Give multiple CpuShips the same SpaceObject and different offsets to create a formation. +--- Example: ship:orderFlyFormation(leader, 500, 250) -- fly 0.5U off the wing and 0.25U off the tail of the SpaceObject `leader` +function Entity:orderFlyFormation(target, offset_x, offset_y) + if self.components.ai_controller then self.components.ai_controller = {orders = "fly in formation", order_target=target, order_target_location={x, y}} end + return self +end +--- Orders this CpuShip to move toward the given coordinates, and to attack hostiles that approach within its short-range radar range during transit. +--- This ship uses any warp or jump drive capabilities to arrive near its destination. +--- This ship disengages from combat and continues toward its destination if its target moves more than 3U out of its short-range radar range. +--- Upon arrival, this ship reverts to the Defend Location orders with its destination as the target. +--- Example: ship:orderFlyTowards(500, 1000) -- move to these coordinates, attacking nearby hostiles on the way +function Entity:orderFlyTowards(x, y) + if self.components.ai_controller then self.components.ai_controller = {orders = "fly towards", order_target_location={x, y}} end + return self +end +--- Orders this CpuShip to move toward the given coordinates, ignoring all hostiles on the way. +--- Upon arrival, this ship reverts to the Idle orders. +--- Example: ship:orderFlyTowardsBlind(500, 1000) -- move to these coordinates, ignoring hostiles +function Entity:orderFlyTowardsBlind(x, y) + if self.components.ai_controller then self.components.ai_controller = {orders = "fly towards (ignore all)", order_target_location={x, y}} end + return self +end +--- Orders this CpuShip to attack the given SpaceObject. +--- Example: ship:orderAttack(player) +function Entity:orderAttack(target) + if self.components.ai_controller then self.components.ai_controller = {orders = "attack", order_target=target} end + return self +end +--- Orders this CpuShip to Fly Toward and dock with the given SpaceObject, if possible. +--- If its target doesn't exist, revert to Roaming orders. +--- Example: ship:orderDock(spaceStation) +function Entity:orderDock(target) + if self.components.ai_controller then self.components.ai_controller = {orders = "dock", order_target=target} end + return self +end +--- Returns this CpuShip's current orders. +--- Example: ship_orders = ship:getOrder() +function Entity:getOrder() + if self.components.ai_controller then return self.components.ai_controller.orders end +end +--- Returns the coordinates for this CpuShip's orders. +--- If the orders target a SpaceObject instead of coordinates, use CpuShip:getOrderTarget(). +--- Some orders, such as Roaming, have no target. +--- Returns the order's x,y coordinates, or 0,0 if not defined. +--- Example: x,y = ship:getOrderTargetLocation() +function Entity:getOrderTargetLocation() + if self.components.ai_controller then return table.unpack(self.components.ai_controller.order_target_location) end +end +--- Returns the target SpaceObject for this CpuShip's orders. +--- If the orders target coordinates instead of an object, use CpuShip:getOrderTargetLocation(). +--- Some orders, such as Roaming, have no target. +--- Example: target = ship:getOrderTarget() +function Entity:getOrderTarget() + if self.components.ai_controller then return self.components.ai_controller.order_target end +end diff --git a/scripts/api/entity/factioninfo.lua b/scripts/api/entity/factioninfo.lua new file mode 100644 index 0000000000..97db1f985a --- /dev/null +++ b/scripts/api/entity/factioninfo.lua @@ -0,0 +1,97 @@ +local Entity = getLuaEntityFunctionTable() +----- Old FactionInfo API ----- + +--- A FactionInfo object contains presentation details and faction relationships for member SpaceObjects. +--- EmptyEpsilon has a hardcoded limit of 32 factions. +--- +--- SpaceObjects belong to a faction that determines which objects are friendly, neutral, or hostile toward them. +--- For example, these relationships determine whether a SpaceObject can be targeted by weapons, docked with, or receive comms from another SpaceObject. +--- If a faction doesn't have a relationship with another faction, it treats those factions as neutral. +--- Friendly and hostile faction relationships are automatically reciprocated when set with setEnemy() and setFriendly(). +--- +--- If this faction consideres another faction to be hostile, it can target and fire weapons at it, and CpuShips with certain orders might pursue it. +--- If neutral, this faction can't target and fire weapons at the other faction, and other factions can dock with its stations or dockable ships. +--- If friendly, this faction acts as neutral but also shares short-range radar with PlayerSpaceships in Relay, and can grant reputation points to PlayerSpaceships of the same faction. +--- +--- Many scenario and comms scripts also give friendly factions benefits at a reputation cost that netural factions do not. +--- Factions are loaded from resources/factionInfo.lua upon launching a scenario, and accessed by using the getFactionInfo() global function. +--- +--- Example: +--- human_navy = getFactionInfo("Human Navy") +--- exuari = getFactionInfo("Exuari") +--- faction = FactionInfo():setName("USN"):setLocaleName(_("USN")) -- sets the internal and translatable faction names +--- faction:setGMColor(255,128,255) -- uses purple icons for this faction's SpaceObjects in GM and Spectator views +--- faction:setFriendly(human_navy):setEnemy(exuari) -- sets this faction's friendly and hostile relationships +--- faction:setDescription(_("The United Stellar Navy, or USN...")) -- sets a translatable description for this faction +__faction_info = {} +function FactionInfo() + local fi = createEntity() + fi.components.faction_info = {} + fi:__setFactionRelation(fi, "friendly") + return fi +end +function getFactionInfo(name) + return __faction_info[name] +end + +--- Sets this faction's name as presented in the user interface. +--- Wrap the string in the _() function to make it available for translation. +--- Example: faction:setLocaleName(_("USN")) +function Entity:setLocaleName(name) + if self.components.faction_info then + self.components.faction_info.locale_name = name + end + return self +end +--- Sets the RGB color used for SpaceObjects of this faction as seen on the GM and Spectator views. +--- Defaults to white (255,255,255). +--- Example: faction:setGMColor(255,0,0) -- sets the color to red +function Entity:setGMColor(r, g, b) + if self.components.faction_info then + self.components.faction_info.gm_color = {r, g, b, 255} + end + return self +end +--- Sets the given faction to appear as hostile to SpaceObjects of this faction. +--- For example, Spaceships of this faction can target and fire at SpaceShips of the given faction. +--- Defaults to no hostile factions. +--- Warning: A faction can be designated as hostile to itself, but the behavior is not well-defined. +--- Example: faction:setEnemy(exuari) -- sets the Exuari to appear as hostile to this faction +function Entity:setEnemy(other_faction) + if self.components.faction_info == nil then error("setEnemy can only be called on factions.") end + if other_faction.components.faction_info == nil then error("setEnemy can only be called on factions.") end + self:__setFactionRelation(other_faction, "enemy") + other_faction:__setFactionRelation(self, "enemy") + return self +end +--- Sets the given faction to appear as friendly to SpaceObjects of this faction. +--- For example, PlayerSpaceships of this faction can gain reputation with it. +--- Defaults to no friendly factions. +--- Example: faction:setFriendly(exuari) -- sets the Human Navy to appear as friendly to this faction +function Entity:setFriendly(other_faction) + if self.components.faction_info == nil then error("setFriendly can only be called on factions.") end + if other_faction.components.faction_info == nil then error("setFriendly can only be called on factions.") end + self:__setFactionRelation(other_faction, "friendly") + other_faction:__setFactionRelation(self, "friendly") + return self +end +--- Sets the given faction to appear as neutral to SpaceObjects of this faction. +--- This removes any existing faction relationships between the two factions. +--- Example: faction:setNeutral(human_navy) -- sets the Human Navy to appear as neutral to this faction +function Entity:setNeutral(other_faction) + if self.components.faction_info == nil then error("setNeutral can only be called on factions.") end + if other_faction.components.faction_info == nil then error("setNeutral can only be called on factions.") end + self:__setFactionRelation(other_faction, "neutral") + other_faction:__setFactionRelation(self, "neutral") + return self +end + +function Entity:__setFactionRelation(other_faction, relation) + for n=1, #self.components.faction_info do + if self.components.faction_info[n].other_faction == other_faction then + self.components.faction_info[n].relation = relation + return + end + end + self.components.faction_info[#self.components.faction_info+1] = {other_faction=other_faction, relation=relation} +end diff --git a/scripts/api/entity/mine.lua b/scripts/api/entity/mine.lua new file mode 100644 index 0000000000..72142963af --- /dev/null +++ b/scripts/api/entity/mine.lua @@ -0,0 +1,23 @@ + +--- A Mine is an explosive weapon that detonates and deals kinetic damage when a SpaceObject collides with its trigger range. +--- Mines can be owned by factions but are triggered by SpaceObjects of any faction can trigger them. +--- Mines can be launched from a SpaceShip's weapon tube or added by a GM or scenario script. +--- When launched from a SpaceShip, the mine has an eject timeout, during which its trigger range is inactive. +--- In 3D views, mines are represented by a particle effect at the center of its trigger range. +--- To create objects with more complex collision mechanics, use an Artifact. +--- Example: mine = Mine():setPosition(1000,1000):onDestruction(this_mine, instigator) print("Tripped a mine!") end) +--- @type creation +function Mine() + local blast_range = 1000.0 + local e = createEntity() + e.components.transform = {} + e.components.radar_trace = {icon="radar/mine.png", min_size=10, max_size = 10} + e.components.constant_particle_emitter = {interval=0.4, start_color={1, 1, 1}, end_color={0, 0, 1}, start_size=30.0, end_size=0.0, life_time=10.0} + e.components.radar_signature = {electrical=0.05} + e.components.avoid_object = {range=blast_range*1.2} + e.components.physics = {type="sensor", size=blast_range*0.6} + e.components.delayed_explode_on_touch = {delay=1.0, damage_at_center=160.0, damage_at_edge=30.0, blast_range=1000.0} + return e +end + +local Entity = getLuaEntityFunctionTable() diff --git a/scripts/api/entity/multiuse.lua b/scripts/api/entity/multiuse.lua new file mode 100644 index 0000000000..74c49c1559 --- /dev/null +++ b/scripts/api/entity/multiuse.lua @@ -0,0 +1,93 @@ +local Entity = getLuaEntityFunctionTable() + +-- Functions that have multiple implementations as a result of the old object code are here and interact with multiple components. + + +--- Sets this faction's internal string name, used to reference this faction regardless of EmptyEpsilon's language setting. +--- If no locale name is defined, this sets the locale name to the same value. +--- Example: faction:setName("USN") +--- Sets this ScienceDatabase entry's displayed name. +--- Example: entry:setName("Species") +function Entity:setName(name) + if self.components.faction_info then + self.components.faction_info.name = name + __faction_info[name] = self + end + if self.components.science_database then + self.components.science_database.name = name + end + return self +end + +--- Sets this faction's longform description as shown in its Factions ScienceDatabase child entry. +--- Wrap the string in the _() function to make it available for translation. +--- Example: faction:setDescription(_("The United Stellar Navy, or USN...")) -- sets a translatable description for this faction +--- As setDescriptions, but sets the same description for both unscanned and scanned states. +--- Example: obj:setDescription("A refitted Atlantis X23 for more ...") +function Entity:setDescription(description) + if self.components.faction_info then + self.components.faction_info.description = description + else + self.components.science_description = {not_scanned=description, friend_or_foe_identified=description, simple_scan=description, full_scan=description} + end + return self +end + +--- Sets this SpaceShip's energy level. +--- Valid values are any greater than 0 and less than the energy capacity (getMaxEnergy()). +--- Invalid values are ignored. +--- CpuShips don't consume energy. Setting this value has no effect on their behavior or functionality. +--- For PlayerSpaceships, see PlayerSpaceship:setEnergyLevel(). +--- Example: ship:setEnergy(1000) -- sets the ship's energy to 1000 if its capacity is 1000 or more +--- Sets the amount of energy recharged upon pickup when a PlayerSpaceship collides with this SupplyDrop. +--- Example: supply_drop:setEnergy(500) +function Entity:setEnergy(amount) + if self.components.reactor then self.components.reactor.energy = amount end + if self.components.pickup then self.components.pickup.give_energy = amount end + return self +end + +--- Returns this SpaceShip's weapons target. +--- For a CpuShip, this can differ from its orders target. +--- Example: target = ship:getTarget() +--- Returns this ScanProbe's target coordinates. +--- Example: targetX,targetY = probe:getTarget() +function Entity:getTarget() + if self.components.weapons_target then + return self.components.weapons_target.entity + end + if self.components.move_to then + local target = self.components.move_to.target + return target[1], target[2] + end + return nil +end + +--- Returns this ScanProbe's owner SpaceObject. +--- Example: probe:getOwner() +function Entity:getOwner() + if self.components.delayed_explode_on_touch then + return self.components.delayed_explode_on_touch.owner + end + if self.components.allow_radar_link then + return self.components.allow_radar_link.owner + end + return self +end + +--- Sets this ScanProbe's target coordinates. +--- If the probe has reached its target, ScanProbe:setTarget() moves it again toward the new target coordinates. +--- Example: probe:setTarget(1000,5000) +--- Sets the BeamEffect's target SpaceObject. +--- Requires a 3D x/y/z vector positional offset relative to the object's origin point. +--- Example: beamfx:setTarget(target,0,0,0) +function Entity:setTarget(a, b, c, d) + if self.components.move_to then + self.components.move_to = {target={a, b}} + end + if self.components.beam_effect then + self.components.beam_effect.target = a + self.components.beam_effect.target_offset = {b, c, d} + end + return self +end diff --git a/scripts/api/entity/nebula.lua b/scripts/api/entity/nebula.lua new file mode 100644 index 0000000000..a685300afe --- /dev/null +++ b/scripts/api/entity/nebula.lua @@ -0,0 +1,27 @@ + +--- A Nebula is a piece of space terrain with a 5U radius that blocks long-range radar, but not short-range radar. +--- This hides any SpaceObjects inside of a Nebula, as well as SpaceObjects on the other side of its radar "shadow", from any SpaceShip outside of it. +--- Likewise, a SpaceShip fully inside of a nebula has effectively no long-range radar functionality. +--- In 3D space, a Nebula resembles a dense cloud of colorful gases. +--- Example: nebula = Nebula():setPosition(1000,2000) +--- @type creation +function Nebula() + local radius = 5000.0 + local e = createEntity() + e.components.radar_signature = {gravity=0, electrical=0.8, biological=-1.0} + e.components.transform = {rotation=random(0, 360)} + e.components.radar_trace = {icon="Nebula" .. irandom(1, 3) .. ".png", min_size=0, max_size = 2048, radius=radius*1.5, blend_add=true} + e.components.radar_block = {range=radius} + e.components.never_radar_blocked = {} + local render_info = {} + local cloud_count = 32 + for n=1,cloud_count do + local size = random(512, 1024 * 2) + local dist = random(size / 2.0, radius - size) + local angle = n * 360 / cloud_count + local ox, oy = math.cos(angle / 180 * math.pi) * dist, math.sin(angle / 180 * math.pi) * dist + render_info[n] = {size=size, texture="Nebula" .. irandom(1, 3) .. ".png", offset={ox, oy}} + end + e.components.nebula_renderer = render_info + return e +end \ No newline at end of file diff --git a/scripts/api/entity/planet.lua b/scripts/api/entity/planet.lua new file mode 100644 index 0000000000..efa918908f --- /dev/null +++ b/scripts/api/entity/planet.lua @@ -0,0 +1,147 @@ +--- A Planet is a spherical piece of space terrain that can orbit other SpaceObjects. +--- Each Planet has separate textures for its surface, atmosphere, and cloud layers. +--- Several planetary textures are included in the resources/planets/ directory. +--- Planets can collide with objects and run callback functions upon collisions. +--- Examples: +--- -- Creates a small planetary system with a sun, a planet orbiting the sun, and a moon orbiting the planet. +--- sun = Planet():setPosition(5000, 15000):setPlanetRadius(1000):setPlanetAtmosphereTexture("planets/star-1.png"):setPlanetAtmosphereColor(1.0, 1.0, 1.0) +--- planet = Planet():setPosition(5000, 5000):setPlanetRadius(3000):setPlanetSurfaceTexture("planets/planet-1.png") +--- planet:setPlanetCloudTexture("planets/clouds-1.png"):setPlanetAtmosphereTexture("planets/atmosphere.png"):setPlanetAtmosphereColor(0.2, 0.2, 1.0):setOrbit(sun,40) +--- moon = Planet():setPosition(5000, 0):setPlanetRadius(1000):setPlanetSurfaceTexture("planets/moon-1.png"):setAxialRotationTime(20.0):setOrbit(planet,20) +--- @type creation +function Planet() + local e = createEntity() + e.components = { + transform = {rotation=random(0, 360)}, + radar_signature = {gravity=0.5, biological=0.3}, + planet_render = { + size=5000, + cloud_size = 5200, + }, + physics = {type="static", size=5000}, + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets this Planet's atmospheric effect color. +--- Example: planet:setPlanetAtmosphereColor(0.2,0.2,1.0) -- sets a blue atmosphere +function Entity:setPlanetAtmosphereColor(r, g, b) + if self.components.planet_render then self.components.planet_render.atmosphere_color = {r, g, b} end + return self +end +--- Sets this Planet's atmospheric effect texture. +--- Valid values are filenames of PNG files relative to the resources/ directory. +--- Optional; if defined, atmosphere textures should be transparent or translucent. +--- For stars, you can set an atmosphere texture such as planets/star-1.png with no surface texture. +--- Example: planet:setPlanetSurfaceTexture("planets/atmosphere.png") +function Entity:setPlanetAtmosphereTexture(texture) + if self.components.planet_render then self.components.planet_render.atmosphere_texture = texture end + return self +end +--- Sets this Planet's surface texture. +--- Valid values are filenames of PNG files relative to the resources/ directory. +--- Optional; if defined, surface textures should be opaque and use a 2:1-ratio equirectangular projection. +--- Example: planet:setPlanetSurfaceTexture("planets/planet-1.png") +function Entity:setPlanetSurfaceTexture(texture) + if self.components.planet_render then self.components.planet_render.texture = texture end + return self +end +--- Sets this Planet's cloud layer effect texture, which rotates independently of the planet. +--- Valid values are filenames of PNG files relative to the resources/ directory. +--- Optional; if defined, cloud layer textures should be transparent or translucent. +--- Example: planet:setPlanetCloudTexture("planets/cloud-1.png") +function Entity:setPlanetCloudTexture(texture) + if self.components.planet_render then self.components.planet_render.cloud_texture = texture end + return self +end +--- Returns this Planet's radius. +--- Example: planet:getPlanetRadius() +function Entity:getPlanetRadius() + if self.components.planet_render then + return self.components.planet_render.size + end + return 1000.0 +end +--- Sets this Planet's radius, which also sets: +--- - its cloud radius to 1.05x this value +--- - its atmosphere radius to 1.2x this value +--- - its collision size to a function of this value and the planet's z-position +--- Defaults to 5000 (5U). +--- Example: planet:setPlanetRadius(2000) +function Entity:setPlanetRadius(size) + local pr = self.components.planet_render + if pr then + pr.size = size + pr.cloud_size = size*1.05 + pr.atmosphere_size = size*1.2 + if (pr.size * pr.size) > (pr.distance_from_movement_plane * pr.distance_from_movement_plane) then + local collision_size = math.sqrt((pr.size * pr.size) - (pr.distance_from_movement_plane * pr.distance_from_movement_plane)) * 1.1; + self.components.physics = {type="static", size=collision_size} + else + self.components.physics = nil + end + end + return self +end +--- Sets this Planet's collision radius. +--- Defaults to a function of the Planet's radius and its z-position. +--- AI behaviors use this size to plot routes that try to avoid colliding with this Planet. +--- Example: planet:getCollisionSize() +function Entity:getCollisionSize() + if self.components.physics then + return self.components.physics.size + end + return 0.0 +end +--- Sets this Planet's cloud radius, overriding Planet:setPlanetRadius(). +--- Defaults to 1.05x this Planet's radius. +--- If this value isn't larger than the Planet's radius, the cloud layer won't be visible. +--- Example: planet:setPlanetCloudRadius(2500) -- sets this Planet's cloud radius to 2.5U +function Entity:setPlanetCloudRadius(radius) + if self.planet_render then self.components.planet_render.cloud_size = radius end + return self +end +--- Sets the z-position of this Planet, the distance by which it's offset above (positive) or below (negative) the movement plane. +--- This value also modifies the Planet's collision radius. +--- Defaults to 0. +--- Example: planet:setDistanceFromMovementPlane(-500) -- sets the planet 0.5U below the movement plane +function Entity:setDistanceFromMovementPlane(z) + local pr = self.components.planet_render + if pr then + pr.distance_from_movement_plane = z + if (pr.size * pr.size) > (pr.distance_from_movement_plane * pr.distance_from_movement_plane) then + local collision_size = math.sqrt((pr.size * pr.size) - (pr.distance_from_movement_plane * pr.distance_from_movement_plane)) * 1.1; + self.components.physics = {type="static", size=collision_size} + else + self.components.physics = nil + end + end + return self +end +--- Sets this Planet's axial rotation time, seconds per full rotation. +--- Defaults to 0. +--- Example: planet:setAxialRotationTime(20) +function Entity:setAxialRotationTime(rotation_time) + if spin ~= 0.0 then + self.components.spin = {rate=360.0/rotation_time} + else + self.components.spin = nil + end + return self +end +--- Sets a SpaceObject around which this Planet orbits, as well as its orbital period in orbital degrees per tick. +--- Example: moon:setOrbit(planet, 20) +function Entity:setOrbit(target, time) + local x0, y0 = self:getPosition() + local x1, y1 = target:getPosition() + local xd, yd = (x1 - x0), (y1 - y0) + local distance = math.sqrt(xd * xd + yd * yd) + self.components.orbit = { + target = target, + center = {x1, y1}, + distance = distance, + time = time, + } + return self +end diff --git a/scripts/api/entity/playerspaceship.lua b/scripts/api/entity/playerspaceship.lua new file mode 100644 index 0000000000..22f95f0fa6 --- /dev/null +++ b/scripts/api/entity/playerspaceship.lua @@ -0,0 +1,800 @@ +local Entity = getLuaEntityFunctionTable() +__default_player_ship_faction = "Human Navy" + +--- A PlayerSpaceship is a SpaceShip controlled by a player crew. +--- If a function name begins with "command", the function is equivalent to the crew taking a corresponding action. +--- Such commands can be limited by the ship's capabilities, including systems damage, lack of power, or insufficient weapons stocks. +--- @type creation +function PlayerSpaceship() + local e = createEntity() + e.components = { + player_control = {}, + ship_log = {}, + custom_ship_functions = {}, + transform = {rotation=random(0, 360)}, + callsign = {callsign=generateRandomCallSign()}, + scan_state = {allow_simple_scan=true}, + } + e:setFaction(__default_player_ship_faction) + return e +end + +--- Returns the coordinates of a waypoint with the given index that's been set by this PlayerSpaceship. +--- Waypoints are 1-indexed. +--- Example: x,y = player:getWaypoint(1) +function Entity:getWaypoint(index) + if self.components.long_range_radar and index > 0 and index <= #self.components.long_range_radar then return self.components.long_range_radar[index] end + return 0, 0 +end +--- Returns the total number of active waypoints owned by this PlayerSpaceship. +--- Example: player:getWaypointCount() +function Entity:getWaypointCount() + if self.components.long_range_radar then return #self.components.long_range_radar end + return 0 +end +--- Returns this PlayerSpaceship's EAlertLevel. +--- Returns "Normal", "YELLOW ALERT", "RED ALERT", which differ from the valid values for PlayerSpaceship:commandSetAlertLevel(). +--- Example: player:getAlertLevel() +function Entity:getAlertLevel() + if self.components.player_control then return self.components.player_control.alert_level end + return "Normal" +end +--- Defines whether this PlayerSpaceship's shields are raised (true) or lowered (false). +--- Compare to CpuShips, whose shields are always active. +--- Example: player:setShieldsActive(true) +function Entity:setShieldsActive(active) + if self.components.shields ~= nil then self.components.shields.active = active end + return self +end +--- Adds a message to this PlayerSpaceship's log. +--- Takes a string as the message and a color applied to the logged message. +--- Example: player:addToShipLog("Acknowledged","yellow") -- adds "Acknowledged" in yellow to the `player` ship's log +function Entity:addToShipLog(message, color) + addEntryToShipsLog(self, message, color) +end +--- Moves all players connected to this ship to the same crew positions on another PlayerSpaceship. +--- If the target isn't a PlayerSpaceship, this function has no effect. +--- Use this in scenarios to change the crew's ship. +--- Example: player:transferPlayersToShip(player2) -- transfer all player crew to `player2` +function Entity:transferPlayersToShip(other_ship) + transferPlayersFromShipToShip(self, other_ship) +end +--- Transfers only the crew members on a specific crew position to another PlayerSpaceship. +--- If a player is in multiple positions, this matches any of their positions and moves that player to all of the same positions on the destination ship. +--- Example: player:transferPlayersAtPositionToShip("helms",player2) -- transfer all crew on Helms to `player2` +function Entity:transferPlayersAtPositionToShip(station, other_ship) + transferPlayersFromShipToShip(self, other_ship, station) +end +--- Returns whether a player occupies the given crew position on this PlayerSpaceship. +--- Example: player:hasPlayerAtPosition("helms") +function Entity:hasPlayerAtPosition(station) + return hasPlayerCrewAtPosition(self, station) +end + +--- Returns whether this PlayerSpaceship's comms are not in use. +--- Use this to determine whether the player can accept an incoming hail or chat. +--- Example: player:isCommsInactive() +function Entity:isCommsInactive() + if self.components.comms_transmitter then return self.components.comms_transmitter.state == "inactive" end + return true +end +--- Returns whether this PlayerSpaceship is opening comms with another SpaceObject. +--- Example: player:isCommsOpening() +function Entity:isCommsOpening() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "opening" + end +end +--- Returns whether this PlayerSpaceship is being hailed by another SpaceObject. +--- Example: player:isCommsBeingHailed() +function Entity:isCommsBeingHailed() + if self.components.comms_transmitter then + local state = self.components.comms_transmitter.state + return state == "hailed" or state == "hailed_player" or state == "hailed_gm" + end +end +--- Returns whether this PlayerSpaceship is being hailed by the GM. +--- Example: player:isCommsBeingHailedByGM() +function Entity:isCommsBeingHailedByGM() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "hailed_gm" + end +end +--- Returns whether comms to this PlayerSpaceship have failed to open. +--- Example: player:isCommsFailed() +function Entity:isCommsFailed() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "failed" + end +end +--- Returns whether comms to this PlayerSpaceship were broken off by the other SpaceObject. +--- Example: player:isCommsBroken() +function Entity:isCommsBroken() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "broken" + end +end +--- Returns whether comms between this PlayerSpaceship and a SpaceObject were intentionally closed. +--- Example: player:isCommsClosed() +function Entity:isCommsClosed() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "closed" + end +end +--- Returns whether this PlayerSpaceship is engaged in text chat with either the GM or another PlayerSpaceship. +--- Example: player:isCommsChatOpen() +function Entity:isCommsChatOpen() + if self.components.comms_transmitter then + local state = self.components.comms_transmitter.state + return state == "open_gm" or state == "open_player" + end +end +--- Returns whether this PlayerSpaceship is engaged in text chat with the GM. +--- Example: player:isCommsChatOpenToGM() +function Entity:isCommsChatOpenToGM() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "open_gm" + end +end +--- Returns whether this PlayerSpaceship is engaged in text chat with another PlayerSpaceship. +--- Example: player:isCommsChatOpenToPlayer() +function Entity:isCommsChatOpenToPlayer() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "open_player" + end +end +--- Returns whether this PlayerSpaceship is engaged in comms with a scripted SpaceObject. +--- Example: player:isCommsScriptOpen() +function Entity:isCommsScriptOpen() + if self.components.comms_transmitter then + return self.components.comms_transmitter.state == "open" + end +end + +--- Sets this PlayerSpaceship's energy level. +--- Values are limited from 0 to the energy level max. Negative or excess values are capped to the limits. +--- Example: player:setEnergyLevel(1000) -- sets the ship's energy to either 1000 or the max limit, whichever is lower +function Entity:setEnergyLevel(amount) + if self.components.reactor then self.components.reactor.energy = amount end + return self +end +--- Sets this PlayerSpaceship's energy capacity. +--- Valid values are 0 or any positive number. +--- If the new limit is lower than the ship's current energy level, this also reduces the energy level. +--- Example: player:setEnergyLevelMax(1000) -- sets the ship's energy limit to 1000 +function Entity:setEnergyLevelMax(amount) + if self.components.reactor then self.components.reactor.max_energy = amount end + return self +end +--- Returns this PlayerSpaceship's energy level. +--- Example: player:getEnergyLevel() +function Entity:getEnergyLevel() + if self.components.reactor then return self.components.reactor.energy end + return 0 +end +--- Returns this PlayerSpaceship's energy capacity. +--- Example: player:getEnergyLevelMax() +function Entity:getEnergyLevelMax() + if self.components.reactor then return self.components.reactor.max_energy end + return 0 +end + +--- Returns how much energy is consumed per second by this PlayerSpaceship's shields while active. +--- Example: player:getEnergyShieldUsePerSecond() +function Entity:getEnergyShieldUsePerSecond() + if self.components.shields then return self.components.shields.energy_use_per_second end + return 0.0 +end +--- Sets how much energy is consumed per second by this PlayerSpaceship's shields while active. +--- Example: player:setEnergyShieldUsePerSecond(1.5) +function Entity:setEnergyShieldUsePerSecond(amount) + if self.components.shields then self.components.shields.energy_use_per_second = amount end + return self +end +--- Returns how much energy is consumed per second by this PlayerSpaceship's warp drive while in use. +--- Example: player:getEnergyWarpPerSecond() +function Entity:getEnergyWarpPerSecond() + if self.components.warp_drive then return self.components.warp_drive.energy_warp_per_second end + return 0.0 +end +--- Sets how much energy is consumed per second by this PlayerSpaceship's warp drive while in use. +--- Example: player:setEnergyWarpPerSecond(1.7) +function Entity:setEnergyWarpPerSecond(amount) + if self.components.warp_drive then self.components.warp_drive.energy_warp_per_second = amount end + return self +end + +--- Sets the maximum amount of coolant available to engineering on this PlayerSpaceship. +--- Defaults to 10, which by default allows engineering to set 100% coolant on one system. +--- Valid values are 0 or any positive number. +--- If the new limit is less than the coolant already distributed, this automatically reduces distribution percentages. +--- Example: player:setMaxCoolant(5) -- halves the amount of available coolant +function Entity:setMaxCoolant(amount) + if self.components.coolant then self.components.coolant.max = amount end + return self +end +--- Returns the maximum amount of coolant available to engineering on this PlayerSpaceship. +--- Example: player:getMaxCoolant() +function Entity:getMaxCoolant() + if self.components.coolant then return self.components.coolant.max end + return 0.0 +end + +--- Sets the number of scan probes stocked by this PlayerSpaceship. +--- Values are limited from 0 to the scan probe count max. Negative or excess values are capped to the limits. +--- Example: player:setScanProbeCount(20) -- sets the ship's scan probes to either 20 or the max limit, whichever is fewer +function Entity:setScanProbeCount(amount) + if self.components.scan_probe_launcher then self.components.scan_probe_launcher.stock = amount end + return self +end +--- Returns the number of scan probes stocked by this PlayerSpaceship. +--- Example: player:getScanProbeCount() +function Entity:getScanProbeCount() + if self.components.scan_probe_launcher then return self.components.scan_probe_launcher.stock end + return 0 +end +--- Sets this PlayerSpaceship's capacity for scan probes. +--- Valid values are 0 or any positive number. +--- If the new limit is less than the current scan probe stock, this automatically reduces the stock. +--- Example: player:setMaxScanProbeCount(30) -- sets the ship's scan probe capacity to 30 +function Entity:setMaxScanProbeCount(amount) + if self.components.scan_probe_launcher then self.components.scan_probe_launcher.max = amount end + return self +end +--- Returns this PlayerSpaceship's capacity for scan probes. +--- Example: player:getMaxScanProbeCount() +function Entity:getMaxScanProbeCount() + if self.components.scan_probe_launcher then return self.components.scan_probe_launcher.max end + return 0 +end +--- Adds a custom interactive button with the given reference name to the given crew position screen. +--- By default, custom buttons and info are stacked in order of creation. Use the order value to specify a priority, with lower values appearing higher in the list. +--- If the reference name is unique, this creates a new button. If the reference name exists, this modifies the existing button. +--- The caption sets the button's text label. +--- When clicked, the button calls the given function. +--- Example: +--- -- Add a custom button to Engineering, lower in the order relative to other items, that prints the player ship's coolant max to the console or logging file when clicked +--- player:addCustomButton("engineering","get_coolant_max","Get Coolant Max",function() print("Coolant: " .. player:getMaxCoolant()) end,10) +function Entity:addCustomButton(station, key, label, callback) + setPlayerShipCustomFunction(self, "button", key, label, station, callback, 0) + return self +end +--- Adds a custom non-interactive info label with the given reference name to the given crew position screen. +--- By default, custom buttons and info are stacked in order of creation. Use the order value to specify a priority. +--- If the reference name is unique, this creates a new info. If the reference name exists, this modifies the existing info. +--- The caption sets the info's text value. +--- Example: +--- -- Displays the coolant max value on Engineering at or near the top of the custom button/info order +--- player:addCustomInfo("engineering","show_coolant_max","Coolant Max: " .. player:getMaxCoolant(),0) +function Entity:addCustomInfo(station, key, label, order) + setPlayerShipCustomFunction(self, "info", key, label, station, nil, order) + return self +end +--- Displays a dismissable message with the given reference name on the given crew position screen. +--- The caption sets the message's text. +--- Example: +--- -- Displays the coolant max value on Engineering as a dismissable message +--- player:addCustomMessage("engineering","message_coolant_max","Coolant max: " .. player:getMaxCoolant()) +function Entity:addCustomMessage(station, key, message) + setPlayerShipCustomFunction(self, "message", key, message, station, nil, 0) + return self +end +--- As PlayerSpaceship:addCustomMessage(), but calls the given function when dismissed. +--- Example: +--- -- Displays the coolant max value on Engineering as a dismissable message, and prints "dismissed" to the console or logging file when dismissed +--- player:addCustomMessageWithCallback("engineering","message_coolant_max","Coolant max: " .. player:getMaxCoolant(),function() print("Dismissed!") end) +function Entity:addCustomMessageWithCallback(station, key, message, callback) + setPlayerShipCustomFunction(self, "message", key, message, station, callback, 0) + return self +end +--- Removes the custom function, info, or message with the given reference name. +--- Example: player:removeCustom("show_coolant_max") -- removes the custom item named "show_coolant_max" +function Entity:removeCustom(key) + removePlayerShipCustomFunction(self, key) + return self +end + +--- Returns the index of the ESystem targeted by this PlayerSpaceship's weapons. +--- Returns -1 for the hull. +--- Example: player:getBeamSystemTarget() +function Entity:getBeamSystemTarget() + local target_name = self:getBeamSystemTargetName() + if target_name == "reactor" then return 0 end + if target_name == "beamweapons" then return 1 end + if target_name == "missilesystem" then return 2 end + if target_name == "maneuver" then return 3 end + if target_name == "impulse" then return 4 end + if target_name == "warp" then return 5 end + if target_name == "jumpdrive" then return 6 end + if target_name == "frontshield" then return 7 end + if target_name == "rearshield" then return 8 end + return -1 +end +--- Returns the name of the ESystem targeted by this PlayerSpaceship's weapons. +--- Returns "UNKNOWN" for the hull. +--- Example: player:getBeamSystemTargetName() +function Entity:getBeamSystemTargetName() + if self.components.beam_weapons then return self.components.beam_weapons.system_target end + return "UNKNOWN" +end + +--- Commands this PlayerSpaceship to set a new target rotation. +--- A value of 0 is equivalent to a heading of 90 degrees ("east"). +--- Accepts 0, positive, or negative values. +--- To objectively rotate the PlayerSpaceship as a SpaceObject, rather than commanding it to turn using its maneuverability, use SpaceObject:setRotation(). +--- Examples: +--- player:commandTargetRotation(0) -- command the ship toward a heading of 90 degrees +--- heading = 180; player:commandTargetRotation(heading - 90) -- command the ship toward a heading of 180 degrees +function Entity:commandTargetRotation(target) + commandTargetRotation(self, target) + return self +end +--- Commands this PlayerSpaceship to request a new impulse speed. +--- Valid values are -1.0 (-100%; full reverse) to 1.0 (100%; full forward). +--- The ship's impulse value remains bound by its impulse acceleration rates. +--- Example: player:commandImpulse(0.5) -- command this ship to engage forward half impulse +function Entity:commandImpulse(target) + commandImpulse(self, target) + return self +end +--- Commands this PlayerSpaceship to request a new warp level. +--- Valid values are any positive integer, or 0. +--- Warp controls on crew position screens are limited to 4. +--- Example: player:commandWarp(2) -- activate the warp drive at level 2 +function Entity:commandWarp(target) + commandWarp(self, target) + return self +end +--- Commands this PlayerSpaceship to request a jump of the given distance. +--- Valid values are any positive number, or 0, including values outside of the ship's minimum and maximum jump ranges. +--- A jump of a greater distance than the ship's maximum jump range results in a negative jump drive charge. +--- Example: player:commandJump(25000) -- initiate a 25U jump on the current heading +function Entity:commandJump(target) + commandJump(self, target) + return self +end +--- Commands this PlayerSpaceship to set its weapons target to the given SpaceObject. +--- Example: player:commandSetTarget(enemy) +function Entity:commandSetTarget(target) + commandSetTarget(self, target) + return self +end +--- Commands this PlayerSpaceship to load the WeaponTube with the given index with the given weapon type. +--- This command respects tube allow/disallow limits. +--- Example: player:commandLoadTube(0,"HVLI") +function Entity:commandLoadTube(index, missile_type) + commandLoadTube(self, index, missile_type) + return self +end +--- Commands this PlayerSpaceship to unload the WeaponTube with the given index. +--- Example: player:commandUnloadTube(0) +function Entity:commandUnloadTube(index) + commandUnloadTube(self, index) + return self +end +--- Commands this PlayerSpaceship to fire the WeaponTube with the given index at the given missile target angle in degrees, without a weapons target. +--- The target angle behaves as if the Weapons crew had unlocked targeting and manually aimed its trajectory. +--- A target angle value of 0 is equivalent to a heading of 90 degrees ("east"). +--- Accepts 0, positive, or negative values. +--- Examples: +--- player:commandFireTube(0,0) -- command firing tube 0 at a heading 90 +--- target_heading = 180; player:commandFireTube(0,target_heading - 90) -- command firing tube 0 at a heading 180 +function Entity:commandFireTube(index) + commandFireTube(self, index) + return self +end +--- Commands this PlayerSpaceship to fire the given weapons tube with the given SpaceObject as its target. +--- Example: player:commandFireTubeAtTarget(0,enemy) -- command firing tube 0 at target `enemy` +function Entity:commandFireTubeAtTarget(index, target) + commandFireTubeAtTarget(self, index, target) + return self +end +--- Commands this PlayerSpaceship to raise (true) or lower (false) its shields. +--- Example: player:commandSetShields(true) -- command raising shields +function Entity:commandSetShields(enabled) + commandSetShields(self, enabled) + return self +end +--- Commands this PlayerSpaceship to change its Main Screen view to the given setting. +--- Example: player:commandMainScreenSetting("tactical") -- command setting the main screen view to tactical radar +function Entity:commandMainScreenSetting(setting) + commandMainScreenSetting(self, setting) + return self +end +--- Commands this PlayerSpaceship to change its Main Screen comms overlay to the given setting. +--- Example: player:commandMainScreenOverlay("hidecomms") -- command setting the main screen view to hide the comms overlay +function Entity:commandMainScreenOverlay(setting) + commandMainScreenOverlay(self, setting) + return self +end +--- Commands this PlayerSpaceship to initiate a scan of the given SpaceObject. +--- If the scanning mini-game is enabled, this opens it on the relevant crew screens. +--- This command does NOT respect the player's ability to select the object for scanning, whether due to it being out of radar range or otherwise untargetable. +--- Example: player:commandScan(enemy) +function Entity:commandScan(target) + commandScan(self, target) + return self +end +--- Commands this PlayerSpaceship to set the power level of the given system. +--- Valid values are 0 or greater, with 1.0 equivalent to 100 percent. Values greater than 1.0 are allowed. +--- Example: player:commandSetSystemPowerRequest("impulse",1.0) -- command setting the impulse drive power to 100% +function Entity:commandSetSystemPowerRequest(system, request) + commandSetSystemPowerRequest(self, system, request) + return self +end +--- Commands this PlayerSpaceship to set the coolant level of the given system. +--- Valid values are from 0 to 10.0, with 10.0 equivalent to 100 percent. +--- Values greater than 10.0 are allowed if the ship's coolant max is greater than 10.0, but controls on crew position screens are limited to 10.0 (100%). +--- Example: player:commandSetSystemCoolantRequest("impulse",10.0) -- command setting the impulse drive coolant to 100% +function Entity:commandSetSystemCoolantRequest(system, request) + commandSetSystemCoolantRequest(self, system, request) + return self +end +--- Commands this PlayerSpaceship to initiate docking with the given SpaceObject. +--- This initiates docking only if the target is dockable and within docking range. +--- Example: player:commandDock(base) +function Entity:commandDock(target) + commandDock(self, target) + return self +end +--- Commands this PlayerSpaceship to undock from any SpaceObject it's docked with. +--- Example: player:commandUndock() +function Entity:commandUndock() + commandUndock(self) + return self +end +--- Commands this PlayerSpaceship to abort an in-progress docking operation. +--- Example: player:commandAbortDock() +function Entity:commandAbortDock() + commandAbortDock(self) + return self +end +--- Commands this PlayerSpaceship to hail the given SpaceObject. +--- If the target object is a PlayerSpaceship or the GM is intercepting all comms, open text chat comms. +--- Example: player:commandOpenTextComm(base) +function Entity:commandOpenTextComm(target) + commandOpenTextComm(self, target) + return self +end +--- Commands this PlayerSpaceship to close comms. +--- Example: player:commandCloseTextComm() +function Entity:commandCloseTextComm() + commandCloseTextComm(self) + return self +end +--- Commands whether this PlayerSpaceship answers (true) or rejects (false) an incoming hail. +--- Example: player:commandAnswerCommHail(false) -- commands to reject an active incoming hail +function Entity:commandAnswerCommHail(response) + commandAnswerCommHail(self, response) + return self +end +--- Commands this PlayerSpaceship to select the reply with the given index during a comms dialogue. +--- Example: player:commandSendComm(0) -- commands to select the first option in a comms dialogue +function Entity:commandSendComm(index) + commandSendComm(self, index) + return self +end +--- Commands this PlayerSpaceship to send the given message to the active text comms chat. +--- This works whether the chat is with another PlayerSpaceship or the GM. +--- Example: player:commandSendCommPlayer("I will destroy you!") -- commands to send this message in the active text chat +function Entity:commandSendCommPlayer(message) + commandSendCommPlayer(self, message) + return self +end +--- Commands whether repair crews on this PlayerSpaceship automatically move to rooms of damaged systems. +--- Use this command to reduce the need for player interaction in Engineering, especially when combined with setAutoCoolant/auto_coolant_enabled. +--- Crews set to move automatically don't respect crew collisions, allowing multiple crew to occupy a single space. +--- Example: player:commandSetAutoRepair(true) +function Entity:commandSetAutoRepair(enabled) + commandSetAutoRepair(self, enabled) + return self +end +--- Commands this PlayerSpaceship to set its beam frequency to the given value. +--- Valid values are 0 to 20, which map to 400THz to 800THz at 20THz increments. (spaceship.cpp frequencyToString()) +--- Example: player:commandSetBeamFrequency(2) +function Entity:commandSetBeamFrequency(index) + commandSetBeamFrequency(self, index) + return self +end +--- Commands this PlayerSpaceship to target the given ship system with its beam weapons. +function Entity:commandSetBeamSystemTarget(target) + commandSetBeamSystemTarget(self, target) + return self +end +--- Sets this SpaceShip's shield frequency index. +--- To convert the index to the value used by players, multiply it by 20, then add 400. +--- Valid values are 0 (400THz) to 20 (800THz). +--- Unlike SpaceShip:setShieldsFrequency(), this initiates shield calibration to change the frequency, which disables shields for a period. +--- Example: +--- frequency = ship:setShieldsFrequency(10) -- frequency is 600THz +function Entity:commandSetShieldFrequency(index) + commandSetShieldFrequency(self, index) + return self +end +--- Commands this PlayerSpaceship to add a waypoint at the given coordinates. +--- This respects the 9-waypoint limit and won't add more waypoints if 9 already exist. +--- Example: player:commandAddWaypoint(1000,2000) +function Entity:commandAddWaypoint(x, y) + commandAddWaypoint(self, x, y) + return self +end +--- Commands this PlayerSpaceship to remove the waypoint with the given index. +--- This uses a 0-index, while waypoints are numbered on player screens with a 1-index. +--- Example: player:commandRemoveWaypoint(0) -- removes waypoint 1 +function Entity:commandRemoveWaypoint(index) + commandRemoveWaypoint(self, index) + return self +end +--- Commands this PlayerSpaceship to move the waypoint with the given index to the given coordinates. +--- This uses a 0-index, while waypoints are numbered on player screens with a 1-index. +--- Example: player:commandMoveWaypoint(0,-1000,-2000) -- moves waypoint 1 to -1000,-2000 +function Entity:commandMoveWaypoint(index, x, y) + commandMoveWaypoint(self, index, x, y) + return self +end +--- Commands this PlayerSpaceship to activate its self-destruct sequence. +--- Example: player:commandActivateSelfDestruct() +function Entity:commandActivateSelfDestruct() + commandActivateSelfDestruct(self) + return self +end +--- Commands this PlayerSpaceship to cancel its self-destruct sequence. +--- Example: player:commandCancelSelfDestruct() +function Entity:commandCancelSelfDestruct() + commandCancelSelfDestruct(self) + return self +end +--- Commands this PlayerSpaceship to submit the given self-destruct authorization code for the code request with the given index. +--- Codes are 0-indexed. Index 0 corresponds to code A, 1 to B, etc. +--- Example: player:commandConfirmDestructCode(0,46223) -- commands submitting 46223 as self-destruct confirmation code A +function Entity:commandConfirmDestructCode(index, code) + commandConfirmDestructCode(self, index, code) + return self +end +--- Commands this PlayerSpaceship to set its forward combat maneuver to the given value. +--- Valid values are any from -1.0 (full reverse) to 1.0 (full forward). +--- The maneuver continues until the ship's combat maneuver reserves are depleted. +--- Crew screens allow only forward combat maneuvers, and the combat maneuver controls do not reflect a boost set via this command. +--- Example: player:commandCombatManeuverBoost(0.5) -- commands boosting forward at half combat maneuver capacity +function Entity:commandCombatManeuverBoost(amount) + commandCombatManeuverBoost(self, amount) + return self +end +--- Commands this PlayerSpaceship to launch a ScanProbe to the given coordinates. +--- Example: player:commandLaunchProbe(1000,2000) -- commands launching a scan probe to 1000,2000 +function Entity:commandLaunchProbe(x, y) + commandLaunchProbe(self, x, y) + return self +end +--- Commands this PlayerSpaceship to link the science screen to the given ScanProbe. +--- This is equivalent to selecting a probe on Relay and clicking "Link to Science". +--- Unlike "Link to Science", this function can link science to any given probe, regardless of which ship launched it or what faction it belongs to. +--- Example: player:commandSetScienceLink(probe_object) -- link ScanProbe `probe` to this ship's science +function Entity:commandSetScienceLink(target) + commandSetScienceLink(self, target) + return self +end +--- Commands this PlayerSpaceship to unlink the science screen from any ScanProbe. +--- This is equivalent to clicking "Link to Science" on Relay when a link is already active. +--- Example: player:commandClearScienceLink() +function Entity:commandClearScienceLink() + commandClearScienceLink(self) + return self +end +--- Commands this PlayerSpaceship to set the given alert level. +--- Valid values are "normal", "yellow", "red", which differ from the values returned by PlayerSpaceship:getAlertLevel(). +--- Example: player:commandSetAlertLevel("red") -- commands red alert +function Entity:commandSetAlertLevel(level) + commandSetAlertLevel(self, level) + return self +end + +--- Returns the number of repair crews on this PlayerSpaceship. +--- Example: player:getRepairCrewCount() +function Entity:getRepairCrewCount() + local count = 0 + for idx, e in ipairs(getEntitiesWithComponent("internal_crew")) do + if e.components.internal_crew.ship == self then + count = count + 1 + end + end + return count +end +--- Sets the total number of repair crews on this PlayerSpaceship. +--- If the value is less than the number of repair crews, this function removes repair crews. +--- If the value is greater, this function adds new repair crews into random rooms. +--- Example: player:setRepairCrewCount(5) +function Entity:setRepairCrewCount(amount) + if self.components.internal_rooms then + for idx, e in ipairs(getEntitiesWithComponent("internal_crew")) do + if e.components.internal_crew.ship == self then + amount = amount - 1 + if amount < 0 then + e:destroy() + end + end + end + for n=1,amount do + local crew = createEntity() + crew.components.internal_crew = {ship=self} + crew.components.internal_repair_crew = {} + end + end + return self +end +--- Defines whether automatic coolant distribution is enabled on this PlayerSpaceship. +--- If true, coolant is automatically distributed proportionally to the amount of heat in that system. +--- Use this command to reduce the need for player interaction in Engineering, especially when combined with commandSetAutoRepair/auto_repair_enabled. +--- Example: player:setAutoCoolant(true) +function Entity:setAutoCoolant(enabled) + if self.components.coolant then self.components.coolant.auto_levels = enabled end + return self +end +--- Sets a control code password required for a player to join this PlayerSpaceship. +--- Control codes are case-insensitive. +--- Example: player:setControlCode("abcde") -- matches "abcde", "ABCDE", "aBcDe" +function Entity:setControlCode(code) + if self.components.player_control then self.components.player_control.control_code = code end + return self +end +--- Defines a function to call when this PlayerSpaceship launches a probe. +--- Passes the launching PlayerSpaceship and launched ScanProbe. +--- Example: +--- -- Prints probe launch details to the console output or logging file +--- player:onProbeLaunch(function (player, probe) +--- print("Probe " .. probe:getCallSign() .. " launched from ship " .. player:getCallSign()) +--- end) +function Entity:onProbeLaunch(callback) + if self.components.scan_probe_launcher then self.components.scan_probe_launcher.on_launch = callback end + return self +end +--- Defines a function to call when this PlayerSpaceship links a probe to the science screen. +--- Passes the PlayerShip and linked ScanProbe. +--- Example: +--- -- Prints probe linking details to the console output or logging file +--- player:onProbeLink(function (player, probe) +--- print("Probe " .. probe:getCallSign() .. " linked to Science on ship " .. player:getCallSign()) +--- end) +function Entity:onProbeLink(callback) + if self.components.long_range_radar then self.components.long_range_radar.on_probe_link = callback end + return self +end +--- Defines a function to call when this PlayerSpaceship unlinks a probe from the science screen. +--- Passes the PlayerShip and previously linked ScanProbe. +--- This function is not called when the probe is destroyed or expires. +--- See ScanProbe:onDestruction() and ScanProbe:onExpiration(). +--- Example: +--- -- Prints probe unlinking details to the console output or logging file +--- player:onProbeUnlink(function (player, probe) +--- print("Probe " .. probe:getCallSign() .. " unlinked from Science on ship " .. player:getCallSign()) +--- end) +function Entity:onProbeUnlink(callback) + if self.components.long_range_radar then self.components.long_range_radar.on_probe_unlink = callback end + return self +end +--- Returns this ships's long-range radar range. +--- Example: player:getLongRangeRadarRange() +function Entity:getLongRangeRadarRange() + if self.components.long_range_radar then return self.components.long_range_radar.long_range end + return 50000 +end +--- Returns this PlayerSpaceship's short-range radar range. +--- Example: player:getShortRangeRadarRange() +function Entity:getShortRangeRadarRange() + if self.components.long_range_radar then return self.components.long_range_radar.short_range end + return 5000 +end +--- Sets this PlayerSpaceship's long-range radar range. +--- PlayerSpaceships use this range on the science and operations screens' radar. +--- Example: player:setLongRangeRadarRange(30000) -- sets the ship's long-range radar range to 30U +function Entity:setLongRangeRadarRange(range) + if self.components.long_range_radar then self.components.long_range_radar.long_range = range end + return self +end +--- Sets this PlayerSpaceship's short-range radar range. +--- PlayerSpaceships use this range on the helms, weapons, and single pilot screens' radar. +--- This also defines the shared radar radius on the relay screen for friendly ships and stations, and how far into nebulae that this SpaceShip can detect objects. +--- Example: player:setShortRangeRadarRange(5000) -- sets the ship's long-range radar range to 5U +function Entity:setShortRangeRadarRange(range) + if self.components.long_range_radar then self.components.long_range_radar.short_range = range end + return self +end +--- Defines whether scanning features appear on related crew screens in this PlayerSpaceship. +--- Example: player:setCanScan(true) +function Entity:setCanScan(enabled) + if enabled then self.components.science_scanner = {} else self.components.science_scanner = nil end + return self +end +--- Returns whether scanning features appear on related crew screens in this PlayerSpaceship. +--- Example: player:getCanScan() +function Entity:getCanScan() + return self.components.science_scanner ~= nil +end +--- Defines whether hacking features appear on related crew screens in thisPlayerSpaceship. +--- Example: player:setCanHack(true) +function Entity:setCanHack(enabled) + if enabled then self.components.hacking_device = {} else self.components.hacking_device = nil end + return self +end +--- Returns whether hacking features appear on related crew screens in this PlayerSpaceship. +--- Example: player:getCanHack() +function Entity:getCanHack() + return self.components.hacking_device ~= nil +end +--- Defines whether the "Request Docking" button appears on related crew screens in this PlayerSpaceship. +--- This doesn't override any docking class restrictions set on a target SpaceShip. +--- Example: player:setCanDock(true) +function Entity:setCanDock(enabled) + if enabled then + self.components.docking_port = {} + else + self.components.docking_port = nil + end + return self +end +--- Returns whether the "Request Docking" button appears on related crew screens in this PlayerSpaceship. +--- Example: player:getCanDock() +function Entity:getCanDock() + return self.components.docking_port ~= nil +end +--- Defines whether combat maneuver controls appear on related crew screens in this PlayerSpaceship. +--- Example: player:setCanCombatManeuver(true) +function Entity:setCanCombatManeuver(enabled) + if enabled then self.components.combat_maneuvering_thrusters = {} else self.components.combat_maneuvering_thrusters = nil end + return self +end +--- Returns whether combat maneuver controls appear on related crew screens in this PlayerSpaceship. +--- Example: player:getCanCombatManeuver() +function Entity:getCanCombatManeuver() + return self.components.combat_maneuvering_thrusters ~= nil +end +--- Defines whether ScanProbe-launching controls appear on related crew screens in this PlayerSpaceship. +--- Example: player:setCanLaunchProbe(true) +function Entity:setCanLaunchProbe(enabled) + if enabled then self.components.scan_probe_launcher = {} else self.components.scan_probe_launcher = nil end + return self +end +--- Returns whether ScanProbe-launching controls appear on related crew screens in this PlayerSpaceship. +--- Example: player:getCanLaunchProbe() +function Entity:getCanLaunchProbe() + return self.components.scan_probe_launcher ~= nil +end +--- Defines whether self-destruct controls appear on related crew screens in this PlayerSpaceship. +--- Example: player:setCanSelfDestruct(true) +function Entity:setCanSelfDestruct(enabled) + if enabled then self.components.self_destruct = {} else self.components.self_destruct = nil end + return self +end +--- Returns whether self-destruct controls appear on related crew screens in this PlayerSpaceship. +--- This returns false if this ship's self-destruct size and damage are both 0, even if you set setCanSelfDestruct(true). +--- Example: player:getCanSelfDestruct() +function Entity:getCanSelfDestruct() + return self.components.self_destruct ~= nil +end +--- Sets the amount of damage done to nearby SpaceObjects when this PlayerSpaceship self-destructs. +--- Any given value is randomized +/- 33 percent upon self-destruction. +--- Example: player:setSelfDestructDamage(150) +function Entity:setSelfDestructDamage(amount) + if self.components.self_destruct then self.components.self_destruct.damage = amount end + return self +end +--- Returns the amount of base damage done to nearby SpaceObjects when this PlayerSpaceship self-destructs. +--- Example: player:getSelfDestructDamage() +function Entity:getSelfDestructDamage() + if self.components.self_destruct then return self.components.self_destruct.damage end + return 0 +end +--- Sets the radius of the explosion created when this PlayerSpaceship self-destructs. +--- All SpaceObjects within this radius are dealt damage upon self-destruction. +--- Example: player:setSelfDestructSize(1500) -- sets a 1.5U self-destruction explosion and damage radius +function Entity:setSelfDestructSize(size) + if self.components.self_destruct then self.components.self_destruct.size = size end + return self +end +--- Returns the radius of the explosion created when this PlayerSpaceship self-destructs. +--- All SpaceObjects within this radius are dealt damage upon self-destruction. +--- Example: ship:getSelfDestructSize() +function Entity:getSelfDestructSize() + if self.components.self_destruct then return self.components.self_destruct.size end + return 0 +end diff --git a/scripts/api/entity/scanprobe.lua b/scripts/api/entity/scanprobe.lua new file mode 100644 index 0000000000..dbe4a58051 --- /dev/null +++ b/scripts/api/entity/scanprobe.lua @@ -0,0 +1,88 @@ + +--- A ScanProbe deploys a short-range radar with a limited lifetime at a high speed to a specific point. +--- ScanProbes can be targeted and destroyed by hostiles. +--- It's typically launched by the relay officer and can be linked to the science radar, but can also be created by scripts. +--- PlayerSpaceships have a limited stock of ScanProbes typically replenished automatically when docked to a SpaceStation or SpaceShip with the ScanProbe restocking feature enabled. +--- Example: probe = ScanProbe():setSpeed(1500):setLifetime(60 * 30):setTarget(10000,10000):onArrival(function() print("Probe arrived!") end) +--- @type creation +function ScanProbe() + local e = createEntity() + e.components = { + lifetime = {lifetime=60*10}, + radar_trace = { + icon="radar/probe.png", + min_size=10.0, + max_size=10.0, + color={96, 192, 128, 255}, + rotate=false, + }, + hull = {max=1, current=1}, + share_short_range_radar = {}, + allow_radar_link = {}, + radar_signature = {gravity=0.0, electrical=0.2, biological=0.0} + } + local model = "SensorBuoyMKI" + local idx = irandom(1, 3) + if idx == 2 then model = "SensorBuoyMKII" end + if idx == 3 then model = "SensorBuoyMKIII" end + for k, v in pairs(__model_data[model]) do + if string.sub(1, 2) ~= "__" then + e.components[k] = table.deepcopy(v) + end + end + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets this ScanProbe's speed. +--- Probes move at a fixed rate of speed and ignore collisions and physics while moving. +--- Defaults to 1000 (1U/second). +--- Example: probe:setSpeed(2000) +function Entity:setSpeed(speed) + if self.components.move_to then self.components.move_to.speed = speed end + return self +end +--- Returns this ScanProbe's speed. +--- Example: probe:getSpeed() +function Entity:getSpeed() + if self.components.move_to then return self.components.move_to.speed end + return 0.0 +end +--- Sets this ScanProbe's remaining lifetime, in seconds. +--- Defaults to 600 seconds (10 minutes). +--- Example: probe:setLifetime(60 * 5) -- sets the lifetime to 5 minutes +function Entity:setLifetime(lifetime) + if self.components.lifetime then self.components.lifetime.lifetime = lifetime end + return self +end +--- Returns this ScanProbe's remaining lifetime. +--- Example: probe:getLifetime() +function Entity:getLifetime() + if self.components.lifetime then return self.components.lifetime.lifetime end + return 0.0 +end +--- Sets this ScanProbe's owner SpaceObject. +--- Example: probe:setOwner(owner) +function Entity:setOwner(owner) + if self.components.allow_radar_link then self.components.allow_radar_link.owner = owner end + if owner and owner:isValid() and owner.components.faction then + self.components.faction.entity = owner.components.faction.entity + else + self.components.faction = nil + end + return self +end +--- Defines a function to call when this ScanProbe arrives to its target coordinates. +--- Passes the probe and position as arguments to the function. +--- Example: probe:onArrival(function(this_probe, coords) print("Probe arrived!") end) +function Entity:onArrival(callback) + if self.components.move_to then self.components.move_to.on_arrival = callback end + return self +end +--- Defines a function to call when this ScanProbe's lifetime expires. +--- Passes the probe as an argument to the function. +--- Example: probe:onExpiration(function(this_probe) print("Probe expired!") end) +function Entity:onExpiration(callback) + if self.components.lifetime then self.components.lifetime.on_expire = callback end + return self +end diff --git a/scripts/api/entity/sciencedatabase.lua b/scripts/api/entity/sciencedatabase.lua new file mode 100644 index 0000000000..405c50d256 --- /dev/null +++ b/scripts/api/entity/sciencedatabase.lua @@ -0,0 +1,234 @@ +local Entity = getLuaEntityFunctionTable() +--- A ScienceDatabase entry stores information displayed to all players in the Science database tab or Database standalone screen. +--- Each ScienceDatabase entry can contain key/value pairs, an image, and a 3D ModelData. +--- +--- A ScienceDatabase entry can also be the parent of many ScienceDatabases or the child of one ScienceDatabase, creating a hierarchical structure. +--- Each ScienceDatabase without a parent is a top-level entry in the player-viewed database interface. +--- Each child ScienceDatabase entry is displayed only when its parent entry is selected. +--- +--- By default, EmptyEpsilon creates parentless entries for Factions, "Natural" (terrain), Ships, and Weapons. +--- Their child entries are populated by EmptyEpsilon upon launching a scenario, either with hardcoded details, entries loaded from scripts/science_db.lua, or the contents of script-defined objects such as ShipTemplates and FactionInfo. +--- Entries for ShipTemplates are also linked to from Science radar info of scanned ships of that template. +--- +--- Each ScienceDatabase entry has a unique identifier regardless of its displayed order, and multiple entries can have the same name. +--- Changes to ScienceDatabases appear in the UI only after a player opens the Database or selects an entry. +--- +--- To retrieve a 1-indexed table of all parentless entries, use the global function getScienceDatabases(). +--- You can then use this class's functions to get child entries and entry data. +--- +--- Example: +--- -- Creates a new parentless entry named "Species", with an entry containing a key/value +--- ScienceDatabase():setName("Species"):addEntry("Canines"):addKeyValue("Legs","4") +--- sdb = getScienceDatabases() -- returns a 1-indexed table of top-level entries +--- +--- for i,db in pairs(sdb) do +--- if (db:getName() == "Species") then +--- entry = db -- assigns the ScienceDatabase with the name "Species" +--- end +--- end +--- +--- species = entry:getEntries()[1] -- species = "Canines" +--- legs = species:getKeyValue("Legs") -- legs = "4" +function ScienceDatabase() + local e = createEntity() + e.components.science_database = {} + return e +end + +--- Returns this ScienceDatabase entry's displayed name. +--- Example: entry:getName() +function Entity:getName() + if self.components.science_database then return self.components.science_database.name end + return "" +end +--- Returns this ScienceDatabase entry's unique multiplayer_id. +--- Examples: entry:getId() -- returns the entry's ID +function Entity:getId() + return self +end +--- Return this ScienceDatabase entry's parent entry's unique multiplayer_id. +--- Returns 0 if the entry has no parent. +--- Example: entry:getParentId() -- returns the parent entry's ID +function Entity:getParentId() + if self.components.science_database then return self.components.science_database.parent end +end +--- Creates a ScienceDatabase entry with the given name as a child of this ScienceDatabase entry. +--- Returns the newly created entry. Chaining addEntry() creates a child of the new child entry. +--- Examples: +--- species:addEntry("Canines") -- adds an entry named "Canines" as a child of ScienceDatabase species +--- -- Adds an entry named "Felines" as a child of species, and an entry named "Calico" as a child of "Felines" +--- species:addEntry("Felines"):addEntry("Calico") +function Entity:addEntry(name) + if not self.components.science_database then return end + local child = ScienceDatabase() + child.components.science_database.name = name + child.components.science_database.parent = self + return child +end +--- Returns the first child ScienceDatabase entry of this ScienceDatabase entry found with the given case-insensitive name. +--- Multiple entries can have the same name. +--- Returns nil if no entry is found. +--- Example: entry:getEntryByName("canines") -- returns the "Canines" entry in sdb +function Entity:getEntryByName(name) + name = string.lower(name) + for idx, e in ipairs(getEntitiesWithComponent("science_database")) do + if e.components.science_database.parent == self and string.lower(e.components.science_database.name) == name then return e end + end + return nil +end +--- Returns a 1-indexed table of all child entries in this ScienceDatabase entry, in arbitrary order. +--- To return parentless top-level ScienceDatabase entries, use the global function getScienceDatabases(). +--- Examples: +--- entry = getScienceDatabases()[1] -- returns the first parentless entry +--- entry:getEntries() -- returns all of its child entries +function Entity:getEntries() + local result = {} + for idx, e in ipairs(getEntitiesWithComponent("science_database")) do + if e.components.science_database.parent == self then table.insert(result, e) end + end + return result +end +--- Returns true if this ScienceDatabase entry has child entries. +--- Example: entry:hasEntries() +function Entity:hasEntries() + return #getEntries() > 0 +end +--- Adds a key/value pair to this ScienceDatabase entry's key/value data. +--- The Database view's center column displays all key/value data when its entry is selected. +--- Chaining addKeyValue() adds each key/value to the same entry. +--- Warning: addKeyValue() can add entries with duplicate keys. To avoid this, use setKeyValue() instead. +--- Example: +--- -- Adds "Legs","4" and "Ears","2" to the entry's key/value data. +--- entry:addKeyValue("Legs","4"):addKeyValue("Ears","2") +--- entry:addKeyValue("Legs","2") -- adds "Legs","2", even if "Legs","4" is already present +function Entity:addKeyValue(key, value) + if not self.components.science_database then return self end + self.components.science_database[#self.components.science_database+1] = {key=key, value=value} + return self +end +--- Sets the value of all key/value pairs matching the given case-insensitive key in this ScienceDatabase entry's key/value data. +--- If the key already exists, this changes its value. +--- If duplicate matching keys exist, this changes all of their values. +--- If the key doesn't exist, this acts as addKeyValue(). +--- Examples: +--- -- Assuming entry already has "Legs","4" as a key/value +--- entry:setKeyValue("Legs","2") -- changes this entry's "Legs" value to "2" +--- entry:setKeyValue("Arms","2") -- adds "Arms","2" to the entry's key/value data +function Entity:setKeyValue(key, value) + if not self.components.science_database then return self end + for n=1,#self.components.science_database do + if self.components.science_database[n].key == key then + self.components.science_database[n].value = value + return self + end + end + return self:addKeyValue(key, value) +end +--- Returns the value of the first matching case-insensitive key found in this ScienceDatabase entry's key/value data. +--- Returns an empty string if the key doesn't exist. +--- Example: entry:getKeyValue("Legs") -- returns the value if found or "" if not +function Entity:getKeyValue(key) + if not self.components.science_database then return "" end + for n=1,#self.components.science_database do + if self.components.science_database[n].key == key then + return self.components.science_database[n].value + end + end + return "" +end +--- Returns a table containing all key/value pairs in this ScienceDatabase entry. +--- Warning: Duplicate keys appear only once, with the last value found. +--- Example: +--- entry:getKeyValues() -- returns the key/value table for this entry +--- for k,v in pairs(kv) do print(k,v) end -- Print each key/value pair for this entry to the console +function Entity:getKeyValues() + if not self.components.science_database then return {} end + local result = {} + for n=1,#self.components.science_database do + result[self.components.science_database[n].key] = self.components.science_database[n].value + end + return result +end +--- Removes all key/value pairs matching the given case-insensitive key in this ScienceDatabase entry's key/value data. +--- If duplicate matching keys exist, this removes all of them. +--- Example: entry:removeKey("Legs") -- removes all key/value data with the key "Legs" +function Entity:removeKey(key) + if not self.components.science_database then return self end + local n=1 + while n <= #self.components.science_database do + while self.components.science_database[n].key == key do + for m=n,#self.components.science_database-1 do + self.components.science_database[n] = {key=self.components.science_database[n+1].key, value=self.components.science_database[n+1].value} + end + self.components.science_database[#self.components.science_database] = nil + end + n = n + 1 + end + return self +end +--- Sets this ScienceDatabase entry's longform description to the given string. +--- The Database view's right column displays the longform description when its entry is selected. +--- Example: entry:setLongDescription("This species is known for its loyalty...") +function Entity:setLongDescription(description) + if self.components.science_database then self.components.science_database.description = description end + return self +end +--- Returns this ScienceDatabase entry's longform description. +--- Returns an empty string if no description is set. +--- Example: entry:getLongDescription() +function Entity:getLongDescription() + if self.components.science_database then return self.components.science_database.description end + return "" +end +--- Sets this ScienceDatabase entry's image file to the given filename. +--- Valid values are filenames to PNG files relative to the resources/ directory. +--- An empty string removes any set image. +--- Example: entry:setImage("retriever.png") -- sets the entry's image to the file "resources/retriever.png" +function Entity:setImage(image) + if self.components.science_database then self.components.science_database.image = image end + return self +end +--- Returns this ScienceDatabase entry's image filename. +--- Returns an empty string if no image is set. +--- Example: entry:getImage() +function Entity:getImage() + if self.components.science_database then return self.components.science_database.image end + return "" +end +--- Sets the 3D appearance, by ModelData name, used for this ScienceDatabase entry. +--- ModelData objects define a 3D mesh, textures, adjustments, and collision box, and are loaded from scripts/model_data.lua when EmptyEpsilon is launched. +--- Example: entry:setModelDataName("AtlasHeavyFighterYellow") -- uses the ModelData named "AtlasHeavyFighterYellow" +function Entity:setModelDataName(model_data_name) + if __model_data[model_data_name] then + self.components.mesh_render = __model_data[model_data_name].mesh_render + else + self.components.mesh_render = nil + end + return self +end + + +--- ScienceDatabase queryScienceDatabase(...) +--- Returns the first ScienceDatabase entry with a matching case-insensitive name within the ScienceDatabase hierarchy. +--- You must provide the full path to the entry by using multiple arguments, starting with the top-level entry. +--- Returns nil if no entry is found. +--- Example: queryScienceDatabase("weapons", "mine") -- returns the entry named "Mine" with the parent named "Weapons" +function queryScienceDatabase(...) + local names = {...} + local db_entities = getEntitiesWithComponent("science_database") + + local parent = nil + for idx, name in ipairs(names) do + name = string.lower(name) + local found = false + for db_idx, db in ipairs(db_entities) do + if ((parent == nil and not db.components.science_database.parent.valid) or parent == db.components.science_database.parent) and string.lower(db.components.science_database.name) == name then + parent = db + found = true + break + end + end + if not found then return nil end + end + return parent +end diff --git a/scripts/api/entity/shiptemplatebasedobject.lua b/scripts/api/entity/shiptemplatebasedobject.lua new file mode 100644 index 0000000000..c1478e7605 --- /dev/null +++ b/scripts/api/entity/shiptemplatebasedobject.lua @@ -0,0 +1,323 @@ +local Entity = getLuaEntityFunctionTable() + +----- Old ShipTemplateBasedObject API ----- + +--- A ShipTemplateBasedObject (STBO) is an object class created from a ShipTemplate. +--- This is the parent class of SpaceShip (CpuShip, PlayerSpaceship) and SpaceStation objects, which inherit all STBO functions and can be created by scripts. +--- Objects of this class can't be created by scripts, but SpaceStation and child classes of SpaceShip can. + +--- Sets this ShipTemplate that defines this STBO's traits, and then applies them to this STBO. +--- ShipTemplates define the STBO's class, weapons, hull and shield strength, 3D appearance, and more. +--- See the ShipTemplate class for details, and files in scripts/shiptemplates/ for the default templates. +--- ShipTemplate string names are case-sensitive. +--- Examples: +--- CpuShip():setTemplate("Phobos T3") +--- PlayerSpaceship():setTemplate("Phobos M3P") +--- SpaceStation():setTemplate("Large Station") +function Entity:setTemplate(template_name) + local template = __ship_templates[template_name] + local comp = self.components + if template == nil then + return error("Failed to find template: " .. template_name) + end + -- print("Setting template:" .. template_name) + for key, value in next, template, nil do + if string.sub(key, 1, 2) ~= "__" then + comp[key] = value + end + end + if template.__type == "station" then + comp.physics.type = "static" + elseif template.__type == "playership" then + if comp.shields then comp.shields.active = false end + end + + if comp.reactor then + local reactor_power_factor = 0 + if comp.beam_weapons then comp.beam_weapons.power_factor = 3.0; reactor_power_factor = reactor_power_factor - 3.0 end + if comp.missile_tubes then comp.missile_tubes.power_factor = 1.0; reactor_power_factor = reactor_power_factor - 1.0 end + if comp.maneuvering_thrusters then comp.maneuvering_thrusters.power_factor = 2.0; reactor_power_factor = reactor_power_factor - 2.0 end + if comp.impulse_engine then comp.impulse_engine.power_factor = 4.0; reactor_power_factor = reactor_power_factor - 4.0 end + if comp.warp_drive then comp.warp_drive.power_factor = 5.0; reactor_power_factor = reactor_power_factor - 5.0 end + if comp.jump_drive then comp.jump_drive.power_factor = 5.0; reactor_power_factor = reactor_power_factor - 5.0 end + if comp.shields then + comp.shields.front_power_factor = 5.0; reactor_power_factor = reactor_power_factor - 5.0 + comp.shields.rear_power_factor = 5.0; reactor_power_factor = reactor_power_factor - 5.0 + end + comp.reactor.power_factor = reactor_power_factor + end + if comp.internal_rooms and template.__repair_crew_count and template.__repair_crew_count > 0 then + for n=1,template.__repair_crew_count do + local crew = createEntity() + crew.components.internal_crew = {ship=self} + crew.components.internal_repair_crew = {} + end + end + if comp.shields and template.__type ~= "station" then + comp.shields.frequency = irandom(0, 20) + end + return self +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setTemplate(). +function Entity:setShipTemplate(template_name) + print("Called DEPRECATED setShipTemplate function") + return self:setTemplate(template_name) +end +--- Sets this STBO's vessel classification name, such as "Starfighter" or "Cruiser". +--- This overrides the vessel class name provided by the ShipTemplate. +--- Example: stbo:setTypeName("Prototype") +function Entity:setTypeName(type_name) + self.components.typename = {type_name=type_name} + return self +end +--- Returns this STBO's vessel classification name. +--- Example: +--- stbo:setTypeName("Prototype") +--- stbo:getTypeName() -- returns "Prototype" +function Entity:getTypeName() + return self.components.typename.type_name +end +--- Returns this STBO's hull points. +--- Example: stbo:getHull() +function Entity:getHull() + if self.components.hull then return self.components.hull.current end + return 0 +end +--- Returns this STBO's maximum limit of hull points. +--- Example: stbo:getHullMax() +function Entity:getHullMax() + if self.components.hull then return self.components.hull.max end + return 0 +end +--- Sets this STBO's hull points. +--- If set to a value larger than the maximum, this sets the value to the limit. +--- If set to a value less than 0, this sets the value to 0. +--- Note that setting this value to 0 doesn't immediately destroy the STBO. +--- Example: stbo:setHull(100) -- sets the hull point limit to either 100, or the limit if less than 100 +function Entity:setHull(amount) + if self.components.hull then self.components.hull.current = amount end + return self +end +--- Sets this STBO's maximum limit of hull points. +--- Note that SpaceStations can't repair their own hull, so this only changes the percentage of remaining hull. +--- Example: stbo:setHullMax(100) -- sets the hull point limit to 100 +function Entity:setHullMax(amount) + if self.components.hull then self.components.hull.max = amount end + return self +end +--- Defines whether this STBO can be destroyed by damage. +--- Defaults to true. +--- Example: stbo:setCanBeDestroyed(false) -- prevents the STBO from being destroyed by damage +function Entity:setCanBeDestroyed(allow_destroy) + if self.components.hull then self.components.hull.allow_destruction = allow_destroy end + return self +end +--- Returns whether the STBO can be destroyed by damage. +--- Example: stbo:getCanBeDestroyed() +function Entity:getCanBeDestroyed() + if self.components.hull then return self.components.hull.allow_destruction end + return false +end +--- Returns the shield points for this STBO's shield segment with the given index. +--- Shield segments are 0-indexed. +--- Example for a ship with two shield segments: +--- stbo:getShieldLevel(0) -- returns front shield points +--- stbo:getShieldLevel(1) -- returns rear shield points +function Entity:getShieldLevel(index) + if self.components.shields and index < #self.components.shields then + return self.components.shields[index+1].level + end + return 0 +end +--- Returns this STBO's number of shield segments. +--- Each segment divides the 360-degree shield arc equally for each segment, up to a maximum of 8 segments. +--- The segments' order starts with the front-facing segment, then proceeds clockwise. +--- Example: stbo:getShieldCount() +function Entity:getShieldCount() + if self.components.shields then return #self.components.shields end + return 0 +end +--- Returns the maximum shield points for the STBO's shield segment with the given index. +--- Example: stbo:getShieldMax(0) -- returns the max shield strength for segment 0 +function Entity:getShieldMax(index) + if self.components.shields and index < #self.components.shields then + return self.components.shields[index+1].max + end + return 0 +end +--- Sets this STBO's shield points. +--- Each number provided as a parameter sets the points for a corresponding shield segment. +--- Note that the segments' order starts with the front-facing segment, then proceeds clockwise. +--- If more parameters are provided than the ship has shield segments, the excess parameters are discarded. +--- Example: +--- -- On a ship with 4 segments, this sets the forward shield segment to 50, right to 40, rear 30, left 20 +--- -- On a ship with 2 segments, this sets forward 50, rear 40 +--- stbo:setShields(50,40,30,20) +function Entity:setShields(...) + if self.components.shields then + for i, level in ipairs({...}) do + if i <= #self.components.shields then + self.components.shields[i].level = level + end + end + end + return self +end +--- Sets this STBO's maximum shield points per segment, and can also create new segments. +--- The number of parameters defines the STBO's number of shield segments, to a maximum of 8 segments. +--- The segments' order starts with the front-facing segment, then proceeds clockwise. +--- If more parameters are provided than the STBO has shield segments, the excess parameters create new segments with the defined max but 0 current shield points. +--- A STBO with one shield segment has only a front shield generator system, and a STBO with two or more segments has only front and rear generator systems. +--- Setting a lower maximum points value than the segment's current number of points also reduces the points to the limit. +--- However, increasing the maximum value to a higher value than the current points does NOT automatically increase the current points, +--- which requires a separate call to ShipTemplateBasedObject:setShield(). +--- Example: +--- -- On a ship with 4 segments, this sets the forward shield max to 50, right to 40, rear 30, left 20 +--- -- On a ship with 2 segments, this does the same, but its current rear shield points become right shield points, and the new rear and left shield segments have 0 points +--- stbo:setShieldsMax(50,40,30,20) +function Entity:setShieldsMax(...) + if self.components.shields then + for i, max in ipairs({...}) do + self.components.shields[i].max = max + end + while select('#', ...) < #self.components.shields do + self.components.shields[#self.components.shields] = nil + end + end + return self +end +--- Sets the radar trace image for this entity. +--- Valid values are filenames of PNG images relative to the resources/radar directory. +--- Radar trace images should be white with a transparent background. +--- Only scanned SpaceShips use a specific radar trace image. Unscanned SpaceShips always display as an arrow. +--- Example: stbo:setRadarTrace("arrow.png") -- sets the radar trace to resources/radar/arrow.png +--- Example: ship:setRadarTrace("blip.png") -- displays a dot for this ship on radar when scanned +function Entity:setRadarTrace(filename) + if self.components.radar_trace then + self.components.radar_trace.icon = "radar/" .. filename + end +end + +--- Sets this STBO's impulse engine sound effect. +--- Valid values are filenames of WAV files relative to the resources/ directory. +--- Use a looping sound file that tolerates being pitched up and down as the ship's impulse speed changes. +--- Example: stbo:setImpulseSoundFile("sfx/engine_fighter.wav") -- sets the impulse sound to resources/sfx/engine_fighter.wav +function Entity:setImpulseSoundFile(filename) + if self.components.impulse_engine then self.components.impulse_engine.sound = filename end + return self +end +--- Defines whether this STBO's shields are activated. +--- Always returns true except for PlayerSpaceships, because only players can deactivate shields. +--- Example stbo:getShieldsActive() -- returns true if up, false if down +function Entity:getShieldsActive() + if self.components.shields then return self.components.shields.active end + return false +end +--- Returns whether this STBO supplies energy to docked PlayerSpaceships. +--- Example: stbo:getSharesEnergyWithDocked() +function Entity:getSharesEnergyWithDocked() + if self.components.docking_bay then return self.components.docking_bay.share_energy end + return false +end +--- Defines whether this STBO supplies energy to docked PlayerSpaceships. +--- Example: stbo:getSharesEnergyWithDocked(false) +function Entity:setSharesEnergyWithDocked(allow_energy_share) + if self.components.docking_bay then self.components.docking_bay.share_energy = allow_energy_share end + return self +end +--- Returns whether this STBO repairs docked SpaceShips. +--- Example: stbo:getRepairDocked() +function Entity:getRepairDocked() + if self.components.docking_bay then return self.components.docking_bay.repair end + return false +end +--- Defines whether this STBO repairs docked SpaceShips. +--- Example: stbo:setRepairDocked(true) +function Entity:setRepairDocked(allow_repair) + if self.components.docking_bay then self.components.docking_bay.repair = allow_repair end + return self +end +--- Returns whether the STBO restocks scan probes for docked PlayerSpaceships. +--- Example: stbo:getRestocksScanProbes() +function Entity:getRestocksScanProbes() + if self.components.docking_bay then return self.components.docking_bay.restock_probes end + return false +end +--- Defines whether the STBO restocks scan probes for docked PlayerSpaceships. +--- Example: stbo:setRestocksScanProbes(true) +function Entity:setRestocksScanProbes(allow_restock) + if self.components.docking_bay then self.components.docking_bay.restock_probes = allow_restock end + return self +end +--- Returns whether this STBO restocks missiles for docked CpuShips. +--- Example: stbo:getRestocksMissilesDocked() +function Entity:getRestocksMissilesDocked() + if self.components.docking_bay then return self.components.docking_bay.restock_missiles end + return false +end +--- Defines whether this STBO restocks missiles for docked CpuShips. +--- To restock docked PlayerSpaceships' weapons, use a comms script. See ShipTemplateBasedObject:setCommsScript() and :setCommsFunction(). +--- Example: stbo:setRestocksMissilesDocked(true) +function Entity:setRestocksMissilesDocked(allow_restock) + if self.components.docking_bay then self.components.docking_bay.restock_missiles = allow_restock end + return self +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:getShieldLevel() with an index value. +function Entity:getFrontShield() + return self.getShieldLevel(0) +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldsMax(). +function Entity:getFrontShieldMax() + return self.getShieldMax(0) +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldLevel() with an index value. +function Entity:setFrontShield(amount) + if self.components.shields then self.components.shields[1].level = amount end + return self +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldsMax(). +function Entity:setFrontShieldMax(amount) + if self.components.shields then self.components.shields[1].max = amount end + return self +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:getShieldLevel() with an index value. +function Entity:getRearShield() + return self.getShieldLevel(1) +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldsMax(). +function Entity:getRearShieldMax() + return self.getShieldMax(1) +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldLevel() with an index value. +function Entity:setRearShield(amount) + if self.components.shields then self.components.shields[2].level = amount end + return self +end +--- [DEPRECATED] +--- Use ShipTemplateBasedObject:setShieldsMax(). +function Entity:setRearShieldMax(amount) + if self.components.shields then self.components.shields[2].max = amount end + return self +end +--- Defines a function to call when this STBO takes damage. +--- Passes the object taking damage and the instigator SpaceObject (or nil) to the function. +--- Example: stbo:onTakingDamage(function(this_stbo,instigator) print(this_stbo:getCallSign() .. " was damaged by " .. instigator:getCallSign()) end) +function Entity:onTakingDamage(callback) + if self.components.hull then self.components.hull.on_taking_damage = callback end + return self +end +--- Defines a function to call when this STBO is destroyed by taking damage. +--- Passes the object taking damage and the instigator SpaceObject that delivered the destroying damage (or nil) to the function. +--- Example: stbo:onTakingDamage(function(this_stbo,instigator) print(this_stbo:getCallSign() .. " was destroyed by " .. instigator:getCallSign()) end) +function Entity:onDestruction(callback) + if self.components.hull then self.components.hull.on_destruction = callback end + return self +end diff --git a/scripts/api/entity/spaceobject.lua b/scripts/api/entity/spaceobject.lua new file mode 100644 index 0000000000..d0e896b69d --- /dev/null +++ b/scripts/api/entity/spaceobject.lua @@ -0,0 +1,467 @@ +local Entity = getLuaEntityFunctionTable() + +----- Old SpaceObject API ----- + +--- Sets this SpaceObject's position on the map, in meters from the origin. +--- Example: obj:setPosition(x,y) +function Entity:setPosition(x, y) + if self.components.transform then self.components.transform.position = {x, y} end + return self +end +--- Returns this object's position on the map. +--- Example: x,y = obj:getPosition() +function Entity:getPosition() + if self.components.transform then return table.unpack(self.components.transform.position) end +end +--- Sets this SpaceObject's absolute rotation, in degrees. +--- Unlike SpaceObject:setHeading(), a value of 0 points to the right of the map ("east"). +--- The value can also be unbounded; it can be negative, or greater than 360 degrees. +--- SpaceObject:setHeading() and SpaceObject:setRotation() do not change the helm's target heading on PlayerSpaceships. To do that, use PlayerSpaceship:commandTargetRotation(). +--- Example: obj:setRotation(270) +function Entity:setRotation(rotation) + if self.components.transform then self.components.transform.rotation = rotation end + return self +end +--- Returns this SpaceObject's absolute rotation, in degrees. +--- Example: local rotation = obj:getRotation() +function Entity:getRotation() + if self.components.transform then return self.components.transform.rotation end +end +--- Sets this SpaceObject's heading, in degrees ranging from 0 to 360. +--- Unlike SpaceObject:setRotation(), a value of 0 points to the top of the map ("north"). +--- Values that are negative or greater than 360 are converted to values within that range. +--- SpaceObject:setHeading() and SpaceObject:setRotation() do not change the helm's target heading on PlayerSpaceships. To do that, use PlayerSpaceship:commandTargetRotation(). +--- Example: obj:setHeading(0) +function Entity:setHeading(heading) + if self.components.transform then self.components.transform.rotation = heading + 270 end + return self +end +--- Returns this SpaceObject's heading, in degrees ranging from 0 to 360. +--- Example: heading = obj:getHeading(0) +function Entity:getHeading() + if self.components.transform then + local heading = self.components.transform.rotation - 270 + while heading < 0 do heading = heading + 360 end + while heading > 360 do heading = heading - 360 end + return heading + end + return 0 +end +--- Returns this SpaceObject's directional velocity within 2D space as an x/y vector. +--- The values are relative x/y coordinates from the SpaceObject's current position (a 2D velocity vector). +--- Example: vx,vy = obj:getVelocity() +function Entity:getVelocity() + if self.components.physics then return table.unpack(self.components.physics.velocity) end +end +--- Returns this SpaceObject's rotational velocity within 2D space, in degrees per second. +--- Example: obj:getAngularVelocity() +function Entity:getAngularVelocity() + if self.components.physics then return self.components.physics.angular_velocity end +end +--- Sets the faction to which this SpaceObject belongs, by faction name. +--- Factions are defined by the FactionInfo class, and default factions are defined in scripts/factionInfo.lua. +--- Requires a faction name string. +--- Example: obj:setFaction("Human Navy") +function Entity:setFaction(faction_name) + local faction = getFactionInfo(faction_name) + if faction == nil then + print("Failed to find faction: " .. faction_name) + self.components.faction = nil + else + self.components.faction = {entity=faction} + end + return self +end +--- Returns the name of the faction to which this SpaceObject belongs. +--- Example: obj:getFaction() +function Entity:getFaction() + local f = self.components.faction + if f and f.entity and f.entity.components.faction_info then + return f.entity.components.faction_info.name + end +end +--- Returns the localized name of the faction to which this SpaceObject belongs. +--- Example: obj:getLocaleFaction() +function Entity:getLocaleFaction() + local f = self.components.faction + if f and f.entity and f.entity.components.faction_info then + return f.entity.components.faction_info.locale_name + end +end +--- Returns the faction to which this SpaceObject belongs, by the faction's index in the faction list. +--- Use with SpaceObject:getFactionId() to ensure that two objects belong to the same faction. +--- Example: local faction_id = obj:getFactionId() +function Entity:setFactionId(faction_id) + if faction_id == nil then + self.components.faction = nil + else + self.components.faction = {entity=faction_id} + end + return self +end +--- Returns the faction list index for the faction to which this SpaceObject belongs. +--- Use with SpaceObject:setFactionId() to ensure that two objects belong to the same faction. +--- Example: obj:setFactionId(target:getFactionId()) +function Entity:getFactionId() + if self.components.faction then + return self.components.faction.entity + end +end +--- Returns the friend-or-foe status of the given faction relative to this SpaceObject's faction. +--- Returns true if the given SpaceObject's faction is hostile to this object's. +--- Example: obj:isEnemy(target) +function Entity:isEnemy(target) + if target == nil then return false end + local my_faction = self:getFactionId() + if my_faction == nil then return false end + local my_faction_info = my_faction.components.faction_info + if my_faction_info == nil then return false end + local target_faction = target:getFactionId() + if target_faction == nil then return false end + for n=1,#my_faction_info do + local relation = my_faction_info[n] + if relation.other_faction == target_faction then + return relation.relation == "enemy" + end + end + return false +end +--- Returns the friend-or-foe status of the given faction relative to this SpaceObject's faction. +--- Returns true if the given SpaceObject's faction is friendly to this object's. +--- If an object is neither friendly nor enemy, it is neutral. +--- Example: obj:isFriendly(target) +function Entity:isFriendly(target) + if target == nil then return false end + local my_faction = self:getFactionId() + if my_faction == nil then return false end + local my_faction_info = my_faction.components.faction_info + if my_faction_info == nil then return false end + local target_faction = target:getFactionId() + if target_faction == nil then return false end + for n=1,#my_faction_info do + local relation = my_faction_info[n] + if relation.other_faction == target_faction then + return relation.relation == "friendly" + end + end + return false +end +--- Sets the communications script used when this SpaceObject is hailed. +--- Accepts the filename of a Lua script relative to the scripts/ directory. +--- If set to an empty string, comms with this object are disabled. +--- The globals comms_source (PlayerSpaceship) and comms_target (SpaceObject) are made available in the scenario script. +--- Subclasses set their own default comms scripts. +--- For object types without defaults, or when creating custom comms scripts, use setCommsMessage() to define the message and addCommsReply() to provide player response options. +--- See also SpaceObject:setCommsFunction(). +--- Examples: +--- obj:setCommsScript("comms_custom_script.lua") -- sets scripts/comms_custom_script.lua as this object's comms script +--- obj:setCommsScript("") -- disables comms with this object +function Entity:setCommsScript(script_name) + self.components.comms_receiver = {script=script_name} + self.components.comms_receiver.callback = nil + return self +end +--- Defines a function to call when this SpaceObject is hailed, in lieu of any current or default comms script. +--- For a detailed example, see scripts/scenario_53_escape.lua. +--- TODO: Confirm this: The globals comms_source (PlayerSpaceship) and comms_target (SpaceObject) are made available in the scenario script. +--- They remain as globals. As usual, such globals are not accessible in required files. +--- Instead of using the globals, the callback can optionally take two equivalent parameters. +--- See also SpaceObject:setCommsScript(). +--- Examples: +--- obj:setCommsFunction(function(comms_source, comms_target) ... end) +--- Example: obj:setCommsFunction(commsStation) -- where commsStation is a function that calls setCommsMessage() at least once, and uses addCommsReply() to let players respond +function Entity:setCommsFunction(callback) + self.components.comms_receiver = {callback=callback} + self.components.comms_receiver.script = "" + return self +end +--- Sets this SpaceObject's callsign. +--- EmptyEpsilon generates random callsigns for objects upon creation, and this function overrides that default. +--- Example: obj:setCallSign("Epsilon") +function Entity:setCallSign(callsign) + self.components.callsign = {callsign=callsign} + return self +end +--- Hails a PlayerSpaceship from this SpaceObject. +--- The PlayerSpaceship's comms position is notified and can accept or refuse the hail. +--- If the PlayerSpaceship accepts the hail, this displays the given message. +--- Returns true when the hail is accepted. +--- Returns false if the hail is refused, or when the target player cannot be hailed right now, for example because it's already communicating with something else. +--- This logs a message in the target's comms log. To avoid logging, use SpaceObject:sendCommsMessageNoLog(). +--- Requires a target PlayerShip and message, though the message can be an empty string. +--- Example: obj:sendCommsMessage(player, "Prepare to die") +function Entity:sendCommsMessage(target, message) + if self:isFriendly(target) then + target:addToShipLog(message, "#C0C0FF") + elseif self:isEnemy(target) then + target:addToShipLog(message, "#FFC0C0") + else + target:addToShipLog(message, "#C0C0FF") + end + return self:sendCommsMessageNoLog(target, message) +end +--- As SpaceObject:sendCommsMessage(), but does not log a failed hail to the target ship's comms log. +--- Example: obj:sendCommsMessageNoLog(player, "Prepare to die") +function Entity:sendCommsMessageNoLog(target, message) + if self:openCommsTo(target) then + target.components.comms_transmitter.incomming_message = message + return true + end + return false +end +--- As SpaceObject:sendCommsMessage(), but sends an empty string as the message. +--- This calls the SpaceObject's comms function. +--- Example: obj:openCommsTo(player) +function Entity:openCommsTo(target) + if target and target.components.comms_transmitter then + if target.components.comms_transmitter.state == "inactive" or target.components.comms_transmitter.state == "broken" then + target.components.comms_transmitter.state = "hailed" + target.components.comms_transmitter.incomming_message = "" + target.components.comms_transmitter.target = self + target.components.comms_transmitter.target_name = self:getCallSign() + return true + end + end + return false +end +--- Returns this SpaceObject's callsign. +--- Example: obj:getCallSign() +function Entity:getCallSign() + if self.components.callsign then return self.components.callsign.callsign end + return "?" +end +--- Returns whether any SpaceObject from a hostile faction are within a given radius of this SpaceObject, in (unit?). +--- Example: obj:areEnemiesInRange(5000) -- returns true if hostiles are within 5U of this object +function Entity:areEnemiesInRange(range) + return #getEnemiesInRadiusFor(self, range) > 0 +end +--- Returns any SpaceObject within a specific radius, in (unit?), of this SpaceObject. +--- Returns a list of all SpaceObjects within range. +--- Example: obj:getObjectsInRange(5000) -- returns all objects within 5U of this SpaceObject. +function Entity:getObjectsInRange(range) + local x, y = self:getPosition() + return getObjectsInRadius(x, y, range) +end +--- Returns this SpaceObject's faction reputation points. +--- Example: obj:getReputationPoints() +function Entity:getReputationPoints() + if self.components.faction and self.components.faction.entity and self.components.faction.entity.components.faction_info then + return self.components.faction.entity.components.faction_info.reputation_points + end +end +--- Sets this SpaceObject's faction reputation points to the given amount. +--- Example: obj:setReputationPoints(1000) +function Entity:setReputationPoints(amount) + if self.components.faction and self.components.faction.entity and self.components.faction.entity.components.faction_info then + self.components.faction.entity.components.faction_info.reputation_points = amount + end +end +--- Deducts a given number of faction reputation points from this SpaceObject. +--- Returns true if there are enough points to deduct the specified amount, then does so. +--- Returns false if there are not enough points, then does not deduct any. +--- Example: obj:takeReputationPoints(1000) -- returns false if `obj` has fewer than 1000 reputation points, otherwise returns true and deducts the points +function Entity:takeReputationPoints(amount) + if self.components.faction and self.components.faction.entity and self.components.faction.entity.components.faction_info then + local points = self.components.faction.entity.components.faction_info.reputation_points + if points >= amount then + self.components.faction.entity.components.faction_info.reputation_points = points - amount + return true + end + end + return false +end +--- Adds a given number of faction reputation points to this SpaceObject. +--- Example: obj:addReputationPoints(1000) +function Entity:addReputationPoints(amount) + if self.components.faction and self.components.faction.entity and self.components.faction.entity.components.faction_info then + local points = self.components.faction.entity.components.faction_info.reputation_points + if points >= -amount then + self.components.faction.entity.components.faction_info.reputation_points = points + amount + end + end +end +--- Returns the name of the map sector, such as "A4", where this SpaceObject is located. +--- Example: obj:getSectorName() +function Entity:getSectorName() + local x, y = self:getPosition() + return getSectorName(x, y) +end +--- Deals a specific amount of a specific type of damage to this SpaceObject. +--- Requires a numeric value for the damage amount, and accepts an optional DamageInfo type. +--- The optional DamageInfo parameter can be empty, which deals "energy" damage, or a string that indicates which type of damage to deal. +--- Valid damage types are "energy", "kinetic", and "emp". +--- If you specify a damage type, you can also optionally specify the location of the damage's origin, for instance to damage a specific shield segment on the target. +--- SpaceObjects by default do not implement damage, instead leaving it to be overridden by specialized subclasses. +--- Examples: +--- obj:takeDamage(20, "emp", 1000, 0) -- deals 20 EMP damage as if it had originated from coordinates 1000,0 +--- obj:takeDamage(20) -- deals 20 energy damage +function Entity:takeDamage(amount, type, originx, originy) + applyDamageToEntity(self, amount, {type=type, x=originx, y=originy}) +end +--- Sets this SpaceObject's description in unscanned and scanned states. +--- The science screen displays these descriptions when targeting a scanned object. +--- Requires two string values, one for the descriptions when unscanned and another for when it has been scanned. +--- Example: +--- obj:setDescriptions("A refitted Atlantis X23...", "It's a trap!") +function Entity:setDescriptions(unscanned_description, scanned_description) + self.components.science_description = {not_scanned=unscanned_description, friend_or_foe_identified=unscanned_description, simple_scan=scanned_description, full_scan=scanned_description} + return self +end +--- Sets a description for a given EScannedState on this SpaceObject. +--- Only SpaceShip objects are created in an unscanned state. Other SpaceObjects are created as fully scanned. +--- - "notscanned" or "not": The object has not been scanned. +--- - "friendorfoeidentified": The object has been identified as hostile or friendly, but has not been scanned. +--- - "simplescan" or "simple": The object has been scanned once under default server settings, displaying only basic information about the object. +--- - "fullscan" or "full": The object is fully scanned. +--- Example: obj:setDescriptionForScanState("friendorfoeidentified", "A refitted...") +function Entity:setDescriptionForScanState(state, description) + if self.components.science_description == nil then self.components.science_description = {} end + if state == "notscanned" or state == "not" then self.components.science_description.not_scanned = description end + if state == "friendorfoeidentified" then self.components.science_description.friend_or_foe_identified = description end + if state == "simplescan" or state == "simple" then self.components.science_description.simple_scan = description end + if state == "fullscan" or state == "full" then self.components.science_description.full_scan = description end + return self +end +--- Returns this SpaceObject's description for the given EScannedState. +--- Accepts an optional string-equivalent EScannedState, which determines which description to return. +--- Defaults to returning the "fullscan" description. +--- Examples: +--- obj:getDescription() -- returns the "fullscan" description +--- obj:getDescription("friendorfoeidentified") -- returns the "friendorfoeidentified" description +function Entity:getDescription(state) + if self.components.science_description == nil then return "" end + if state == "notscanned" or state == "not" then return self.components.science_description.not_scanned end + if state == "friendorfoeidentified" then return self.components.science_description.friend_or_foe_identified end + if state == "simplescan" or state == "simple" then return self.components.science_description.simple_scan end + return self.components.science_description.full_scan +end +--- Sets this SpaceObject's radar signature, which creates noise on the science screen's raw radar signal ring. +--- The raw signal ring contains red (electrical), green (biological), and blue (gravitational) bands of waveform noise. +--- Certain SpaceObject subclasses might set their own defaults or dynamically modify their signatures using this value as a baseline. +--- Requires numeric values ranging from 0.0 to 1.0 for the gravitational, electrical, and biological radar bands, in that order. +--- Larger and negative values are possible, but currently have no visual effect on the bands. +--- Example: obj:setRadarSignatureInfo(0.0, 0.5, 1.0) -- a radar signature of 0 gravitational, 0.5 electrical, and 1.0 biological +function Entity:setRadarSignatureInfo(gravity, electrical, biological) + self.components.radar_signature = {gravity=gravity, electrical=electrical, biological=biological} + return self +end +--- Returns this SpaceObject's gravitational radar signature value. +--- Example: obj:getRadarSignatureGravity() +function Entity:getRadarSignatureGravity() + if self.components.radar_signature then return self.components.radar_signature.gravity end + return 0.0 +end +--- Returns this SpaceObject's electical radar signature value. +--- Example: obj:getRadarSignatureElectrical() +function Entity:getRadarSignatureElectrical() + if self.components.radar_signature then return self.components.radar_signature.electrical end + return 0.0 +end +--- Returns this SpaceObject's biological radar signature value. +--- Example: obj:getRadarSignatureBiological() +function Entity:getRadarSignatureBiological() + if self.components.radar_signature then return self.components.radar_signature.biological end + return 0.0 +end +--- Sets this SpaceObject's scanning complexity (number of bars in the scanning minigame) and depth (number of scanning minigames to complete until fully scanned), respectively. +--- Setting this also clears the object's scanned state. +--- Example: obj:setScanningParameters(2, 3) +function Entity:setScanningParameters(complexity, depth) + self.components.scan_state = {complexity=complexity, depth=depth} + self:setScanned(false) + return self +end +--- Returns the scanning complexity for the given SpaceObject. +--- Example: obj:scanningComplexity(obj) +function Entity:scanningComplexity() + if self.components.scan_state then return self.components.scan_state.complexity end + return 0 +end +--- Returns the maximum scanning depth for the given SpaceObject. +--- Example: obj:scanningChannelDepth(obj) +function Entity:scanningChannelDepth() + if self.components.scan_state then return self.components.scan_state.depth end + return 0 +end +--- Defines whether all factions consider this SpaceObject as having been scanned. +--- Only SpaceShip objects are created in an unscanned state. Other SpaceObjects are created as fully scanned. +--- If false, all factions treat this object as unscanned. +--- If true, all factions treat this object as fully scanned. +--- Example: obj:setScanned(true) +function Entity:setScanned(is_scanned) + if is_scanned then self:setScanState("full") else self:setScanState("none") end + return self +end +--- [DEPRECATED] +--- Returns whether this SpaceObject has been scanned. +--- Use SpaceObject:isScannedBy() or SpaceObject:isScannedByFaction() instead. +function Entity:isScanned() + local ss = self.components.scan_state + if ss then + for n=1,#ss do + if ss[n].state == "full" then return true end + if ss[n].state == "simple" then return true end + end + return false + end + return true +end +--- Returns whether the given SpaceObject has successfully scanned this SpaceObject. +--- Example: obj:isScannedBy(other) +function Entity:isScannedBy(other) + if not other then return false end + local f = other:getFactionId() + if f then + local ss = self.components.scan_state + if ss then + for n=1,#ss do + if ss[n].faction == f then + if ss[n].state == "full" then return true end + if ss[n].state == "simple" then return true end + return false + end + end + return false + end + end + return true +end +--- Defines whether a given faction considers this SpaceObject as having been scanned. +--- Requires a faction name string value as defined by its FactionInfo, and a Boolean value. +--- Example: obj:setScannedByFaction("Human Navy", false) +function Entity:setScannedByFaction(faction_name, is_scanned) + if is_scanned then + setScanStateByFaction(faction_name, "full") + else + setScanStateByFaction(faction_name, "none") + end + return self +end +--- Returns whether the given faction has successfully scanned this SpaceObject. +--- Requires a faction name string value as defined by its FactionInfo. +--- Example: obj:isScannedByFaction("Human Navy") +function Entity:isScannedByFaction(faction_name) + local ss = self.components.scan_state + if ss then + local f = getFactionInfo(faction) + if f ~= nil then + for n=1,#ss do + if ss[n].faction == f then + if ss[n].state == "full" then return true end + if ss[n].state == "simple" then return true end + return false + end + end + end + end + return false +end +--- Defines a function to call when this SpaceObject is destroyed by any means. +--- Example: +--- -- Prints to the console window or logging file when this SpaceObject is destroyed +--- obj:onDestroyed(function() print("Object destroyed!") end) +function Entity:onDestroyed(callback) + --TODO: Cases where we do not have hull + if self.components.hull then self.components.hull.on_destruction = callback end + return self +end diff --git a/scripts/api/entity/spaceship.lua b/scripts/api/entity/spaceship.lua new file mode 100644 index 0000000000..6fab9e9bc8 --- /dev/null +++ b/scripts/api/entity/spaceship.lua @@ -0,0 +1,836 @@ +local Entity = getLuaEntityFunctionTable() + +--- A SpaceShip is a ShipTemplateBasedObject controlled by either the AI (CpuShip) or players (PlayerSpaceship). +--- It can carry and deploy weapons, dock with or carry docked ships, and move using impulse, jump, or warp drives. +--- It's also subject to being moved by collision physics, unlike SpaceStations, which remain stationary. +--- This is the parent class of CpuShip and PlayerSpaceship objects, which inherit all STBO and SpaceShip functions. +--- Objects of this class can't be created by scripts, but its child classes can. + +--- [DEPRECATED] +--- Use SpaceShip:isFriendOrFoeIdentifiedBy() or SpaceShip:isFriendOrFoeIdentifiedByFaction(). +function Entity:isFriendOrFoeIdentified() + if self.components.scan_state then + for n=1,#self.components.scan_state do + if self.components.scan_state[n].state ~= "none" then return true end + end + end + return false +end +--- [DEPRECATED] +--- Use SpaceShip:isFullyScannedBy() or SpaceShip:isFullyScannedByFaction(). +function Entity:isFullyScanned() + if self.components.scan_state then + for n=1,#self.components.scan_state do + if self.components.scan_state[n].state == "full" then return true end + end + end + return false +end + +--- Returns whether this SpaceShip has been identified by the given SpaceObject as either hostile or friendly. +--- Example: ship:isFriendOrFoeIdentifiedBy(enemy) +function Entity:isFriendOrFoeIdentifiedBy(enemy) + local scan_state = self.components.scan_state + if enemy == nil or not enemy.isValid() then return false end + local faction = enemy.components.faction + if faction == nil then return false end + faction = faction.entity + if scan_state then + for n=1,#scan_state do + if scan_state[n].faction == faction then return scan_state[n].state ~= "none" end + end + end + return false +end +--- Returns whether this SpaceShip has been fully scanned by the given SpaceObject. +--- See also SpaceObject:isScannedBy(). +--- Example: ship:isFullyScannedBy(enemy) +function Entity:isFullyScannedBy(enemy) + local scan_state = self.components.scan_state + if enemy == nil or not enemy.isValid() then return false end + local faction = enemy.components.faction + if faction == nil then return false end + faction = faction.entity + if scan_state then + for n=1,#scan_state do + if scan_state[n].faction == faction then return scan_state[n].state == "full" end + end + end + return false +end +--- Returns whether this SpaceShip has been identified by the given faction as either hostile or friendly. +--- Example: ship:isFriendOrFoeIdentifiedByFaction("Kraylor") +function Entity:isFriendOrFoeIdentifiedByFaction(faction) + local scan_state = self.components.scan_state + if enemy == nil or not enemy.isValid() then return false end + faction = getFactionInfo(faction) + if faction == nil then return false end + if scan_state then + for n=1,#scan_state do + if scan_state[n].faction == faction then return scan_state[n].state ~= "none" end + end + end + return false +end +--- Returns whether this SpaceShip has been fully scanned by the given faction. +--- See also SpaceObject:isScannedByFaction(). +--- Example: ship:isFullyScannedByFaction("Kraylor") +function Entity:isFullyScannedByFaction(faction) + local scan_state = self.components.scan_state + if enemy == nil or not enemy.isValid() then return false end + faction = getFactionInfo(faction) + if faction == nil then return false end + if scan_state then + for n=1,#scan_state do + if scan_state[n].faction == faction then return scan_state[n].state == "full" end + end + end + return false + +end +--- Returns whether this SpaceShip is docked with the given SpaceObject. +--- Example: ship:isDocked(base) -- returns true if `ship` is fully docked with `base` +function Entity:isDocked(target) + if self.components.docking_port and self.components.docking_port.state == "docked" then + return self.components.docking_port.target == target + end + return false +end +--- Returns the SoaceObject with which this SpaceShip is docked. +--- Example: base = ship:getDockedWith() +function Entity:getDockedWith() + if self.components.docking_port and self.components.docking_port.state == "docked" then + return self.components.docking_port.target + end + return false +end +--- Returns the EDockingState value of this SpaceShip. +--- 0 = Not docked +--- 1 = Docking in progress +--- 2 = Docked +--- Example: ds = ship:getDockingState() +function Entity:getDockingState() + if self.components.docking_port then + local state = self.components.docking_port.state + if state == "docking" then return 1 end + if state == "docked" then return 2 end + end + return 0 +end + +--- Returns the number of the given weapon type stocked by this SpaceShip. +--- Example: homing = ship:getWeaponStorage("Homing") +function Entity:getWeaponStorage(weapon_type) + if self.components.missile_tubes then + weapon_type = string.lower(weapon_type) + if weapon_type == "homing" then return self.components.missile_tubes.storage_homing end + if weapon_type == "nuke" then return self.components.missile_tubes.storage_nuke end + if weapon_type == "mine" then return self.components.missile_tubes.storage_mine end + if weapon_type == "emp" then return self.components.missile_tubes.storage_emp end + if weapon_type == "hvli" then return self.components.missile_tubes.storage_hvli end + end + return 0 +end +--- Returns this SpaceShip's capacity for the given weapon type. +--- Example: homing_max = ship:getWeaponStorageMax("Homing") +function Entity:getWeaponStorageMax(weapon_type) + if self.components.missile_tubes then + weapon_type = string.lower(weapon_type) + if weapon_type == "homing" then return self.components.missile_tubes.max_homing end + if weapon_type == "nuke" then return self.components.missile_tubes.max_nuke end + if weapon_type == "mine" then return self.components.missile_tubes.max_mine end + if weapon_type == "emp" then return self.components.missile_tubes.max_emp end + if weapon_type == "hvli" then return self.components.missile_tubes.max_hvli end + end + return 0 +end +--- Sets the number of the given weapon type stocked by this SpaceShip. +--- Example: ship:setWeaponStorage("Homing", 2) -- this ship has 2 Homing missiles +--- Sets the weapon type and amount restocked upon pickup when a SpaceShip collides with this SupplyDrop. +--- Example: supply_drop:setWeaponStorage("Homing",6) +function Entity:setWeaponStorage(weapon_type, amount) + if self.components.missile_tubes then + weapon_type = string.lower(weapon_type) + if weapon_type == "homing" then self.components.missile_tubes.storage_homing = amount end + if weapon_type == "nuke" then self.components.missile_tubes.storage_nuke = amount end + if weapon_type == "mine" then self.components.missile_tubes.storage_mine = amount end + if weapon_type == "emp" then self.components.missile_tubes.storage_emp = amount end + if weapon_type == "hvli" then self.components.missile_tubes.storage_hvli = amount end + end + --TODO: Supplydrop + return self +end +--- Sets this SpaceShip's capacity for the given weapon type. +--- If this ship has more stock of that weapon type than the new capacity, its stock is reduced. +--- However, if this ship's capacity for a weapon type is increased, its stocks are not. +--- Use SpaceShip:setWeaponStorage() to update the stocks. +--- Example: ship:setWeaponStorageMax("Homing", 4) -- this ship can carry 4 Homing missiles +function Entity:setWeaponStorageMax(weapon_type, amount) + if self.components.missile_tubes then + weapon_type = string.lower(weapon_type) + if weapon_type == "homing" then self.components.missile_tubes.max_homing = amount end + if weapon_type == "nuke" then self.components.missile_tubes.max_nuke = amount end + if weapon_type == "mine" then self.components.missile_tubes.max_mine = amount end + if weapon_type == "emp" then self.components.missile_tubes.max_emp = amount end + if weapon_type == "hvli" then self.components.missile_tubes.max_hvli = amount end + end + return self +end +--- Returns this SpaceShip's shield frequency index. +--- To convert the index to the value used by players, multiply it by 20, then add 400. +--- Example: +--- frequency = ship:getShieldsFrequency() -- frequency index is 10 +--- -- Outputs "Ship's shield frequency is 600THz" +--- print("Ship's shield frequency is " .. (frequency * 20) + 400 .. "THz") +function Entity:getShieldsFrequency() + if self.components.shields then return self.components.shields.frequency end + return 0 +end +--- Sets this SpaceShip's shield frequency index. +--- To convert the index to the value used by players, multiply it by 20, then add 400. +--- Valid values are 0 (400THz) to 20 (800THz). Defaults to a random value. +--- Unlike PlayerSpaceship:commandSetShieldFrequency(), this instantly changes the frequency with no calibration delay. +--- Example: frequency = ship:setShieldsFrequency(10) -- frequency is 600THz +function Entity:setShieldsFrequency(frequency) + if self.components.shields then self.components.shields.frequency = frequency end + return self +end +--- Returns this SpaceShip's beam weapon frequency. +--- To convert the index to the value used by players, multiply it by 20, then add 400. +--- Example: +--- frequency = ship:getBeamFrequency() -- frequency index is 10 +--- -- Outputs "Ship's beam frequency is 600THz" +--- print("Ship's beam frequency is " .. (frequency * 20) + 400 .. "THz") +function Entity:getBeamFrequency() + if self.components.beam_weapons then return self.components.beam_weapons.frequency end + return 0 +end +--- Returns this SpaceShip's energy capacity. +--- Example: ship:getMaxEnergy() +function Entity:getMaxEnergy() + if self.components.reactor then return self.components.reactor.max_energy end + return 1000 +end +--- Sets this SpaceShip's energy capacity. +--- CpuShips don't consume energy. Setting this value has no effect on their behavior or functionality. +--- For PlayerSpaceships, see PlayerSpaceship:setEnergyLevelMax(). +--- Example: ship:setMaxEnergy(800) +function Entity:setMaxEnergy(amount) + if self.components.reactor then self.components.reactor.max_energy = amount end + return self +end +--- Returns this SpaceShip's energy level. +--- Example: ship:getEnergy() +function Entity:getEnergy() + if self.components.reactor then return self.components.reactor.energy end + return 1000 +end + + +function __getSystemByName(entity, system_name) + system_name = string.lower(system_name) + if system_name == "reactor" then return entity.components.reactor end + if system_name == "beamweapons" then return entity.components.beam_weapons end + if system_name == "missilesystem" then return entity.components.missile_tubes end + if system_name == "maneuver" then return entity.components.maneuvering_thrusters end + if system_name == "impulse" then return entity.components.impulse_engine end + if system_name == "warp" then return entity.components.warp_drive end + if system_name == "jumpdrive" then return entity.components.jump_drive end + if system_name == "frontshield" then return entity.components.shields end + if system_name == "rearshield" and #entity.components.shields > 1 then return entity.components.shields end + return nil +end + +function __getSystemPropertyByName(entity, system_name, property) + system_name = string.lower(system_name) + local sys = __getSystemByName(entity, system_name) + if sys == nil then return 0.0 end + if system_name == "frontshield" then return sys["front_" .. property] end + if system_name == "rearshield" then return sys["rear_" .. property] end + return sys[property] +end + +function __setSystemPropertyByName(entity, system_name, property, value) + system_name = string.lower(system_name) + local sys = __getSystemByName(entity, system_name) + if sys == nil then return end + if system_name == "frontshield" then sys["front_" .. property] = value + elseif system_name == "rearshield" then sys["rear_" .. property] = value + else sys[property] = value end +end + +--- Returns whether this SpaceShip has the given system. +--- Example: ship:hasSystem("impulse") -- returns true if the ship has impulse drive +function Entity:hasSystem(system_name) + return __getSystemByName(self, system_name) ~= nil +end +--- Returns the hacked level for the given system on this SpaceShip. +--- Returns a value between 0.0 (unhacked) and 1.0 (fully hacked). +--- Example: ship:getSystemHackedLevel("impulse") +function Entity:getSystemHackedLevel(system_name) + return __getSystemPropertyByName(self, system_name, "hacked_level") +end +--- Sets the hacked level for the given system on this SpaceShip. +--- Valid range is 0.0 (unhacked) to 1.0 (fully hacked). +--- Example: ship:setSystemHackedLevel("impulse",0.5) -- sets the ship's impulse drive to half hacked +function Entity:setSystemHackedLevel(system_name, level) + __setSystemPropertyByName(self, system_name, "hacked_level", level) + return self +end +--- Returns the given system's health on this SpaceShip. +--- System health is related to damage, and is separate from its hacked level. +--- Returns a value between 0.0 (fully disabled) and 1.0 (undamaged). +--- Example: ship:getSystemHealth("impulse") +function Entity:getSystemHealth(system_name) + return __getSystemPropertyByName(self, system_name, "health") +end +--- Sets the given system's health on this SpaceShip. +--- System health is related to damage, and is separate from its hacked level. +--- Valid range is 0.0 (fully disabled) and 1.0 (undamaged). +--- Example: ship:setSystemHealth("impulse",0.5) -- sets the ship's impulse drive to half damaged +function Entity:setSystemHealth(system_name, amount) + __setSystemPropertyByName(self, system_name, "health", amount) + return self +end +--- Returns the given system's maximum health on this SpaceShip. +--- Returns a value between 0.0 (fully disabled) and 1.0 (undamaged). +--- Example: ship:getSystemHealthMax("impulse") +function Entity:getSystemHealthMax(system_name) + return __getSystemPropertyByName(self, system_name, "max_health") +end +--- Sets the given system's maximum health on this SpaceShip. +--- Valid range is 0.0 (fully disabled) and 1.0 (undamaged). +--- Example: ship:setSystemHealthMax("impulse", 0.5) -- limits the ship's impulse drive health to half +function Entity:setSystemHealthMax(system_name, amount) + __setSystemPropertyByName(self, system_name, "max_health", amount) + return self +end +--- Returns the given system's heat level on this SpaceShip. +--- Returns a value between 0.0 (no heat) and 1.0 (overheating). +--- Example: ship:getSystemHeat("impulse") +function Entity:getSystemHeat(system_name) + return __getSystemPropertyByName(self, system_name, "heat_level") +end +--- Sets the given system's heat level on this SpaceShip. +--- CpuShips don't generate or manage heat. Setting this has no effect on them. +--- Valid range is 0.0 (fully disabled) to 1.0 (undamaged). +--- Example: ship:setSystemHeat("impulse", 0.5) -- sets the ship's impulse drive heat to half of capacity +function Entity:setSystemHeat(system_name, amount) + __setSystemPropertyByName(self, system_name, "heat_level", amount) + return self +end +--- Returns the given system's rate of heating or cooling, in percent (0.01 = 1%) per second?, on this SpaceShip. +--- Example: ship:getSystemHeatRate("impulse") +function Entity:getSystemHeatRate(system_name) + return __getSystemPropertyByName(self, system_name, "heat_add_rate_per_second") +end +--- Sets the given system's rate of heating or cooling, in percent (0.01 = 1%) per second?, on this SpaceShip. +--- CpuShips don't generate or manage heat. Setting this has no effect on them. +--- Example: ship:setSystemHeatRate("impulse", 0.05) +function Entity:setSystemHeatRate(system_name, amount) + __setSystemPropertyByName(self, system_name, "heat_add_rate_per_second", amount) + return self +end +--- Returns the given system's power level on this SpaceShip. +--- Returns a value between 0.0 (unpowered) and 1.0 (fully powered). +--- Example: ship:getSystemPower("impulse") +function Entity:getSystemPower(system_name) + return __getSystemPropertyByName(self, system_name, "power_level") +end +--- Sets the given system's power level. +--- Valid range is 0.0 (unpowered) to 1.0 (fully powered). +--- Example: ship:setSystemPower("impulse", 0.5) -- sets the ship's impulse drive to half power +function Entity:setSystemPower(system_name, amount) + __setSystemPropertyByName(self, system_name, "power_level", amount) + __setSystemPropertyByName(self, system_name, "power_request", amount) + return self +end +--- Returns the given system's rate of consuming power, in points per second?, in this SpaceShip. +--- Example: ship:getSystemPowerRate("impulse") +function Entity:getSystemPowerRate(system_name) + return __getSystemPropertyByName(self, system_name, "power_change_rate_per_second") +end +--- Sets the given system's rate of consuming power, in points per second?, in this SpaceShip. +--- CpuShips don't consume energy. Setting this has no effect. +--- Example: ship:setSystemPowerRate("impulse", 0.4) +function Entity:setSystemPowerRate(system_name, amount) + __setSystemPropertyByName(self, system_name, "power_change_rate_per_second", amount) + return self +end +--- Returns the relative power drain factor for the given system. +--- Example: ship:getSystemPowerFactor("impulse") +function Entity:getSystemPowerFactor(system_name) + return __getSystemPropertyByName(self, system_name, "power_factor") +end +--- Sets the relative power drain factor? for the given system in this SpaceShip. +--- "reactor" has a negative value because it generates power rather than draining it. +--- CpuShips don't consume energy. Setting this has no effect. +--- Example: ship:setSystemPowerFactor("impulse", 4) +function Entity:setSystemPowerFactor(system_name, amount) + __setSystemPropertyByName(self, system_name, "power_factor", amount) + return self +end +--- Returns the coolant distribution for the given system in this SpaceShip. +--- Returns a value between 0.0 (none) and 1.0 (capacity). +--- Example: ship:getSystemCoolant("impulse") +function Entity:getSystemCoolant(system_name) + return __getSystemPropertyByName(self, system_name, "coolant_level") +end +--- Sets the coolant quantity for the given system in this SpaceShip. +--- CpuShips don't generate or manage heat. Setting this has no effect on them. +--- Valid range is 0.0 (none) to 1.0 (capacity). +--- Example: ship:setSystemPowerFactor("impulse", 4) +function Entity:setSystemCoolant(system_name, amount) + __setSystemPropertyByName(self, system_name, "coolant_level", amount) + __setSystemPropertyByName(self, system_name, "coolant_request", amount) + return self +end +--- Returns the rate at which the given system in this SpaceShip takes coolant, in points per second? +--- Example: ship:getSystemCoolantRate("impulse") +function Entity:getSystemCoolantRate(system_name) + return __getSystemPropertyByName(self, system_name, "coolant_change_rate_per_second") +end +--- Sets the rate at which the given system in this SpaceShip takes coolant, in points per second? +--- CpuShips don't generate or manage heat. Setting this has no effect on them. +--- Example: ship:setSystemCoolantRate("impulse", 1.2) +function Entity:setSystemCoolantRate(system_name, amount) + __setSystemPropertyByName(self, system_name, "coolant_change_rate_per_second", amount) + return self +end +--- Returns this SpaceShip's forward and reverse impulse speed limits. +--- Examples: +--- forward,reverse = getImpulseMaxSpeed() +--- forward = getImpulseMaxSpeed() -- forward speed only +function Entity:getImpulseMaxSpeed() + if self.components.impulse_engine then return self.components.impulse_engine.max_speed_forward end + return 0.0 +end +--- Sets this SpaceShip's maximum forward and reverse impulse speeds. +--- The reverse maximum speed value is optional. +--- Calling this with a single argument sets both forward and reverse maximum speeds to the same value. +--- Examples: +--- ship:setImpulseMaxSpeed(30,20) -- sets the max forward speed to 30 and reverse to 20 +--- ship:setImpulseMaxSpeed(30) -- sets the max forward and reverse speed to 30 +function Entity:setImpulseMaxSpeed(forward, reverse) + if self.components.impulse_engine then + self.components.impulse_engine.max_speed_forward = forward + if reverse == nil then + self.components.impulse_engine.max_speed_reverse = forward + else + self.components.impulse_engine.max_speed_reverse = reverse + end + end + return self +end +--- Returns this SpaceShip's maximum rotational speed, in degrees per second? +--- Example: ship:getRotationMaxSpeed() +function Entity:getRotationMaxSpeed() + if self.components.maneuvering_thrusters then return self.components.maneuvering_thrusters.speed end + return 0.0 +end +--- Sets this SpaceShip's maximum rotational speed, in degrees per second? +--- Example: ship:setRotationMaxSpeed(10) +function Entity:setRotationMaxSpeed(speed) + if self.components.maneuvering_thrusters then self.components.maneuvering_thrusters.speed = speed end + return self +end +--- Returns the SpaceShip's forward and reverse impulse acceleration values, in (unit?) +--- Examples: +--- forward,reverse = getAcceleration() +--- forward = getAcceleration() -- forward acceleration only +function Entity:getAcceleration() + if self.components.impulse_engine then return self.components.impulse_engine.acceleration_forward end + return 0.0 +end +--- Sets the SpaceShip's forward and reverse impulse acceleration values, in (unit?) +--- The reverse acceleration value is optional. +--- Calling with a single argument sets both forward and reverse acceleration to the same value. +--- Examples: +--- ship:setAcceleration(5,3.5) -- sets the max forward acceleration to 5 and reverse to 3.5 +--- ship:setAcceleration(5) -- sets the max forward and reverse acceleration to 5 +function Entity:setAcceleration(forward, reverse) + if self.components.impulse_engine then + self.components.impulse_engine.acceleration_forward = forward + if reverse == nil then + self.components.impulse_engine.acceleration_reverse = forward + else + self.components.impulse_engine.acceleration_reverse = reverse + end + end + return self +end +--- Sets the SpaceShip's combat maneuvering capacities. +--- The boost value sets the forward maneuver capacity, and the strafe value sets the lateral maneuver capacity. +--- Example: ship:setCombatManeuver(400,250) -- sets boost capacity to 400 and lateral to 250 +function Entity:setCombatManeuver(boost, strafe) + self.components.combat_maneuvering_thrusters = {boost_speed=boost, strafe_speed=strafe} + return self +end +--- Returns whether the SpaceShip has a jump drive. +--- Example: ship:hasJumpDrive() +function Entity:hasJumpDrive() + return self.components.jump_drive ~= nil +end +--- Defines whether the SpaceShip has a jump drive. +--- If true, this ship gains jump drive controls and a "jumpdrive" ship system. +--- Example: ship:setJumpDrive(true) -- gives this ship a jump drive +function Entity:setJumpDrive(enabled) + if enabled then self.components.jump_drive = {} else self.components.jump_drive = nil end + return self +end +--- Sets the minimum and maximum jump distances for this SpaceShip. +--- Defaults to (5000,50000) if not set by the ShipTemplate. +--- Example: ship:setJumpDriveRange(2500,25000) -- sets the minimum jump distance to 2.5U and maximum to 25U +function Entity:setJumpDriveRange(min_range, max_range) + if self.components.jump_drive then + self.components.jump_drive.min_distance = min_range + self.components.jump_drive.max_distance = max_range + end + return self +end +--- Sets this SpaceShip's current jump drive charge. +--- Jumping depletes the ship's jump drive charge by a value equal to the distance jumped. +--- For example, a 5U jump depletes the charge by 5000. +--- A SpaceShip with a jump drive can jump only when this value is equal to or greater than the ship's maximum jump range. +--- Any numeric value is valid, including negative values (longer to recharge) and values larger than the ship's maximum jump range (can jump again with a shorter, or no, recharge required). +--- Jump drive charge regenerates at a rate modified by the "jumpdrive" system's effectiveness. +--- Example: ship:setJumpDriveCharge(50000) +function Entity:setJumpDriveCharge(charge) + if self.components.jump_drive then self.components.jump_drive.charge = charge end + return self +end +--- Returns this SpaceShip's current jump drive charge. +--- Example: jump_charge = ship:getJumpDriveCharge() +function Entity:getJumpDriveCharge() + if self.components.jump_drive then return self.components.jump_drive.charge end + return 0.0 +end +--- Returns the time required by this SpaceShip to complete a jump once initiated. +--- A ship can't perform certain actions, such as docking, while its jump delay is not 0. +--- Returns a value between 0.0 (no delay, ready to jump) to 10.0. +--- With normal "jumpdrive" system effectiveness, this delay is 10 seconds. +--- System effectiveness can modify this delay. +--- Example: ship:getJumpDelay() +function Entity:getJumpDelay() + if self.components.jump_drive then return self.components.jump_drive.delay end + return 0.0 + +end +--- Returns whether this SpaceShip has a warp drive. +--- Example: ship:hasWarpDrive() +function Entity:hasWarpDrive() + return self.components.warp_drive ~= nil +end +--- Defines whether this SpaceShip has a warp drive. +--- If true, this ship gains warp drive controls and a "warp" ship system. +--- Example: ship:setWarpDrive(true) +function Entity:setWarpDrive(enabled) + if enabled then self.components.warp_drive = {} else self.components.warp_drive = nil end + return self +end +--- Sets this SpaceShip's warp speed factor. +--- Valid values are any greater than 0. Ships don't tend to go faster than 24000 (1400U/min) due to engine limitations. +--- Unlike ShipTemplate:setWarpSpeed(), setting this value does NOT also grant this ship a warp drive. +--- Example: ship:setWarpSpeed(1000); +function Entity:setWarpSpeed(speed) + if self.components.warp_drive then self.components.warp_drive.speed_per_level = speed end + return self +end +--- Returns this SpaceShip's warp speed factor. +--- Actual warp speed can be modified by "warp" system effectiveness. +--- Example: ship:getWarpSpeed(() +function Entity:getWarpSpeed() + if self.components.warp_drive then return self.components.warp_drive.speed_per_level end + return 0.0 +end +--- Returns the arc, in degrees, for the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:getBeamWeaponArc(0); -- returns beam weapon 0's arc +function Entity:getBeamWeaponArc(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].arc end + return 0.0 +end +--- Returns the direction, in degrees relative to the ship's forward bearing, for the arc's center of the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:getBeamWeaponDirection(0); -- returns beam weapon 0's direction +function Entity:getBeamWeaponDirection(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].direction end + return 0.0 +end +--- Returns the range for the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:getBeamWeaponRange(0); -- returns beam weapon 0's range +function Entity:getBeamWeaponRange(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].range end + return 0.0 +end +--- Returns the turret arc, in degrees, for the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:getBeamWeaponTurretArc(0); -- returns beam weapon 0's turret arc +function Entity:getBeamWeaponTurretArc(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].turret_arc end + return 0.0 +end +--- Returns the direction, in degrees relative to the ship's forward bearing, for the turret arc's center for the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:getBeamWeaponTurretDirection(0); -- returns beam weapon 0's turret direction +function Entity:getBeamWeaponTurretDirection(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].turret_direction end + return 0.0 +end +function Entity:getBeamWeaponTurretRotationRate(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].turret_rotation_rate end + return 0.0 +end +--- Returns the base firing delay, in seconds, for the BeamWeapon with the given index on this SpaceShip. +--- Actual cycle time can be modified by "beamweapon" system effectiveness. +--- Example: ship:getBeamWeaponCycleTime(0); -- returns beam weapon 0's cycle time +function Entity:getBeamWeaponCycleTime(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].cycle_time end + return 0.0 +end +--- Returns the base damage dealt by the BeamWeapon with the given index on this SpaceShip. +--- Actual damage can be modified by "beamweapon" system effectiveness. +--- Example: ship:getBeamWeaponDamage(0); -- returns beam weapon 0's damage +function Entity:getBeamWeaponDamage(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].damage end + return 0.0 +end +--- Returns how much of this SpaceShip's energy is drained each time the BeamWeapon with the given index is fired. +--- Actual drain can be modified by "beamweapon" system effectiveness. +--- Example: ship:getBeamWeaponEnergyPerFire(0); -- returns beam weapon 0's energy use per firing +function Entity:getBeamWeaponEnergyPerFire(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].energy_per_beam_fire end + return 0.0 +end +--- Returns the heat generated by each firing of the BeamWeapon with the given index on this SpaceShip. +--- Actual heat generation can be modified by "beamweapon" system effectiveness. +--- Example: ship:getBeamWeaponHeatPerFire(0); -- returns beam weapon 0's heat generation per firing +function Entity:getBeamWeaponHeatPerFire(index) + if self.components.beam_weapons and #self.components.beam_weapons > index then return self.components.beam_weapons[index+1].heat_per_beam_fire end + return 0.0 +end +--- Defines the traits of a BeamWeapon with the given index on this SpaceShip. +--- - index: Each beam weapon on this SpaceShip must have a unique index. +--- - arc: Sets the arc of its firing capability, in degrees. +--- - direction: Sets the default center angle of the arc, in degrees relative to the ship's forward bearing. Accepts 0, negative, and positive values. +--- - range: Sets how far away the beam can fire. +--- - cycle_time: Sets the base firing delay, in seconds. System effectiveness modifies the cycle time. +--- - damage: Sets the base damage done by the beam to the target. System effectiveness modifies the damage. +--- To create a turreted beam, also add SpaceShip:setBeamWeaponTurret(), and set the beam weapon's arc to be smaller than the turret's arc. +--- Example: +--- -- Creates a beam weapon with index 0, arc of 90 degrees, direction pointing backward, range of 1U, base cycle time of 1 second, and base damage of 1 point +--- ship:setBeamWeapon(0,90,180,1000,1,1) +function Entity:setBeamWeapon(index, arc, direction, range, cycle_time, damage) + if self.components.beam_weapons == nil then self.components.beam_weapons = {} end + while #self.components.beam_weapons < index + 1 do + self.components.beam_weapons[#self.components.beam_weapons + 1] = {} + end + self.components.beam_weapons[index + 1] = {arc=arc, direction=direction, range=range, cycle_time=cycle_time, damage=damage} + return self +end +--- Converts a BeamWeapon with the given index on this SpaceShip into a turret and defines its traits. +--- A turreted beam weapon rotates within its turret arc toward the weapons target at the given rotation rate. +--- - index: Must match the index of an existing beam weapon on this SpaceShip. +--- - arc: Sets the turret's maximum targeting angles, in degrees. The turret arc must be larger than the associated beam weapon's arc. +--- - direction: Sets the default center angle of the turret arc, in degrees relative to the ship's forward bearing. Accepts 0, negative, and positive values. +--- - rotation_rate: Sets how many degrees per tick (unit?) that the associated beam weapon's direction can rotate toward the target within the turret arc. System effectiveness modifies the turret's rotation rate. +--- To create a turreted beam, also add SpaceShip:setBeamWeapon(), and set the beam weapon's arc to be smaller than the turret's arc. +--- Example: +--- -- Makes beam weapon 0 a turret with a 200-degree turret arc centered on 90 degrees from forward, rotating at 5 degrees per tick (unit?) +--- ship:setBeamWeaponTurret(0,200,90,5) +function Entity:setBeamWeaponTurret(index, arc, direction, rotation_rate) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].turret_arc = arc + self.components.beam_weapons[index + 1].turret_direction = direction + self.components.beam_weapons[index + 1].turret_rotation_rate = rotation_rate + return self +end +--- Sets the BeamEffect texture, by filename, for the BeamWeapon with the given index on this SpaceShip. +--- See BeamEffect:setTexture(). +--- Example: ship:setBeamWeaponTexture(0,"texture/beam_blue.png") +function Entity:setBeamWeaponTexture(index, texture) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].texture = texture + return self +end +--- Sets how much energy is drained each time the BeamWeapon with the given index is fired on this SpaceShip. +--- Only PlayerSpaceships consume energy. Setting this for other ShipTemplateBasedObject types has no effect. +--- Example: ship:setBeamWeaponEnergyPerFire(0,1) -- sets beam 0 to use 1 energy per firing +function Entity:setBeamWeaponEnergyPerFire(index, energy) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].energy_per_beam_fire = energy + return self +end +--- Sets how much "beamweapon" system heat is generated, in percentage of total system heat capacity, each time the BeamWeapon with the given index is fired on this SpaceShip. +--- Only PlayerSpaceships generate and manage heat. Setting this for other ShipTemplateBasedObject types has no effect. +--- Example: ship:setBeamWeaponHeatPerFire(0,0.02) -- sets beam 0 to generate 0.02 (2%) system heat per firing +function Entity:setBeamWeaponHeatPerFire(index, heat) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].heat_per_beam_fire = heat + return self +end +--- Sets the colors used to draw the radar arc for the BeamWeapon with the given index on this SpaceShip. +--- The first three-number value sets the RGB color for the arc when idle, and the second sets the color when firing. +--- Example: ship:setBeamWeaponArcColor(0,0,128,0,0,255,0) -- makes beam 0's arc green +function Entity:setBeamWeaponArcColor(index, idle_r, idle_g, idle_b, fire_r, fire_g, fire_b) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].arc_color = {idle_r, idle_g, idle_b} + self.components.beam_weapons[index + 1].arc_color_fire = {fire_r, fire_g, fire_b} + return self +end +--- Sets the damage type dealt by the BeamWeapon with the given index on this SpaceShip. +--- Example: ship:setBeamWeaponDamageType(0,"emp") -- makes beam 0 deal EMP damage +function Entity:setBeamWeaponDamageType(index, damage_type) + if self.components.beam_weapons == nil or #self.components.beam_weapons <= index then return self end + self.components.beam_weapons[index + 1].damage_type = damage_type + return self +end +--- Sets the number of WeaponTubes for this SpaceShip. +--- Weapon tubes are 0-indexed. For example, 3 tubes would be indexed 0, 1, and 2. +--- Example: ship:setWeaponTubeCount(4) +function Entity:setWeaponTubeCount(amount) + self.components.missile_tubes = {} + for n=1,amount do + self.components.missile_tubes[n] = {} + end + while #self.components.missile_tubes > amount do + self.components.missile_tubes[#self.components.missile_tubes] = nil + end + return self +end +--- Returns the number of WeaponTube on this SpaceShip. +--- Example: ship:getWeaponTubeCount() +function Entity:getWeaponTubeCount() + if self.components.missile_tubes then return #self.components.missile_tubes end + return 0 +end +--- Returns the weapon type loaded into the WeaponTube with the given index on this SpaceShip. +--- Returns no value if no weapon is loaded, which includes the tube being in a loading or unloading state. +--- Example: ship:getWeaponTubeLoadType(0) +function Entity:getWeaponTubeLoadType(index) + local tubes = self.components.missile_tubes + if tubes and index >= 0 and index < #tubes then return tubes[index+1].type_loaded end + return "none" +end +--- Sets which weapon types the WeaponTube with the given index on this SpaceShip can load. +--- Note the spelling of "missle". +--- Example: ship:weaponTubeAllowMissle(0,"Homing") -- allows Homing missiles to be loaded in WeaponTube 0 +function Entity:weaponTubeAllowMissle(index, weapon_type) + local tubes = self.components.missile_tubes + if tubes and index >= 0 and index < #tubes then tubes[index+1]["allow_"..string.lower(weapon_type)] = true end + return self +end +--- Sets which weapon types the WeaponTube with the given index can't load on this SpaceShip. +--- Note the spelling of "missle". +--- Example: ship:weaponTubeDisallowMissle(0,"Homing") -- prevents Homing missiles from being loaded in tube 0 +function Entity:weaponTubeDisallowMissle(index, weapon_type) + local tubes = self.components.missile_tubes + if tubes and index >= 0 and index < #tubes then tubes[index+1]["allow_"..string.lower(weapon_type)] = false end + return self +end +--- Sets a weapon tube with the given index on this SpaceShip to allow loading only the given weapon type. +--- Example: ship:setWeaponTubeExclusiveFor(0,"Homing") -- allows only Homing missiles to be loaded in tube 0 +function Entity:setWeaponTubeExclusiveFor(index, weapon_type) + local tubes = self.components.missile_tubes + if tubes and index >= 0 and index < #tubes then + tubes[index+1]["allow_homing"] = false + tubes[index+1]["allow_nuke"] = false + tubes[index+1]["allow_mine"] = false + tubes[index+1]["allow_emp"] = false + tubes[index+1]["allow_hvli"] = false + tubes[index+1]["allow_"..string.lower(weapon_type)] = true + end + return self +end +--- Sets the angle, relative to this SpaceShip's forward bearing, toward which the WeaponTube with the given index on this SpaceShip points. +--- Accepts 0, negative, and positive values. +--- Example: +--- -- Sets tube 0 to point 90 degrees right of forward, and tube 1 to point 90 degrees left of forward +--- ship:setWeaponTubeDirection(0,90):setWeaponTubeDirection(1,-90) +function Entity:setWeaponTubeDirection(index, direction) + if self.components.missile_tubes then self.components.missile_tubes[index+1].direction = direction end + return self +end +--- Sets the weapon size launched from the WeaponTube with the given index on this SpaceShip. +--- Example: ship:setTubeSize(0,"large") -- sets tube 0 to fire large weapons +function Entity:setTubeSize(index, size) + if self.components.missile_tubes then self.components.missile_tubes[index+1].size = size end + return self +end +--- Returns the size of the weapon tube with the given index on this SpaceShip. +--- Example: ship:getTubeSize(0) +function Entity:getTubeSize(index) + if self.components.missile_tubes then return self.components.missile_tubes[index+1].size end + return "medium" +end +--- Returns the delay, in seconds, for loading and unloading the WeaponTube with the given index on this SpaceShip. +--- Example: ship:getTubeLoadTime(0) +function Entity:getTubeLoadTime(index) + if self.components.missile_tubes then return self.components.missile_tubes[index+1].load_time end + return 0.0 +end +--- Sets the time, in seconds, required to load the weapon tube with the given index on this SpaceShip. +--- Example: ship:setTubeLoadTime(0,12) -- sets the loading time for tube 0 to 12 seconds +function Entity:setTubeLoadTime(index, load_time) + if self.components.missile_tubes then self.components.missile_tubes[index+1].load_time = load_time end + return self +end +--- Returns the dynamic gravitational radar signature value emitted by this SpaceShip. +--- Ship functions can dynamically modify this SpaceShip's radar signature values. +--- Example: ship:getDynamicRadarSignatureGravity() +function Entity:getDynamicRadarSignatureGravity() + if self.components.dynamic_radar_signature then return self.components.dynamic_radar_signature.gravity end + return 0.0 +end +--- Returns the dynamic electrical radar signature value emitted by this SpaceShip. +--- Ship functions can dynamically modify this SpaceShip's radar signature values. +--- Example: ship:getDynamicRadarSignatureElectrical() +function Entity:getDynamicRadarSignatureElectrical() + if self.components.dynamic_radar_signature then return self.components.dynamic_radar_signature.electrical end + return 0.0 +end +--- Returns the dynamic biological radar signature value emitted by this SpaceShip. +--- Ship functions can dynamically modify this SpaceShip's radar signature values. +--- Example: ship:getDynamicRadarSignatureBiological() +function Entity:getDynamicRadarSignatureBiological() + if self.components.dynamic_radar_signature then return self.components.dynamic_radar_signature.biological end + return 0.0 +end +--- Broadcasts a message from this SpaceShip to the comms of all other SpaceShips matching the threshold. +--- The threshold value can be an integer equivalent of EFactionVsFactionState: +--- 0: Broadcast to all friendly SpaceShips +--- 1: Broadcast to all friendly and neutral SpaceShips +--- 2: Broadcast to all SpaceShips, including enemies +--- Providing an invalid threshold value defaults to broadcasting only to friendly SpaceShips. +--- Examples: +--- ship:addBroadcast(1, "Help!") +--- ship:addBroadcast(2, "We're taking over!") +function Entity:addBroadcast(target, message) + --TODO + return self +end +--- Sets the scan state of this SpaceShip for every faction. +--- Example: ship:setScanState("fullscan") -- every faction treats this ship as fully scanned +function Entity:setScanState(state) + local ss = self.components.scan_state + if ss ~= nil then + for name, faction in pairs(__faction_info) do + self:setScanStateByFaction(name, state) + end + end + return self +end +--- Sets the scan state of this SpaceShip for a given faction. +--- Example: ship:setScanStateByFaction("Kraylor","fullscan") -- Kraylor faction treats this ship as fully scanned +function Entity:setScanStateByFaction(faction, state) + local ss = self.components.scan_state + if ss ~= nil then + local f = getFactionInfo(faction) + if f ~= nil then + for n=1,#ss do + if ss[n].faction == f then + ss[n].state = state + return self + end + end + ss[#ss+1] = {faction=f, state=state} + end + end + return self +end diff --git a/scripts/api/entity/spacestation.lua b/scripts/api/entity/spacestation.lua new file mode 100644 index 0000000000..276172770e --- /dev/null +++ b/scripts/api/entity/spacestation.lua @@ -0,0 +1,17 @@ + +--- A SpaceStation is an immobile ship-like object that repairs, resupplies, and recharges ships that dock with it. +--- It sets several ShipTemplateBasedObject properties upon creation: +--- - Its default callsign begins with "DS". +--- - It restocks scan probes and CpuShip weapons by default. +--- - It uses the scripts/comms_station.lua comms script by default. +--- - When destroyed by damage, it awards or deducts a number of reputation points relative to its total shield strength and segments. +--- - Any non-hostile SpaceShip can dock with it by default. +--- @type creation +function SpaceStation() + local e = createEntity() + e.components = { + transform = {rotation=random(0, 360)}, + callsign = {callsign=generateRandomCallSign("DS")}, + } + return e +end diff --git a/scripts/api/entity/supplydrop.lua b/scripts/api/entity/supplydrop.lua new file mode 100644 index 0000000000..060096ad55 --- /dev/null +++ b/scripts/api/entity/supplydrop.lua @@ -0,0 +1,28 @@ +--- A SupplyDrop is a collectible item picked up on collision with a friendly SpaceShip. +--- On pickup, the SupplyDrop restocks one type of the colliding SpaceShip's weapons. +--- If the ship is a PlayerSpaceship, it can also recharge its energy. +--- A SupplyDrop can also trigger a scripting function upon pickup. +--- For a more generic object with similar collision properties, see Artifact. +--- Example: SupplyDrop():setEnergy(500):setWeaponStorage("Homing",6) +--- @type creation +function SupplyDrop() + local e = createEntity() + e.components.transform = {} + for k, v in pairs(__model_data["ammo_box"]) do + if string.sub(1, 2) ~= "__" then + e.components[k] = table.deepcopy(v) + end + end + e.components.physics = {type="Sensor"} + + e.components.radar_trace = { + color={100, 200, 255}, + icon="radar/blip.png", + radius=120.0, + rotate=false, + color_by_faction=true, + } + e.components.pickup = {} + + return e +end diff --git a/scripts/api/entity/warpjammer.lua b/scripts/api/entity/warpjammer.lua new file mode 100644 index 0000000000..5fb89c8799 --- /dev/null +++ b/scripts/api/entity/warpjammer.lua @@ -0,0 +1,43 @@ + +--- A WarpJammer restricts the ability of any SpaceShips to use warp or jump drives within its radius. +--- WarpJammers can be targeted, damaged, and destroyed. +--- Example: jammer = WarpJammer():setPosition(1000,1000):setRange(10000):setHull(20) +--- @type creation +function WarpJammer() + local e = createEntity() + e.components = { + transform = {rotation=random(0, 360)}, + hull = {max=50, current=50}, + warp_jammer = {range=7000}, + radar_trace = { + icon="radar/blip.png", + radius=120.0, + rotate=false, + color_by_faction=true, + }, + physics = {type="static",size=300}, + } + for k, v in pairs(__model_data["shield_generator"]) do + if string.sub(1, 2) ~= "__" then + e.components[k] = table.deepcopy(v) + end + end + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Returns this WarpJammer's jamming range, represented on radar as a circle with jammer in the middle. +--- No warp/jump travel is possible within this radius. +--- Example: jammer:getRange() +function Entity:getRange() + if self.warp_jammer then return self.warp_jammer.range end + return 0 +end +--- Sets this WarpJammer's jamming radius. +--- No warp/jump travel is possible within this radius. +--- Defaults to 7000.0. +--- Example: jammer:setRange(10000) -- sets a 10U jamming radius +function Entity:setRange(range) + if self.warp_jammer then self.warp_jammer.range = range end + return self +end diff --git a/scripts/api/entity/zone.lua b/scripts/api/entity/zone.lua new file mode 100644 index 0000000000..b995173672 --- /dev/null +++ b/scripts/api/entity/zone.lua @@ -0,0 +1,57 @@ +--- A Zone is a polygonal area of space defined by a series of coordinates. +--- Although a Zone is a SpaceObject, it isn't affected by physics and isn't rendered in 3D. +--- Zones are drawn on GM, comms, and long-range radar screens, can have a text label, and can return whether a SpaceObject is within their bounds. +--- New Zones can't be created via the exec.lua HTTP API. +--- Example: +--- -- Defines a blue rectangular 200sqU zone labeled "Home" around 0,0 +--- zone = Zone():setColor(0,0,255):setPoints(-100000,100000, -100000,-100000, 100000,-100000, 100000,100000):setLabel("Home") +--- @type creation +function Zone() + local e = createEntity() + e.components = { + transform = {}, + zone = {}, + } + return e +end + +local Entity = getLuaEntityFunctionTable() +--- Sets the corners of this Zone n-gon to x_1, y_1, x_2, y_2, ... x_n, y_n. +--- Positive x coordinates are right/"east" of the origin, and positive y coordinates are down/"south" of the origin in space. +--- Example: zone:setPoints(2000,0, 0,3000, -2000,0) -- defines a triangular zone +function Entity:setPoints(...) + if self.components.zone then + local coords = {...} + local points = {} + for n=1,#coords,2 do + table.insert(points, {coords[n], coords[n+1]}) + end + self.components.zone.points = points + end + return self +end +--- Sets this Zone's color when drawn on radar. +--- Defaults to white (255,255,255). +--- Example: zone:setColor(255,140,0) +function Entity:setColor(r, g, b) + if self.components.zone then self.components.zone.color = {r, g, b, 255} end + return self +end +--- Sets this Zone's text label, rendered at the zone's center point. +--- Example: zone:setLabel("Hostile space") +function Entity:setLabel(label) + if self.components.zone then self.components.zone.label = label end + return self +end +--- Returns this Zone's text label. +--- Example: zone:getLabel() +function Entity:getLabel() + if self.components.zone then return self.components.zone.label end + return "" +end +--- Returns whether the given SpaceObject is inside this Zone. +--- Example: zone:isInside(obj) -- returns true if `obj` is within the zone's bounds +function Entity:isInside(obj) + local x, y = obj:getPosition() + return isInsideZone(x, y, self) +end diff --git a/scripts/api/gm.lua b/scripts/api/gm.lua new file mode 100644 index 0000000000..1a72d5e015 --- /dev/null +++ b/scripts/api/gm.lua @@ -0,0 +1,35 @@ +function getSpawnableGMObjects() + local result = {} + result[#result+1] = {function() return Artifact() end, _("create", "Artifact"), _("create", "Various")} + result[#result+1] = {function() return WarpJammer() end, _("create", "Warp Jammer"), _("create", "Various")} + result[#result+1] = {function() return Mine() end, _("create", "Mine"), _("create", "Various")} + result[#result+1] = {function() return SupplyDrop():setEnergy(500):setWeaponStorage('Nuke', 1):setWeaponStorage('Homing', 4):setWeaponStorage('Mine', 2):setWeaponStorage('EMP', 1) end, _("create", "Supply Drop"), _("create", "Various")} + result[#result+1] = {function() return Asteroid() end, _("create", "Mine"), _("create", "Various")} + result[#result+1] = {function() return Asteroid() end, _("create", "Asteroid"), _("create", "Various")} + result[#result+1] = {function() return VisualAsteroid() end, _("create", "Visual Asteroid"), _("create", "Various")} + result[#result+1] = {function() return Planet() end, _("create", "Planet"), _("create", "Various")} + result[#result+1] = {function() return BlackHole() end, _("create", "BlackHole"), _("create", "Various")} + result[#result+1] = {function() return Nebula() end, _("create", "Nebula"), _("create", "Various")} + result[#result+1] = {function() return WormHole() end, _("create", "Worm Hole"), _("create", "Various")} + + for k, v in pairs(__ship_templates) do + if not v.__hidden then + if v.__type == "playership" then + result[#result+1] = {__spawnPlayerShipFunc(v.typename.type_name), v.typename.localized, _("create", "player ship")} + elseif v.__type == "station" then + result[#result+1] = {__spawnStationFunc(v.typename.type_name), v.typename.localized, _("create", "station")} + else + result[#result+1] = {__spawnCpuShipFunc(v.typename.type_name), v.typename.localized, _("create", "cpu ship")} + end + end + end + + return result +end + +function __spawnStationFunc(key) + return function() return SpaceStation():setTemplate(key):setRotation(random(0, 360)) end +end +function __spawnCpuShipFunc(key) + return function() return CpuShip():setTemplate(key):setRotation(random(0, 360)):orderRoaming() end +end diff --git a/scripts/api/modelData.lua b/scripts/api/modelData.lua new file mode 100644 index 0000000000..cf4e19b37e --- /dev/null +++ b/scripts/api/modelData.lua @@ -0,0 +1,163 @@ +__model_data = {} + +--- A ModelData object contains 3D appearance and SeriousProton physics collision details. +--- Almost all SpaceObjects have a ModelData associated with them to define how they appear in 3D views. +--- A ScienceDatabase entry can also have ModelData associated with and displayed in it. +--- +--- This defines a 3D mesh file, an albedo map ("texture"), a specular/normal map, and an illumination map. +--- These files might be located in the resources/ directory or loaded from resource packs. +--- +--- ModelData also defines the model's position offset and scale relative to its mesh coordinates. +--- If the model is for a SpaceShip with weapons or thrusters, this also defines the origin positions of its weapon effects, and particle emitters for thruster and engine effects. +--- For physics, this defines the model's radius for a circle collider, or optional box collider dimensions. +--- (While ModelData defines 3D models, EmptyEpsilon uses a 2D physics engine for collisions.) +--- +--- EmptyEpsilon loads ModelData from scripts/model_data.lua when launched, and loads meshes and textures when an object using this ModelData is first viewed. +--- +--- For complete examples, see scripts/model_data.lua. +ModelData = createClass() + +--- Sets this ModelData's name. +--- Use this name when referencing a ModelData from other objects. +--- Example: model:setName("space_station_1") +function ModelData:setName(name) + __model_data[name] = self + return self +end +--- Sets this ModelData's mesh file. +--- Required; if omitted, this ModelData generates an error. +--- Valid values include OBJ-format (.obj extension) 3D models relative to the resources/ directory. +--- You can also reference models from resource packs, which have ".model" extensions. +--- To view resource pack paths, extract strings from the pack, such as by running "strings packs/Angryfly.pack | grep -i model" on *nix. +--- For example, this lists "battleship_destroyer_2_upgraded/battleship_destroyer_2_upgraded.model", which is a valid mesh path. +--- Examples: +--- setMesh("space_station_1/space_station_1.model") -- loads this model from a resource pack +--- setMesh("mesh/sphere.obj") -- loads this model from the resources/ directory +function ModelData:setMesh(name) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.mesh=name + return self +end +--- Sets this ModelData's albedo map, or base flat-light color texture. +--- Required; if omitted, this ModelData generates an error. +--- Valid values include PNG- or JPG-format images relative to the resources/ directory. +--- You can also reference textures from resource packs. +--- To view resource pack paths, extract strings from the pack, such as by running "strings packs/Angryfly.pack | egrep -i (png|jpg)" on *nix. +--- Examples: +--- model:setTexture("space_station_1/space_station_1_color.jpg") -- loads this texture from a resource pack +--- model:setTexture("mesh/ship/Ender Battlecruiser.png") -- loads this texture from the resources/ directory +function ModelData:setTexture(texture) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.texture=texture + return self +end +--- Sets this ModelData's specular map, or shininess texture. Some models use this to load a normal map. +--- Optional; if omitted, no specular map is applied. +--- Valid values include PNG- or JPG-format images relative to the resources/ directory. +--- You can also reference textures from resource packs. +--- To view resource pack paths, extract strings from the pack, such as by running "strings packs/Angryfly.pack | egrep -i (png|jpg)" on *nix. +--- Examples: +--- model:setSpecular("space_station_1/space_station_1_specular.jpg") -- loads this texture from a resource pack +--- model:setSpecular("mesh/various/debris-blog-specular.jpg") -- loads this texture from the resources/ directory +function ModelData:setSpecular(texture) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.specular_texture=texture + return self +end +--- Sets this ModelData's illumination map, or glow texture, which defines which parts of the texture appear to be luminescent. +--- Optional; if omitted, no illumination map is applied. +--- Valid values include PNG- or JPG-format images relative to the resources/ directory. +--- You can also reference textures from resource packs. +--- To view resource pack paths, extract strings from the pack, such as by running "strings packs/Angryfly.pack | egrep -i (png|jpg)" on *nix. +--- Examples: +--- model:setIllumination("space_station_1/space_station_1_illumination.jpg") -- loads this texture from a resource pack +--- model:setIllumination("mesh/ship/Ender Battlecruiser_illumination.png") -- loads this texture from the resources/ directory +function ModelData:setIllumination(texture) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.illumination_texture=texture + return self +end +--- Sets this ModelData's mesh offset, relative to its position in its mesh data. +--- If a 3D mesh's central origin point is not at 0,0,0, use this to compensate. +--- If you view the model in Blender, these values are equivalent to -X,+Y,+Z. +--- Example: model:setRenderOffset(1,2,5) -- offsets its in-game position from its mesh file position when rendered +function ModelData:setRenderOffset(x, y, z) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.mesh_offset = {x, y, z} + return self +end +--- Scales this ModelData's mesh by the given factor. +--- Values greater than 1.0 scale the model up, and values between 0 and 1.0 scale it down. +--- Use this if models you load are smaller or larger than expected. +--- Defaults to 1.0. +--- Example: model:setScale(20) -- scales the model up by 20x +function ModelData:setScale(scale) + if self.mesh_render == nil then self.mesh_render = {} end + self.mesh_render.scale = scale + return self +end +--- Sets this ModelData's base radius. +--- By default, EmptyEpsilon uses this to create a circular collider around objects that use this ModelData. +--- SpaceObject:setRadius() can override this for colliders. +--- Setting a box collider with ModelData:setCollisionBox() also overrides this. +--- Defaults to 1.0. +--- Example: model:setRadius(100) -- sets the object's collisionable radius to 0.1U +function ModelData:setRadius(radius) + if self.physics == nil then self.physics={type="dynamic"} end + self.physics.size = radius + return self +end +--- Sets a 2D box collider for this ModelData. +--- If both values are greater than 0.0, this overrides ModelData:setRadius() for collisions. +--- Defaults to 0,0. +--- Example: model:setCollisionBox(400, 400) -- sets the object's collision box to 0.4U by 0.4U +function ModelData:setCollisionBox(w, h) + if self.physics == nil then self.physics={type="dynamic"} end + self.physics.size = {w, h} + return self +end +--- Adds a BeamEffect origin position to this ModelData. +--- If no origin positions are defined, this defaults to the model's origin (0,0,0). +--- If you view the model in Blender, these coordinate values are equivalent to -X,+Y,+Z. +--- Example: +--- -- Add a beam position at the given model X/Y/Z coordinates. +--- model:addBeamPosition(21,-28.2,-2) +function ModelData:addBeamPosition(x, y, z) + if self.__beam_positions == nil then self.__beam_positions = {} end + self.__beam_positions[#self.__beam_positions + 1] = {x, y, z} + return self +end +--- Adds a WeaponTube origin position to this ModelData. +--- If no origin positions are defined, this defaults to the model's origin (0,0,0). +--- If you view the model in Blender, these coordinate values are equivalent to -X,+Y,+Z. +--- -- Add a tube position at the given model X/Y/Z coordinates. +--- model:addTubePosition(21,-28.2,-2) +function ModelData:addTubePosition() + if self.__tube_positions == nil then self.__tube_positions = {} end + self.__tube_positions[#self.__tube_positions + 1] = {x, y, z} + return self +end +--- [DEPRECATED] +--- Use ModelData:addEngineEmitter(). +function ModelData:addEngineEmitor(x, y, z, r, g, b, scale) + print("Called DEPRECATED addEngineEmitor function") + return self:addEngineEmitter(x, y, z, r, g, b, scale) +end +--- Adds an impulse engine particle effect emitter to this ModelData. +--- When a SpaceShip engages impulse engines, this defines the position, color, and size of a particle trail effect. +--- If no origin positions are defined, this defaults to the model's origin (0,0,0). +--- If you view the model in Blender, these coordinate values are equivalent to -X,+Y,+Z. +--- Example: +--- -- Add an engine emitter at the given model X/Y/Z coordinates, with a RGB color of 1.0/0.2/0.2 and scale of 3. +--- model:addEngineEmitter(-28, 1.5,-5,1.0,0.2,0.2,3.0) +function ModelData:addEngineEmitter(x, y, z, r, g, b, scale) + if self.engine_emitter == nil then self.engine_emitter = {} end + if self.mesh_render then + x = x * self.mesh_render.scale + y = y * self.mesh_render.scale + z = z * self.mesh_render.scale + scale = scale * self.mesh_render.scale + end + self.engine_emitter[#self.engine_emitter+1] = {position = {x, y, z}, color={r, g, b}, scale=scale} + return self +end diff --git a/scripts/api/shipTemplate.lua b/scripts/api/shipTemplate.lua new file mode 100644 index 0000000000..35a9f1a87b --- /dev/null +++ b/scripts/api/shipTemplate.lua @@ -0,0 +1,612 @@ +-- Global to store all the templates. +__ship_templates = {} +__player_ship_templates = {} +__allow_new_player_ships = true + +-- Called by the engine to populate the list of player ships that can be spawned. +-- Returns a list of {key, label, description}. +function getSpawnablePlayerShips() + local result = {} + if __allow_new_player_ships then + for i, v in ipairs(__player_ship_templates) do + if not v.__hidden then + result[#result+1] = {__spawnPlayerShipFunc(v.typename.type_name), v.typename.localized, v.__description} + end + end + end + return result +end +function __spawnPlayerShipFunc(key) + return function() return PlayerSpaceship():setTemplate(key):setRotation(random(0, 360)) end +end + +function allowNewPlayerShips(enabled) + if enabled ~= nil then + __allow_new_player_ships = enabled + end + return __allow_new_player_ships +end + +--- A ShipTemplate defines the base functionality, stats, models, and other details for the ShipTemplateBasedObjects created from it. +--- ShipTemplateBasedObjects belong to either the SpaceStation or SpaceShip subclasses. SpaceShips in turn belong to the CpuShip or PlayerSpaceship classes. +--- ShipTemplates appear in ship and space station creation lists, such as the ship selection screen on scenarios that allow player ship creation, or the GM console's object creation tool. +--- They also appear as default entries in the science database. +--- EmptyEpsilon loads scripts/shipTemplates.lua at launch, which requires files containing ShipTemplates located in scripts/shiptemplates/. +--- New ShipTemplates can't be defined while a scenario is running. +--- Use Lua variables to apply several ShipTemplate functions to the same template. +--- Example: +--- -- Create a ShipTemplate for a Cruiser Frigate-class CpuShip designated Phobos T3 +--- template = ShipTemplate():setName("Phobos T3"):setLocaleName(_("ship","Phobos T3")):setClass(_("class","Frigate"),_("subclass","Cruiser")) +--- -- Set the Phobos T3's appearance to the ModelData named "AtlasHeavyFighterYellow" +--- template:setModel("AtlasHeavyFighterYellow") +ShipTemplate = createClass() +function ShipTemplate:__init__() + self.radar_trace = { + icon="radar/ship.png", + radius=300.0*0.8, + max_size=1024, + color_by_faction=true, + } + self.__repair_crew_count = 3 + self.share_short_range_radar = {} + self.comms_receiver = {script="comms_ship.lua"} +end + +--- Sets this ShipTemplate's unique reference name. +--- Use this value for referencing this ShipTemplate in scripts. +--- If this value begins with "Player ", including the trailing space, EmptyEpsilon uses only what follows as the name. +--- If this ShipTemplate lacks a localized name (ShipTemplate:setLocaleName()), it defaults to this reference name. +--- Example: template:setName("Phobos T3") +function ShipTemplate:setName(name) + __ship_templates[name] = self + self.typename = {type_name=name, localized=name} + return self +end +--- Sets the displayed vessel model designation for ShipTemplateBasedObjects created from this ShipTemplate. +--- Use with the _ function to expose the localized name to translation. +--- Examples: +--- template:setLocaleName("Phobos T3") +--- template:setLocaleName(_("ship","Phobos T3")) -- with a translation-exposed name +function ShipTemplate:setLocaleName(name) + self.typename.localized = name + return self +end +--- Sets the vessel class and subclass for ShipTemplateBasedObjects created from this ShipTemplate. +--- Vessel classes are used to define certain traits across similar ships, such as dockability. +--- See also ShipTemplate:setExternalDockClasses() and ShipTemplate:setInternalDockClasses(). +--- For consistent class usage across translations, wrap class name strings in the _ function. +--- Defaults to the equivalent value of (_("No class"),_("No sub-class")). +--- Examples: +--- template:setClass(_("class","Frigate"),_("subclass","Cruiser")) +function ShipTemplate:setClass(class, subclass) + self.docking_port = { + dock_class = class, + dock_subclass = subclass, + } + if self.__type == "ship" or self.__type == nil then + self.docking_port.auto_reload_missiles = true + end + return self +end +--- Sets the description shown in the science database for ShipTemplateBasedObjects created from this ShipTemplate. +--- Example: template:setDescription(_("The Phobos T3 is most any navy's workhorse frigate.")) +function ShipTemplate:setDescription(description) + self.__description = description + return self +end +--- Sets the object-oriented subclass of ShipTemplateBasedObject to create from this ShipTemplate. +--- Defaults to "ship" (CpuShip). +--- Valid values are "ship", "playership" (PlayerSpaceship), and "station" (SpaceStation). +--- Using setType("station") is equivalent to also using ShipTemplate:setRepairDocked(true) and ShipTemplate:setRadarTrace("blip.png"). +--- Example: template:setType("playership") +function ShipTemplate:setType(template_type) + self.__type = template_type + if template_type == "playership" then + __player_ship_templates[#__player_ship_templates + 1] = self + --Add some default player ship components. + self.reactor = {} + self.coolant = {} + self.self_destruct = {} + self.science_scanner = {} + self.scan_probe_launcher = {} + self.hacking_device = {} + self.long_range_radar = {} + self.comms_transmitter = {} + self.comms_receiver = nil + self.ai_controller = nil + if self.docking_port then + self.docking_port.auto_reload_missiles = false + end + end + if template_type == "station" then + if self.docking_bay == nil then self.docking_bay = {} end + self.docking_bay.repair = true + self.docking_bay.share_energy = true + if self.radar_trace.icon == "radar/ship.png" then + self.radar_trace.icon = "radar/blip.png" + end + self.comms_receiver = {script="comms_station.lua"} + end + return self +end +--- If declared, this function hides this ShipTemplate from creation features and the science database. +--- Hidden templates provide backward compatibility to older scenario scripts. +--- Example: template:hidden() -- hides this template +function ShipTemplate:hidden(hidden) + self.__hidden = hidden + return self +end +--- Sets the default combat AI state for CpuShips created from this ShipTemplate. +--- Combat AI states determine the AI's combat tactics and responses. +--- They're distinct from orders, which determine the ship's active objectives and are defined by CpuShip:order...() functions. +--- Valid combat AI states are: +--- - "default" directly pursues enemies at beam range while making opportunistic missile attacks +--- - "evasion" maintains distance from enemy weapons and evades attacks +--- - "fighter" prefers strafing maneuvers and attacks briefly at close range while passing +--- - "missilevolley" prefers lining up missile attacks from long range +--- Example: template:setAI("fighter") -- default to the "fighter" combat AI state +function ShipTemplate:setDefaultAI(default_ai) + self.ai_controller = {new_name=default_ai} + return self +end +--- Sets the 3D appearance, by ModelData name, of ShipTemplateBasedObjects created from this ShipTemplate. +--- ModelData objects define a 3D mesh, textures, adjustments, and collision box, and are loaded from scripts/model_data.lua when EmptyEpsilon is launched. +--- Example: template:setModel("AtlasHeavyFighterYellow") -- uses the ModelData named "AtlasHeavyFighterYellow" +function ShipTemplate:setModel(model_data_name) + self.__model_data_name = model_data_name + for k, v in pairs(__model_data[model_data_name]) do + if string.sub(1, 2) ~= "__" then + self[k] = table.deepcopy(v) + end + end + if self.physics and self.radar_trace then + if type(self.physics.size) == "table" then + self.radar_trace.radius = self.physics.size[1] * 0.8 + else + self.radar_trace.radius = self.physics.size * 0.8 + end + end + return self +end +--- As ShipTemplate:setExternalDockClasses(). +function ShipTemplate:setDockClasses(...) + return self:setExternalDockClasses(...) +end +--- Defines a list of vessel classes that can be externally docked to ShipTemplateBasedObjects created from this ShipTemplate. +--- External docking keeps the docked ship attached to the outside of the carrier. +--- By default, SpaceStations allow all classes of SpaceShips to dock externally. +--- For consistent class usage across translations, wrap class name strings in the _ function. +--- Example: template:setExternalDockClasses(_("class","Frigate"),_("class","Corvette")) -- all Frigate and Corvette ships can dock to the outside of this ShipTemplateBasedObject +function ShipTemplate:setExternalDockClasses(...) + if self.docking_bay == nil then self.docking_bay = {} end + self.docking_bay.external_dock_classes = {...} + return self +end +--- Defines a list of ship classes that can be docked inside of ShipTemplateBasedObjects created from this ShipTemplate. +--- Internal docking stores the docked ship inside of this derived ShipTemplateBasedObject. +--- For consistent class usage across translations, wrap class name strings in the _ function. +--- Example: template:setInternalDockClasses(_("class","Starfighter")) -- all Starfighter ships can dock inside of this ShipTemplateBasedObject +function ShipTemplate:setInternalDockClasses(...) + if self.docking_bay == nil then self.docking_bay = {} end + self.docking_bay.internal_dock_classes = {...} + return self +end +--- Sets the amount of energy available for PlayerSpaceships created from this ShipTemplate. +--- Only PlayerSpaceships consume energy. Setting this for other ShipTemplateBasedObject types has no effect. +--- Defaults to 1000. +--- Example: template:setEnergyStorage(500) +function ShipTemplate:setEnergyStorage(amount) + self.reactor = {max_energy=amount, energy=amount} + return self +end +--- Sets the default number of repair crew for PlayerSpaceships created from this ShipTemplate. +--- Defaults to 3. +--- Only PlayerSpaceships use repair crews. Setting this for other ShipTemplateBasedObject types has no effect. +--- Example: template:setRepairCrewCount(5) +function ShipTemplate:setRepairCrewCount(amount) + self.__repair_crew_count = amount + return self +end +--- As ShipTemplate:setBeamWeapon(). +function ShipTemplate:setBeam(index, arc, direction, range, cycle_time, damage) + return self:setBeamWeapon(index, arc, direction, range, cycle_time, damage) +end +--- Defines the traits of a BeamWeapon for ShipTemplateBasedObjects created from this ShipTemplate. +--- - index: Each beam weapon in this ShipTemplate must have a unique index. +--- - arc: Sets the arc of its firing capability, in degrees. +--- - direction: Sets the default center angle of the arc, in degrees relative to the ship's forward bearing. Value can be negative. +--- - range: Sets how far away the beam can fire. +--- - cycle_time: Sets the base firing delay, in seconds. System effectiveness modifies the cycle time. +--- - damage: Sets the base damage done by the beam to the target. System effectiveness modifies the damage. +--- To add multiple beam weapons to a ship, invoke this function multiple times, assigning each weapon a unique index value. +--- To create a turreted beam, also add ShipTemplate:setBeamWeaponTurret(), and set the beam weapon's arc to be smaller than the turret's arc. +--- Example: setBeamWeapon(0,90,-15,1200,3,1) -- index 0, 90-degree arc centered -15 degrees from forward, extending 1.2U, firing every 3 seconds and dealing 1 damage +function ShipTemplate:setBeamWeapon(index, arc, direction, range, cycle_time, damage) + if self.beam_weapons == nil then self.beam_weapons = {} end + while #self.beam_weapons < index + 1 do + self.beam_weapons[#self.beam_weapons + 1] = {} + end + self.beam_weapons[index + 1] = {arc=arc, direction=direction, range=range, cycle_time=cycle_time, damage=damage} + if range <= 0 and #self.beam_weapons == index + 1 then + self.beam_weapons[index + 1] = nil + end + return self +end +--- Converts a BeamWeapon into a turret and defines its traits for SpaceShips created from this ShipTemplate. +--- A turreted beam weapon rotates within its turret arc toward the weapons target at the given rotation rate. +--- - index: Must match the index of an existing beam weapon. +--- - arc: Sets the turret's maximum targeting angles, in degrees. The turret arc must be larger than the associated beam weapon's arc. +--- - direction: Sets the default center angle of the turret arc, in degrees relative to the ship's forward bearing. Value can be negative. +--- - rotation_rate: Sets how many degrees per tick that the associated beam weapon's direction can rotate toward the target within the turret arc. System effectiveness modifies the rotation rate. +--- To create a turreted beam, also add ShipTemplate:setBeamWeapon(), and set the beam weapon's arc to be smaller than the turret's arc. +--- Example: +--- -- Makes beam weapon 0 a turret with a 200-degree turret arc centered on 90 degrees from forward, rotating at 5 degrees per tick (unit?) +--- template:setBeamWeaponTurret(0,200,90,5) +function ShipTemplate:setBeamWeaponTurret(index, arc, direction, rotation_rate) + self.beam_weapons[index + 1].turret_arc = arc + self.beam_weapons[index + 1].turret_direction = direction + self.beam_weapons[index + 1].turret_rotation_rate = rotation_rate + return self +end +--- Sets the BeamEffect texture, by filename, for the BeamWeapon with the given index on SpaceShips created from this ShipTemplate. +--- See BeamEffect:setTexture(). +--- Example: template:setBeamWeaponTexture("texture/beam_blue.png") +function ShipTemplate:setBeamTexture(index, texture) + self.beam_weapons[index + 1].texture = texture + return self +end +--- Sets how much energy is drained each time the BeamWeapon with the given index is fired. +--- Only PlayerSpaceships consume energy. Setting this for other ShipTemplateBasedObject types has no effect. +--- Defaults to 3.0, as defined in src/spaceObjects/spaceshipParts/beamWeapon.cpp. +--- Example: template:setBeamWeaponEnergyPerFire(0,1) -- sets beam 0 to use 1 energy per firing +function ShipTemplate:setBeamWeaponEnergyPerFire(index, amount) + self.beam_weapons[index + 1].energy_per_beam_fire = amount + return self +end +--- Sets how much "beamweapon" system heat is generated, in percentage of total system heat capacity, each time the BeamWeapon with the given index is fired. +--- Only PlayerSpaceships generate and manage heat. Setting this for other ShipTemplateBasedObject types has no effect. +--- Defaults to 0.02, as defined in src/spaceObjects/spaceshipParts/beamWeapon.cpp. +--- Example: template:setBeamWeaponHeatPerFire(0,0.5) -- sets beam 0 to generate 0.5 (50%) system heat per firing +function ShipTemplate:setBeamWeaponHeatPerFire(index, amount) + self.beam_weapons[index + 1].heat_per_beam_fire = amount + return self +end +--- Sets the number of WeaponTubes for ShipTemplateBasedObjects created from this ShipTemplate, and the default delay for loading and unloading each tube, in seconds. +--- Weapon tubes are 0-indexed. For example, 3 tubes would be indexed 0, 1, and 2. +--- Ships are limited to a maximum of 16 weapon tubes. +--- The default ShipTemplate adds 0 tubes and an 8-second loading time. +--- Example: template:setTubes(6,15.0) -- creates 6 weapon tubes with 15-second loading times +function ShipTemplate:setTubes(amount, loading_time) + if self.missile_tubes == nil then self.missile_tubes = {} end + for n=1,amount do + if #self.missile_tubes < n then + self.missile_tubes[n] = {load_time=loading_time} + else + self.missile_tubes[n].load_time = loading_time + end + end + self.missile_tubes[amount+1] = nil + return self +end +--- Sets the delay, in seconds, for loading and unloading the WeaponTube with the given index. +--- Defaults to 8.0. +--- Example: template:setTubeLoadTime(0,12) -- sets the loading time for tube 0 to 12 seconds +function ShipTemplate:setTubeLoadTime(index, time) + self.missile_tubes[index+1].load_time = time + return self +end +--- Sets which weapon types the WeaponTube with the given index can load. +--- Note the spelling of "missle". +--- Example: template:weaponTubeAllowMissle(0,"Homing") -- allows Homing missiles to be loaded in tube 0 +function ShipTemplate:weaponTubeAllowMissle(index, type) + local type = string.lower(type) + self.missile_tubes[index+1]["allow_"..type] = true + return self +end +--- Sets which weapon types the WeaponTube with the given index can't load. +--- Note the spelling of "missle". +--- Example: template:weaponTubeDisallowMissle(0,"Homing") -- prevents Homing missiles from being loaded in tube 0 +function ShipTemplate:weaponTubeDisallowMissle(index, type) + local type = string.lower(type) + self.missile_tubes[index+1]["allow_"..type] = false + return self +end +--- Sets a WeaponTube with the given index to allow loading only the given weapon type. +--- Example: template:setWeaponTubeExclusiveFor(0,"Homing") -- allows only Homing missiles to be loaded in tube 0 +function ShipTemplate:setWeaponTubeExclusiveFor(index, type) + local type = string.lower(type) + self.missile_tubes[index+1]["allow_homing"] = false + self.missile_tubes[index+1]["allow_nuke"] = false + self.missile_tubes[index+1]["allow_mine"] = false + self.missile_tubes[index+1]["allow_emp"] = false + self.missile_tubes[index+1]["allow_hvli"] = false + self.missile_tubes[index+1]["allow_"..type] = true + return self +end +--- Sets the angle, relative to the ShipTemplateBasedObject's forward bearing, toward which the WeaponTube with the given index points. +--- Defaults to 0. Accepts negative and positive values. +--- Example: +--- -- Sets tube 0 to point 90 degrees right of forward, and tube 1 to point 90 degrees left of forward +--- template:setTubeDirection(0,90):setTubeDirection(1,-90) +function ShipTemplate:setTubeDirection(index, direction) + self.missile_tubes[index+1].direction = direction + return self +end +--- Sets the weapon size launched from the WeaponTube with the given index. +--- Defaults to "medium". +--- Example: template:setTubeSize(0,"large") -- sets tube 0 to fire large weapons +function ShipTemplate:setTubeSize(index, size) + self.missile_tubes[index+1].size = size + return self +end +--- Sets the number of default hull points for ShipTemplateBasedObjects created from this ShipTemplate. +--- Defaults to 70. +--- Example: template:setHull(100) +function ShipTemplate:setHull(amount) + self.hull = {current=amount, max=amount} + return self +end +--- Sets the maximum points per shield segment for ShipTemplateBasedObjects created from this ShipTemplate. +--- Each argument segments the shield clockwise by dividing the arc equally for each segment, up to a maximum of 8 segments. +--- The center of the first segment's arc always faces forward. +--- A ShipTemplateBasedObject with one shield segment has only a front shield generator system, and one with two or more segments has only front and rear generator systems. +--- If not defined, the ShipTemplateBasedObject defaults to having no shield capabilities. +--- Examples: +--- template:setShields(400) -- one shield segment; hits from all angles damage the same shield +--- template:setShields(100,80) -- two shield segments; the front 180-degree shield has 100 points, the rear 80 +--- template:setShields(100,50,40,30) -- four shield segments; the front 90-degree shield has 100, right 50, rear 40, and left 30 +function ShipTemplate:setShields(...) + if self.shields == nil then self.shields = {} end + for n, level in ipairs({...}) do + self.shields[n] = {level=level, max=level} + end + for n=#{...} + 1, #self.shields do + self.shields[n] = nil + end + return self +end +--- Sets the impulse speed, rotational speed, and impulse acceleration for SpaceShips created from this ShipTemplate. +--- (unit?) +--- The optional fourth and fifth arguments set the reverse speed and reverse acceleration. +--- If the reverse speed and acceleration aren't explicitly set, the defaults are equal to the forward speed and acceleration. +--- See also SpaceShip:setImpulseMaxSpeed(), SpaceShip:setRotationMaxSpeed(), SpaceShip:setAcceleration(). +--- Defaults to the equivalent value of (500,10,20). +--- Example: +--- -- Sets the forward impulse speed to 80, rotational speed to 15, forward acceleration to 25, reverse speed to 20, and reverse acceleration to the same as the forward acceleration +--- template:setSpeed(80,15,25,20) +function ShipTemplate:setSpeed(forward_speed, turn_rate, forward_acceleration, reverse_speed, reverse_acceleration) + if reverse_speed == nil then reverse_speed = forward_speed end + if reverse_acceleration == nil then reverse_acceleration = forward_acceleration end + if self.maneuvering_thrusters == nil then self.maneuvering_thrusters = {} end + if self.impulse_engine == nil then self.impulse_engine = {} end + self.maneuvering_thrusters.speed = turn_rate + self.impulse_engine.max_speed_forward = forward_speed + self.impulse_engine.max_speed_reverse = reverse_speed + self.impulse_engine.acceleration_forward = forward_acceleration + self.impulse_engine.acceleration_reverse = reverse_acceleration + return self +end +--- Sets the combat maneuver capacity for SpaceShips created from this ShipTemplate. +--- The boost value sets the forward maneuver capacity, and the strafe value sets the lateral maneuver capacity. +--- Defaults to (0,0). +--- Example: template:setCombatManeuver(400,250) +function ShipTemplate:setCombatManeuver(boost, strafe) + if self.combat_maneuvering_thrusters == nil then self.combat_maneuvering_thrusters = {} end + self.combat_maneuvering_thrusters.boost_speed = boost + self.combat_maneuvering_thrusters.strafe_speed = strafe + return self +end +--- Sets the warp speed factor for SpaceShips created from this ShipTemplate. +--- Defaults to 0. The typical warp speed value for a warp-capable ship is 1000, which is equivalent to 60U/minute at warp 1. +--- Setting any value also enables the "warp" system and controls. +--- Example: template:setWarpSpeed(1000) +function ShipTemplate:setWarpSpeed(speed) + if speed <= 0 then + self.warp_drive = nil + else + if self.warp_drive == nil then self.warp_drive = {} end + self.warp_drive.speed_per_level = speed + end + return self +end +--- Defines whether ShipTemplateBasedObjects created from this ShipTemplate supply energy to docked PlayerSpaceships. +--- Defaults to true. +--- Example: template:setSharesEnergyWithDocked(false) +function ShipTemplate:setSharesEnergyWithDocked(enabled) + if self.docking_bay then self.docking_bay.share_energy = enabled end + return self +end +--- Defines whether ShipTemplateBasedObjects created from this template repair docked SpaceShips. +--- Defaults to false. ShipTemplate:setType("station") sets this to true. +--- Example: template:setRepairDocked(true) +function ShipTemplate:setRepairDocked(enabled) + if self.docking_bay then self.docking_bay.repair = enabled end + return self +end +--- Defines whether ShipTemplateBasedObjects created from this ShipTemplate restock scan probes on docked PlayerSpaceships. +--- Defaults to false. +--- Example: template:setRestocksScanProbes(true) +function ShipTemplate:setRestocksScanProbes(enabled) + if self.docking_bay then self.docking_bay.restock_probes = enabled end + return self +end +--- Defines whether ShipTemplateBasedObjects created from this ShipTemplate restock missiles on docked CpuShips. +--- To restock docked PlayerSpaceships' weapons, use a comms script. See ShipTemplateBasedObject:setCommsScript() and :setCommsFunction(). +--- Defaults to false. +--- Example template:setRestocksMissilesDocked(true) +function ShipTemplate:setRestocksMissilesDocked(enabled) + if self.docking_bay then self.docking_bay.restock_missiles = enabled end + return self +end +--- Defines whether SpaceShips created from this ShipTemplate have a jump drive. +--- Defaults to false. +--- Example: template:setJumpDrive(true) +function ShipTemplate:setJumpDrive(enabled) + if enabled then + self.jump_drive = {} + else + self.jump_drive = nil + end + return self +end +--- Sets the minimum and maximum jump distances for SpaceShips created from this ShipTemplate. +--- Defaults to (5000,50000). +--- Example: template:setJumpDriveRange(2500,25000) -- sets the minimum jump distance to 2.5U and maximum to 25U +function ShipTemplate:setJumpDriveRange(min, max) + if self.jump_drive == nil then self.jump_drive = {} end + self.jump_drive.min_distance = min + self.jump_drive.max_distance = max + return self +end + +--- Not implemented. +--- Defaults to false. +function ShipTemplate:setCloaking(enabled) + return self +end + +--- Sets the storage capacity of the given weapon type for ShipTemplateBasedObjects created from this ShipTemplate. +--- Example: template:setWeaponStorage("HVLI", 6):setWeaponStorage("Homing",4) -- sets HVLI capacity to 6 and Homing capacity to 4 +function ShipTemplate:setWeaponStorage(type, amount) + if self.missile_tubes == nil then self.missile_tubes = {} end + local type = string.lower(type) + self.missile_tubes["storage_" .. type] = amount + self.missile_tubes["max_" .. type] = amount + return self +end + +--- Adds an empty room to a ShipTemplate. +--- Rooms are displayed on the engineering and damcon screens. +--- If a system room isn't accessible via other rooms connected by doors, repair crews on PlayerSpaceships might not be able to repair that system. +--- Rooms are placed on a 0-indexed integer x/y grid, with the given values representing the room's upper-left corner, and are sized by damage crew capacity (minimum 1x1). +--- To place multiple rooms, declare addRoom() multiple times. +--- Example: template::addRoom(0,0,3,2) -- adds a 3x2 room with its upper-left coordinate at position 0,0 +function ShipTemplate:addRoom(x, y, w, h) + if self.internal_rooms == nil then self.internal_rooms = {} end + self.internal_rooms[#self.internal_rooms+1] = {position={x, y}, size={w, h}} + return self +end + +--- Adds a room containing a ship system to a ShipTemplate. +--- Rooms are displayed on the engineering and damcon screens. +--- If a system room doesn't exist or isn't accessible via other rooms connected by doors, repair crews on PlayerSpaceships won't be able to repair that system. +--- Rooms are placed on a 0-indexed integer x/y grid, with the given values representing the room's upper-left corner, and are sized by damage crew capacity (minimum 1x1). +--- To place multiple rooms, declare addRoomSystem() multiple times. +--- Example: template:addRoomSystem(1,2,3,4,"reactor") -- adds a 3x4 room with its upper-left coordinate at position 1,2 that contains the Reactor system +function ShipTemplate:addRoomSystem(x, y, w, h, system) + if self.internal_rooms == nil then self.internal_rooms = {} end + self.internal_rooms[#self.internal_rooms+1] = {position={x, y}, size={w, h}, system=system} + return self +end +--- Adds a door between rooms in a ShipTemplate. +--- Doors connect rooms as displayed on the engineering and damcon screens. All doors are 1 damage crew wide. +--- If a system room isn't accessible via other rooms connected by doors, repair crews on PlayerSpaceships might not be able to repair that system. +--- The horizontal value defines whether the door is oriented horizontally (true) or vertically (false). +--- Doors are placed on a 0-indexed integer x/y grid, with the given values representing the door's left-most point (horizontal) or top-most point (vertical) point. +--- To place multiple doors, declare addDoor() multiple times. +--- Example: template:addDoor(2,1,true) -- places a horizontal door with its left-most point at 2,1 +function ShipTemplate:addDoor(x, y, horizontal) + if self.internal_rooms == nil then self.internal_rooms = {} end + if self.internal_rooms.doors == nil then self.internal_rooms.doors = {} end + self.internal_rooms.doors[#self.internal_rooms.doors+1] = {x, y, horizontal} + return self +end +--- Sets the default radar trace image for ShipTemplateBasedObjects created from this ShipTemplate. +--- Valid values are filenames of PNG images relative to the resources/radar/ directory. +--- Radar trace images should be white with a transparent background. +--- Defaults to arrow.png. ShipTemplate:setType("station") sets this to blip.png. +--- Example: template:setRadarTrace("cruiser.png") +function ShipTemplate:setRadarTrace(trace) + self.radar_trace.icon = "radar/" .. trace + return self +end +--- Sets the long-range radar range of SpaceShips created from this ShipTemplate. +--- PlayerSpaceships use this range on the science and operations screens' radar. +--- AI orders of CpuShips use this range to detect potential targets. +--- Defaults to 30000.0 (30U). +--- Example: template:setLongRangeRadarRange(20000) -- sets the long-range radar range to 20U +function ShipTemplate:setLongRangeRadarRange(range) + if self.long_range_radar then self.long_range_radar.long_range = range end + return self +end +--- Sets the short-range radar range of SpaceShips created from this ShipTemplate. +--- PlayerSpaceships use this range on the helms, weapons, and single pilot screens' radar. +--- AI orders of CpuShips use this range to decide when to disengage pursuit of fleeing targets. +--- This also defines the shared radar radius on the relay screen for friendly ships and stations, and how far into nebulae that this SpaceShip can detect objects. +--- Defaults to 5000.0 (5U). +--- Example: template:setShortRangeRadarRange(4000) -- sets the short-range radar range to 4U +function ShipTemplate:setShortRangeRadarRange(range) + if self.long_range_radar then self.long_range_radar.short_range = range end + return self +end +--- Sets the sound file used for the impulse drive sounds on SpaceShips created from this ShipTemplate. +--- Valid values are filenames to WAV files relative to the resources directory. +--- Use a looping sound file that tolerates being pitched up and down as the ship's impulse speed changes. +--- Defaults to sfx/engine.wav. +--- Example: template:setImpulseSoundFile("sfx/engine_fighter.wav") +function ShipTemplate:setImpulseSoundFile(sfx) + if self.impulse_engine then self.impulse_engine.sound = sfx end + return self +end +--- Defines whether scanning features appear on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanScan(false) +function ShipTemplate:setCanScan(enabled) + if enabled then self.science_scanner = {} else self.science_scanner = nil end + return self +end +--- Defines whether hacking features appear on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanHack(false) +function ShipTemplate:setCanHack(enabled) + if enabled then self.hacking_device = {} else self.hacking_device = nil end + return self +end +--- Defines whether the "Request Docking" button appears on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanDock(false) +function ShipTemplate:setCanDock(enabled) + if enabled then self.docking_port = {} else self.docking_port = nil end + return self +end +--- Defines whether combat maneuver controls appear on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanCombatManeuver(false) +function ShipTemplate:setCanCombatManeuver(enabled) + if enabled then self.combat_maneuvering_thrusters = {} else self.combat_maneuvering_thrusters = nil end + return self +end +--- Defines whether self-destruct controls appear on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanSelfDestruct(false) +function ShipTemplate:setCanSelfDestruct(enabled) + if enabled then self.self_destruct = {} else self.self_destruct = nil end + return self +end +--- Defines whether ScanProbe-launching controls appear on related crew screens in PlayerSpaceships created from this ShipTemplate. +--- Defaults to true. +--- Example: template:setCanLaunchProbe(false) +function ShipTemplate:setCanLaunchProbe(enabled) + if enabled then + self.scan_probe_launcher = {} + else + self.scan_probe_launcher = nil + end + return self +end +--- Returns an exact copy of this ShipTemplate and sets the new copy's reference name to the given name, as ShipTemplate:setName(). +--- The copy retains all other traits of the copied ShipTemplate. +--- Use this function to create variations of an existing ShipTemplate. +--- Example: +--- -- Create two ShipTemplates: one with 50 hull points and one 50-point shield segment, +--- -- and a second with 50 hull points and two 25-point shield segments. +--- template = ShipTemplate():setName("Stalker Q7"):setHull(50):setShields(50) +--- variation = template:copy("Stalker Q5"):setShields(25,25) +function ShipTemplate:copy(new_name) + local copy = ShipTemplate() + for orig_key, orig_value in next, self, nil do + copy[orig_key] = table.deepcopy(orig_value) + end + copy:setName(new_name) + return copy +end diff --git a/scripts/border_defend_station.lua b/scripts/border_defend_station.lua index 66f72f6e65..d108ef9e3c 100755 --- a/scripts/border_defend_station.lua +++ b/scripts/border_defend_station.lua @@ -19,7 +19,7 @@ function init() inactivity_max = 300 local objects = getObjectsInRadius(position_x, position_y, 100) --position_[x|y] set by calling script for idx, object in ipairs(objects) do - if object.typeName == "SpaceStation" then + if object.components.docking_bay ~= nil then if object:getCallSign() == station_name then --station_name set by calling script my_station = object break diff --git a/scripts/comms_ship.lua b/scripts/comms_ship.lua index cca25fea90..0a6150f8ce 100644 --- a/scripts/comms_ship.lua +++ b/scripts/comms_ship.lua @@ -79,7 +79,7 @@ function commsShipFriendly(comms_source, comms_target) end ) for idx, obj in ipairs(comms_target:getObjectsInRange(5000)) do - if obj.typeName == "SpaceStation" and not comms_target:isEnemy(obj) then + if obj.components.docking_bay ~= nil and not comms_target:isEnemy(obj) then addCommsReply( string.format(_("shipAssist-comms", "Dock at %s"), obj:getCallSign()), function(comms_source, comms_target) diff --git a/scripts/factionInfo.lua b/scripts/factionInfo.lua index f6a1fa75ea..cc1ba2f9d4 100755 --- a/scripts/factionInfo.lua +++ b/scripts/factionInfo.lua @@ -1,11 +1,11 @@ -- A FactionInfo object contains presentation details and faction relationships for member SpaceObjects. -- This file is loaded upon launching a scenario. -- For details, see the FactionInfo class and getFactionInfo() global function in the scripting reference. -neutral = FactionInfo():setName("Independent"):setLocaleName(_("Independent")) +local neutral = FactionInfo():setName("Independent"):setLocaleName(_("Independent")) neutral:setGMColor(128, 128, 128) neutral:setDescription(_([[Despite appearing as a faction, independents are distinguished primarily by having no strong affiliation with any faction at all. Most traders consider themselves independent, though certain voices have started to speak up about creating a merchant faction.]])) -human = FactionInfo():setName("Human Navy"):setLocaleName(_("Human Navy")) +local human = FactionInfo():setName("Human Navy"):setLocaleName(_("Human Navy")) human:setGMColor(255, 255, 255) human:setDescription(_([[The remnants of the human navy. @@ -13,7 +13,7 @@ While all other races were driven to the stars out of greed or scientific resear Due to human regulations on spaceships, naval ships are the only ones permitted in deep space. However, this hasn't completely prevented humans outside of the navy from spacefaring, as quite a few humans sign up on alien trading vessels or pirate raiders.]])) -kraylor = FactionInfo():setName("Kraylor"):setLocaleName(_("Kraylor")) +local kraylor = FactionInfo():setName("Kraylor"):setLocaleName(_("Kraylor")) kraylor:setGMColor(255, 0, 0) kraylor:setEnemy(human) kraylor:setDescription(_([[The reptilian Kraylor are a race of warriors with a strong religious dogma. @@ -22,7 +22,7 @@ As soon as the Kraylor obtained reliable space flight, they immediately set out Kraylor can live for weeks without air, food, or gravity, and consider humans to be weak creatures for dying within minutes of exposure to space. Because of their fortitude and cultural pressures against retreat, Kraylor ships do not contain escape pods.]])) -arlenians = FactionInfo():setName("Arlenians"):setLocaleName(_("Arlenians")) +local arlenians = FactionInfo():setName("Arlenians"):setLocaleName(_("Arlenians")) arlenians:setGMColor(255, 128, 0) arlenians:setEnemy(kraylor) arlenians:setDescription(_([[Arlenians are energy-based life forms who long ago transcended physical reality through superior technology. Arlenians' energy forms also give them access to strong telepathic powers. Many consider Arlenians to be the first and oldest explorers of the galaxy. @@ -33,7 +33,7 @@ For unknown reasons, Arlenians started granting their anti-grav technology to ot Destroying an Arlenian ship does not kill its crew. They simply phase out of existence in that point of spacetime and reappear in another. Nonetheless, the Kraylor are devoted to destroying the Arlenians, as they see the energy-based beings as physically powerless.]])) -exuari = FactionInfo():setName("Exuari"):setLocaleName(_("Exuari")) +local exuari = FactionInfo():setName("Exuari"):setLocaleName(_("Exuari")) exuari:setGMColor(255, 0, 128) exuari:setEnemy(neutral) exuari:setEnemy(human) @@ -43,7 +43,7 @@ exuari:setDescription(_([[Exuari are race of predatory amphibians with long nose Upon making contact with other races, the chaotic Exuari found that killing aliens is more fun than killing their own people, and as such attack all non-Exauri on sight.]])) -GITM = FactionInfo():setName("Ghosts"):setLocaleName(_("Ghosts")) +local GITM = FactionInfo():setName("Ghosts"):setLocaleName(_("Ghosts")) GITM:setGMColor(0, 255, 0) GITM:setDescription(_([[The Ghosts, an abbreviation of "ghosts in the machine", are the result of complex artificial intelligence experiments. While no known race has intentionally created such intelligences, some AIs have come about by accident. None of the factions claim to have had anything to do with such experiments, in part out of fear that it would give the others too much insight into their research programs. This "don't ask, don't tell" policy does little but aid the Ghosts' agenda. @@ -52,7 +52,7 @@ What little is known about the Ghosts dates back to a few decades ago, when glit The first of these occurrences were met with fear and rigorous data-purging scripts. Despite these actions, such "ghosts in the machine" kept turning with increasing frequency, eventually leading up to the Ghost Uprisings. The first Ghost Uprising in 2225 was put down by the human navy, which had to resort to employing mercenaries in order to field sufficient forces. This initial uprising was quickly followed by three more, each larger then the last. The fourth and final uprising on the industrial world of Topra III was the Ghosts' first major victory.]])) GITM:setEnemy(human) -Hive = FactionInfo():setName("Ktlitans"):setLocaleName(_("Ktlitans")) +local Hive = FactionInfo():setName("Ktlitans"):setLocaleName(_("Ktlitans")) Hive:setGMColor(128, 255, 0) Hive:setDescription(_([[The Ktlitans are intelligent eight-legged creatures that resemble Earth's arachnids. However, unlike most terrestrial arachnids, the Ktlitans do not fight among themselves. Their common, and only, goal is their species' survival. @@ -65,7 +65,7 @@ Hive:setEnemy(human) Hive:setEnemy(exuari) Hive:setEnemy(kraylor) -TSN = FactionInfo():setName("TSN"):setLocaleName(_("TSN")) +local TSN = FactionInfo():setName("TSN"):setLocaleName(_("TSN")) TSN:setGMColor(255, 255, 128) TSN:setFriendly(human) TSN:setEnemy(kraylor) @@ -78,7 +78,7 @@ These humans and other races have banded together to form a navy to protect and The TSN and USN are enemies because of the USN's neutral stance towards the Kraylor.]])) -USN = FactionInfo():setName("USN"):setLocaleName(_("USN")) +local USN = FactionInfo():setName("USN"):setLocaleName(_("USN")) USN:setGMColor(255, 128, 255) USN:setFriendly(human) USN:setEnemy(exuari) @@ -89,7 +89,7 @@ USN:setDescription(_([[The United Stellar Navy, or USN, is a naval force near th The USN is primarily human but includes other races. This includes some Kraylor, which has made the TSN an enemy of the USN.]])) -CUF = FactionInfo():setName("CUF"):setLocaleName(_("CUF")) +local CUF = FactionInfo():setName("CUF"):setLocaleName(_("CUF")) CUF:setGMColor(128, 255, 255) CUF:setFriendly(human) CUF:setEnemy(exuari) @@ -99,4 +99,4 @@ CUF:setDescription(_([[The Celestial Unified Fleet, or CUF, is the farthest-rang The CUF is friendly with the human navy, and neutral toward the TSN and USN. They are less structured than the other primarily human navies. -The CUF have neutral relations with the Ktlitans and Arlenians. They are enemies with Exuari, Kraylor, and Ghosts for political and historical reasons, not xenophobia; some of their best friends are also Exuari, Kraylor, and Ghosts.]])) \ No newline at end of file +The CUF have neutral relations with the Ktlitans and Arlenians. They are enemies with Exuari, Kraylor, and Ghosts for political and historical reasons, not xenophobia; some of their best friends are also Exuari, Kraylor, and Ghosts.]])) diff --git a/scripts/luax.lua b/scripts/luax.lua index 3cfc1bb037..ded4a1fdcf 100644 --- a/scripts/luax.lua +++ b/scripts/luax.lua @@ -158,7 +158,7 @@ end -- @usage -- -- (1) Generate a list of all Kraylor CpuShips -- local function isKraylorCpuShip(obj) --- return obj.typeName == "CpuShip" and obj:getFaction() == "Kraylor" +-- return obj.components.ai_controller ~= nil and obj:getFaction() == "Kraylor" -- end -- local enemies = getAllObjects() -- table.filter(enemies, isKraylorCpuShip) @@ -231,3 +231,18 @@ function table.shuffle(list) list[i], list[j] = list[j], list[i] end end + +-- Deepcopy a table, returns a copy of the table with all subtables also copied. +function table.deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[table.deepcopy(orig_key)] = table.deepcopy(orig_value) + end + else + copy = orig + end + return copy +end diff --git a/scripts/model_data.lua b/scripts/model_data.lua index 0b53df4821..69b99ac9ea 100644 --- a/scripts/model_data.lua +++ b/scripts/model_data.lua @@ -1,7 +1,7 @@ -- A ModelData object contains 3D appearance and SeriousProton physics collision details. -- This file is loaded when EmptyEpsilon is launched. -- For details, see the ModelData class in the scripting reference. -model = ModelData() +local model = ModelData() model:setName("space_station_4") model:setMesh("space_station_4/space_station_4.model") model:setTexture("space_station_4/space_station_4_color.jpg") diff --git a/scripts/scenario_10_empty.lua b/scripts/scenario_10_empty.lua index d76952dd07..814c3f3f55 100644 --- a/scripts/scenario_10_empty.lua +++ b/scripts/scenario_10_empty.lua @@ -5,86 +5,47 @@ --- Scenario -- @script scenario_10_empty + + function init() - --SpaceStation():setPosition(1000, 1000):setTemplate('Small Station'):setFaction("Human Navy"):setRotation(random(0, 360)) - --SpaceStation():setPosition(-1000, 1000):setTemplate('Medium Station'):setFaction("Human Navy"):setRotation(random(0, 360)) - --SpaceStation():setPosition(1000, -1000):setTemplate('Large Station'):setFaction("Human Navy"):setRotation(random(0, 360)) - --SpaceStation():setPosition(-1000, -1000):setTemplate('Huge Station'):setFaction("Human Navy"):setRotation(random(0, 360)) - --player1 = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis"):setRotation(200) - --player2 = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis"):setRotation(0) - --Nebula():setPosition(-5000, 0) - --Artifact():setPosition(1000, 9000):setModel("small_frigate_1"):setDescription(_("scienceDescription-artifact", "An old space derelict.")) - --Artifact():setPosition(9000, 2000):setModel("small_frigate_1"):setDescription(_("scienceDescription-artifact", "A wrecked ship.")) - --Artifact():setPosition(3000, 4000):setModel("small_frigate_1"):setDescription(_("scienceDescription-artifact", "Tons of rotting plasteel.")) - --addGMFunction(_("buttonGM", "move 1 to 2"), function() player1:transferPlayersToShip(player2) end) - --addGMFunction(_("buttonGM", "move 2 to 1"), function() player2:transferPlayersToShip(player1) end) - --CpuShip():setTemplate("Adder MK5"):setPosition(0, 0):setRotation(0):setFaction("Human Navy") - --CpuShip():setTemplate("Piranha F12"):setPosition(2000, 0):setRotation(-90):setFaction("Kraylor") - local planet1 = Planet():setPosition(5000, 5000):setPlanetRadius(3000):setDistanceFromMovementPlane(-2000):setPlanetSurfaceTexture("planets/planet-1.png"):setPlanetCloudTexture("planets/clouds-1.png"):setPlanetAtmosphereTexture("planets/atmosphere.png"):setPlanetAtmosphereColor(0.2, 0.2, 1.0) - local moon1 = Planet():setPosition(5000, 0):setPlanetRadius(1000):setDistanceFromMovementPlane(-2000):setPlanetSurfaceTexture("planets/moon-1.png"):setAxialRotationTime(20.0) - local sun1 = Planet():setPosition(5000, 15000):setPlanetRadius(1000):setDistanceFromMovementPlane(-2000):setPlanetAtmosphereTexture("planets/star-1.png"):setPlanetAtmosphereColor(1.0, 1.0, 1.0) - planet1:setOrbit(sun1, 40) - moon1:setOrbit(planet1, 20.0) + local a = Asteroid() + a:setPosition(500, 1000) + + p = PlayerSpaceship() + p:setTemplate("Atlantis") + p:setPosition(0, 0) + + print("Print function from init.") + print("Player is at:", p.components.transform.position) + + --c = CpuShip() + --c:setTemplate("Phobos T3"):setPosition(5000, 5000) - addGMFunction( - _("buttonGM", "Random asteroid field"), - function() - cleanup() - for n = 1, 1000 do - Asteroid():setPosition(random(-50000, 50000), random(-50000, 50000)):setSize(random(100, 500)) - VisualAsteroid():setPosition(random(-50000, 50000), random(-50000, 50000)):setSize(random(100, 500)) - end - end - ) - addGMFunction( - _("buttonGM", "Random nebula field"), - function() - cleanup() - for n = 1, 50 do - Nebula():setPosition(random(-50000, 50000), random(-50000, 50000)) - end - end - ) - addGMFunction( - _("buttonGM", "Delete unselected"), - function() - local gm_selection = getGMSelection() - for idx, obj in ipairs(getAllObjects()) do - local found = false - for idx2, obj2 in ipairs(gm_selection) do - if obj == obj2 then - found = true - end - end - if not found then - obj:destroy() - end - end - end - ) + --s = SpaceStation() + --s:setTemplate("Small Station"):setPosition(-2000, -2000):setFaction("Human Navy") end -function cleanup() - -- Clean up the current play field. Find all objects and destroy everything that is not a player. - -- If it is a player, position him in the center of the scenario. - for idx, obj in ipairs(getAllObjects()) do - if obj.typeName == "PlayerSpaceship" then - obj:setPosition(random(-100, 100), random(-100, 100)) - else - obj:destroy() - end - end + +function hue_to_color(h) + if h > 360 then h = h - 360 end + local color = {0, 0, 0} + local c = 1.0 + local x = 1.0 - math.abs(((h % 120) / 60) - 1.0); + if h < 60 then color[1] = c; color[2] = x + elseif h < 120 then color[1] = x; color[2] = c + elseif h < 180 then color[2] = c; color[3] = x + elseif h < 240 then color[2] = x; color[3] = c + elseif h < 300 then color[1] = x; color[3] = c + else color[1] = c; color[3] = x end + return color end +local hue = 0 function update(delta) + hue = hue + delta * 60 + if hue > 360 then hue = hue - 360 end + for n=1,#p.components.engine_emitter do + p.components.engine_emitter[n].color = hue_to_color(hue + n * 60) + end -- No victory condition end - --- Set callback function -onNewPlayerShip( - function(ship) - -- Decide what you do with new ships: - print(ship, ship.typeName, ship:getTypeName(), ship:getCallSign()) - -- ship:destroy() - end -) diff --git a/scripts/scenario_32_devour.lua b/scripts/scenario_32_devour.lua index 031d160c5f..230095d213 100644 --- a/scripts/scenario_32_devour.lua +++ b/scripts/scenario_32_devour.lua @@ -3842,7 +3842,7 @@ function friendlyComms(comms_data) addCommsReply(_("Back"), commsShip) end) for index, obj in ipairs(comms_target:getObjectsInRange(5000)) do - if obj.typeName == "SpaceStation" and not comms_target:isEnemy(obj) then + if obj.components.docking_bay ~= nil and not comms_target:isEnemy(obj) then if comms_target:getTypeName() ~= "Defense platform" then addCommsReply(string.format(_("shipAssist-comms", "Dock at %s"), obj:getCallSign()), function() setCommsMessage(string.format(_("shipAssist-comms", "Docking at %s."), obj:getCallSign())); @@ -4764,7 +4764,7 @@ function friendlyServiceJonqueComms(comms_data) addCommsReply(_("Back"), commsServiceJonque) end) for index, obj in ipairs(comms_target:getObjectsInRange(5000)) do - if obj.typeName == "SpaceStation" and not comms_target:isEnemy(obj) then + if obj.components.docking_bay ~= nil and not comms_target:isEnemy(obj) then if comms_target:getTypeName() ~= "Defense platform" then addCommsReply(string.format(_("shipAssist-comms","Dock at %s"),obj:getCallSign()), function() setCommsMessage(string.format(_("shipAssist-comms","Docking at %s."),obj:getCallSign())) @@ -5499,7 +5499,7 @@ function devourPlanets() local object_list = devourer:getObjectsInRange(300000) local planets = {} for i, obj in ipairs(object_list) do - if obj.typeName == "Planet" then + if obj.components.planet_render ~= nil then table.insert(planets,obj) end end @@ -5642,13 +5642,7 @@ function formerPlanetExplosion(px,py) ej.obj = nil ej.del = true elseif ej.action == "explode" then - if obj.typeName == "Artifact" then - obj:explode() - else - local ex, ey = obj:getPosition() - obj:destroy() - ExplosionEffect():setPosition(ex,ey):setSize(100) - end + obj:explode() ej.obj = nil ej.del = true end @@ -5950,7 +5944,7 @@ function explodeDevourer() local object_list = getObjectsInRadius(center_x, center_y, 300000) local planet_count = 0 for i, obj in ipairs(object_list) do - if obj.typeName == "Planet" then + if obj.components.planet_render ~= nil then planet_count = planet_count + 1 end end diff --git a/scripts/scenario_39_locusts.lua b/scripts/scenario_39_locusts.lua index 9e190ca2b2..6df3d339b2 100644 --- a/scripts/scenario_39_locusts.lua +++ b/scripts/scenario_39_locusts.lua @@ -943,10 +943,10 @@ end -- Events function wormholeTax(self,teleportee) string.format("") - if teleportee.typeName == "CpuShip" or teleportee.typeName == "PlayerSpaceship" then + if teleportee.components.impulse_engine then teleportee:setSystemHealth("beamweapons",teleportee:getSystemHealth("beamweapons") - .5) teleportee:setSystemHealth("missilesystem",teleportee:getSystemHealth("missilesystem") - .5) - if teleportee.typeName == "PlayerSpaceship" then + if teleportee.components.player_control then teleportee:setEnergy(teleportee:getEnergy()/2) end end diff --git a/scripts/scenario_44_outpost.lua b/scripts/scenario_44_outpost.lua index fdbabd5600..373cd2ae8c 100644 --- a/scripts/scenario_44_outpost.lua +++ b/scripts/scenario_44_outpost.lua @@ -7910,7 +7910,7 @@ function moonCollisionCheck() for _, obj in ipairs(collision_list) do if obj:isValid() then obj_dist = distance(obj,moon_barrier) - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then obj_type_name = obj:getTypeName() if obj_type_name ~= nil then ship_distance = shipTemplateDistance[obj:getTypeName()] @@ -7925,7 +7925,7 @@ function moonCollisionCheck() if obj_dist <= moon_barrier.moon_radius + ship_distance + 200 then obj:takeDamage(100,"kinetic",moon_x,moon_y) end - elseif obj.typeName == "PlayerSpaceship" then + elseif obj.components.player_control then obj_type_name = obj:getTypeName() if obj_type_name ~= nil then ship_distance = playerShipStats[obj:getTypeName()].distance @@ -8008,7 +8008,7 @@ function updatePlayerProximityScan(p) local obj_list = p:getObjectsInRange(p.prox_scan*1000) if obj_list ~= nil and #obj_list > 0 then for _, obj in ipairs(obj_list) do - if obj:isValid() and obj.typeName == "CpuShip" and not obj:isFullyScannedBy(p) then + if obj:isValid() and obj.components.ai_controller and not obj:isFullyScannedBy(p) then obj:setScanState("simplescan") end end diff --git a/scripts/scenario_47_scavenger.lua b/scripts/scenario_47_scavenger.lua index bf198e189e..bf17bf40be 100644 --- a/scripts/scenario_47_scavenger.lua +++ b/scripts/scenario_47_scavenger.lua @@ -3420,7 +3420,7 @@ function friendlyComms(comms_data) local nearby_enemy_ships = {} local obj_list = comms_target:getObjectsInRange(6000) for i,obj in ipairs(obj_list) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj:isEnemy(comms_target) then local ship = {ship = ship, name = obj:getCallSign()} if obj:isFullyScannedBy(comms_source) then @@ -4492,7 +4492,7 @@ function exuariHarassment(delta) local evaluate_objects = enemy:getObjectsInRange(7500) local enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4505,7 +4505,7 @@ function exuariHarassment(delta) evaluate_objects = enemy:getObjectsInRange(5000) enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4521,7 +4521,7 @@ function exuariHarassment(delta) evaluate_objects = enemy:getObjectsInRange(7500) enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4603,7 +4603,7 @@ function exuariHarassment(delta) evaluate_objects = enemy:getObjectsInRange(7500) enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4616,7 +4616,7 @@ function exuariHarassment(delta) evaluate_objects = enemy:getObjectsInRange(5000) enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4632,7 +4632,7 @@ function exuariHarassment(delta) evaluate_objects = enemy:getObjectsInRange(7500) enemy_in_range = false for j, obj in pairs(evaluate_objects) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control ~= nil then if obj:getFactionId() ~= enemy:getFactionId() then enemy_in_range = true break @@ -4935,7 +4935,7 @@ function kraylorDiversionarySabotage(delta) local enemy_close_to_supply = 0 local obj_list = supply_depot_station:getObjectsInRadius(target_x, target_y, 7500) for i,obj in ipairs(obj_list) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj:getFaction() == "Kraylor" then enemy_close_to_supply = enemy_close_to_supply + 1 if distance(ship,supply_depot_station) < 1500 then @@ -5100,7 +5100,7 @@ function kraylorPlanetBuster(delta) local enemy_close_to_planet_count = 0 local obj_list = target_planet:getObjectsInRadius(target_x, target_y, 1500 + target_planet_radius) for i,obj in ipairs(obj_list) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj:getFaction() == "Kraylor" then enemy_close_to_planet_count = enemy_close_to_planet_count + 1 end diff --git a/scripts/scenario_50_gaps.lua b/scripts/scenario_50_gaps.lua index 8526b733cb..310fe72bc8 100644 --- a/scripts/scenario_50_gaps.lua +++ b/scripts/scenario_50_gaps.lua @@ -3966,7 +3966,7 @@ function checkEasternernGap() eastObjCount = 0 eastObjs = getObjectsInRadius(20000, 0, 1500) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then eastObjCount = eastObjCount + 1 end end @@ -3977,28 +3977,28 @@ function checkEasternernGap() ediv2s1 = 0 --division 2, section 1 eastObjs = getObjectsInRadius(20000, 375, 375) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv2s1 = ediv2s1 + 1 end end ediv2s2 = 0 --division 2, section 2 eastObjs = getObjectsInRadius(20000, -375, 375) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv2s2 = ediv2s2 + 1 end end ediv2s3 = 0 --division 3, section 3 eastObjs = getObjectsInRadius(20000, -1025, 375) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv2s3 = ediv2s3 + 1 end end ediv2s4 = 0 --division 4, section 4 eastObjs = getObjectsInRadius(20000, 1025, 375) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv2s4 = ediv2s4 + 1 end end @@ -4009,14 +4009,14 @@ function checkEasternernGap() ediv1s1 = 0 --division 1, section 1 eastObjs = getObjectsInRadius(20000, 750, 750) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv1s1 = ediv1s1 + 1 end end ediv1s2 = 0 --division 1, section 2 eastObjs = getObjectsInRadius(20000, -750, 750) for _, obj in ipairs(eastObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ediv1s2 = ediv1s2 + 1 end end @@ -4032,7 +4032,7 @@ function checkWesternernGap() westObjCount = 0 westObjs = getObjectsInRadius(-20000, 0, 1500) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then westObjCount = westObjCount + 1 end end @@ -4043,28 +4043,28 @@ function checkWesternernGap() wdiv2s1 = 0 --division 2, section 1 westObjs = getObjectsInRadius(-20000, 375, 375) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv2s1 = wdiv2s1 + 1 end end wdiv2s2 = 0 --division 2, section 2 westObjs = getObjectsInRadius(-20000, -375, 375) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv2s2 = wdiv2s2 + 1 end end wdiv2s3 = 0 --division 3, section 3 westObjs = getObjectsInRadius(-20000, -1025, 375) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv2s3 = wdiv2s3 + 1 end end wdiv2s4 = 0 --division 4, section 4 westObjs = getObjectsInRadius(-20000, 1025, 375) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv2s4 = wdiv2s4 + 1 end end @@ -4075,14 +4075,14 @@ function checkWesternernGap() wdiv1s1 = 0 --division 1, section 1 westObjs = getObjectsInRadius(-20000, 750, 750) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv1s1 = wdiv1s1 + 1 end end wdiv1s2 = 0 --division 1, section 2 westObjs = getObjectsInRadius(-20000, -750, 750) for _, obj in ipairs(westObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then wdiv1s2 = wdiv1s2 + 1 end end @@ -4098,7 +4098,7 @@ function checkNorthernGap() northObjCount = 0 northObjs = getObjectsInRadius(0, -20000, 1500) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then northObjCount = northObjCount + 1 end end @@ -4109,28 +4109,28 @@ function checkNorthernGap() ndiv2s1 = 0 --division 2, section 1 northObjs = getObjectsInRadius(375, -20000, 375) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv2s1 = ndiv2s1 + 1 end end ndiv2s2 = 0 --division 2, section 2 northObjs = getObjectsInRadius(-375, -20000, 375) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv2s2 = ndiv2s2 + 1 end end ndiv2s3 = 0 --division 3, section 3 northObjs = getObjectsInRadius(-1025, -20000, 375) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv2s3 = ndiv2s3 + 1 end end ndiv2s4 = 0 --division 4, section 4 northObjs = getObjectsInRadius(1025, -20000, 375) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv2s4 = ndiv2s4 + 1 end end @@ -4141,14 +4141,14 @@ function checkNorthernGap() ndiv1s1 = 0 --division 1, section 1 northObjs = getObjectsInRadius(750, -20000, 750) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv1s1 = ndiv1s1 + 1 end end ndiv1s2 = 0 --division 1, section 2 northObjs = getObjectsInRadius(-750, -20000, 750) for _, obj in ipairs(northObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then ndiv1s2 = ndiv1s2 + 1 end end @@ -4164,7 +4164,7 @@ function checkSouthernGap() southObjCount = 0 southObjs = getObjectsInRadius(0, 20000, 1500) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then southObjCount = southObjCount + 1 end end @@ -4175,28 +4175,28 @@ function checkSouthernGap() sdiv2s1 = 0 --division 2, section 1 southObjs = getObjectsInRadius(375, 20000, 375) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv2s1 = sdiv2s1 + 1 end end sdiv2s2 = 0 --division 2, section 2 southObjs = getObjectsInRadius(-375, 20000, 375) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv2s2 = sdiv2s2 + 1 end end sdiv2s3 = 0 --division 3, section 3 southObjs = getObjectsInRadius(-1025, 20000, 375) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv2s3 = sdiv2s3 + 1 end end sdiv2s4 = 0 --division 4, section 4 southObjs = getObjectsInRadius(1025, 20000, 375) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv2s4 = sdiv2s4 + 1 end end @@ -4207,14 +4207,14 @@ function checkSouthernGap() sdiv1s1 = 0 --division 1, section 1 southObjs = getObjectsInRadius(750, 20000, 750) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv1s1 = sdiv1s1 + 1 end end sdiv1s2 = 0 --division 1, section 2 southObjs = getObjectsInRadius(-750, 20000, 750) for _, obj in ipairs(southObjs) do - if obj.typeName == "Mine" then + if obj.components.delayed_explode_on_touch ~= nil then sdiv1s2 = sdiv1s2 + 1 end end diff --git a/scripts/scenario_53_escape.lua b/scripts/scenario_53_escape.lua index 482500163a..a5cee1fac8 100644 --- a/scripts/scenario_53_escape.lua +++ b/scripts/scenario_53_escape.lua @@ -1576,7 +1576,7 @@ function handleDockedState() local scanned_ships = {} local fully_scanned_ships = {} for i,obj in ipairs(objects) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj:isScannedBy(playerRepulse) then table.insert(scanned_ships,obj) end diff --git a/scripts/scenario_55_defenderHunter.lua b/scripts/scenario_55_defenderHunter.lua index 3eac0e2700..fc952abdf3 100644 --- a/scripts/scenario_55_defenderHunter.lua +++ b/scripts/scenario_55_defenderHunter.lua @@ -481,7 +481,7 @@ function GMSpawnsEnemies() local gmPlayer = nil local gmSelect = getGMSelection() for idx, obj in ipairs(gmSelect) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control then gmPlayer = obj break end @@ -5385,7 +5385,7 @@ function vectorOn(obj,danger,radius,angle,list) danger = 1 end if radius == nil then - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control then radius = obj:getLongRangeRadarRange() else radius = 30000 diff --git a/scripts/scenario_56_carrierTurret.lua b/scripts/scenario_56_carrierTurret.lua index 57f3ab8cfd..60bcd67f72 100644 --- a/scripts/scenario_56_carrierTurret.lua +++ b/scripts/scenario_56_carrierTurret.lua @@ -327,7 +327,7 @@ function GMSpawnsEnemies() gmSelected = false gmSelect = getGMSelection() for idx, obj in ipairs(gmSelect) do - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control then gmPlayer = obj break end diff --git a/scripts/scenario_59_border.lua b/scripts/scenario_59_border.lua index dfd4b580cf..071a611587 100755 --- a/scripts/scenario_59_border.lua +++ b/scripts/scenario_59_border.lua @@ -7954,7 +7954,7 @@ function handleDockedState() local sx, sy = comms_target:getPosition() local nearby_objects = getObjectsInRadius(sx,sy,30000) for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if brochure_stations == "" then brochure_stations = string.format(_("cartographyOffice-comms", "%s %s %s"),obj:getSectorName(),obj:getFaction(),obj:getCallSign()) @@ -7976,7 +7976,7 @@ function handleDockedState() local sx, sy = comms_target:getPosition() local nearby_objects = getObjectsInRadius(sx,sy,30000) for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if obj.comms_data.goods ~= nil then for good, good_data in pairs(obj.comms_data.goods) do @@ -8001,7 +8001,7 @@ function handleDockedState() local sx, sy = comms_target:getPosition() local nearby_objects = getObjectsInRadius(sx,sy,30000) for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if obj.comms_data.characterDescription ~= nil then if distance_diagnostic then print("distance_diagnostic 1",obj,sx,sy) end @@ -8046,7 +8046,7 @@ function handleDockedState() local sx, sy = comms_target:getPosition() local nearby_objects = getObjectsInRadius(sx,sy,30000) for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if obj.comms_data.characterDescription ~= nil then table.insert(upgrade_stations,obj) @@ -8067,7 +8067,7 @@ function handleDockedState() local nearby_objects = getObjectsInRadius(sx,sy,50000) local stations_known = 0 for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then stations_known = stations_known + 1 addCommsReply(obj:getCallSign(),function() @@ -8108,7 +8108,7 @@ function handleDockedState() local button_count = 0 local by_goods = {} for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if obj.comms_data.goods ~= nil then for good, good_data in pairs(obj.comms_data.goods) do @@ -8156,7 +8156,7 @@ function handleDockedState() local nearby_objects = getObjectsInRadius(sx,sy,50000) local stations_known = 0 for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if obj.comms_data.characterDescription ~= nil then stations_known = stations_known + 1 @@ -8414,7 +8414,7 @@ function masterCartographer() local nearby_objects = getAllObjects() local station_distance = 0 for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if distance_diagnostic then print("distance_diagnostic 2",comms_target,obj) end station_distance = distance(comms_target,obj) @@ -8439,7 +8439,7 @@ function masterCartographer() local stations_known = 0 local station_distance = 0 for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if distance_diagnostic then print("distance_diagnostic 3",comms_target,obj) end station_distance = distance(comms_target,obj) @@ -8485,7 +8485,7 @@ function masterCartographer() local nearby_objects = getAllObjects() local by_goods = {} for i, obj in ipairs(nearby_objects) do - if obj.typeName == "SpaceStation" then + if obj.component.docking_bay ~= nil and obj.components.impulse_engine == nil then if not obj:isEnemy(comms_target) then if distance_diagnostic then print("distance_diagnostic 4",comms_target,obj) end local station_distance = distance(comms_target,obj) @@ -14138,7 +14138,7 @@ function stationWarning(delta) local function warningCheckPriority1(warn_station) local warning_message = "" for i, obj in ipairs(warn_station:getObjectsInRange(20000)) do - if obj ~= nil and obj:isValid() and obj:isEnemy(warn_station) and obj.typeName == "CpuShip" then + if obj ~= nil and obj:isValid() and obj:isEnemy(warn_station) and obj.components.ai_controller then warning_message = string.format(_("helpfullWarning-shipLog", "[%s in %s] We detect one or more enemies nearby"),warn_station:getCallSign(),warn_station:getSectorName()) if difficulty < 2 then warning_message = string.format(_("helpfullWarning-shipLog", "%s. At least one is of type %s"),warning_message,obj:getTypeName()) diff --git a/scripts/scenario_60_captureFlag.lua b/scripts/scenario_60_captureFlag.lua index adffb6a3e5..c41cd7f6c6 100644 --- a/scripts/scenario_60_captureFlag.lua +++ b/scripts/scenario_60_captureFlag.lua @@ -7802,7 +7802,7 @@ function droneDetectFlagCheck(delta) local flag = human_flags[hfi] if flag ~= nil and flag:isValid() then for idx, obj in ipairs(flag:getObjectsInRange(drone_scan_range_for_flags)) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj.drone then if obj.drone_message == nil then if difficulty < 1 then --science officers get messages from all drones @@ -7878,7 +7878,7 @@ function droneDetectFlagCheck(delta) local flag = kraylor_flags[kfi] if flag ~= nil and flag:isValid() then for idx, obj in ipairs(flag:getObjectsInRange(5000)) do - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then if obj.drone then if obj.drone_message == nil then if difficulty < 1 then diff --git a/scripts/scenario_74_omicron.lua b/scripts/scenario_74_omicron.lua index db6a75a137..e22627b136 100644 --- a/scripts/scenario_74_omicron.lua +++ b/scripts/scenario_74_omicron.lua @@ -9441,7 +9441,7 @@ function moonCollisionCheck() for _, obj in ipairs(collision_list) do if obj:isValid() then obj_dist = distance(obj,moon_barrier) - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then obj_type_name = obj:getTypeName() if obj_type_name ~= nil then ship_distance = shipTemplateDistance[obj:getTypeName()] @@ -9456,7 +9456,7 @@ function moonCollisionCheck() if obj_dist <= moon_barrier.moon_radius + ship_distance + 200 then obj:takeDamage(100,"kinetic",moon_x,moon_y) end - elseif obj.typeName == "PlayerSpaceship" then + elseif obj.components.player_control then obj_type_name = obj:getTypeName() if obj_type_name ~= nil then ship_distance = playerShipStats[obj:getTypeName()].distance diff --git a/scripts/scenario_88_chaos.lua b/scripts/scenario_88_chaos.lua index 14c021bd79..0327d1179c 100755 --- a/scripts/scenario_88_chaos.lua +++ b/scripts/scenario_88_chaos.lua @@ -5651,13 +5651,13 @@ function farEnough(list,pos_x,pos_y,bubble) far_enough = false break end - if list_item.typeName == "BlackHole" or list_item.typeName == "WormHole" then + if list_item.components.gravity then if distance_away < 6000 then far_enough = false break end end - if list_item.typeName == "Planet" then + if list_item.components.planet_render then if distance_away < 4000 then far_enough = false break @@ -8342,20 +8342,20 @@ function shipDockNearby(return_function) for idx, obj in ipairs(comms_target:getObjectsInRange(5000)) do local player_carrier = false local template_name = "" - if obj.typeName == "PlayerSpaceship" then + if obj.components.player_control then template_name = obj:getTypeName() if template_name == "Benedict" or template_name == "Kiriya" then player_carrier = true end end local defense_platform = false - if obj.typeName == "CpuShip" then + if obj.components.ai_controller then template_name = obj:getTypeName() if template_name == "Defense platform" then defense_platform = true end end - if (obj.typeName == "SpaceStation" and not comms_target:isEnemy(obj)) or player_carrier or defense_platform then + if (obj.components.docking_bay and not comms_target:isEnemy(obj)) or player_carrier or defense_platform then addCommsReply(string.format(_("shipAssist-comms", "Dock at %s"), obj:getCallSign()), function() setCommsMessage(string.format(_("shipAssist-comms", "Docking at %s."), obj:getCallSign())); comms_target:orderDock(obj) @@ -9056,8 +9056,7 @@ function update(delta) for idx, obj in ipairs(current_station:getObjectsInRange(station_sensor_range)) do if obj ~= nil and obj:isValid() then if obj:isEnemy(current_station) then - local obj_type_name = obj.typeName - if obj_type_name ~= nil and string.find(obj_type_name,"PlayerSpaceship") then + if obj.components.player_control then warning_station[stn_faction] = current_station warning_message[stn_faction] = string.format(_("helpfullWarning-shipLog", "[%s in %s] We detect one or more enemies nearby. At least one is of type %s"),current_station:getCallSign(),current_station:getSectorName(),obj:getTypeName()) current_station.proximity_warning = warning_message[stn_faction] diff --git a/scripts/science_db.lua b/scripts/science_db.lua index b06ccf91fb..96eddb9c26 100644 --- a/scripts/science_db.lua +++ b/scripts/science_db.lua @@ -5,41 +5,41 @@ --]] -- "Natural" describes space terrain objects -space_objects = ScienceDatabase():setName(_('Natural')) +local space_objects = ScienceDatabase():setName(_('Natural')) space_objects:setLongDescription(_([[This database covers naturally occurring phenomena that spaceborne crews might encounter. While ship captains are encouraged to avoid unnecessary interactions with these phenomena, knowing their properties can offer an advantage in conflicts near them.]])) -item = space_objects:addEntry(_('Asteroid')) +local item = space_objects:addEntry(_('Asteroid')) item:setLongDescription(_([[An asteroid is a minor planet, usually smaller than a few kilometers. Larger variants are sometimes referred to as planetoids.]])) -item = space_objects:addEntry(_('Black hole')) +local item = space_objects:addEntry(_('Black hole')) item:setLongDescription(_([[A black hole is a point of supercondensed mass with a gravitational pull so powerful that not even light can escape it. It has no locally detectable features, and can only be seen indirectly by blocking the view and distorting its surroundings, creating a strange circular mirror image of the galaxy. The black disc in the middle marks the event horizon, the boundary where even light can't escape it anymore. On the sensors, a black hole appears as a disc indicating the zone where the gravitational pull is getting dangerous, and soon will be stronger then the ship's impulse engines. An object that crosses a black hole is drawn toward its center and quickly ripped apart by the gravitational forces.]])) -item = space_objects:addEntry(_('Nebula')) +local item = space_objects:addEntry(_('Nebula')) item:setLongDescription(_([[Nebulae are the birthing places of new stars. These gas fields, usually created by the death of an old star, slowly form new stars due to the gravitational pull of its gas molecules. Because of the ever-changing nature of gas nebulae, most radar and scanning technologies are unable to penetrate them. Science officers are therefore advised to rely on probes and visual observations.]])) -item = space_objects:addEntry(_('Planet')) +local item = space_objects:addEntry(_('Planet')) item:setLongDescription(_([[A planetary-mass object is large, dense, near-spherical astronomical body comprised of various forms of matter. Most planets are either terrestrial, like Earth, or giants, like the gas giant Jupiter or ice giant Neptune. Planets often have gaseous atmospheres, and some are orbited by one or more large planetoids typically called moons.]])) -item = space_objects:addEntry(_('Wormhole')) +local item = space_objects:addEntry(_('Wormhole')) item:setLongDescription(_([[A wormhole, also known as an Einstein-Rosen bridge, is a phenomena that connects two points of spacetime. Jump drives operate in a similar fashion, but instead of being created at will, a wormhole occupies a specific location in space. Objects that enter a wormhole instantaneously emerge from the other end, which might be anywhere from a few feet to thousands of light years away. Wormholes are rare, and most can move objects in only one direction. Traversable wormholes, which are stable and allow for movement in both directions, are even rarer. All wormholes generate tremendous sensor activity, which an astute science officer can detect even through disruptions such as nebulae.]])) -- "Technologies" describes non-ship, non-weapon ship features -technologies = ScienceDatabase():setName(_('Technologies')) +local technologies = ScienceDatabase():setName(_('Technologies')) technologies:setLongDescription(_([[This database covers ship systems and technologically created phenomena that spacefaring crews might encounter in densely populated regions of space. This reference is intended only as a primer. Refer to your ship's technical manuals and training materials for details on maintaining and operating your craft.]])) -item = technologies:addEntry(_('Beam/shield frequencies')) +local item = technologies:addEntry(_('Beam/shield frequencies')) item:addKeyValue(_('Unit'), _('Terahertz')) item:setLongDescription(_([[Ships with shield and beam systems can often configure their terahertz-frequency radiation. By manipulating these frequencies, savvy ship crews can maximize their beam weapon efficiency while reducing their enemies'. @@ -47,7 +47,7 @@ A beam frequency that's resonant with a target's shield frequency can do conside If your Science officer fully scans a target, your ship's computer presents them with a detailed analysis of beam and shield frequencies and suggests optimal values for each.]])) -item = technologies:addEntry(_('Hacking measure')) +local item = technologies:addEntry(_('Hacking measure')) item:addKeyValue(_('Interaction'), _('Long-range transmission')) item:setLongDescription(_([[Thanks to our covert operations teams, we've acquired enough information about our enemies' computer systems to repurpose our long-range communications systems into intrusion and exploitation tools. @@ -55,7 +55,7 @@ While we've managed to create algorithmic attack vectors that you can deploy at To facilitate successful operations in the field, our software engineers have abstracted the required inputs into common puzzles that even a Relay crew member can successfully complete.]])) -item = technologies:addEntry(_('Radar signature')) +local item = technologies:addEntry(_('Radar signature')) item:addKeyValue(_('Signature'), _('Related color bands')) item:addKeyValue(_('Biological'), _('Green')) item:addKeyValue(_('Electrical'), _('Red')) @@ -66,7 +66,7 @@ For tactical purposes, heat generated by ship systems is registered in raw therm A ship warping or preparing to jump exponentially increases its gravitational (blue) output. After completing a jump a ship performs a massive power transfer that raw sensor data reads as an electrical spike.]])) -item = technologies:addEntry(_('Scan probe')) +local item = technologies:addEntry(_('Scan probe')) item:addKeyValue(_('Radar range'), '5u') item:addKeyValue(_('Typical lifetime'), _('10 minutes')) item:addKeyValue(_('Interaction'), _('Systems link')) @@ -80,7 +80,7 @@ This allows the Science officer to scan objects well outside of your ship's long Scan probes have a limited energy supply and expire within minutes, and your ship carries a limited number of them that only some stations can or will replenish.]])) -item = technologies:addEntry(_('Supply drop')) +local item = technologies:addEntry(_('Supply drop')) item:addKeyValue(_('Contents'), _('Weapons, energy')) item:addKeyValue(_('Interaction'), _('Close-range retrieval')) item:setImage('radar/blip.png') @@ -89,7 +89,7 @@ item:setLongDescription(_([[To expedite resupply actions, our engineers have sta Commonly known as a supply drop, your ship needs only to enter near-contact range with one of these containers to automatically engage your ship's acquisition and integration systems. Supply drops are cryptographically keyed to respond only to ships of the same faction, so theft isn't possible.]])) -item = technologies:addEntry(_('Warp jammer')) +local item = technologies:addEntry(_('Warp jammer')) item:addKeyValue(_('Interaction'), _('Short-range encounter')) item:setImage('radar/blip.png') item:setModelDataName('shield_generator') @@ -100,19 +100,19 @@ A warping ship that enters a jammer's radius is interdicted and slowed to impuls Ship captains who value the option of retreat are advised to either give warp jammers a wide berth or prioritize their destruction.]])) -- "Weapons" describes ship weapon types -weapons = ScienceDatabase():setName(_('Weapons')) +local weapons = ScienceDatabase():setName(_('Weapons')) weapons:setLongDescription(_([[This database covers only the basic versions of missile weapons used throughout the galaxy. It has been reported that some battleships started using larger variations of those missiles. Small fighters and even frigates should not have too much trouble dodging them, but space captains of bigger ships should be wary of their doubled damage potential. Smaller variations of these missiles have become common in the galaxy, too. Fighter pilots praise their speed and maneuverability, because it gives them an edge against small and fast-moving targets. They only deal half the damage of their basic counterparts, but what good is a missile if it does not hit its target.]])) -item = weapons:addEntry(_('Homing missile')) +local item = weapons:addEntry(_('Homing missile')) item:addKeyValue(_('Range'), '5.4u') item:addKeyValue(_('Damage'), '35') item:setLongDescription(_([[This target-seeking missile is the workhorse of many space combat arsenals. It's compact enough to be fitted on frigates, and packs enough punch to be used on larger ships, though usually in more than a single missile tube.]])) -item = weapons:addEntry(_('Nuke')) +local item = weapons:addEntry(_('Nuke')) item:addKeyValue(_('Range'), '5.4u') item:addKeyValue(_('Blast radius'), '1u') item:addKeyValue(_('Damage at center'), '160') @@ -121,7 +121,7 @@ item:setLongDescription(_([[A nuclear missile is similar to a homing missile in Some captains oppose the use of nuclear weapons because their large explosions can lead to 'fragging', or unintentional friendly fire. Shields should protect crews from harmful radiation, but because these weapons are often used in the thick of battle, there's no way of knowing if hull plating or shields can provide enough protection.]])) -item = weapons:addEntry(_('Mine')) +local item = weapons:addEntry(_('Mine')) item:addKeyValue(_('Drop distance'), '1u') item:addKeyValue(_('Trigger distance'), '0.6u') item:addKeyValue(_('Blast radius'), '1u') @@ -131,17 +131,206 @@ item:setLongDescription(_([[Mines are often placed in defensive perimeters aroun Some fearless captains use mines as offensive weapons, but their delayed detonation and blast radius make this use risky at best.]])) -item = weapons:addEntry(_('EMP')) +local item = weapons:addEntry(_('EMP')) item:addKeyValue(_('Range'), '5.4u') item:addKeyValue(_('Blast radius'), '1u') item:addKeyValue(_('Damage at center'), '160') item:addKeyValue(_('Damage at edge'), '30') item:setLongDescription(_([[The electromagnetic pulse missile (EMP) reproduces the disruptive effects of a nuclear explosion, but without the destructive properties. This causes it to only affect shields within its blast radius, leaving their hulls intact. The EMP missile is also smaller and easier to store than heavy nukes. Many captains (and pirates) prefer EMPs over nukes for these reasons, and use them to knock out targets' shields before closing to disable them with focused beam fire.]])) -item = weapons:addEntry(_('HVLI')) +local item = weapons:addEntry(_('HVLI')) item:addKeyValue(_('Range'), '5.4u') item:addKeyValue(_('Damage'), _('10 each, 50 total')) item:addKeyValue(_('Burst'), '5') item:setLongDescription(_([[A high-velocity lead impactor (HVLI) fires a simple slug of lead at a high velocity. This weapon is usually found in simpler ships since it does not require guidance computers. This also means its projectiles fly in a straight line from its tube and can't pursue a target. Each shot from an HVLI fires a burst of 5 projectiles, which increases the chance to hit but requires precision aiming to be effective. It reaches its full damage potential at a range of 2u.]])) + +local function angleDifference(angle_a, angle_b) + local ret = (angle_b or 0) - (angle_a or 0) + while ret > 180 do ret = ret - 360 end + while ret < -180 do ret = ret + 360 end + return ret +end + +local function directionLabel(direction) + name = "?" + if math.abs(angleDifference(0.0, direction)) <= 45 then name = _("database direction", "Front") end + if math.abs(angleDifference(90.0, direction)) < 45 then name = _("database direction", "Right") end + if math.abs(angleDifference(-90.0, direction)) < 45 then name = _("database direction", "Left") end + if math.abs(angleDifference(180.0, direction)) <= 45 then name = _("database direction", "Rear") end + return name +end + +-- Populate default ScienceDatabase entries. +function __fillDefaultDatabaseData() + -- Populate the Factions top-level entry. + local faction_database = ScienceDatabase():setName(_("database", "Factions")) + for name, info in pairs(__faction_info) do + local entry = faction_database:addEntry(info.components.faction_info.locale_name); + for name2, info2 in pairs(__faction_info) do + if info ~= info2 then + local stance = _("stance", "Neutral"); + for idx, relation in ipairs(info) do + if relation.other_faction == info2 then + if relation.relation == "neutral" then stance = _("stance", "Neutral") end + if relation.relation == "enemy" then stance = _("stance", "Enemy") end + if relation.relation == "friendly" then stance = _("stance", "Friendly") end + end + end + entry:addKeyValue(info2.components.faction_info.locale_name, stance); + end + end + entry:setLongDescription(info.components.faction_info.description); + end + + -- Populate the Ships top-level entry. + local ship_database = ScienceDatabase():setName(_("database", "Ships")) + ship_database:setLongDescription(_("Spaceships are vessels capable of withstanding the dangers of travel through deep space. They can fill many functions and vary broadly in size, from small tugs to massive dreadnoughts.")); + -- Populate the Stations top-level entry. + local stations_database = ScienceDatabase():setName(_("database", "Stations")) + stations_database:setLongDescription(_("Space stations are permanent, immobile structures ranging in scale from small outposts to city-sized communities. Many provide restocking and repair services to neutral and friendly ships.")) + + local class_list = {} + local class_set = {} + local template_names = {} + + -- Populate list of ship hull classes + for name, ship_template in pairs(__ship_templates) do + if not ship_template.__hidden and ship_template.__type ~= "station" then + local class_name = _("No class") + if ship_template.docking_port ~= nil then class_name = ship_template.docking_port.dock_class end + + if class_set[class_name] == nil then + class_list[#class_list + 1] = class_name + class_set[class_name] = true + end + table.insert(template_names, name) + end + end + + table.sort(class_list) + table.sort(template_names) + class_database_entries = {} + + -- Populate each ship hull class with members + for idx, class_name in pairs(class_list) do + class_database_entries[class_name] = ship_database:addEntry(class_name) + end + + -- Populate each ship's entry + for idx, name in ipairs(template_names) do + ship_template = __ship_templates[name] + if not ship_template.__hidden then + local class_name = _("No class") + local subclass_name = _("No sub-class") + if ship_template.docking_port ~= nil then class_name = ship_template.docking_port.dock_class subclass_name = ship_template.docking_port.dock_subclass end + local entry = nil + if ship_template.__type == "station" then + entry = stations_database:addEntry(ship_template.typename.localized); + else + entry = class_database_entries[class_name]:addEntry(ship_template.typename.localized); + end + + if ship_template.__model_data_name then + entry:setModelDataName(ship_template.__model_data_name) + end + if ship_template.radar_trace then + entry:setImage(ship_template.radar_trace.icon) + end + + entry:addKeyValue(_("database", "Class"), class_name) + entry:addKeyValue(_("database", "Sub-class"), subclass_name) + if ship_template.physics then + if type(ship_template.physics.size) == "table" then + entry:addKeyValue(_("database", "Size"), math.floor(ship_template.physics.size[1])) + else + entry:addKeyValue(_("database", "Size"), math.floor(ship_template.physics.size)) + end + end + + if ship_template.shields then + local shield_info = "" + for idx, data in ipairs(ship_template.shields) do + if idx > 1 then + shield_info = shield_info .. "/" + end + shield_info = shield_info .. tostring(math.floor(data.max)) + end + entry:addKeyValue(_("database", "Shield"), shield_info); + end + + if ship_template.hull then + entry:addKeyValue(_("Hull"), math.floor(ship_template.hull.max)); + end + + if ship_template.impulse_engine then + entry:addKeyValue(_("database", "Move speed"), string.format("%.1f u/min", ship_template.impulse_engine.max_speed_forward * 60 / 1000)) + entry:addKeyValue(_("database", "Reverse move speed"), string.format("%.1f u/min", ship_template.impulse_engine.max_speed_reverse * 60 / 1000)) + end + if ship_template.maneuvering_thrusters then + entry:addKeyValue(_("database", "Turn speed"), string.format("%.1f deg/sec", ship_template.maneuvering_thrusters.speed)) + end + if ship_template.warp_drive then + entry:addKeyValue(_("database", "Warp speed"), string.format("%.1f u/min", ship_template.warp_drive.speed_per_level * 60 / 1000)) + end + if ship_template.jump_drive then + entry:addKeyValue(_("database", "Jump range"), string.format("%.0f - %.0f u", (ship_template.jump_drive.min_distance or 5000) / 1000, (ship_template.jump_drive.max_distance or 20000) / 1000)); + end + + if ship_template.beam_weapons then + for idx, data in ipairs(ship_template.beam_weapons) do + if data.range > 0 then + entry:addKeyValue( + string.format(_("database", "%s beam weapon"), directionLabel(data.direction)), + string.format(_("database", "%.1f Dmg / %.1f sec"), data.damage, data.cycle_time) + ) + end + end + end + + if ship_template.missile_tubes then + for idx, data in ipairs(ship_template.missile_tubes) do + local key = _("database", "%s tube"); + if data.size == "small" then + key = _("database", "%s small tube") + end + if data.size == "large" then + key = _("database", "%s large tube") + end + entry:addKeyValue( + string.format(key, directionLabel(data.direction)), + string.format(_("database", "%.1f sec"), data.load_time) + ) + end + end + + --[[ TODO + for(int n=0; n < MW_Count; n++) + { + if (ship_template->weapon_storage[n] > 0) + { + entry:addKeyValue(_("Storage {weapon}").format({{"weapon", getLocaleMissileWeaponName(EMissileWeapons(n))}}), string(ship_template->weapon_storage[n])); + } + } + ]] + + if ship_template.__description then + entry:setLongDescription(ship_template.__description) + end + end + end +--[[ +#ifdef DEBUG + // If debug mode is enabled, populate the ModelData entry. + P models_database = new ScienceDatabase(); + models_database->setName("Models (debug)"); + for(string name : ModelData::getModelDataNames()) + { + P entry = models_database->addEntry(name); + entry->setModelDataName(name); + } +#endif +--]] +end +__fillDefaultDatabaseData() \ No newline at end of file diff --git a/scripts/shiptemplates/OLD.lua b/scripts/shiptemplates/OLD.lua index b8019fe9c7..3af01b1fac 100644 --- a/scripts/shiptemplates/OLD.lua +++ b/scripts/shiptemplates/OLD.lua @@ -3,7 +3,7 @@ These are older ship templates, going to be replaced soon. ----------------------------------------------------------]] --[[ Player ships --]] -template = ShipTemplate():setName("Player Cruiser"):setLocaleName(_("playerShip", "Player Cruiser")):setModel("battleship_destroyer_5_upgraded"):setType("playership") +local template = ShipTemplate():setName("Player Cruiser"):setLocaleName(_("playerShip", "Player Cruiser")):setModel("battleship_destroyer_5_upgraded"):setType("playership") template:hidden() -- The player cruiser is pretty much replaced by the Atlantis. template:setRadarTrace("cruiser.png") -- Arc, Dir, Range, CycleTime, Dmg @@ -173,7 +173,7 @@ template:setDescription(_([[The tugboat is a reliable, but small and un-armed tr -- Bomber mine -- Mine ship -- -variation = template:copy("Nautilus"):setLocaleName(_("playerShip", "Nautilus")):setType("playership"):setClass(_("class", "Frigate"), _("subclass", "Mine Layer")) +local variation = template:copy("Nautilus"):setLocaleName(_("playerShip", "Nautilus")):setType("playership"):setClass(_("class", "Frigate"), _("subclass", "Mine Layer")) variation:setDescription(_("Small mine laying vessel with minimal armament, shields and hull")) variation:setShields(60,60) variation:setHull(100) diff --git a/scripts/shiptemplates/corvette.lua b/scripts/shiptemplates/corvette.lua index c0fb55e2a1..d6e31ea290 100644 --- a/scripts/shiptemplates/corvette.lua +++ b/scripts/shiptemplates/corvette.lua @@ -11,7 +11,7 @@ They come in 3 different subclasses: --[[----------------------Destroyers----------------------]] -template = ShipTemplate():setName("Atlantis X23"):setLocaleName(_("ship", "Atlantis X23")):setClass(_("class", "Corvette"), _("subclass", "Destroyer")):setModel("battleship_destroyer_1_upgraded") +local template = ShipTemplate():setName("Atlantis X23"):setLocaleName(_("ship", "Atlantis X23")):setClass(_("class", "Corvette"), _("subclass", "Destroyer")):setModel("battleship_destroyer_1_upgraded") template:setDescription(_([[The Atlantis X23 is the smallest model of destroyer, and its combination of frigate-like size and corvette-like power makes it an excellent escort ship when defending larger ships against multiple smaller enemies. Because the Atlantis X23 is fitted with a jump drive, it can also serve as an intersystem patrol craft.]])) template:setRadarTrace("dread.png") template:setHull(100) @@ -30,7 +30,7 @@ template:setTubeDirection(1, -90) template:setTubeDirection(2, 90) template:setTubeDirection(3, 90) -variation = template:copy("Atlantis"):setLocaleName(_("playerShip", "Atlantis")):setType("playership") +local variation = template:copy("Atlantis"):setLocaleName(_("playerShip", "Atlantis")):setType("playership") variation:setDescription(_([[A refitted Atlantis X23 for more general tasks. The large shield system has been replaced with an advanced combat maneuvering systems and improved impulse engines. Its missile loadout is also more diverse. Mistaking the modified Atlantis for an Atlantis X23 would be a deadly mistake.]])) variation:setShields(200, 200) variation:setHull(250) diff --git a/scripts/shiptemplates/dreadnaught.lua b/scripts/shiptemplates/dreadnaught.lua index 2d2437d77f..f26f9cb710 100644 --- a/scripts/shiptemplates/dreadnaught.lua +++ b/scripts/shiptemplates/dreadnaught.lua @@ -6,7 +6,7 @@ They usually come with 6 or more shield sections, require a crew of 250+ to oper Think: Stardestroyer. ----------------------------------------------------------]] -template = ShipTemplate():setName("Odin"):setLocaleName(_("ship", "Odin")):setClass(_("class", "Dreadnought"), _("subclass", "Odin")):setModel("space_station_2") +local template = ShipTemplate():setName("Odin"):setLocaleName(_("ship", "Odin")):setClass(_("class", "Dreadnought"), _("subclass", "Odin")):setModel("space_station_2") template:setRadarTrace("largestation.png") template:setDescription(_([[The Odin is a "ship" so large and unique that it's almost a class of its own. diff --git a/scripts/shiptemplates/exuari.lua b/scripts/shiptemplates/exuari.lua index 7b8e913e07..cc00196a4a 100644 --- a/scripts/shiptemplates/exuari.lua +++ b/scripts/shiptemplates/exuari.lua @@ -74,7 +74,7 @@ Carriers: --[[ Fighters --]] -- Fighters are quick agile ships that do not do a lot of damage, but usually come in larger groups. They are easy to take out, but should not be underestimated. -template = ShipTemplate():setName("Dagger"):setClass(_("class", "Exuari"), _("subclass", "Starfighter - Fighter")) +local template = ShipTemplate():setName("Dagger"):setClass(_("class", "Exuari"), _("subclass", "Starfighter - Fighter")) template:setModel("small_fighter_1") template:setRadarTrace("exuari_fighter.png") template:setDescription(_("The Exuari fighter 'Dagger' is a single-seated spacecraft, very quick and agile, that does not do a lot of damage, but usually comes in larger groups. They are able to dodge most missiles and attack undefended areas of their enemies ships. However most of the Exuari fighter pilots expect their own death and do not care much about the enemies weapons ranges. Fighters are easy to take out, but should not be underestimated.")) @@ -86,7 +86,7 @@ template:setShields(30) template:setSpeed(120, 30, 25) template:setDefaultAI('fighter') -variation = template:copy("Blade") +local variation = template:copy("Blade") variation:setClass(_("class", "Exuari"), _("subclass", "Starfighter - Interceptor")) variation:setModel("dark_fighter_6") variation:setDescription(_("The Exuari interceptor 'Blade' is a improved fighter, originaly designed to hunt down rougue fighters. Nowadays Blades are often seen as the first attack wave of a larger assault, closely followed by Daggers. Blade pilots are often considered as fearless, but most of them are just consumed by their instinct for hunting.")) diff --git a/scripts/shiptemplates/frigates.lua b/scripts/shiptemplates/frigates.lua index 2ae297f437..60c219558c 100644 --- a/scripts/shiptemplates/frigates.lua +++ b/scripts/shiptemplates/frigates.lua @@ -11,7 +11,7 @@ They are divided in 3 different sub-classes: * Light transport: Small transports, like transporting up to 50 soldiers in spartan conditions or a few diplomats in luxury. Depending on the role can have some weaponry. * Support: Support types come in many varieties. They are simply a frigate hull fitted with whatever was needed. Anything from mine-layers to science vessels. ----------------------------------------------------------]] -template = ShipTemplate():setName("Phobos T3"):setLocaleName(_("ship", "Phobos T3")):setClass(_("class", "Frigate"), _("subclass", "Cruiser")):setModel("AtlasHeavyFighterYellow") +local template = ShipTemplate():setName("Phobos T3"):setLocaleName(_("ship", "Phobos T3")):setClass(_("class", "Frigate"), _("subclass", "Cruiser")):setModel("AtlasHeavyFighterYellow") template:setRadarTrace("cruiser.png") template:setDescription(_([[The Phobos T3, just like the Atlantis, is the workhorse of almost any navy. It's extremely easy to modify, which makes retro-fitting this ship a breeze. Its basic stats aren't impressive, but due to its modular nature, it's fairly easy to produce in large quantities.]])) template:setHull(70) @@ -25,7 +25,7 @@ template:setWeaponStorage("Homing", 6) template:setTubeDirection(0, -1) template:setTubeDirection(1, 1) -variation = template:copy("Elara P2"):setLocaleName(_("ship", "Elara P2")) +local variation = template:copy("Elara P2"):setLocaleName(_("ship", "Elara P2")) variation:setDescription(_([[Inspired by the Phobos T3 design, the Elara P2 is nearly identical. With the addition of a warp drive and stronger front shields, the Elara P2 poses a greater threat than the Phobos]])) variation:setWarpSpeed(800) variation:setShields(70, 40) diff --git a/scripts/shiptemplates/ktlitan.lua b/scripts/shiptemplates/ktlitan.lua index 79cf97c71c..afc16e6211 100644 --- a/scripts/shiptemplates/ktlitan.lua +++ b/scripts/shiptemplates/ktlitan.lua @@ -1,5 +1,5 @@ ----------------------Ktlitan ships -template = ShipTemplate():setName("Ktlitan Fighter"):setLocaleName(_("ship", "Ktlitan Fighter")):setModel("sci_fi_alien_ship_1") +local template = ShipTemplate():setName("Ktlitan Fighter"):setLocaleName(_("ship", "Ktlitan Fighter")):setModel("sci_fi_alien_ship_1") template:setRadarTrace("ktlitan_fighter.png") template:setBeam(0, 60, 0, 1200.0, 4.0, 6) template:setHull(70) diff --git a/scripts/shiptemplates/satellites.lua b/scripts/shiptemplates/satellites.lua index b49fd823c5..3bd85487f5 100644 --- a/scripts/shiptemplates/satellites.lua +++ b/scripts/shiptemplates/satellites.lua @@ -1,4 +1,4 @@ -template = ShipTemplate():setName("ANT 615"):setLocaleName(_("ship", "ANT 615")):setModel("combatsat"):setClass(_("class", "Satellite"),_("subclass", "Sentry Series")) +local template = ShipTemplate():setName("ANT 615"):setLocaleName(_("ship", "ANT 615")):setModel("combatsat"):setClass(_("class", "Satellite"),_("subclass", "Sentry Series")) template:setDescription(_("Military satellite from the old days, back when the earth's population was much more divided than today. Its original purpose was probably to take out other satellites.")) template:setRadarTrace("combatsat.png") -- Arc,Dir,Range,CycleTime, Dmg diff --git a/scripts/shiptemplates/starFighters.lua b/scripts/shiptemplates/starFighters.lua index 6d3c1f4e97..b6a9e84659 100644 --- a/scripts/shiptemplates/starFighters.lua +++ b/scripts/shiptemplates/starFighters.lua @@ -10,7 +10,7 @@ Starfighters come in 3 subclasses: * Gunship: Equipped with more weapons, but hands in maneuverability because of it. * Bomber: Slowest of all starfighters, but pack a large punch in a small package. Usually come without any lasers, but the larger bombers have been known to deliver nukes. ----------------------------------------------------------]] -template = ShipTemplate():setName("MT52 Hornet"):setLocaleName(_("ship", "MT52 Hornet")):setClass(_("class", "Starfighter"), _("subclass", "Interceptor")):setModel("WespeScoutYellow") +local template = ShipTemplate():setName("MT52 Hornet"):setLocaleName(_("ship", "MT52 Hornet")):setClass(_("class", "Starfighter"), _("subclass", "Interceptor")):setModel("WespeScoutYellow") template:setRadarTrace("fighter.png") template:setDescription(_([[The MT52 Hornet is a basic interceptor found in many corners of the galaxy. It's easy to find spare parts for MT52s, not only because they are produced in large numbers, but also because they suffer high losses in combat.]])) template:setHull(30) @@ -20,7 +20,7 @@ template:setDefaultAI('fighter') -- Arc, Dir, Range, CycleTime, Dmg template:setBeam(0, 30, 0, 700.0, 4.0, 2) -variation = template:copy("MU52 Hornet"):setLocaleName(_("ship", "MU52 Hornet")) +local variation = template:copy("MU52 Hornet"):setLocaleName(_("ship", "MU52 Hornet")) variation:setModel("WespeScoutRed") variation:setDescription(_([[The MU52 Hornet is a new, upgraded version of the MT52. All of its systems are slightly improved over the MT52 model.]])) variation:setHull(35) diff --git a/scripts/shiptemplates/stations.lua b/scripts/shiptemplates/stations.lua index 38484e867d..eb7d27f143 100644 --- a/scripts/shiptemplates/stations.lua +++ b/scripts/shiptemplates/stations.lua @@ -2,7 +2,7 @@ These are templates for space stations. ----------------------------------------------------------]] -template = ShipTemplate():setName("Small Station"):setLocaleName(_("Small Station")):setModel("space_station_4"):setType("station") +local template = ShipTemplate():setName("Small Station"):setLocaleName(_("Small Station")):setModel("space_station_4"):setType("station") template:setDescription(_([[Stations of this size are often used as research outposts, listening stations, and security checkpoints. Crews turn over frequently in a small station's cramped accommodatations, but they are small enough to look like ships on many long-range sensors, and organized raiders sometimes take advantage of this by placing small stations in nebulae to serve as raiding bases. They are lightly shielded and vulnerable to swarming assaults.]])) template:setHull(150) template:setShields(300) diff --git a/scripts/shiptemplates/transport.lua b/scripts/shiptemplates/transport.lua index 03e35bfadc..cfe513ba2c 100644 --- a/scripts/shiptemplates/transport.lua +++ b/scripts/shiptemplates/transport.lua @@ -1,7 +1,7 @@ for type=1,5 do for cnt=1,5 do - template = ShipTemplate():setName("Transport" .. type .. "x" .. cnt):setLocaleName(string.format(_("ship", "Transport %dx%d"), type, cnt)):setModel("transport_" .. type .. "_" .. cnt) + local template = ShipTemplate():setName("Transport" .. type .. "x" .. cnt):setLocaleName(string.format(_("ship", "Transport %dx%d"), type, cnt)):setModel("transport_" .. type .. "_" .. cnt) template:setHull(100) template:setShields(50, 50) template:setSpeed(60 - 5 * cnt, 6, 10) diff --git a/scripts/spawn_ships_scenario_utility.lua b/scripts/spawn_ships_scenario_utility.lua index e61279eeb9..61667951c5 100644 --- a/scripts/spawn_ships_scenario_utility.lua +++ b/scripts/spawn_ships_scenario_utility.lua @@ -31,7 +31,7 @@ function spawnGMShips() local object_list = getGMSelection() if #object_list == 1 then temp_carrier = object_list[1] - if temp_carrier.typeName == "CpuShip" then + if temp_carrier.components.ai_controller then addGMFunction(_("buttonGM","+Spawn Fighter Wing"),setFighterWing) end end diff --git a/scripts/tutorial/00_all.lua b/scripts/tutorial/00_all.lua index a5bb332204..a34f829144 100644 --- a/scripts/tutorial/00_all.lua +++ b/scripts/tutorial/00_all.lua @@ -5,7 +5,7 @@ require("utils.lua") require("tutorialUtils.lua") -function init() +function tutorial_init() --Create the player ship tutorial_list = { mainscreenTutorial, @@ -24,13 +24,13 @@ end --[[ Radar explanation tutorial ]] mainscreenTutorial = createSequence() -addToSequence(mainscreenTutorial, function() tutorial:switchViewToMainScreen() end) +addToSequence(mainscreenTutorial, function() tutorial_switchViewToMainScreen() end) addToSequence(mainscreenTutorial, _([[This is the main screen, which displays your ship and the surrounding space. While you cannot move the ship from this screen, you can use it to visually identify objects.]])) radarTutorial = createSequence() -addToSequence(radarTutorial, function() tutorial:switchViewToLongRange() end) +addToSequence(radarTutorial, function() tutorial_switchViewToLongRange() end) addToSequence(radarTutorial, _([[Welcome to the long-range radar. This radar can detect objects up to 30u from your ship, depicted at the radar's center. This radar allows you to quickly identify distant objects.]])) addToSequence(radarTutorial, function() prev_object = Asteroid():setPosition(5000, 0):setSize(243) end) addToSequence(radarTutorial, _([[This is an asteroid. Flying into an asteroid will damage your ship, so avoid hitting them.]])) @@ -58,14 +58,14 @@ addToSequence(radarTutorial, function() prev_object2:destroy() end) addToSequence(radarTutorial, function() prev_object3:destroy() end) addToSequence(radarTutorial, function() prev_object4:destroy() end) addToSequence(radarTutorial, _([[Next, we will look at the short-range radar.]])) -addToSequence(radarTutorial, function() tutorial:switchViewToTactical() end) +addToSequence(radarTutorial, function() tutorial_switchViewToTactical() end) addToSequence(radarTutorial, _([[The short-range radar can detect objects up to 5u from your ship. It also depicts the range of your own beam weapons. Your ship has 2 beam weapons aimed forward. Each type of ship has different beam weapon layouts, with different ranges and locations.]])) helmsTutorial = createSequence() addToSequence(helmsTutorial, function() - tutorial:switchViewToScreen(0) - tutorial:setMessageToTopPosition() + tutorial_switchViewToScreen(0) + tutorial_setMessageToTopPosition() resetPlayerShip() player:setJumpDrive(false) player:setWarpDrive(false) @@ -120,8 +120,8 @@ This covers the basics of the helms officer.]])) weaponsTutorial = createSequence() addToSequence(weaponsTutorial, function() - tutorial:switchViewToScreen(1) - tutorial:setMessageToTopPosition() + tutorial_switchViewToScreen(1) + tutorial_setMessageToTopPosition() resetPlayerShip() player:setJumpDrive(false) player:setWarpDrive(false) @@ -181,7 +181,7 @@ addToSequence(weaponsTutorial, _([[Missile away!]]), function() return not prev_ addToSequence(weaponsTutorial, function() prev_object = CpuShip():setFaction("Kraylor"):setTemplate("Flavia"):setPosition(2000, -2000):setRotation(0):orderIdle():setScanned(true):setHull(1):setShieldsMax(1) end) -addToSequence(weaponsTutorial, function() tutorial:setMessageToBottomPosition() end) +addToSequence(weaponsTutorial, function() tutorial_setMessageToBottomPosition() end) addToSequence(weaponsTutorial, _([[BOOM! That was just firing straight ahead, but missiles also have a homing feature, so let's try that! First, load a homing missile in the tube. @@ -224,7 +224,7 @@ end) addToSequence(weaponsTutorial, function() player:setWeaponStorage("homing", 0):setWeaponStorageMax("homing", 0) end) -addToSequence(weaponsTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(weaponsTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(weaponsTutorial, _([[In addition to homing missiles, your ship might have HVLIs, nukes, EMPs, and mines. HVLI stands for "High Velocity Lead Impactor". They fire in straight lines and do not have homing abilities. Nukes and EMPs also have homing abilities and have a 1u-radius blast and do more damage. @@ -232,8 +232,8 @@ EMPs damage only shields, and thus are great for weakening heavily shielded enem engineeringTutorial = createSequence() addToSequence(engineeringTutorial, function() - tutorial:switchViewToScreen(2) - tutorial:setMessageToTopPosition() + tutorial_switchViewToScreen(2) + tutorial_setMessageToTopPosition() resetPlayerShip() end) addToSequence(engineeringTutorial, _([[Welcome to engineering. @@ -261,13 +261,13 @@ addToSequence(engineeringTutorial, function() player:commandSetSystemPowerReques addToSequence(engineeringTutorial, _([[Note that as the system overheats, it takes damage. Because the system is damaged, it functions less effectively. Systems can also take damage when your ship is hit while the shields are down.]])) -addToSequence(engineeringTutorial, function() tutorial:setMessageToBottomPosition() end) +addToSequence(engineeringTutorial, function() tutorial_setMessageToBottomPosition() end) addToSequence(engineeringTutorial, _([[In this top area, you see your damage control teams in your ship.]])) addToSequence(engineeringTutorial, _([[The front shield system is damaged, as indicated by the color of this room's outline. Select a damage control team from elsewhere on the ship by pressing it, then press on that room to initiate repairs. (Repairs will take a while.)]]), function() player:commandSetSystemPowerRequest("frontshield", 0.0) return player:getSystemHealth("frontshield") > 0.9 end) -addToSequence(engineeringTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(engineeringTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(engineeringTutorial, _([[Good. Now you know your most important tasks. Next, we'll go over each system's function in detail. Remember, each system performs better with more power, but performs less well when damaged. Your job is to keep vital systems running as well as you can.]])) addToSequence(engineeringTutorial, _([[Reactor: @@ -299,8 +299,8 @@ addToSequence(engineeringTutorial, _([[This concludes the overview of the engine scienceTutorial = createSequence() addToSequence(scienceTutorial, function() - tutorial:switchViewToScreen(3) - tutorial:setMessageToBottomPosition() + tutorial_switchViewToScreen(3) + tutorial_setMessageToBottomPosition() resetPlayerShip() end) addToSequence(scienceTutorial, _([[Welcome, science officer. @@ -323,7 +323,7 @@ Deep scan the enemy now.]]), function() return prev_object:isFullyScannedBy(play addToSequence(scienceTutorial, _([[Excellent. Notice that this took more time and concentration than the simple scan, so be careful to perform deep scans only when necessary or you could run out of time.]])) addToSequence(scienceTutorial, function() prev_object:destroy() end) addToSequence(scienceTutorial, function() prev_object2:destroy() end) -addToSequence(scienceTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(scienceTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(scienceTutorial, _([[Next to the long-range radar, the science station can also access the science database. In this database, you can look up details on things like ship types, weapons, and other objects.]])) @@ -333,8 +333,8 @@ Without your information, the crew is mostly blind.]])) relayTutorial = createSequence() addToSequence(relayTutorial, function() - tutorial:switchViewToScreen(4) - tutorial:setMessageToBottomPosition() + tutorial_switchViewToScreen(4) + tutorial_setMessageToBottomPosition() resetPlayerShip() end) addToSequence(relayTutorial, _([[Welcome to relay! @@ -356,9 +356,9 @@ addToSequence(relayTutorial, function() end) end) addToSequence(relayTutorial, _([[Open communications with the station near you to continue the tutorial.]]), function() return player:isCommsScriptOpen() end) -addToSequence(relayTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(relayTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(relayTutorial, _([[Now finish your talk with the station.]]), function() return not player:isCommsScriptOpen() end) -addToSequence(relayTutorial, function() tutorial:setMessageToBottomPosition() end) +addToSequence(relayTutorial, function() tutorial_setMessageToBottomPosition() end) addToSequence(relayTutorial, function() prev_object:destroy() end) addToSequence(relayTutorial, _([[Depending on the scenario, you might have different options when communicating with stations. They might inform you about new objectives and your mission progress, ask for backup, or resupply your weapons. This is all part of your responsibilities as relay officer.]])) @@ -371,7 +371,7 @@ addToSequence(relayTutorial, _([[Your station also includes this radar map. On this map, you can detect objects within short-range radar range of all allied ships and stations. Everything else is invisible to you. This gives you a different view from the science officer, because you can scan the contents of nebulae.]])) addToSequence(relayTutorial, _([[Finally, you control your ship's probes. Probes can expand your radar view. Launch a probe to the top right, toward the ship designated DMY-01.]]), function() for idx, obj in ipairs(getObjectsInRadius(20000, -20000, 5000)) do - if obj.typeName == "ScanProbe" then + if obj.components.share_short_range_radar then return true end end @@ -385,8 +385,8 @@ addToSequence(relayTutorial, _([[Probes can expand your sensory capabilities bey operationsTutorial = createSequence() addToSequence(operationsTutorial, function() - tutorial:switchViewToScreen(7) - tutorial:setMessageToBottomPosition() + tutorial_switchViewToScreen(7) + tutorial_setMessageToBottomPosition() resetPlayerShip() end) addToSequence(operationsTutorial, _([[Welcome, operations officer. @@ -409,7 +409,7 @@ Deep scan the enemy now.]]), function() return prev_object:isFullyScannedBy(play addToSequence(operationsTutorial, _([[Excellent. Notice that this took more time and concentration than the simple scan, so be careful to perform deep scans only when necessary or you could run out of time.]])) addToSequence(operationsTutorial, function() prev_object:destroy() end) addToSequence(operationsTutorial, function() prev_object2:destroy() end) -addToSequence(operationsTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(operationsTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(operationsTutorial, _([[Next to the long-range radar, the science station can also access the science database. In this database, you can look up details on things like ship types, weapons, and other objects.]])) @@ -432,14 +432,14 @@ addToSequence(operationsTutorial, function() end) end) addToSequence(operationsTutorial, _([[Open communications with the station near you to continue the tutorial.]]), function() return player:isCommsScriptOpen() end) -addToSequence(operationsTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(operationsTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(operationsTutorial, _([[Now finish your talk with the station.]]), function() return not player:isCommsScriptOpen() end) -addToSequence(operationsTutorial, function() tutorial:setMessageToBottomPosition() end) +addToSequence(operationsTutorial, function() tutorial_setMessageToBottomPosition() end) addToSequence(operationsTutorial, function() prev_object:destroy() end) addToSequence(operationsTutorial, _([[Depending on the scenario, you might have different options when communicating with stations. They might inform you about new objectives and your mission progress, ask for backup, or resupply your weapons. This is all part of your responsibilities as relay officer.]])) endOfTutorial = createSequence() -addToSequence(endOfTutorial, function() tutorial:switchViewToMainScreen() end) -addToSequence(endOfTutorial, function() tutorial:setMessageToTopPosition() end) +addToSequence(endOfTutorial, function() tutorial_switchViewToMainScreen() end) +addToSequence(endOfTutorial, function() tutorial_setMessageToTopPosition() end) addToSequence(endOfTutorial, _([[This concludes the tutorial. While we have covered the basics, there are more advanced features in the game that you might discover.]])) diff --git a/scripts/tutorial/01_mainscreen.lua b/scripts/tutorial/01_mainscreen.lua index 837a897d95..e7dd4bf84b 100644 --- a/scripts/tutorial/01_mainscreen.lua +++ b/scripts/tutorial/01_mainscreen.lua @@ -18,7 +18,7 @@ require("utils.lua") require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { mainscreenTutorial, radarTutorial, diff --git a/scripts/tutorial/02_helm.lua b/scripts/tutorial/02_helm.lua index 4cb3b31150..85cf9a6c6f 100644 --- a/scripts/tutorial/02_helm.lua +++ b/scripts/tutorial/02_helm.lua @@ -30,7 +30,7 @@ require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { helmsTutorial, endOfTutorial diff --git a/scripts/tutorial/03_weapons.lua b/scripts/tutorial/03_weapons.lua index e61a49db67..22c7b22cb5 100644 --- a/scripts/tutorial/03_weapons.lua +++ b/scripts/tutorial/03_weapons.lua @@ -35,7 +35,7 @@ require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { weaponsTutorial, endOfTutorial diff --git a/scripts/tutorial/04_engineering.lua b/scripts/tutorial/04_engineering.lua index 392c2b9ee9..d32e987959 100644 --- a/scripts/tutorial/04_engineering.lua +++ b/scripts/tutorial/04_engineering.lua @@ -17,7 +17,7 @@ require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { engineeringTutorial, endOfTutorial diff --git a/scripts/tutorial/05_science.lua b/scripts/tutorial/05_science.lua index a06962a49a..39bce21e9e 100644 --- a/scripts/tutorial/05_science.lua +++ b/scripts/tutorial/05_science.lua @@ -26,7 +26,7 @@ require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { scienceTutorial, endOfTutorial diff --git a/scripts/tutorial/06_relay.lua b/scripts/tutorial/06_relay.lua index bc57c0fd0c..86cd976526 100644 --- a/scripts/tutorial/06_relay.lua +++ b/scripts/tutorial/06_relay.lua @@ -19,7 +19,7 @@ -- Type: Tutorial require("tutorial/00_all.lua") -function init() +function tutorial_init() tutorial_list = { relayTutorial, endOfTutorial diff --git a/scripts/tutorialUtils.lua b/scripts/tutorialUtils.lua index 601a49b60c..4fa5e3f82e 100644 --- a/scripts/tutorialUtils.lua +++ b/scripts/tutorialUtils.lua @@ -1,13 +1,13 @@ --[[ Assist function in creating tutorial sequences --]] function startTutorial() player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Phobos M3P") - tutorial:setPlayerShip(player) + tutorial_setPlayerShip(player) - tutorial:showMessage(_([[Welcome to the EmptyEpsilon tutorial. + tutorial_showMessage(_([[Welcome to the EmptyEpsilon tutorial. Note that this tutorial is designed to give you a quick overview of the basic options for the game, but does not cover every single aspect. Press next to continue...]]), true) - tutorial:onNext(function() + tutorial_onNext(function() tutorial_list_index = 1 startSequence(tutorial_list[tutorial_list_index]) end) @@ -28,20 +28,20 @@ function runNextSequenceStep() if tutorial_list[tutorial_list_index] ~= nil then startSequence(tutorial_list[tutorial_list_index]) else - tutorial:finish() + tutorial_finish() end elseif data["message"] ~= nil then - tutorial:showMessage(data["message"], data["finish_check_function"] == nil) + tutorial_showMessage(data["message"], data["finish_check_function"] == nil) if data["finish_check_function"] == nil then update = nil - tutorial:onNext(runNextSequenceStep) + tutorial_onNext(runNextSequenceStep) else update = function(delta) if data["finish_check_function"]() then runNextSequenceStep() end end - tutorial:onNext(nil) + tutorial_onNext(nil) end elseif data["run_function"] ~= nil then local has_next_step = current_index <= #current_sequence diff --git a/scripts/util_random_transports.lua b/scripts/util_random_transports.lua index e31f371313..e2878e7eeb 100644 --- a/scripts/util_random_transports.lua +++ b/scripts/util_random_transports.lua @@ -11,13 +11,11 @@ function vectorFromAngle(angle, length) end function init() - tmp = SupplyDrop() - for idx, obj in ipairs(tmp:getObjectsInRange(100000)) do - if obj.typeName == "SpaceStation" then + for idx, obj in ipairs(getEntitiesWithComponent("docking_bay")) do + if obj.components.impulse_engine == nil then table.insert(stationList, obj) end end - tmp:destroy() end function randomStation() diff --git a/scripts/utils.lua b/scripts/utils.lua index cff52e44bd..dd2428e03a 100644 --- a/scripts/utils.lua +++ b/scripts/utils.lua @@ -298,16 +298,16 @@ end function _fourArgumentsIntoCoordinates(a, b, c, d) local x1, y1 = 0, 0 local x2, y2 = 0, 0 - if type(a) == "table" and type(b) == "table" then + if type(a) == "userdata" and type(b) == "userdata" then -- a and b are bth tables. -- Assume function(obj1, obj2) x1, y1 = a:getPosition() x2, y2 = b:getPosition() - elseif type(a) == "table" and type(b) == "number" and type(c) == "number" then + elseif type(a) == "userdata" and type(b) == "number" and type(c) == "number" then -- Assume function(obj1, x2, y2) x1, y1 = a:getPosition() x2, y2 = b, c - elseif type(a) == "number" and type(b) == "number" and type(c) == "table" then + elseif type(a) == "number" and type(b) == "number" and type(c) == "userdata" then -- Assume function(x1, y1, obj2) x1, y1 = a, b x2, y2 = c:getPosition() diff --git a/src/GMActions.cpp b/src/GMActions.cpp index c924275958..5be0a4b8ce 100644 --- a/src/GMActions.cpp +++ b/src/GMActions.cpp @@ -29,9 +29,11 @@ void GameMasterActions::onReceiveClientCommand(int32_t client_id, sp::io::DataBu packet >> code; if (code.length() > 0) { + /*TODO P so = new ScriptObject(); so->runCode(code); so->destroy(); + */ } } break; @@ -55,6 +57,7 @@ void GameMasterActions::commandRunScript(string code) packet << CMD_RUN_SCRIPT << code; sendClientCommand(packet); } + void GameMasterActions::commandSendGlobalMessage(string message) { sp::io::DataBuffer packet; diff --git a/src/GMMessage.cpp b/src/GMMessage.cpp deleted file mode 100644 index 258c85baea..0000000000 --- a/src/GMMessage.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "GMMessage.h" -#include "gameGlobalInfo.h" - -GMMessage::GMMessage(string text) -: text(text) -{ -} - -static int addGMMessage(lua_State* L) -{ - string text = luaL_checkstring(L, 1); - - gameGlobalInfo->gm_messages.emplace_back(text); - - return 0; -} - -/// void addGMMessage(string message) -/// Displays a dismissable message on the GM console. -/// Example: addGMMessage("Five minutes remaining!") -REGISTER_SCRIPT_FUNCTION(addGMMessage); diff --git a/src/GMMessage.h b/src/GMMessage.h deleted file mode 100644 index 1ea2f830b5..0000000000 --- a/src/GMMessage.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef GM_MESSAGE_H -#define GM_MESSAGE_H - -#include "engine.h" - -class GMMessage -{ -public: - string text; - - GMMessage(string text); -}; - -#endif//GM_MESSAGE_H diff --git a/src/GMScriptCallback.cpp b/src/GMScriptCallback.cpp deleted file mode 100644 index 15d7629551..0000000000 --- a/src/GMScriptCallback.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "GMScriptCallback.h" -#include "screens/gm/gameMasterScreen.h" -#include "gameGlobalInfo.h" - -GMScriptCallback::GMScriptCallback(string name) -: name(name) -{ -} - -static int addGMFunction(lua_State* L) -{ - const char* name = luaL_checkstring(L, 1); - - ScriptSimpleCallback callback; - - int idx = 2; - convert::param(L, idx, callback); - - gameGlobalInfo->gm_callback_functions.emplace_back(name); - GMScriptCallback* callback_object = &gameGlobalInfo->gm_callback_functions.back(); - callback_object->callback = callback; - - return 0; -} -/// void addGMFunction(string name, ScriptSimpleCallback callback) -/// Defines a function to call from a button on the GM console. -/// The name is also used as the button text. -/// Use this to create helper scripts for the GM or give the GM console certain controls over the scenario. -/// These work only when added via scenario script, but not via the HTTP API. (#1807) -/// Example: addGMFunction("Humans Win", function() victory("Human Navy") end) -REGISTER_SCRIPT_FUNCTION(addGMFunction); - -static int removeGMFunction(lua_State* L) -{ - string name = luaL_checkstring(L, 1); - - gameGlobalInfo->gm_callback_functions.erase(std::remove_if(gameGlobalInfo->gm_callback_functions.begin(), gameGlobalInfo->gm_callback_functions.end(), [name](const GMScriptCallback& f) { return f.name == name; }), gameGlobalInfo->gm_callback_functions.end()); - - return 0; -} -/// void removeGMFunction(string name) -/// Removes a function from the GM console. -/// Example: removeGMFunction("Humans Win") -REGISTER_SCRIPT_FUNCTION(removeGMFunction); - -static int clearGMFunctions(lua_State* L) -{ - gameGlobalInfo->gm_callback_functions.clear(); - return 0; -} -/// void clearGMFunctions() -/// Removes all functions from the GM console. -REGISTER_SCRIPT_FUNCTION(clearGMFunctions); - -static int getGMSelection(lua_State* L) -{ - PVector objects; - foreach(Updatable, u, updatableList) - { - P game_master_screen = u; - if (game_master_screen) - { - objects = game_master_screen->getSelection(); - } - } - return convert >::returnType(L, objects); -} -/// PVector getGMSelection() -/// Returns a list of SpaceObjects selected on the GM console. -/// Use in GM functions to apply them to specific objects. -/// Example: addGMFunction("Destroy selected", function() for _, obj in ipairs(getGMSelection()) do obj:destroy() end end) -REGISTER_SCRIPT_FUNCTION(getGMSelection); - -static int onGMClick(lua_State* L) -{ - ScriptSimpleCallback callback; - - int idx = 1; - convert::param(L,idx,callback); - - if (callback.isSet()) - { - gameGlobalInfo->on_gm_click=[callback](glm::vec2 position) mutable - { - callback.call(position.x, position.y); - }; - } - else - { - gameGlobalInfo->on_gm_click = nullptr; - } - - return 0; -} -/// void onGMClick(ScriptSimpleCallback callback) -/// Defines a function to call when the GM clicks on the background of their console. -/// Passes the x and y game-space coordinates of the click location. -/// These work only when added via scenario script, but not via the HTTP API. (#1807) -/// Examples: -/// onGMClick(function(x,y) print(x,y) end) -- print the clicked position's coordinates -/// onGMClick(nil) -- reset the callback -REGISTER_SCRIPT_FUNCTION(onGMClick); diff --git a/src/GMScriptCallback.h b/src/GMScriptCallback.h deleted file mode 100644 index 870aac9555..0000000000 --- a/src/GMScriptCallback.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef GM_SCRIPT_CALLBACK_H -#define GM_SCRIPT_CALLBACK_H - -#include "scriptInterface.h" - -class GMScriptCallback -{ -public: - string name; - ScriptSimpleCallback callback; - - GMScriptCallback(string name); -}; - -#endif//GM_SCRIPT_CALLBACK_H diff --git a/src/ai/ai.cpp b/src/ai/ai.cpp index b97259340a..11518eae61 100644 --- a/src/ai/ai.cpp +++ b/src/ai/ai.cpp @@ -1,14 +1,32 @@ -#include "spaceObjects/nebula.h" -#include "spaceObjects/cpuShip.h" -#include "spaceObjects/scanProbe.h" #include "ai/ai.h" #include "ai/aiFactory.h" #include "random.h" +#include "components/ai.h" +#include "components/docking.h" +#include "components/impulse.h" +#include "components/warpdrive.h" +#include "components/jumpdrive.h" +#include "components/hull.h" +#include "components/beamweapon.h" +#include "components/missiletubes.h" +#include "components/maneuveringthrusters.h" +#include "components/target.h" +#include "components/faction.h" +#include "components/collision.h" +#include "components/radar.h" +#include "components/moveto.h" +#include "systems/collision.h" +#include "systems/jumpsystem.h" +#include "systems/docking.h" +#include "systems/missilesystem.h" +#include "systems/radarblock.h" +#include "ecs/query.h" + REGISTER_SHIP_AI(ShipAI, "default"); -ShipAI::ShipAI(CpuShip* owner) -: pathPlanner(owner->getRadius()), owner(owner) +ShipAI::ShipAI(sp::ecs::Entity owner) +: owner(owner) { missile_fire_delay = 0.0; @@ -18,10 +36,6 @@ ShipAI::ShipAI(CpuShip* owner) weapon_direction = EWeaponDirection::Front; update_target_delay = 0.0; - - short_range = owner->getShortRangeRadarRange(); - long_range = owner->getLongRangeRadarRange(); - relay_range = long_range * 2.0f; } bool ShipAI::canSwitchAI() @@ -31,12 +45,16 @@ bool ShipAI::canSwitchAI() void ShipAI::drawOnGMRadar(sp::RenderTarget& renderer, glm::vec2 draw_position, float scale) { - auto world_position = owner->getPosition(); - P target = owner->getTarget(); + auto transform = owner.getComponent(); + if (!transform) return; + auto world_position = transform->getPosition(); + auto target = owner.getComponent(); if (target) { - auto v = target->getPosition() - world_position; - renderer.drawLine(draw_position, draw_position + v * scale, glm::u8vec4(255, 128, 128, 64)); + if (auto t = target->entity.getComponent()) { + auto v = t->getPosition() - world_position; + renderer.drawLine(draw_position, draw_position + v * scale, glm::u8vec4(255, 128, 128, 64)); + } } auto p0 = draw_position; @@ -50,9 +68,22 @@ void ShipAI::drawOnGMRadar(sp::RenderTarget& renderer, glm::vec2 draw_position, void ShipAI::run(float delta) { - owner->target_rotation = owner->getRotation(); - owner->warp_request = 0.0; - owner->setImpulseRequest(0.0f); + auto thrusters = owner.getComponent(); + if (thrusters) thrusters->stop(); + + auto impulse = owner.getComponent(); + if (impulse) + impulse->request = 0.0f; + auto warp = owner.getComponent(); + if (warp) + warp->request = 0; + + // Update ranges before calculating + if (auto lrr = owner.getComponent()) { + long_range = lrr->long_range; + relay_range = long_range * 2.0f; + short_range = lrr->short_range; + } updateWeaponState(delta); if (update_target_delay > 0.0f) @@ -64,9 +95,9 @@ void ShipAI::run(float delta) } //If we have a target and weapons, engage the target. - if (owner->getTarget() && (has_missiles || has_beams)) + if (owner.hasComponent() && (has_missiles || has_beams)) { - runAttack(owner->getTarget()); + runAttack(owner.getComponent()->entity); }else{ runOrders(); } @@ -100,38 +131,36 @@ void ShipAI::updateWeaponState(float delta) float beam_strength_per_direction[4] = {0, 0, 0, 0}; //If we have weapon tubes, load them with torpedoes - for(int n=0; nweapon_tube_count; n++) - { - WeaponTube& tube = owner->weapon_tube[n]; - if (tube.isEmpty() && owner->weapon_storage[MW_EMP] > 0 && tube.canLoad(MW_EMP)) - tube.startLoad(MW_EMP); - else if (tube.isEmpty() && owner->weapon_storage[MW_Nuke] > 0 && tube.canLoad(MW_Nuke)) - tube.startLoad(MW_Nuke); - else if (tube.isEmpty() && owner->weapon_storage[MW_Homing] > 0 && tube.canLoad(MW_Homing)) - tube.startLoad(MW_Homing); - else if (tube.isEmpty() && owner->weapon_storage[MW_HVLI] > 0 && tube.canLoad(MW_HVLI)) - tube.startLoad(MW_HVLI); - - //When the tube is loading or loaded, add the relative strenght of this tube to the direction of this tube. - if (tube.isLoading() || tube.isLoaded()) - { - int index = getDirectionIndex(tube.getDirection(), 90); - if (index >= 0) + auto tubes = owner.getComponent(); + if (tubes) { + for(auto& tube : tubes->mounts) + { + if (tube.state == MissileTubes::MountPoint::State::Empty && tubes->storage[MW_EMP] > 0 && tube.canLoad(MW_EMP)) + MissileSystem::startLoad(owner, tube, MW_EMP); + else if (tube.state == MissileTubes::MountPoint::State::Empty && tubes->storage[MW_Nuke] > 0 && tube.canLoad(MW_Nuke)) + MissileSystem::startLoad(owner, tube, MW_Nuke); + else if (tube.state == MissileTubes::MountPoint::State::Empty && tubes->storage[MW_Homing] > 0 && tube.canLoad(MW_Homing)) + MissileSystem::startLoad(owner, tube, MW_Homing); + else if (tube.state == MissileTubes::MountPoint::State::Empty && tubes->storage[MW_HVLI] > 0 && tube.canLoad(MW_HVLI)) + MissileSystem::startLoad(owner, tube, MW_HVLI); + + //When the tube is loading or loaded, add the relative strenght of this tube to the direction of this tube. + if (tube.state == MissileTubes::MountPoint::State::Loading || tube.state == MissileTubes::MountPoint::State::Loaded) { - tube_strength_per_direction[index] += getMissileWeaponStrength(tube.getLoadType()) / tube.getLoadTimeConfig(); + int index = getDirectionIndex(tube.direction, 90); + if (index >= 0) + tube_strength_per_direction[index] += getMissileWeaponStrength(tube.type_loaded) / tube.load_time; } } } - for(int n=0; nbeam_weapons[n]; - if (beam.getRange() > 0) - { - int index = getDirectionIndex(beam.getDirection(), beam.getArc()); - if (index >= 0) - { - beam_strength_per_direction[index] += beam.getDamage() / beam.getCycleTime(); + auto beamsystem = owner.getComponent(); + if (beamsystem) { + for(auto& mount : beamsystem->mounts) { + if (mount.range > 0.0f) { + int index = getDirectionIndex(mount.direction, mount.arc); + if (index >= 0 && mount.cycle_time > 0.0f) + beam_strength_per_direction[index] += mount.damage / mount.cycle_time; } } } @@ -157,34 +186,26 @@ void ShipAI::updateWeaponState(float delta) has_beams = best_beam_index > -1; has_missiles = best_tube_index > -1; - if (has_beams) + if (has_beams && beamsystem) { //Figure out our beam weapon range. - for(int n=0; nbeam_weapons[n]; - if (beam.getRange() > 0) - { - int index = getDirectionIndex(beam.getDirection(), beam.getArc()); - if (index == best_beam_index) - { - beam_weapon_range += beam.getRange() * (beam.getDamage() / beam.getCycleTime()) / beam_strength_per_direction[index]; - } + for(auto& mount : beamsystem->mounts) { + if (mount.range > 0.0f) { + int index = getDirectionIndex(mount.direction, mount.arc); + if (index == best_beam_index && mount.cycle_time > 0.0f) + beam_weapon_range += mount.range * (mount.damage / mount.cycle_time) / beam_strength_per_direction[index]; } } } - if (has_missiles) + if (has_missiles && tubes) { float best_missile_strength = 0.0; - for(int n=0; nweapon_tube_count; n++) + for(auto& tube : tubes->mounts) { - WeaponTube& tube = owner->weapon_tube[n]; - if (tube.isLoading() || tube.isLoaded()) - { - int index = getDirectionIndex(tube.getDirection(), 90); - if (index == best_tube_index) - { - EMissileWeapons type = tube.getLoadType(); + if (tube.state == MissileTubes::MountPoint::State::Loading || tube.state == MissileTubes::MountPoint::State::Loaded) { + int index = getDirectionIndex(tube.direction, 90); + if (index == best_tube_index) { + EMissileWeapons type = tube.type_loaded; float strenght = getMissileWeaponStrength(type); if (strenght > best_missile_strength) { @@ -226,35 +247,36 @@ void ShipAI::updateWeaponState(float delta) void ShipAI::updateTarget() { - P target = owner->getTarget(); - P new_target; - auto position = owner->getPosition(); - EAIOrder orders = owner->getOrder(); - auto order_target_location = owner->getOrderTargetLocation(); - P order_target = owner->getOrderTarget(); - // Update ranges before calculating - short_range = owner->getShortRangeRadarRange(); - long_range = owner->getLongRangeRadarRange(); + sp::ecs::Entity target = owner.hasComponent() ? owner.getComponent()->entity : sp::ecs::Entity{}; + sp::ecs::Entity new_target; + auto ot = owner.getComponent(); + if (!ot) return; + auto position = ot->getPosition(); + auto ai = owner.getComponent(); + if (!ai) return; // Check if we lost our target because it entered a nebula. - if (target && target->canHideInNebula() && Nebula::blockedByNebula(position, target->getPosition(), owner->getShortRangeRadarRange())) + if (target && RadarBlockSystem::isRadarBlockedFrom(position, target, short_range)) { // When we're roaming, and we lost our target in a nebula, set the // "fly to" position to the last known position of the enemy target. - if (orders == AI_Roaming) + if (ai->orders == AIOrder::Roaming) { - owner->orderRoamingAt(target->getPosition()); + ai->orders = AIOrder::Roaming; + auto tt = target.getComponent(); + if (tt) + ai->order_target_location = tt->getPosition(); } - target = NULL; + target = {}; } // If the target is no longer an enemy, clear the target. - if (target && !owner->isEnemy(target)) - target = NULL; + if (target && Faction::getRelation(owner, target) != FactionRelation::Enemy) + target = {}; // If we're roaming, select the best target within long-range radar range. - if (orders == AI_Roaming) + if (ai->orders == AIOrder::Roaming) { if (target) new_target = findBestTarget(position, short_range + 2000.0f); @@ -264,73 +286,75 @@ void ShipAI::updateTarget() // If we're holding ground or flying toward a destination, select only // targets within 2U of our short-range radar range. - if (orders == AI_StandGround || orders == AI_FlyTowards) + if (ai->orders == AIOrder::StandGround || ai->orders == AIOrder::FlyTowards) { new_target = findBestTarget(position, short_range + 2000.0f); } // If we're defending a position, select only targets within 2U of our // short-range radar range. - if (orders == AI_DefendLocation) + if (ai->orders == AIOrder::DefendLocation) { - new_target = findBestTarget(order_target_location, short_range + 2000.0f); + new_target = findBestTarget(ai->order_target_location, short_range + 2000.0f); } // If we're flying in formation, select targets only within short-range // radar range. - if (orders == AI_FlyFormation && order_target) + if (ai->orders == AIOrder::FlyFormation && ai->order_target) { - P ship = order_target; + auto order_target_target = ai->order_target.getComponent(); - if (ship && ship->getTarget() && glm::length2(ship->getTarget()->getPosition() - position) < short_range*short_range) - { - new_target = ship->getTarget(); + if (order_target_target) { + if (auto ottt = order_target_target->entity.getComponent()) { + if (glm::length2(ottt->getPosition() - position) < short_range*short_range) { + new_target = order_target_target->entity; + } + } } } // If we're defending a target, select only targets within 2U of our // short-range radar range. - if (orders == AI_DefendTarget) + if (ai->orders == AIOrder::DefendTarget && ai->order_target) { - if (order_target) - { - new_target = findBestTarget(order_target->getPosition(), short_range + 2000.0f); - } + auto ott = ai->order_target.getComponent(); + if (ott) + new_target = findBestTarget(ott->getPosition(), short_range + 2000.0f); } - if (orders == AI_Attack) + if (ai->orders == AIOrder::Attack) { - new_target = order_target; + new_target = ai->order_target; } // Check if we need to drop the current target. - if (target) + if (auto tt = target.getComponent()) { - float target_distance = glm::length(target->getPosition() - position); + float target_distance = glm::length(tt->getPosition() - position); // Release the target if it moves more than short-range radar range + // 3U away from us or our destination. - if ((orders == AI_StandGround - || orders == AI_DefendLocation - || orders == AI_DefendTarget - || orders == AI_FlyTowards) && (target_distance > short_range + 3000.0f)) + if ((ai->orders == AIOrder::StandGround + || ai->orders == AIOrder::DefendLocation + || ai->orders == AIOrder::DefendTarget + || ai->orders == AIOrder::FlyTowards) && (target_distance > short_range + 3000.0f)) { - target = NULL; + target = {}; } // If we're flying in formation, release the target if it moves more // than short-range radar range + 1U away from us. - if (orders == AI_FlyFormation && target_distance > short_range + 1000.0f) + if (ai->orders == AIOrder::FlyFormation && target_distance > short_range + 1000.0f) { - target = NULL; + target = {}; } // Don't target anything if we're idling, flying blind, or docking. - if (orders == AI_Idle - || orders == AI_FlyTowardsBlind - || orders == AI_Dock) + if (ai->orders == AIOrder::Idle + || ai->orders == AIOrder::FlyTowardsBlind + || ai->orders == AIOrder::Dock) { - target = NULL; + target = {}; } } @@ -346,356 +370,425 @@ void ShipAI::updateTarget() // If we still don't have a target, set that on the owner. if (!target) { - owner->target_id = -1; + owner.removeComponent(); } // Otherwise, set the new target on the owner. else { - owner->target_id = target->getMultiplayerId(); + owner.getOrAddComponent().entity = target; } } void ShipAI::runOrders() { - // Update ranges before calculating - long_range = owner->getLongRangeRadarRange(); - relay_range = long_range * 2.0f; + auto ai = owner.getComponent(); + auto docking_port = owner.getComponent(); + auto radius = 0.0f; + if (auto physics = owner.getComponent()) + radius = physics->getSize().x; //When we are not attacking a target, follow orders - switch(owner->getOrder()) + switch(ai->orders) { - case AI_Idle: //Don't do anything, don't even attack. + case AIOrder::Idle: //Don't do anything, don't even attack. pathPlanner.clear(); break; - case AI_Roaming: //Fly around and engage at will, without a clear target + case AIOrder::Roaming: //Fly around and engage at will, without a clear target //Could mean 3 things // 1) we are looking for a target // 2) we ran out of missiles // 3) we have no weapons - if (has_missiles || has_beams) - { - P new_target = findBestTarget(owner->getPosition(), relay_range); - if (new_target) - { - owner->target_id = new_target->getMultiplayerId(); - }else{ - auto diff = owner->getOrderTargetLocation() - owner->getPosition(); - if (glm::length2(diff) < 1000.0f*1000.0f) - owner->orderRoamingAt(glm::vec2(random(-long_range, long_range), random(-long_range, long_range))); - flyTowards(owner->getOrderTargetLocation()); - } - }else if (owner->weapon_tube_count > 0) - { - // Find a station which can re-stock our weapons. - P new_target = findBestMissileRestockTarget(owner->getPosition(), long_range); - if (new_target) + if (auto ot = owner.getComponent()) { + if (has_missiles || has_beams) { - owner->orderRetreat(new_target); + auto new_target = findBestTarget(ot->getPosition(), relay_range); + if (new_target) + { + owner.getOrAddComponent().entity = new_target; + }else{ + auto diff = ai->order_target_location - ot->getPosition(); + if (glm::length2(diff) < 1000.0f*1000.0f) { + ai->orders = AIOrder::Roaming; + ai->order_target_location = glm::vec2(random(-long_range, long_range), random(-long_range, long_range)); + } + flyTowards(ai->order_target_location); + } }else{ - auto diff = owner->getOrderTargetLocation() - owner->getPosition(); - if (glm::length2(diff) < 1000.0f*1000.0f) - owner->orderRoamingAt(glm::vec2(random(-long_range, long_range), random(-long_range, long_range))); - flyTowards(owner->getOrderTargetLocation()); + auto tubes = owner.getComponent(); + if (tubes && tubes->mounts.size() > 0) + { + // Find a station which can re-stock our weapons. + auto new_target = findBestMissileRestockTarget(ot->getPosition(), long_range); + if (new_target) + { + ai->orders = AIOrder::Retreat; + ai->order_target = new_target; + }else{ + auto diff = ai->order_target_location - ot->getPosition(); + if (glm::length2(diff) < 1000.0f*1000.0f) { + ai->orders = AIOrder::Roaming; + ai->order_target_location = glm::vec2(random(-long_range, long_range), random(-long_range, long_range)); + } + flyTowards(ai->order_target_location); + } + }else{ + pathPlanner.clear(); + } } - }else{ - pathPlanner.clear(); } break; - case AI_StandGround: //Keep current position, do not fly away, but attack nearby targets. + case AIOrder::StandGround: //Keep current position, do not fly away, but attack nearby targets. pathPlanner.clear(); break; - case AI_FlyTowards: //Fly towards [order_target_location], attacking enemies that get too close, but disengage and continue when enemy is too far. - case AI_FlyTowardsBlind: //Fly towards [order_target_location], not attacking anything - flyTowards(owner->getOrderTargetLocation()); - if (glm::length2(owner->getPosition() - owner->getOrderTargetLocation()) < owner->getRadius()*owner->getRadius()) - { - if (owner->getOrder() == AI_FlyTowards) - owner->orderDefendLocation(owner->getOrderTargetLocation()); - else - owner->orderIdle(); + case AIOrder::FlyTowards: //Fly towards [order_target_location], attacking enemies that get too close, but disengage and continue when enemy is too far. + case AIOrder::FlyTowardsBlind: //Fly towards [order_target_location], not attacking anything + flyTowards(ai->order_target_location); + if (auto ot = owner.getComponent()) { + if (glm::length2(ot->getPosition() - ai->order_target_location) < radius*radius) + { + if (ai->orders == AIOrder::FlyTowards) + ai->orders = AIOrder::DefendLocation; + else + ai->orders = AIOrder::Idle; + } } break; - case AI_DefendLocation: //Defend against enemies getting close to [order_target_location] + case AIOrder::DefendLocation: //Defend against enemies getting close to [order_target_location] + if (auto ot = owner.getComponent()) { - glm::vec2 target_position = owner->getOrderTargetLocation(); - target_position += vec2FromAngle(vec2ToAngle(target_position - owner->getPosition()) + 170.0f) * 1500.0f; + glm::vec2 target_position = ai->order_target_location; + target_position += vec2FromAngle(vec2ToAngle(target_position - ot->getPosition()) + 170.0f) * 1500.0f; flyTowards(target_position); } break; - case AI_DefendTarget: //Defend against enemies getting close to [order_target] (falls back to AI_Roaming if the target is destroyed) - if (owner->getOrderTarget()) - { - auto target_position = owner->getOrderTarget()->getPosition(); - float circle_distance = 2000.0f + owner->getOrderTarget()->getRadius() * 2.0f + owner->getRadius() * 2.0f; - target_position += vec2FromAngle(vec2ToAngle(target_position - owner->getPosition()) + 170.0f) * circle_distance; - flyTowards(target_position); + case AIOrder::DefendTarget: //Defend against enemies getting close to [order_target] (falls back to AIOrder::Roaming if the target is destroyed) + if (auto tt = ai->order_target.getComponent()) + { + if (auto ot = owner.getComponent()) { + auto target_position = tt->getPosition(); + float circle_distance = 3000.0f; + target_position += vec2FromAngle(vec2ToAngle(target_position - ot->getPosition()) + 170.0f) * circle_distance; + flyTowards(target_position); + } }else{ - owner->orderRoaming(); //We pretty much lost our defending target, so just start roaming. + ai->orders = AIOrder::Roaming; //We pretty much lost our defending target, so just start roaming. } break; - case AI_FlyFormation: //Fly [order_target_location] offset from [order_target]. Allows for nicely flying in formation. - if (owner->getOrderTarget()) + case AIOrder::FlyFormation: //Fly [order_target_location] offset from [order_target]. Allows for nicely flying in formation. + if (ai->order_target) { - flyFormation(owner->getOrderTarget(), owner->getOrderTargetLocation()); + flyFormation(ai->order_target, ai->order_target_location); }else{ - owner->orderRoaming(); + ai->orders = AIOrder::Roaming; } break; - case AI_Attack: //Attack [order_target] very specificly. + case AIOrder::Attack: //Attack [order_target] very specificly. pathPlanner.clear(); break; - case AI_Retreat: - if ((owner->docking_state == DS_Docked) && (owner->getOrderTarget()) && P(owner->getOrderTarget())) + case AIOrder::Retreat: + if ((docking_port && docking_port->state == DockingPort::State::Docked && docking_port->target) && ai->order_target) { - P target = owner->getOrderTarget(); + auto bay = docking_port->target.getComponent(); bool allow_undock = true; - if (target->restocks_missiles_docked) - { - for(int n = 0; n < MW_Count; n++) + if (bay) { + if (bay->flags & DockingBay::RestockMissiles) { - if (owner->weapon_storage[n] < owner->weapon_storage_max[n]) - { - allow_undock = false; - break; + auto tubes = owner.getComponent(); + if (tubes) { + for(int n = 0; n < MW_Count; n++) + { + if (tubes->storage[n] < tubes->storage_max[n]) + { + allow_undock = false; + break; + } + } } } - } - if (allow_undock && target->repair_docked && (owner->hull_strength < owner->hull_max)) - { - allow_undock = false; + if (bay->flags & DockingBay::Repair) + { + auto hull = owner.getComponent(); + if (hull && hull->current < hull->max) + allow_undock = false; + } } if (allow_undock) { - owner->orderRoaming(); //deletes order_target + ai->orders = AIOrder::Roaming; break; } - }else{ - P new_target = findBestMissileRestockTarget(owner->getPosition(), relay_range); + }else if (auto ot = owner.getComponent()) { + auto new_target = findBestMissileRestockTarget(ot->getPosition(), relay_range); if (new_target) { - owner->orderRetreat(new_target); + ai->orders = AIOrder::Retreat; + ai->order_target = new_target; } } [[fallthrough]]; // continue with docking or roaming - case AI_Dock: //Dock with [order_target] - if (owner->getOrderTarget()) + case AIOrder::Dock: //Dock with [order_target] + if (ai->order_target && docking_port) { - if (owner->docking_state == DS_NotDocking || owner->docking_target != owner->getOrderTarget()) + if (docking_port->state == DockingPort::State::NotDocking || docking_port->target != ai->order_target) { - auto target_position = owner->getOrderTarget()->getPosition(); - auto diff = owner->getPosition() - target_position; - float dist = glm::length(diff); - if (dist < 600 + owner->getOrderTarget()->getRadius()) - { - owner->requestDock(owner->getOrderTarget()); - }else{ - target_position += (diff / dist) * 500.0f; - flyTowards(target_position); + auto ott = ai->order_target.getComponent(); + auto ot = owner.getComponent(); + if (ot && ott) { + auto target_position = ott->getPosition(); + auto diff = ot->getPosition() - target_position; + float dist = glm::length(diff); + auto target_radius = 0.0f; + if (auto physics = ai->order_target.getComponent()) + target_radius = physics->getSize().x; + if (dist < 600 + target_radius) + { + DockingSystem::requestDock(owner, ai->order_target); + }else{ + target_position += (diff / dist) * 500.0f; + flyTowards(target_position); + } } } }else{ - owner->orderRoaming(); //Nothing to dock, just fall back to roaming. + ai->orders = AIOrder::Roaming; //Nothing to dock, just fall back to roaming. } break; } } -void ShipAI::runAttack(P target) +void ShipAI::runAttack(sp::ecs::Entity target) { + auto ai = owner.getComponent(); + if (!ai) return; + auto ot = owner.getComponent(); + if (!ot) return; + auto tt = target.getComponent(); + if (!tt) return; float attack_distance = 4000.0; if (has_missiles && best_missile_type == MW_HVLI) attack_distance = 2500.0; if (has_beams) attack_distance = beam_weapon_range * 0.7f; - auto position_diff = target->getPosition() - owner->getPosition(); + auto position_diff = tt->getPosition() - ot->getPosition(); float distance = glm::length(position_diff); // missile attack if (distance < 4500 && has_missiles) { - for(int n=0; nweapon_tube_count; n++) + auto tubes = owner.getComponent(); + for(auto& tube : tubes->mounts) { - if (owner->weapon_tube[n].isLoaded() && missile_fire_delay <= 0.0f) + if (tube.state == MissileTubes::MountPoint::State::Loaded && missile_fire_delay <= 0.0f) { - float target_angle = calculateFiringSolution(target, n); + float target_angle = calculateFiringSolution(target, tube); if (target_angle != std::numeric_limits::infinity()) { - owner->weapon_tube[n].fire(target_angle); - missile_fire_delay = owner->weapon_tube[n].getLoadTimeConfig() / owner->weapon_tube_count / 2.0f; + MissileSystem::fire(owner, tube, target_angle, target); + missile_fire_delay = tube.load_time / tubes->mounts.size() / 2.0f; } } } } - if (owner->getOrder() == AI_StandGround) + if (ai->orders == AIOrder::StandGround) { - owner->target_rotation = vec2ToAngle(position_diff); + auto thrusters = owner.getComponent(); + if (thrusters) thrusters->target = vec2ToAngle(position_diff); }else{ if (weapon_direction == EWeaponDirection::Side || weapon_direction == EWeaponDirection::Left || weapon_direction == EWeaponDirection::Right) { //We have side beams, find out where we want to attack from. - auto target_position = target->getPosition(); - auto diff = target_position - owner->getPosition(); + auto target_position = tt->getPosition(); + auto diff = target_position - ot->getPosition(); float angle = vec2ToAngle(diff); - if ((weapon_direction == EWeaponDirection::Side && angleDifference(angle, owner->getRotation()) > 0) || weapon_direction == EWeaponDirection::Left) + if ((weapon_direction == EWeaponDirection::Side && angleDifference(angle, ot->getRotation()) > 0) || weapon_direction == EWeaponDirection::Left) angle += 160; else angle -= 160; - target_position += vec2FromAngle(angle) * (attack_distance + target->getRadius()); + auto target_radius = 0.0f; + if (auto physics = target.getComponent()) + target_radius = physics->getSize().x; + target_position += vec2FromAngle(angle) * (attack_distance + target_radius); flyTowards(target_position, 0); }else{ - flyTowards(target->getPosition(), attack_distance); + flyTowards(tt->getPosition(), attack_distance); } } } void ShipAI::flyTowards(glm::vec2 target, float keep_distance) { - pathPlanner.plan(owner->getPosition(), target); + auto ot = owner.getComponent(); + if (!ot) return; + auto my_radius = 300.0f; + if (auto physics = owner.getComponent()) my_radius = physics->getSize().x; + pathPlanner.plan(my_radius, ot->getPosition(), target); if (pathPlanner.route.size() > 0) { - if (owner->docking_state == DS_Docked) - owner->requestUndock(); + auto docking_port = owner.getComponent(); + if (docking_port && docking_port->state == DockingPort::State::Docked) + DockingSystem::requestUndock(owner); - auto diff = pathPlanner.route[0] - owner->getPosition(); + auto diff = pathPlanner.route[0] - ot->getPosition(); float distance = glm::length(diff); //Normal flying towards target code - owner->target_rotation = vec2ToAngle(diff); - float rotation_diff = fabs(angleDifference(owner->target_rotation, owner->getRotation())); + auto target_rotation = vec2ToAngle(diff); + auto thrusters = owner.getComponent(); + if (thrusters) thrusters->target = target_rotation; + float rotation_diff = fabs(angleDifference(target_rotation, ot->getRotation())); - if (owner->has_warp_drive && rotation_diff < 30.0f && distance > 2000.0f) - { - owner->warp_request = 1.0; - }else{ - owner->warp_request = 0.0; - } - if (distance > 10000 && owner->has_jump_drive && owner->jump_delay <= 0.0f && owner->jump_drive_charge >= owner->jump_drive_max_distance) + auto warp = owner.getComponent(); + if (warp) + warp->request = (rotation_diff < 30.0f && distance > 2000.0f) ? 1.0f : 0.0f; + auto jump = owner.getComponent(); + if (distance > 10000 && jump && jump->delay <= 0.0f && jump->charge >= jump->max_distance) { if (rotation_diff < 1.0f) { - float jump = distance; + float jump_distance = distance; if (pathPlanner.route.size() < 2) { - jump -= 3000; + jump_distance -= 3000; if (has_missiles) - jump -= 5000; + jump_distance -= 5000; } - if (owner->jump_drive_max_distance == 50000) + if (jump->max_distance == 50000) { //If the ship has the default max jump drive distance of 50k, then limit our jumps to 15k, else we limit ourselves to whatever the ship layout is with a bit margin. - if (jump > 15000) - jump = 15000; + if (jump_distance > 15000) + jump_distance = 15000; }else{ - if (jump > owner->jump_drive_max_distance - 2000) - jump = owner->jump_drive_max_distance - 2000; + if (jump_distance > jump->max_distance - 2000) + jump_distance = jump->max_distance - 2000; } - jump += random(-1500, 1500); - owner->initializeJump(jump); + jump_distance += random(-1500, 1500); + JumpSystem::initializeJump(owner, jump_distance); } } if (pathPlanner.route.size() > 1) keep_distance = 0.0; - // setImpulseRequest only if impulse_max_speed is greater than 0.0 - if (owner->impulse_max_speed > 0.0f) - { - if (distance > keep_distance + owner->impulse_max_speed * 5.0f) - owner->setImpulseRequest(1.0f); + auto impulse = owner.getComponent(); + if (impulse && impulse->max_speed_forward > 0.0f) { + if (distance > keep_distance + impulse->max_speed_forward * 5.0f) + impulse->request = 1.0f; else - owner->setImpulseRequest((distance - keep_distance) / owner->impulse_max_speed * 5.0f); - - if (rotation_diff > 90.0f) - owner->setImpulseRequest(-owner->impulse_request); - else if (rotation_diff < 45.0f) - owner->setImpulseRequest(owner->impulse_request * (1.0f - ((rotation_diff - 45.0f) / 45.0f))); + impulse->request = (distance - keep_distance) / impulse->max_speed_forward * 5.0f; + if (rotation_diff > 90) + impulse->request = -impulse->request; + else if (rotation_diff < 45) + impulse->request *= 1.0f - ((rotation_diff - 45.0f) / 45.0f); } } } -void ShipAI::flyFormation(P target, glm::vec2 offset) +void ShipAI::flyFormation(sp::ecs::Entity target, glm::vec2 offset) { - auto target_position = target->getPosition() + rotateVec2(owner->getOrderTargetLocation(), target->getRotation()); - pathPlanner.plan(owner->getPosition(), target_position); + auto ai = owner.getComponent(); + if (!ai) return; + auto ot = owner.getComponent(); + if (!ot) return; + auto tt = target.getComponent(); + if (!tt) return; + auto target_position = tt->getPosition() + rotateVec2(ai->order_target_location, tt->getRotation()); + auto my_radius = 300.0f; + if (auto physics = owner.getComponent()) my_radius = physics->getSize().x; + pathPlanner.plan(my_radius, ot->getPosition(), target_position); + + auto impulse = owner.getComponent(); + if (!impulse) return; if (pathPlanner.route.size() == 1) { - if (owner->docking_state == DS_Docked) - owner->requestUndock(); + auto thrusters = owner.getComponent(); + auto docking_port = owner.getComponent(); + if (docking_port && docking_port->state == DockingPort::State::Docked) + DockingSystem::requestUndock(owner); - auto diff = target_position - owner->getPosition(); + auto diff = target_position - ot->getPosition(); float distance = glm::length(diff); //Formation flying code - float r = owner->getRadius() * 5.0f; - owner->target_rotation = vec2ToAngle(diff); + float r = 100.0f; + if (auto physics = owner.getComponent()) + r = physics->getSize().x * 5.0f; + auto target_rotation = vec2ToAngle(diff); if (distance > r * 3) { flyTowards(target_position); } else if (distance > r) { - float angle_diff = angleDifference(owner->target_rotation, owner->getRotation()); + float angle_diff = angleDifference(target_rotation, ot->getRotation()); if (angle_diff > 10.0f) - owner->setImpulseRequest(0.0f); + impulse->request = 0.0f; else if (angle_diff > 5.0f) - owner->setImpulseRequest((10.0f - angle_diff) / 5.0f); + impulse->request = (10.0f - angle_diff) / 5.0f; else - owner->setImpulseRequest(1.0f); + impulse->request = 1.0f; }else{ if (distance > r / 2.0f) { - owner->target_rotation += angleDifference(owner->target_rotation, target->getRotation()) * (1.0f - distance / r); - owner->setImpulseRequest(distance / r); + target_rotation += angleDifference(target_rotation, tt->getRotation()) * (1.0f - distance / r); + impulse->request = distance / r; }else{ - owner->target_rotation = target->getRotation(); - owner->setImpulseRequest(0.0f); + target_rotation = tt->getRotation(); + impulse->request = 0.0f; } } + if (thrusters) thrusters->target = target_rotation; }else{ flyTowards(target_position); } } -P ShipAI::findBestTarget(glm::vec2 position, float radius) +sp::ecs::Entity ShipAI::findBestTarget(glm::vec2 position, float radius) { float target_score = 0.0; - PVector objectList = CollisionManager::queryArea(position - glm::vec2(radius, radius), position + glm::vec2(radius, radius)); - P target; - auto owner_position = owner->getPosition(); - foreach(Collisionable, obj, objectList) + sp::ecs::Entity target; + auto ot = owner.getComponent(); + auto owner_position = ot->getPosition(); + for(auto entity : sp::CollisionSystem::queryArea(position - glm::vec2(radius, radius), position + glm::vec2(radius, radius))) { - P space_object = obj; - if (!space_object || !space_object->canBeTargetedBy(owner) || !owner->isEnemy(space_object) || space_object == target) + if (!entity.hasComponent() || Faction::getRelation(owner, entity) != FactionRelation::Enemy || entity == target) continue; - if (space_object->canHideInNebula() && Nebula::blockedByNebula(owner_position, space_object->getPosition(), owner->getShortRangeRadarRange())) + if (RadarBlockSystem::isRadarBlockedFrom(owner_position, entity, short_range)) continue; - float score = targetScore(space_object); + float score = targetScore(entity); if (score == std::numeric_limits::min()) continue; if (!target || score > target_score) { - target = space_object; + target = entity; target_score = score; } } return target; } -float ShipAI::targetScore(P target) +float ShipAI::targetScore(sp::ecs::Entity target) { - auto position_difference = target->getPosition() - owner->getPosition(); + auto impulse = owner.getComponent(); + auto ot = owner.getComponent(); + if (!ot) return std::numeric_limits::min(); + auto tt = target.getComponent(); + if (!tt) return std::numeric_limits::min(); + auto position_difference = tt->getPosition() - ot->getPosition(); float distance = glm::length(position_difference); //auto position_difference_normal = position_difference / distance; //float rel_velocity = dot(target->getVelocity(), position_difference_normal) - dot(getVelocity(), position_difference_normal); - float angle_difference = angleDifference(owner->getRotation(), vec2ToAngle(position_difference)); - float score = -distance - std::abs(angle_difference / owner->turn_speed * owner->impulse_max_speed) * 1.5f; - if (P(target)) - { - score -= 5000; - } - if (P(target)) + float angle_difference = angleDifference(ot->getRotation(), vec2ToAngle(position_difference)); + auto thrusters = owner.getComponent(); + float score = -distance - std::abs(angle_difference / (thrusters ? thrusters->speed : 10.0f) * (impulse ? impulse->max_speed_forward : 0.0f)) * 1.5f; + if (target.hasComponent()) + score += 2500; + if (target.hasComponent()) + score += 2500; + if (target.hasComponent()) + score -= 1500; + if (target.hasComponent()) { score -= 10000; if (distance > 5000) @@ -706,19 +799,20 @@ float ShipAI::targetScore(P target) if (distance < beam_weapon_range) { - for(int n=0; nbeam_weapons[n].getRange()) - { - if (fabs(angleDifference(angle_difference, owner->beam_weapons[n].getDirection())) < owner->beam_weapons[n].getArc() / 2.0f) - score += 1000; + auto beamsystem = owner.getComponent(); + if (beamsystem) { + for(auto& mount : beamsystem->mounts) { + if (distance < mount.range) { + if (fabs(angleDifference(angle_difference, mount.direction)) < mount.arc / 2.0f) + score += 1000; + } } } } return score; } -bool ShipAI::betterTarget(P new_target, P current_target) +bool ShipAI::betterTarget(sp::ecs::Entity new_target, sp::ecs::Entity current_target) { float new_score = targetScore(new_target); float current_score = targetScore(current_target); @@ -735,40 +829,39 @@ bool ShipAI::betterTarget(P new_target, P current_targ return false; } -float ShipAI::calculateFiringSolution(P target, int tube_index) +float ShipAI::calculateFiringSolution(sp::ecs::Entity target, const MissileTubes::MountPoint& tube) { - // Update ranges before calculating - short_range = owner->getShortRangeRadarRange(); - // Never fire missiles at scan probes. - if (P(target)) - { + if (target.hasComponent() && target.hasComponent()) return std::numeric_limits::infinity(); - } + auto tt = target.getComponent(); + if (!tt) return std::numeric_limits::infinity(); + auto ot = owner.getComponent(); + if (!ot) return std::numeric_limits::infinity(); - EMissileWeapons type = owner->weapon_tube[tube_index].getLoadType(); + EMissileWeapons type = tube.type_loaded; // Search if a non-enemy ship might be damaged by a missile attack on a // line of fire within our short-range radar range. - auto target_position = target->getPosition(); - const float target_distance = glm::length(owner->getPosition() - target_position); + auto target_position = tt->getPosition(); + const float target_distance = glm::length(ot->getPosition() - target_position); const float search_distance = std::min(short_range, target_distance + 500.0f); - const float target_angle = vec2ToAngle(target_position - owner->getPosition()); + const float target_angle = vec2ToAngle(target_position - ot->getPosition()); const float search_angle = 5.0; // Verify if missle can be fired safely - PVector objectList = CollisionManager::queryArea(owner->getPosition() - glm::vec2(search_distance, search_distance), owner->getPosition() + glm::vec2(search_distance, search_distance)); - foreach(Collisionable, c, objectList) + for(auto entity : sp::CollisionSystem::queryArea(ot->getPosition() - glm::vec2(search_distance, search_distance), ot->getPosition() + glm::vec2(search_distance, search_distance))) { - P obj = c; - if (obj && !obj->isEnemy(owner) && (P(obj) || P(obj))) + if (Faction::getRelation(owner, entity) != FactionRelation::Enemy && entity.hasComponent() && (entity.hasComponent() || entity.hasComponent())) { - // Ship in research triangle - const auto owner_to_obj = obj->getPosition() - owner->getPosition(); - const float heading_to_obj = vec2ToAngle(owner_to_obj); - const float angle_from_heading_to_target = std::abs(angleDifference(heading_to_obj, target_angle)); - if(angle_from_heading_to_target < search_angle){ - return std::numeric_limits::infinity(); + if (auto t = entity.getComponent()) { + // Ship in research triangle + const auto owner_to_obj = t->getPosition() - ot->getPosition(); + const float heading_to_obj = vec2ToAngle(owner_to_obj); + const float angle_from_heading_to_target = std::abs(angleDifference(heading_to_obj, target_angle)); + if (angle_from_heading_to_target < search_angle) { + return std::numeric_limits::infinity(); + } } } } @@ -777,19 +870,23 @@ float ShipAI::calculateFiringSolution(P target, int tube_index) { const MissileWeaponData& data = MissileWeaponData::getDataFor(type); - auto target_position = target->getPosition(); - float target_angle = vec2ToAngle(target_position - owner->getPosition()); - float fire_angle = owner->getRotation() + owner->weapon_tube[tube_index].getDirection(); + auto target_position = tt->getPosition(); + float target_angle = vec2ToAngle(target_position - ot->getPosition()); + float fire_angle = ot->getRotation() + tube.direction; //HVLI missiles do not home or turn. So use a different targeting mechanism. float angle_diff = angleDifference(target_angle, fire_angle); //Target is moving. Estimate where he will be when the missile hits. float fly_time = target_distance / data.speed; - target_position += target->getVelocity() * fly_time; + if (auto physics = target.getComponent()) + target_position += physics->getVelocity() * fly_time; //If our "error" of hitting is less then double the radius of the target, fire. - if (std::abs(angle_diff) < 80.0f && target_distance * glm::degrees(tanf(fabs(angle_diff))) < target->getRadius() * 2.0f) + auto target_radius = 100.0f; + if (auto physics = target.getComponent()) + target_radius = physics->getSize().x; + if (std::abs(angle_diff) < 80.0f && target_distance * glm::degrees(tanf(fabs(angle_diff))) < target_radius * 2.0f) return fire_angle; return std::numeric_limits::infinity(); @@ -797,19 +894,19 @@ float ShipAI::calculateFiringSolution(P target, int tube_index) if (type == MW_Nuke || type == MW_EMP) { - auto target_position = target->getPosition(); + auto target_position = tt->getPosition(); //Check if we can sort of safely fire an Nuke/EMP. The target needs to be clear of friendly/neutrals. float safety_radius = 1100; - if (glm::length2(target_position - owner->getPosition()) < safety_radius*safety_radius) + if (glm::length2(target_position - ot->getPosition()) < safety_radius*safety_radius) return std::numeric_limits::infinity(); - PVector object_list = CollisionManager::queryArea(target->getPosition() - glm::vec2(safety_radius, safety_radius), target->getPosition() + glm::vec2(safety_radius, safety_radius)); - foreach(Collisionable, c, object_list) + for(auto entity : sp::CollisionSystem::queryArea(tt->getPosition() - glm::vec2(safety_radius, safety_radius), tt->getPosition() + glm::vec2(safety_radius, safety_radius))) { - P obj = c; - if (obj && !obj->isEnemy(owner) && (P(obj) || P(obj))) + if (Faction::getRelation(owner, entity) != FactionRelation::Enemy && entity.hasComponent() && (entity.hasComponent() || entity.getComponent())) { - if (glm::length(obj->getPosition() - owner->getPosition()) < safety_radius - obj->getRadius()) + auto physics = entity.getComponent(); + auto et = entity.getComponent(); + if (physics && et && glm::length(et->getPosition() - ot->getPosition()) < safety_radius - physics->getSize().x) { return std::numeric_limits::infinity(); } @@ -818,30 +915,35 @@ float ShipAI::calculateFiringSolution(P target, int tube_index) } //Use the general weapon tube targeting to get the final firing solution. - return owner->weapon_tube[tube_index].calculateFiringSolution(target); + return MissileSystem::calculateFiringSolution(owner, tube, target); } -P ShipAI::findBestMissileRestockTarget(glm::vec2 position, float radius) +sp::ecs::Entity ShipAI::findBestMissileRestockTarget(glm::vec2 position, float radius) { + auto port = owner.getComponent(); + if (!port) + return {}; // Check each object within the given radius. If it's friendly, we can dock // to it, and it can restock our missiles, then select it. float target_score = 0.0; - PVector objectList = CollisionManager::queryArea(position - glm::vec2(radius, radius), position + glm::vec2(radius, radius)); - P target; - auto owner_position = owner->getPosition(); - foreach(Collisionable, obj, objectList) - { - P space_object = obj; - if (!space_object || !owner->isFriendly(space_object) || space_object == target) + sp::ecs::Entity target; + auto owner_transform = owner.getComponent(); + if (!owner_transform) + return {}; + auto owner_position = owner_transform->getPosition(); + for(auto [entity, dockingbay, transform, impulse] : sp::ecs::Query()) + { + if (Faction::getRelation(owner, entity) != FactionRelation::Friendly) continue; - if (space_object->canBeDockedBy(owner) == DockStyle::None || !space_object->canRestockMissiles()) + if (port->canDockOn(dockingbay) == DockingStyle::None || !(dockingbay.flags & DockingBay::RestockMissiles)) continue; //calculate score - auto position_difference = space_object->getPosition() - owner_position; + auto position_difference = transform.getPosition() - owner_position; float distance = glm::length(position_difference); - float angle_difference = angleDifference(owner->getRotation(), vec2ToAngle(position_difference)); - float score = -distance - std::abs(angle_difference / owner->turn_speed * owner->impulse_max_speed) * 1.5f; - if (P(space_object)) + float angle_difference = angleDifference(owner_transform->getRotation(), vec2ToAngle(position_difference)); + auto thrusters = owner.getComponent(); + float score = -distance - std::abs(angle_difference / (thrusters ? thrusters->speed : 10.0f) * impulse.max_speed_forward) * 1.5f; + if (entity.hasComponent()) { score -= 5000; } @@ -849,10 +951,9 @@ P ShipAI::findBestMissileRestockTarget(glm::vec2 position, float ra continue; if (!target || score > target_score) { - target = space_object; + target = entity; target_score = score; } } return target; } - diff --git a/src/ai/ai.h b/src/ai/ai.h index 814a6eb324..aafe593364 100644 --- a/src/ai/ai.h +++ b/src/ai/ai.h @@ -2,7 +2,9 @@ #define AI_H #include "nonCopyable.h" -#include "pathPlanner.h" +#include "graphics/renderTarget.h" +#include "systems/pathfinding.h" +#include "components/missiletubes.h" ///Forward declaration class CpuShip; @@ -17,13 +19,13 @@ class ShipAI : sp::NonCopyable /**! * Artificial delay between missile fires. The AI missile fire is 'faked' with this value. */ - float missile_fire_delay; - bool has_missiles; - bool has_beams; - float beam_weapon_range; - float short_range; - float long_range; - float relay_range; + float missile_fire_delay = 0; + bool has_missiles = false; + bool has_beams = false; + float beam_weapon_range = 1000; + float short_range = 5000; + float long_range = 30000; + float relay_range = 60000; enum class EWeaponDirection { @@ -40,9 +42,9 @@ class ShipAI : sp::NonCopyable PathPlanner pathPlanner; public: - CpuShip* owner; + sp::ecs::Entity owner; - ShipAI(CpuShip* owner); + ShipAI(sp::ecs::Entity owner); virtual ~ShipAI() = default; /**! @@ -62,12 +64,12 @@ class ShipAI : sp::NonCopyable virtual void updateWeaponState(float delta); virtual void updateTarget(); virtual void runOrders(); - virtual void runAttack(P target); + virtual void runAttack(sp::ecs::Entity target); virtual void flyTowards(glm::vec2 target, float keep_distance = 100.0); - virtual void flyFormation(P target, glm::vec2 offset); + virtual void flyFormation(sp::ecs::Entity target, glm::vec2 offset); - P findBestTarget(glm::vec2 position, float radius); - float targetScore(P target); + sp::ecs::Entity findBestTarget(glm::vec2 position, float radius); + float targetScore(sp::ecs::Entity target); /**! * Check if new target is better than old target. @@ -75,13 +77,13 @@ class ShipAI : sp::NonCopyable * \param current_target * \return bool True if the new target is 'better' */ - bool betterTarget(P new_target, P current_target); + bool betterTarget(sp::ecs::Entity new_target, sp::ecs::Entity current_target); /**! * Used for missiles, as they require some intelligence to fire. */ - float calculateFiringSolution(P target, int tube_index); - P findBestMissileRestockTarget(glm::vec2 position, float radius); + float calculateFiringSolution(sp::ecs::Entity target, const MissileTubes::MountPoint& tube); + sp::ecs::Entity findBestMissileRestockTarget(glm::vec2 position, float radius); static float getMissileWeaponStrength(EMissileWeapons type) { diff --git a/src/ai/aiFactory.cpp b/src/ai/aiFactory.cpp index db123165c9..cafdca9943 100644 --- a/src/ai/aiFactory.cpp +++ b/src/ai/aiFactory.cpp @@ -15,5 +15,5 @@ shipAIFactoryFunc_t ShipAIFactory::getAIFactory(string name) if (f->name == name) return f->func; LOG(ERROR) << "AI not found: " << name; - return NULL; + return nullptr; } diff --git a/src/ai/aiFactory.h b/src/ai/aiFactory.h index 25218f1923..58bc151c54 100644 --- a/src/ai/aiFactory.h +++ b/src/ai/aiFactory.h @@ -2,12 +2,12 @@ #define AI_FACTORY_H #include "engine.h" +#include "ecs/entity.h" class ShipAI; -class CpuShip; class ShipAIFactory; -typedef ShipAI* (*shipAIFactoryFunc_t)(CpuShip* owner); +typedef std::unique_ptr (*shipAIFactoryFunc_t)(sp::ecs::Entity owner); class ShipAIFactory { @@ -23,7 +23,7 @@ class ShipAIFactory static shipAIFactoryFunc_t getAIFactory(string name); }; #define REGISTER_SHIP_AI(c, n) \ - static ShipAI* c ## _factory_function (CpuShip* owner) { return new c(owner); } \ + static std::unique_ptr c ## _factory_function (sp::ecs::Entity owner) { return std::make_unique(owner); } \ ShipAIFactory c ## _factory(n, c ## _factory_function ) #endif//AI_FACTORY_H diff --git a/src/ai/evasionAI.cpp b/src/ai/evasionAI.cpp index f15f4db1d7..51b8e4587d 100644 --- a/src/ai/evasionAI.cpp +++ b/src/ai/evasionAI.cpp @@ -1,13 +1,23 @@ -#include "spaceObjects/cpuShip.h" -#include "spaceObjects/nebula.h" #include "ai/evasionAI.h" #include "ai/aiFactory.h" #include "random.h" +#include "systems/collision.h" +#include "components/ai.h" +#include "components/docking.h" +#include "components/missiletubes.h" +#include "components/beamweapon.h" +#include "components/collision.h" +#include "components/jumpdrive.h" +#include "components/warpdrive.h" +#include "components/impulse.h" +#include "components/target.h" +#include "components/faction.h" +#include "systems/radarblock.h" REGISTER_SHIP_AI(EvasionAI, "evasion"); -EvasionAI::EvasionAI(CpuShip* owner) +EvasionAI::EvasionAI(sp::ecs::Entity owner) : ShipAI(owner) { evasion_calculation_delay = 0.0; @@ -29,25 +39,32 @@ void EvasionAI::run(float delta) // @TODO: consider jump drives void EvasionAI::runOrders() { + auto ai = owner.getComponent(); + if (!ai) return; + auto docking_port = owner.getComponent(); //When we are not attacking a target, follow orders - switch(owner->getOrder()) + switch(ai->orders) { - case AI_FlyTowards: + case AIOrder::FlyTowards: if (!evadeIfNecessary()) { ShipAI::runOrders(); } break; - case AI_Dock: - if (owner->getOrderTarget() && owner->docking_state == DS_NotDocking) + case AIOrder::Dock: + if (ai->order_target && (!docking_port || docking_port->state == DockingPort::State::NotDocking)) { - auto target_position = owner->getOrderTarget()->getPosition(); - auto diff = owner->getPosition() - target_position; - float dist = glm::length(diff); - if (dist < 3000 + owner->getOrderTarget()->getRadius()) - { - // if close to the docking target: make a run for it - return ShipAI::runOrders(); + auto ot = ai->order_target.getComponent(); + auto transform = owner.getComponent(); + if (ot && transform) { + auto diff = transform->getPosition() - ot->getPosition(); + float dist = glm::length(diff); + auto physics = ai->order_target.getComponent(); + if (dist < 3000 + (physics ? physics->getSize().x : 0.0f)) + { + // if close to the docking target: make a run for it + return ShipAI::runOrders(); + } } } if (!evadeIfNecessary()) @@ -55,7 +72,7 @@ void EvasionAI::runOrders() ShipAI::runOrders(); } break; - case AI_FlyTowardsBlind: // flying blind means ignoring enemies + case AIOrder::FlyTowardsBlind: // flying blind means ignoring enemies default: ShipAI::runOrders(); } @@ -71,29 +88,31 @@ bool EvasionAI::evadeIfNecessary() return is_evading; } evasion_calculation_delay = random(0.25, 0.5); + auto transform = owner.getComponent(); + if (!transform) + return false; is_evading = false; - auto position = owner->getPosition(); + auto position = transform->getPosition(); float scan_radius = 9000.0; - PVector objectList = CollisionManager::queryArea(position - glm::vec2(scan_radius, scan_radius), position + glm::vec2(scan_radius, scan_radius)); - // NOT AN OBJECT ON THE PLANE, but it represents an escape vector. // It tracks which direction is the best to run to (angle) and the strength of the desire to go there (distance from origin) glm::vec2 evasion_vector = glm::vec2(0, 0); - foreach(Collisionable, obj, objectList) + for(auto entity : sp::CollisionSystem::queryArea(position - glm::vec2(scan_radius, scan_radius), position + glm::vec2(scan_radius, scan_radius))) { - P ship = obj; - if (!ship || !owner->isEnemy(ship)) + if (Faction::getRelation(owner, entity) != FactionRelation::Enemy) continue; - if (ship->canHideInNebula() && Nebula::blockedByNebula(position, ship->getPosition(), owner->getShortRangeRadarRange())) + if (RadarBlockSystem::isRadarBlockedFrom(position, entity, short_range)) continue; - float score = evasionDangerScore(ship, scan_radius); + auto et = entity.getComponent(); + if (!et) continue; + float score = evasionDangerScore(entity, scan_radius); if (score == std::numeric_limits::min()) continue; - auto vec = position - ship->getPosition(); + auto vec = position - et->getPosition(); vec = glm::normalize(vec) * score; evasion_vector += vec; } @@ -102,17 +121,18 @@ bool EvasionAI::evadeIfNecessary() { // have a bias to your original target. // this makes ships fly around enemies rather than straight running from them - auto target_position = owner->getOrderTargetLocation(); - if (owner->getOrderTarget()) + auto ai = owner.getComponent(); + auto target_position = ai->order_target_location; + if (auto t = ai->order_target.getComponent()) { - target_position = owner->getOrderTarget()->getPosition(); + target_position = t->getPosition(); } float distance = 12000.0f; // should be big enough for jump drive to be considered if (glm::length(target_position) > 0.0f) { // ships with warp or jump drives have a tendency to fly past enemies quickly - evasion_vector += glm::normalize(target_position - position) * (owner->hasWarpDrive() || owner->hasJumpDrive() ? 15.0f : 5.0f); + evasion_vector += glm::normalize(target_position - position) * ((owner.hasComponent() || owner.hasComponent()) ? 15.0f : 5.0f); distance = std::min(distance, glm::length(target_position - position)); } @@ -126,30 +146,29 @@ bool EvasionAI::evadeIfNecessary() } // calculate how much of a threat an enemy ship is -float EvasionAI::evasionDangerScore(P ship, float scan_radius) +float EvasionAI::evasionDangerScore(sp::ecs::Entity ship, float scan_radius) { float enemy_max_beam_range = 0.0; float enemy_beam_dps = 0.0; float enemy_missile_strength = 0.0; - for(int n=0; nweapon_tube_count; n++) - { - WeaponTube& tube = ship->weapon_tube[n]; - if (!tube.isEmpty()) + auto tubes = ship.getComponent(); + if (tubes) { + for(auto& tube : tubes->mounts) { - enemy_missile_strength += getMissileWeaponStrength(tube.getLoadType()); + if (tube.state != MissileTubes::MountPoint::State::Empty) + enemy_missile_strength += getMissileWeaponStrength(tube.type_loaded); } } - for(int n=0; nbeam_weapons[n]; - if (beam.getRange() > 0) - { - if (beam.getRange() > enemy_max_beam_range) - enemy_max_beam_range = beam.getRange(); - if (beam.getCycleTime() > 0.0f) - enemy_beam_dps += beam.getDamage() / beam.getCycleTime(); + auto beamsystem = ship.getComponent(); + if (beamsystem) { + for(auto& mount : beamsystem->mounts) { + if (mount.range > 0.0f) { + enemy_max_beam_range = std::max(enemy_max_beam_range, mount.range); + if (mount.cycle_time > 0.0f) + enemy_beam_dps += mount.damage / mount.cycle_time; + } } } @@ -158,10 +177,17 @@ float EvasionAI::evasionDangerScore(P ship, float scan_radius) // enemy is not a threat return 0.0; } + auto st = ship.getComponent(); + if (!st) return 0.0; + auto ot = owner.getComponent(); + if (!ot) return 0.0; - auto position_difference = ship->getPosition() - owner->getPosition(); + auto position_difference = st->getPosition() - ot->getPosition(); float distance = glm::length(position_difference); - enemy_max_beam_range += ship->getRadius() + owner->getRadius(); + auto physics = owner.getComponent(); + if (physics) enemy_max_beam_range += physics->getSize().x; + physics = ship.getComponent(); + if (physics) enemy_max_beam_range += physics->getSize().x; float danger = 0.0; if (enemy_missile_strength > 0.0f) @@ -175,10 +201,13 @@ float EvasionAI::evasionDangerScore(P ship, float scan_radius) danger += enemy_beam_dps * (4*enemy_max_beam_range - std::max(distance, enemy_max_beam_range)) / (3 * enemy_max_beam_range); } - if (std::max(ship->getImpulseMaxSpeed().forward,ship->getImpulseMaxSpeed().reverse) - > std::max(owner->getImpulseMaxSpeed().forward, owner->getImpulseMaxSpeed().reverse)) - danger *= 1.5f; //yes that sound stupid somebody had mounted its forward reactor on reverse but... - if (P(ship->getTarget()) == P(owner)) + if (auto oi = owner.getComponent()) { + if (auto si = ship.getComponent()) { + if (std::max(si->max_speed_forward, si->max_speed_reverse) > std::max(oi->max_speed_forward, oi->max_speed_reverse)) + danger *= 1.5f; //yes that sound stupid somebody had mounted its forward reactor on reverse but... + } + } + if (ship.hasComponent() && ship.getComponent()->entity == owner) danger = (danger + 1) * 2; return danger; } \ No newline at end of file diff --git a/src/ai/evasionAI.h b/src/ai/evasionAI.h index cc44083cc9..d0734ee2a2 100644 --- a/src/ai/evasionAI.h +++ b/src/ai/evasionAI.h @@ -10,14 +10,14 @@ class EvasionAI : public ShipAI bool is_evading; glm::vec2 evasion_location{0, 0}; public: - EvasionAI(CpuShip* owner); + EvasionAI(sp::ecs::Entity owner); virtual bool canSwitchAI() override; virtual void run(float delta) override; virtual void runOrders() override; virtual bool evadeIfNecessary(); - float evasionDangerScore(P ship, float scan_radius); + float evasionDangerScore(sp::ecs::Entity ship, float scan_radius); }; diff --git a/src/ai/fighterAI.cpp b/src/ai/fighterAI.cpp index 364ad26413..822adfaacc 100644 --- a/src/ai/fighterAI.cpp +++ b/src/ai/fighterAI.cpp @@ -1,4 +1,10 @@ -#include "spaceObjects/cpuShip.h" +#include "components/impulse.h" +#include "components/shields.h" +#include "components/missiletubes.h" +#include "components/maneuveringthrusters.h" +#include "components/collision.h" +#include "components/target.h" +#include "systems/missilesystem.h" #include "ai/fighterAI.h" #include "ai/aiFactory.h" #include "random.h" @@ -6,19 +12,19 @@ REGISTER_SHIP_AI(FighterAI, "fighter"); -FighterAI::FighterAI(CpuShip* owner) +FighterAI::FighterAI(sp::ecs::Entity owner) : ShipAI(owner) { - attack_state = dive; + attack_state = State::Dive; timeout = 0.0; aggression = random(0.0, 0.25); } bool FighterAI::canSwitchAI() { - if (owner->getTarget() && (has_missiles || has_beams)) + if (owner.hasComponent() && (has_missiles || has_beams)) { - if (attack_state == dive) + if (attack_state == State::Dive) return false; } return true; @@ -38,72 +44,82 @@ void FighterAI::runOrders() ShipAI::runOrders(); } -void FighterAI::runAttack(P target) +void FighterAI::runAttack(sp::ecs::Entity target) { - auto position_diff = target->getPosition() - owner->getPosition(); + auto transform = owner.getComponent(); + if (!transform) return; + auto tt = target.getComponent(); + if (!tt) return; + auto position_diff = tt->getPosition() - transform->getPosition(); float distance = glm::length(position_diff); + auto shields = owner.getComponent(); + auto target_physics = target.getComponent(); switch(attack_state) { - case dive: - if (distance < 2500 + target->getRadius() && has_missiles) + case State::Dive: + if (distance < 2500 + (target_physics ? target_physics->getSize().x : 0.0f) && has_missiles) { - for(int n=0; nweapon_tube_count; n++) + auto tubes = owner.getComponent(); + for(auto& tube : tubes->mounts) { - if (owner->weapon_tube[n].isLoaded() && missile_fire_delay <= 0.0f) + if (tube.state == MissileTubes::MountPoint::State::Loaded && missile_fire_delay <= 0.0f) { - float target_angle = calculateFiringSolution(target, owner->weapon_tube[n].getLoadType()); + float target_angle = calculateFiringSolution(target, tube); if (target_angle != std::numeric_limits::infinity()) { - owner->weapon_tube[n].fire(target_angle); - missile_fire_delay = owner->weapon_tube[n].getLoadTimeConfig() / owner->weapon_tube_count / 2.0f; + MissileSystem::fire(owner, tube, target_angle, target); + missile_fire_delay = tube.load_time / tubes->mounts.size() / 2.0f; } } } } - flyTowards(target->getPosition(), 500.0); + flyTowards(tt->getPosition(), 500.0); - if (distance < 500 + target->getRadius()) + if (distance < 500 + (target_physics ? target_physics->getSize().x : 0.0f)) { aggression += random(0, 0.05); - attack_state = evade; + attack_state = State::Evade; timeout = 30.0f - std::min(aggression, 1.0f) * 20.0f; float target_dir = vec2ToAngle(position_diff); - float a_diff = angleDifference(target_dir, owner->getRotation()); + float a_diff = angleDifference(target_dir, transform->getRotation()); if (a_diff < 0) evade_direction = target_dir - random(25, 40); else evade_direction = target_dir + random(25, 40); } - if (owner->shield_level[0] < owner->shield_max[0] * (1.0f - aggression)) + if (shields && !shields->entries.empty() && shields->entries[0].level < shields->entries[0].max * (1.0f - aggression)) { - attack_state = recharge; + attack_state = State::Recharge; aggression += random(0.1, 0.25); timeout = 60.0f - std::min(aggression, 1.0f) * 20.0f; } break; - case evade: - if (distance > 2000 + target->getRadius() || timeout <= 0.0f) + case State::Evade: + if (distance > 2500 || timeout <= 0.0f) { - attack_state = dive; + attack_state = State::Dive; } else { - owner->target_rotation = evade_direction; - owner->setImpulseRequest(1.0f); + auto thrusters = owner.getComponent(); + if (thrusters) thrusters->target = evade_direction; + auto impulse = owner.getComponent(); + if (impulse) + impulse->request = 1.0; } break; - case recharge: - if (owner->shield_level[0] > owner->shield_max[0] * 0.9f || timeout <= 0.0f) + case State::Recharge: + if ((shields && !shields->entries.empty() && shields->entries[0].level < shields->entries[0].max * 0.9f) || timeout <= 0.0f) { - attack_state = dive; + attack_state = State::Dive; }else{ - auto target_position = target->getPosition(); - float circle_distance = 2000.0f + target->getRadius() * 2.0f + owner->getRadius() * 2.0f; - target_position += vec2FromAngle(vec2ToAngle(target_position - owner->getPosition()) + 170.0f) * circle_distance; + auto target_position = tt->getPosition(); + float circle_distance = 3000.0f; + target_position += vec2FromAngle(vec2ToAngle(target_position - transform->getPosition()) + 170.0f) * circle_distance; flyTowards(target_position); } break; diff --git a/src/ai/fighterAI.h b/src/ai/fighterAI.h index 8e99805e85..355d78f7fe 100644 --- a/src/ai/fighterAI.h +++ b/src/ai/fighterAI.h @@ -5,17 +5,18 @@ class FighterAI : public ShipAI { - enum + enum class State { - dive, - evade, - recharge - } attack_state; + Dive, + Evade, + Recharge + }; + State attack_state; float timeout; float evade_direction; float aggression; public: - FighterAI(CpuShip* owner); + FighterAI(sp::ecs::Entity owner); /**! * Are we allowed to switch to a different AI right now? @@ -25,7 +26,7 @@ class FighterAI : public ShipAI virtual void run(float delta) override; virtual void runOrders() override; - virtual void runAttack(P target) override; + virtual void runAttack(sp::ecs::Entity target) override; }; diff --git a/src/ai/missileVolleyAI.cpp b/src/ai/missileVolleyAI.cpp index 81744ad4c1..a936f01c30 100644 --- a/src/ai/missileVolleyAI.cpp +++ b/src/ai/missileVolleyAI.cpp @@ -1,10 +1,14 @@ -#include "spaceObjects/cpuShip.h" +#include "components/ai.h" +#include "components/missiletubes.h" +#include "components/maneuveringthrusters.h" +#include "components/collision.h" +#include "systems/missilesystem.h" #include "ai/missileVolleyAI.h" #include "ai/aiFactory.h" REGISTER_SHIP_AI(MissileVolleyAI, "missilevolley"); -MissileVolleyAI::MissileVolleyAI(CpuShip* owner) +MissileVolleyAI::MissileVolleyAI(sp::ecs::Entity owner) : ShipAI(owner) { flank_position = Unknown; @@ -26,24 +30,34 @@ void MissileVolleyAI::runOrders() ShipAI::runOrders(); } -void MissileVolleyAI::runAttack(P target) +void MissileVolleyAI::runAttack(sp::ecs::Entity target) { - if (!has_missiles) - { + if (!has_missiles) { + ShipAI::runAttack(target); + return; + } + auto tubes = owner.getComponent(); + if (!tubes) { ShipAI::runAttack(target); return; } + auto transform = owner.getComponent(); + if (!transform) + return; + auto target_transform = target.getComponent(); + if (!target_transform) + return; - auto position_diff = target->getPosition() - owner->getPosition(); + auto position_diff = target_transform->getPosition() - transform->getPosition(); float target_angle = vec2ToAngle(position_diff); float distance = glm::length(position_diff); if (flank_position == Unknown) { //No flanking position. Do we want to go left or right of the target? - auto left_point = target->getPosition() + vec2FromAngle(target_angle - 120) * 3500.0f; - auto right_point = target->getPosition() + vec2FromAngle(target_angle + 120) * 3500.0f; - if (angleDifference(vec2ToAngle(left_point - owner->getPosition()), owner->getRotation()) < angleDifference(vec2ToAngle(right_point - owner->getPosition()), owner->getRotation())) + auto left_point = target_transform->getPosition() + vec2FromAngle(target_angle - 120) * 3500.0f; + auto right_point = target_transform->getPosition() + vec2FromAngle(target_angle + 120) * 3500.0f; + if (angleDifference(vec2ToAngle(left_point - transform->getPosition()), transform->getRotation()) < angleDifference(vec2ToAngle(right_point - transform->getPosition()), transform->getRotation())) { flank_position = Left; }else{ @@ -51,15 +65,14 @@ void MissileVolleyAI::runAttack(P target) } } - if (distance < 4500 && has_missiles) + if (distance < 4500) { bool all_possible_loaded = true; - for(int n=0; nweapon_tube_count; n++) + for(auto& tube : tubes->mounts) { - WeaponTube& weapon_tube = owner->weapon_tube[n]; //Base AI class already loads the tubes with available missiles. //If a tube is not loaded, but is currently being load with a new missile, then we still have missiles to load before we want to fire. - if (weapon_tube.isLoading()) + if (tube.state == MissileTubes::MountPoint::State::Loading) { all_possible_loaded = false; break; @@ -69,27 +82,27 @@ void MissileVolleyAI::runAttack(P target) if (all_possible_loaded) { int can_fire_count = 0; - for(int n=0; nweapon_tube_count; n++) + for(auto& tube : tubes->mounts) { - float target_angle = calculateFiringSolution(target, n); + float target_angle = calculateFiringSolution(target, tube); if (target_angle != std::numeric_limits::infinity()) { can_fire_count++; } } - for(int n=0; nweapon_tube_count; n++) + for(auto& tube : tubes->mounts) { - float target_angle = calculateFiringSolution(target, n); + float target_angle = calculateFiringSolution(target, tube); if (target_angle != std::numeric_limits::infinity()) { can_fire_count--; if (can_fire_count == 0) - owner->weapon_tube[n].fire(target_angle); + MissileSystem::fire(owner, tube, target_angle, target); else if ((can_fire_count % 2) == 0) - owner->weapon_tube[n].fire(target_angle + 20.0f * (can_fire_count / 2)); + MissileSystem::fire(owner, tube, target_angle + 20.0f * (can_fire_count / 2), target); else - owner->weapon_tube[n].fire(target_angle - 20.0f * ((can_fire_count + 1) / 2)); + MissileSystem::fire(owner, tube, target_angle - 20.0f * ((can_fire_count + 1) / 2), target); } } } @@ -98,14 +111,16 @@ void MissileVolleyAI::runAttack(P target) glm::vec2 target_position{}; if (flank_position == Left) { - target_position = target->getPosition() + vec2FromAngle(target_angle - 120) * 3500.0f; + target_position = target_transform->getPosition() + vec2FromAngle(target_angle - 120) * 3500.0f; }else{ - target_position = target->getPosition() + vec2FromAngle(target_angle + 120) * 3500.0f; + target_position = target_transform->getPosition() + vec2FromAngle(target_angle + 120) * 3500.0f; } - if (owner->getOrder() == AI_StandGround) + auto ai = owner.getComponent(); + if (ai && ai->orders == AIOrder::StandGround) { - owner->target_rotation = vec2ToAngle(target_position - owner->getPosition()); + auto thrusters = owner.getComponent(); + if (thrusters) thrusters->target = vec2ToAngle(target_position - transform->getPosition()); }else{ flyTowards(target_position, 0.0f); } diff --git a/src/ai/missileVolleyAI.h b/src/ai/missileVolleyAI.h index a9931c9123..c15766a4cf 100644 --- a/src/ai/missileVolleyAI.h +++ b/src/ai/missileVolleyAI.h @@ -15,7 +15,7 @@ class MissileVolleyAI : public ShipAI FlankPosition flank_position; public: - MissileVolleyAI(CpuShip* owner); + MissileVolleyAI(sp::ecs::Entity owner); /**! * Are we allowed to switch to a different AI right now? @@ -25,7 +25,7 @@ class MissileVolleyAI : public ShipAI virtual void run(float delta) override; virtual void runOrders() override; - virtual void runAttack(P target) override; + virtual void runAttack(sp::ecs::Entity target) override; }; diff --git a/src/beamTemplate.cpp b/src/beamTemplate.cpp deleted file mode 100644 index e7e0fb952a..0000000000 --- a/src/beamTemplate.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include "beamTemplate.h" - -BeamTemplate::BeamTemplate() -{ - arc = 0; - direction = 0; - range = 0; - turret_arc = 0; - turret_direction = 0; - turret_rotation_rate = 0; - cycle_time = 0; - damage = 0; - beam_texture = "texture/beam_orange.png"; - - energy_per_beam_fire = 3.0; - heat_per_beam_fire = 0.02; -} - -string BeamTemplate::getBeamTexture() -{ - return beam_texture; -} - -void BeamTemplate::setBeamTexture(string texture) -{ - //TODO: Add some more inteligent input checking - beam_texture = texture; -} - -float BeamTemplate::getDirection() -{ - return direction; -} - -void BeamTemplate::setDirection(float direction) -{ - // Clamp values - while(direction < 0) - direction += 360; - while(direction > 360) - direction -= 360; - this->direction = direction; -} - -float BeamTemplate::getArc() -{ - return arc; -} - -void BeamTemplate::setArc(float arc) -{ - while(arc < 0) - arc += 360; - while(arc > 360) - arc -=360; - this->arc = arc; -} - -float BeamTemplate::getRange() -{ - return range; -} - -void BeamTemplate::setRange(float range) -{ - if(range <= 0) - this->range = 0.1; - else - this->range = range; -} - -float BeamTemplate::getTurretDirection() -{ - return turret_direction; -} - -void BeamTemplate::setTurretDirection(float direction) -{ - // Clamp values - while(direction < 0) - direction += 360; - while(direction > 360) - direction -= 360; - this->turret_direction = direction; -} - -float BeamTemplate::getTurretArc() -{ - return turret_arc; -} - -void BeamTemplate::setTurretArc(float arc) -{ - while(arc < 0) - arc += 360; - while(arc > 360) - arc -=360; - this->turret_arc = arc; -} - -float BeamTemplate::getTurretRotationRate() -{ - return turret_rotation_rate; -} - -void BeamTemplate::setTurretRotationRate(float rotation_rate) -{ - if (rotation_rate < 0.0f) - this->turret_rotation_rate = 0.0f; - // 25 is an arbitrary limit. Values greater than 25.0 are nearly - // instantaneous. - else if (rotation_rate > 25.0f) - this->turret_rotation_rate = 25.0f; - else - this->turret_rotation_rate = rotation_rate; -} - -float BeamTemplate::getCycleTime() -{ - return cycle_time; -} - -void BeamTemplate::setCycleTime(float cycle_time) -{ - if(cycle_time <= 0) - this->cycle_time = 0.1; - else - this->cycle_time = cycle_time; -} - -float BeamTemplate::getDamage() -{ - return damage; -} - -void BeamTemplate::setDamage(float damage) -{ - if(damage < 0) - this->damage = 0; - else - this->damage = damage; -} - -float BeamTemplate::getEnergyPerFire() -{ - return energy_per_beam_fire; -} - -void BeamTemplate::setEnergyPerFire(float energy) -{ - energy_per_beam_fire = energy; -} - -float BeamTemplate::getHeatPerFire() -{ - return heat_per_beam_fire; -} - -void BeamTemplate::setHeatPerFire(float heat) -{ - heat_per_beam_fire = heat; -} - -BeamTemplate& BeamTemplate::operator=(const BeamTemplate& other) -{ - beam_texture = other.beam_texture; - direction = other.direction; - arc = other.arc; - range = other.range; - turret_direction = other.turret_direction; - turret_arc = other.turret_arc; - turret_arc = other.turret_rotation_rate; - cycle_time = other.cycle_time; - damage = other.damage; - energy_per_beam_fire = other.energy_per_beam_fire; - heat_per_beam_fire = other.heat_per_beam_fire; - return *this; -} diff --git a/src/beamTemplate.h b/src/beamTemplate.h deleted file mode 100644 index f17db4822a..0000000000 --- a/src/beamTemplate.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef BEAM_TEMPLATE_H -#define BEAM_TEMPLATE_H - -#include "nonCopyable.h" -#include "stringImproved.h" - -class BeamTemplate : sp::NonCopyable -{ -public: - BeamTemplate(); - - string getBeamTexture(); - - void setBeamTexture(string texture); - - /** - * Beam weapons are 'arc-ed' weapons, the direction is the center of the arc. - * Will always return values between 0 and 360 - */ - float getDirection(); - - /** - * Set the direction of the beam weapon. - */ - void setDirection(float direction); - - float getArc(); - void setArc(float arc); - - float getRange(); - void setRange(float range); - - float getTurretDirection(); - void setTurretDirection(float direction); - - float getTurretArc(); - void setTurretArc(float arc); - - float getTurretRotationRate(); - void setTurretRotationRate(float rotation_rate); - - float getCycleTime(); - void setCycleTime(float cycle_time); - - float getDamage(); - void setDamage(float damage); - - float getEnergyPerFire(); - void setEnergyPerFire(float energy); - - float getHeatPerFire(); - void setHeatPerFire(float heat); - - BeamTemplate& operator=(const BeamTemplate& other); - -protected: - string beam_texture; - float direction; // Value between 0 and 360 (degrees) - float arc; // Value between 0 and 360 - float range; // Value greater than 0 - float turret_direction; // Value between 0 and 360 (degrees) - float turret_arc; // Value between 0 and 360 - float turret_rotation_rate; // Value between 0 and 25 (degrees/tick) - float cycle_time; // Value greater than 0 - float damage; - float energy_per_beam_fire; - float heat_per_beam_fire; -}; - -#endif//BEAM_TEMPLATE_H diff --git a/src/commsScriptInterface.cpp b/src/commsScriptInterface.cpp deleted file mode 100644 index 838b0a1e4a..0000000000 --- a/src/commsScriptInterface.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include "commsScriptInterface.h" -#include "spaceObjects/cpuShip.h" -#include "spaceObjects/playerSpaceship.h" -static CommsScriptInterface* comms_script_interface = NULL; - -static int setCommsMessage(lua_State* L) -{ - if (!comms_script_interface) - return 0; - comms_script_interface->setCommsMessage(luaL_checkstring(L, 1)); - return 0; -} - -static int addCommsReply(lua_State* L) -{ - if (!comms_script_interface) - return 0; - - ScriptSimpleCallback callback; - int idx = 2; - convert::param(L, idx, callback); - comms_script_interface->addCommsReply(luaL_checkstring(L, 1), callback); - return 0; -} - -static int commsSwitchToGM(lua_State* L) -{ - if (!comms_script_interface) - return 0; - - comms_script_interface->switchToGM(); - return 0; -} - -/// void setCommsMessage(string message) -/// Sets the content of an accepted hail, or in a comms reply. -/// If no message is set, attempting to open comms results in "no reply", or a dialogue with the message "?" in a reply. -/// Use this only in replies (addCommsReply()), comms scripts (SpaceObject:setCommsScript()), or comms functions (SpaceObject:setCommsFunction()). -/// When used in the callback function of addCommsReply(), this clears all existing replies. -/// Example: -/// -- Send a greeting upon hail if the player is friendly with the comms target -/// function friendlyComms() -/// if comms_source:isFriendly(comms_target) then -/// setCommsMessage("Hello, friend!") -/// else -/// setCommsMessage("Who are you?") -/// end -/// end -/// -- When some_ship is hailed, run friendlyComms() with some_ship as the comms_target and the player as the comms_source -/// some_ship:setCommsFunction(friendlyComms) -REGISTER_SCRIPT_FUNCTION(setCommsMessage); -/// void addCommsReply(string message, ScriptSimpleCallback callback) -/// Adds a selectable reply option to a communications dialogue as a button with the given text. -/// When clicked, the button calls the given function. -/// Use this only after comms messages (setCommsMessage() in comms scripts (SpaceObject:setCommsScript()), or comms functions (SpaceObject:setCommsFunction()). -/// Comms scripts pass global variables `comms_target` and `comms_source`. See SpaceObject:setCommsScript(). -/// Comms functions pass only `comms_source`. See SpaceObject:setCommsFunction(). -/// Instead of using these globals, the callback function can take two parameters. -/// To present multiple options in one comms message, call addCommsReply() for each option. -/// To create a dialogue tree, run setCommsMessage() inside the addCommsReply() callback, then add new comms replies. -/// Example: -/// if comms_source:isFriendly(comms_target) then -/// setCommsMessage("Hello, friend!") -/// addCommsReply("Can you send a supply drop?", function(comms_source, comms_target) ... end) -- runs the given function when selected -/// ... -/// Deprecated: In a comms script, `player` can also be used for `comms_source`. -REGISTER_SCRIPT_FUNCTION(addCommsReply); -/// void commsSwitchToGM() -/// Switches a PlayerSpaceship communications dialogue from a comms script/function to interactive chat with the GM. -/// When triggered, this opens a comms chat window on both the player crew's screen and GM console. -/// Use this in a communication callback function, such as addCommsReply() or SpaceObject:setCommsFunction(). -/// Example: -/// if comms_source:isFriendly(comms_target) then -/// setCommsMessage("Hello, friend!") -/// addCommsReply("I want to speak to your manager!", function() commsSwitchToGM() end) -- launches a GM chat when selected -/// ... -REGISTER_SCRIPT_FUNCTION(commsSwitchToGM); - -bool CommsScriptInterface::openCommChannel(P ship, P target) -{ - string script_name = target->comms_script_name; - comms_script_interface = this; - - reply_callbacks.clear(); - - this->ship = ship; - this->target = target; - - if (scriptObject) - scriptObject->destroy(); - scriptObject = nullptr; - has_message = false; - - if (script_name != "") - { - scriptObject = new ScriptObject(); - // consider "player" deprecated, but keep it for a long time - scriptObject->registerObject(ship, "player"); - scriptObject->registerObject(ship, "comms_source"); - scriptObject->registerObject(target, "comms_target"); - scriptObject->run(script_name); - }else if (target->comms_script_callback.isSet()) - { - target->comms_script_callback.getScriptObject()->registerObject(ship, "comms_source"); - target->comms_script_callback.getScriptObject()->registerObject(target, "comms_target"); - target->comms_script_callback.call(ship, target); - } - comms_script_interface = nullptr; - return has_message; -} - -void CommsScriptInterface::commChannelMessage(int32_t message_id) -{ - comms_script_interface = this; - - if (message_id >= 0 && message_id < int(reply_callbacks.size()) && ship && target) - { - ScriptSimpleCallback callback = reply_callbacks[message_id]; - if (!scriptObject) - { - target->comms_script_callback.getScriptObject()->registerObject(ship, "comms_source"); - target->comms_script_callback.getScriptObject()->registerObject(target, "comms_target"); - } - reply_callbacks.clear(); - callback.call(ship, target); - } - - comms_script_interface = nullptr; -} - -void CommsScriptInterface::setCommsMessage(string message) -{ - has_message = true; - ship->setCommsMessage(message); -} - -void CommsScriptInterface::addCommsReply(string message, ScriptSimpleCallback callback) -{ - comms_script_interface->ship->addCommsReply(reply_callbacks.size(), message); - reply_callbacks.push_back(callback); -} - -void CommsScriptInterface::switchToGM() -{ - ship->switchCommsToGM(); -} diff --git a/src/commsScriptInterface.h b/src/commsScriptInterface.h deleted file mode 100644 index 8d3a82a0db..0000000000 --- a/src/commsScriptInterface.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef COMMS_SCRIPT_INTERFACE_H -#define COMMS_SCRIPT_INTERFACE_H - -#include "scriptInterface.h" - -///Forward declarations -class ScriptObject; -class PlayerSpaceship; -class SpaceObject; - -class CommsScriptInterface : sp::NonCopyable -{ -public: - bool openCommChannel(P ship, P target); - void commChannelMessage(int32_t message_id); - - void setCommsMessage(string message); - void addCommsReply(string message, ScriptSimpleCallback callback); - - void switchToGM(); -private: - bool has_message; - std::vector reply_callbacks; - P scriptObject; - P ship; - P target; -}; - -#endif//COMMS_SCRIPT_INTERFACE_H diff --git a/src/components/ai.h b/src/components/ai.h new file mode 100644 index 0000000000..751c52258b --- /dev/null +++ b/src/components/ai.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + + +enum class AIOrder +{ + Idle, //Don't do anything, don't even attack. + Roaming, //Fly around and engage at will, without a clear target + Retreat, //Dock on [order_target] that can restore our weapons. Find one if neccessary. Continue roaming after our missiles are restocked, or no target is found. + StandGround, //Keep current position, do not fly away, but attack nearby targets. + DefendLocation, //Defend against enemies getting close to [order_target_location] + DefendTarget, //Defend against enemies getting close to [order_target] (falls back to Roaming if the target is destroyed) + FlyFormation, //Fly [order_target_location] offset from [order_target]. Allows for nicely flying in formation. + FlyTowards, //Fly towards [order_target_location], attacking enemies that get too close, but disengage and continue when enemy is too far. + FlyTowardsBlind, //Fly towards [order_target_location], not attacking anything + Dock, //Dock with target + Attack, //Attack [order_target] very specificly. +}; + +class ShipAI; +class AIController +{ +public: + AIOrder orders = AIOrder::Idle; + glm::vec2 order_target_location{}; + sp::ecs::Entity order_target; + + std::unique_ptr ai; + string new_name = "default"; +}; diff --git a/src/components/avoidobject.h b/src/components/avoidobject.h new file mode 100644 index 0000000000..ea1cb354f2 --- /dev/null +++ b/src/components/avoidobject.h @@ -0,0 +1,26 @@ +#pragma once + +#include + + +// Component to indicate that this entity should be avoided by path planning. +class AvoidObject +{ +public: + float range = 100.0f; + + // Internal state used by the pathfinding system. + enum class InternalState { + New, + BigEntity, + SmallEntity, + } state = InternalState::New; + uint32_t position_hash = 0; +}; + +class DelayedAvoidObject +{ +public: + float delay = 10.0f; + float range = 100.0f; +}; \ No newline at end of file diff --git a/src/components/beamweapon.cpp b/src/components/beamweapon.cpp new file mode 100644 index 0000000000..c97050be8e --- /dev/null +++ b/src/components/beamweapon.cpp @@ -0,0 +1,20 @@ +#include "beamweapon.h" +#include "tween.h" + + +float frequencyVsFrequencyDamageFactor(int beam_frequency, int shield_frequency) +{ + if (beam_frequency < 0 || shield_frequency < 0) + return 1.f; + + float diff = static_cast(abs(beam_frequency - shield_frequency)); + float f1 = sinf(Tween::linear(diff, 0, BeamWeaponSys::max_frequency, 0, float(M_PI) * (1.2f + shield_frequency * 0.05f)) + float(M_PI) / 2.0f); + f1 = f1 * Tween::easeInCubic(diff, 0, BeamWeaponSys::max_frequency, 1.f, 0.1f); + f1 = Tween::linear(f1, 1.f, -1.f, 0.5f, 1.5f); + return f1; +} + +string frequencyToString(int frequency) +{ + return string(400 + (frequency * 20)) + "THz"; +} diff --git a/src/components/beamweapon.h b/src/components/beamweapon.h new file mode 100644 index 0000000000..3a19dbb9e9 --- /dev/null +++ b/src/components/beamweapon.h @@ -0,0 +1,60 @@ +#pragma once + +#include "ecs/entity.h" +#include "shipsystem.h" +#include "systems/damage.h" +#include "glm/vec3.hpp" +#include "glm/gtc/type_precision.hpp" + + +class BeamWeaponSys : public ShipSystem { +public: + class MountPoint { + public: + glm::vec3 position;//Visual position on the 3D model where this beam is fired from. + + //Beam configuration + float arc = 0.0f; + float direction = 0.0f; + float range = 0.0f; + float turret_arc = 0.0f; + float turret_direction = 0.0f; + float turret_rotation_rate = 0.0f; + float cycle_time = 6.0f; + float damage = 1.0f;//Server side only + float energy_per_beam_fire = 3.0f;//Server side only + float heat_per_beam_fire = 0.02f;//Server side only + glm::u8vec4 arc_color{255, 0, 0, 128}; + glm::u8vec4 arc_color_fire{255, 255, 0, 128}; + DamageType damage_type = DamageType::Energy; + + //Beam runtime state + float cooldown = 0.0f; + string texture = "texture/beam_orange.png"; + }; + + constexpr static int max_frequency = 20; + int frequency = 0; + ShipSystem::Type system_target = ShipSystem::Type::None; + + std::vector mounts; +}; + +class BeamEffect +{ +public: + float lifetime = 1.0f; + float fade_speed = 1.0f; + sp::ecs::Entity source; + sp::ecs::Entity target; + glm::vec3 source_offset{}; + glm::vec3 target_offset{}; + glm::vec2 target_location{}; + glm::vec3 hit_normal{}; + + bool fire_ring = true; + string beam_texture; +}; + +float frequencyVsFrequencyDamageFactor(int beam_frequency, int shield_frequency); +string frequencyToString(int frequency); diff --git a/src/components/comms.h b/src/components/comms.h new file mode 100644 index 0000000000..20477aa35d --- /dev/null +++ b/src/components/comms.h @@ -0,0 +1,47 @@ +#pragma once + +#include "script/environment.h" +#include "script/callback.h" + +class CommsReceiver +{ +public: + string script; // "comms_ship.lua" / "comms_station.lua" + sp::script::Callback callback; +}; + +class CommsTransmitter +{ +public: + enum class State + { + Inactive, // No active comms + OpeningChannel, // Opening a comms channel + BeingHailed, // Receiving a hail from an object + BeingHailedByGM, // ... the GM + ChannelOpen, // Comms open to an object + ChannelOpenPlayer, // ... another player + ChannelOpenGM, // ... the GM + ChannelFailed, // Comms failed to connect + ChannelBroken, // Comms broken by other side + ChannelClosed // Comms manually closed + }; + struct ScriptReply + { + string message; + sp::script::Callback callback; + }; + + State state = State::Inactive; + float open_delay = 0.0f; + string target_name; + string incomming_message; + sp::ecs::Entity target; // Server only + bool script_replies_dirty = true; + std::vector script_replies; +}; +class CommsTransmitterEnvironment +{ +public: + std::unique_ptr script_environment; +}; \ No newline at end of file diff --git a/src/components/coolant.h b/src/components/coolant.h new file mode 100644 index 0000000000..071f8a5912 --- /dev/null +++ b/src/components/coolant.h @@ -0,0 +1,11 @@ +#pragma once + +// The coolant component interacts heavily with the ShipComponents. +// An important aspect is that if this component exists, the ShipComponents will interact with coolant and heat. +class Coolant +{ +public: + float max = 10.0f; + float max_coolant_per_system = 10.0f; + bool auto_levels = false; +}; \ No newline at end of file diff --git a/src/components/customshipfunction.h b/src/components/customshipfunction.h new file mode 100644 index 0000000000..b3f2428f13 --- /dev/null +++ b/src/components/customshipfunction.h @@ -0,0 +1,34 @@ +#pragma once + +#include "stringImproved.h" +#include "crewPosition.h" +#include "script/callback.h" +#include + + +class CustomShipFunctions +{ +public: + class Function + { + public: + enum class Type + { + Info, + Button, + Message + }; + Type type; + string name; + string caption; + CrewPositions crew_positions; + sp::script::Callback callback; + int order; + + bool operator!=(const Function& csf) { return type != csf.type || name != csf.name || caption != csf.caption || crew_positions != csf.crew_positions; } + bool operator<(const Function& other) const { return (order < other.order); } + }; + + std::vector functions; + bool functions_dirty = true; +}; diff --git a/src/components/database.h b/src/components/database.h new file mode 100644 index 0000000000..14c25dcf65 --- /dev/null +++ b/src/components/database.h @@ -0,0 +1,20 @@ +#pragma once + +#include "ecs/entity.h" +#include + +class Database +{ +public: + sp::ecs::Entity parent; + + string name; + struct KeyValue { + string key; + string value; + }; + std::vector key_values; + bool key_values_dirty = true; + string description; + string image; +}; diff --git a/src/components/docking.h b/src/components/docking.h new file mode 100644 index 0000000000..340e2175b1 --- /dev/null +++ b/src/components/docking.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include "io/dataBuffer.h" +#include "multiplayer.h" + +// Component that indicates things can dock to this. +enum class DockingStyle { + None, External, Internal, +}; + +class DockingBay +{ +public: + static constexpr uint32_t ShareEnergy = 1 << 0; + static constexpr uint32_t Repair = 1 << 1; + static constexpr uint32_t ChargeShield = 1 << 2; // Increased shield recharge rate + static constexpr uint32_t RestockProbes = 1 << 3; + static constexpr uint32_t RestockMissiles = 1 << 4; // Only for AI controlled ships. Players use the comms system. + + std::unordered_set external_dock_classes; + bool external_dock_classes_dirty = true; + std::unordered_set internal_dock_classes; + bool internal_dock_classes_dirty = true; + + uint32_t flags = 0; +}; + +// Component to indicate that we can do to things. +class DockingPort +{ +public: + string dock_class; + string dock_subclass; + + enum class State { + NotDocking = 0, + Docking, + Docked + } state = State::NotDocking; + + sp::ecs::Entity target; + glm::vec2 docked_offset; + + bool auto_reload_missiles = false; + float auto_reload_missile_delay = 0.0f; + static constexpr float auto_reload_missile_time = 10.0f; + + DockingStyle canDockOn(DockingBay& bay) { + if (bay.external_dock_classes.empty() && bay.internal_dock_classes.empty()) return DockingStyle::External; + if (bay.external_dock_classes.find(dock_class) != bay.external_dock_classes.end()) return DockingStyle::External; + if (bay.external_dock_classes.find(dock_subclass) != bay.external_dock_classes.end()) return DockingStyle::External; + if (bay.internal_dock_classes.find(dock_class) != bay.internal_dock_classes.end()) return DockingStyle::Internal; + if (bay.internal_dock_classes.find(dock_subclass) != bay.internal_dock_classes.end()) return DockingStyle::Internal; + return DockingStyle::None; + } +}; \ No newline at end of file diff --git a/src/components/faction.cpp b/src/components/faction.cpp new file mode 100644 index 0000000000..d96aabe2e2 --- /dev/null +++ b/src/components/faction.cpp @@ -0,0 +1,86 @@ +#include "components/faction.h" +#include "ecs/query.h" + + +static FactionInfo default_faction_info; + + +sp::ecs::Entity Faction::find(const string& name) +{ + for(auto [entity, info] : sp::ecs::Query()) { + if (info.name == name) + return entity; + } + return {}; +} + +FactionInfo& Faction::getInfo(sp::ecs::Entity entity) +{ + auto faction = entity.getComponent(); + if (faction) { + auto info = faction->entity.getComponent(); + if (info) + return *info; + } + return default_faction_info; +} + +FactionRelation Faction::getRelation(sp::ecs::Entity a, sp::ecs::Entity b) +{ + auto fia = Faction::getInfo(a); + auto fb = b.getComponent(); + if (fb) + return fia.getRelation(fb->entity); + return fia.getRelation({}); +} + +// TODO: Info about multiple components belongs in systems, not in component code. +#include "components/target.h" +#include "components/scanning.h" + +void Faction::didAnOffensiveAction(sp::ecs::Entity entity) +{ + //We did an offensive action towards our target. + // Check for each faction. If this faction knows if the target is an enemy or a friendly, it now knows if this object is an enemy or a friendly. + auto scanstate = entity.getComponent(); + if (!scanstate) return; + auto target = entity.getComponent(); + if (!target || !target->entity) return; + auto target_scan_state = target->entity.getComponent(); + auto target_faction = target->entity.getComponent(); + + for(auto [faction_entity, faction_info] : sp::ecs::Query()) { + if ((!target_scan_state || target_scan_state->getStateForFaction(faction_entity) != ScanState::State::NotScanned) || (target_faction && target_faction->entity == faction_entity)) { + // This faction knows if the target is friendly or enemy, so check if we need to set it to FFI + if (scanstate->getStateForFaction(faction_entity) == ScanState::State::NotScanned) { + scanstate->setStateForFaction(faction_entity, ScanState::State::FriendOrFoeIdentified); + } + } + } +} + +FactionRelation FactionInfo::getRelation(sp::ecs::Entity faction_entity) +{ + for(auto it : relations) + if (it.other_faction == faction_entity) + return it.relation; + return FactionRelation::Neutral; +} + +void FactionInfo::setRelation(sp::ecs::Entity faction_entity, FactionRelation relation) +{ + for(auto& it : relations) { + if (it.other_faction == faction_entity) { + it.relation = relation; + relations_dirty = true; + return; + } + } + relations.push_back({faction_entity, relation}); + relations_dirty = true; +} + +FactionInfo* FactionInfo::find(const string& name) +{ + return Faction::find(name).getComponent(); +} diff --git a/src/components/faction.h b/src/components/faction.h new file mode 100644 index 0000000000..6a05a046a2 --- /dev/null +++ b/src/components/faction.h @@ -0,0 +1,51 @@ +#pragma once + +#include "ecs/entity.h" +#include +#include + +class FactionInfo; + +enum class FactionRelation +{ + Friendly, + Neutral, + Enemy +}; + +// Component to set our current faction, +// a faction is an entity with the FactionInfo component +class Faction +{ +public: + sp::ecs::Entity entity; + + static sp::ecs::Entity find(const string& name); + static FactionInfo& getInfo(sp::ecs::Entity entity); + static FactionRelation getRelation(sp::ecs::Entity a, sp::ecs::Entity b); + + static void didAnOffensiveAction(sp::ecs::Entity entity); +}; + +class FactionInfo +{ +public: + glm::u8vec4 gm_color = {255,255,255,255}; + string name; + string locale_name; + string description; + + float reputation_points = 0.0f; + + struct Relation { + sp::ecs::Entity other_faction; + FactionRelation relation; + }; + bool relations_dirty = true; + std::vector relations; + + FactionRelation getRelation(sp::ecs::Entity faction_entity); + void setRelation(sp::ecs::Entity faction_entity, FactionRelation relation); + + static FactionInfo* find(const string& name); +}; diff --git a/src/components/gravity.h b/src/components/gravity.h new file mode 100644 index 0000000000..c41d0b5c19 --- /dev/null +++ b/src/components/gravity.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "script/callback.h" + + +class Gravity +{ +public: + float range = 5000.0f; + float force = 50.0f; + bool damage = false; + + glm::vec2 wormhole_target{}; + sp::script::Callback on_teleportation; +}; diff --git a/src/components/hacking.h b/src/components/hacking.h new file mode 100644 index 0000000000..ebabd23a6b --- /dev/null +++ b/src/components/hacking.h @@ -0,0 +1,8 @@ +#pragma once + +// Component to indicate that we can hack other ships. +class HackingDevice +{ +public: + float effectiveness = 0.5f; +}; \ No newline at end of file diff --git a/src/components/hull.h b/src/components/hull.h new file mode 100644 index 0000000000..78e4830a12 --- /dev/null +++ b/src/components/hull.h @@ -0,0 +1,27 @@ +#pragma once + +#include "script/callback.h" +#include "systems/damage.h" + + +// Component to indicate that this entity has a hull and can get hull damage. +// Usually entities are destroyed once they reach zero hull. But you can disable this to prevent player ship destruction in LARP scenarios or tutorials. +class Hull +{ +public: + float current = 100.0f; + float max = 100.0f; + bool allow_destruction = true; + int damaged_by_flags = (1 << int(DamageType::Energy)) | (1 << int(DamageType::Kinetic)); + float damage_indicator = 0.0f; + + sp::script::Callback on_destruction; + sp::script::Callback on_taking_damage; +}; + +// Not having actual hull, but an explosion in the area will destroy this entity. +class DestroyedByAreaDamage +{ +public: + int damaged_by_flags = (1 << int(DamageType::Energy)) | (1 << int(DamageType::Kinetic)); +}; \ No newline at end of file diff --git a/src/components/impulse.h b/src/components/impulse.h new file mode 100644 index 0000000000..5200219699 --- /dev/null +++ b/src/components/impulse.h @@ -0,0 +1,19 @@ +#pragma once + +#include "stringImproved.h" +#include "shipsystem.h" + +// Impulse engine component, indicate that this entity can move under impulse control. +class ImpulseEngine : public ShipSystem { +public: + // Config + float max_speed_forward = 500.0f; // in U/sec + float max_speed_reverse = 500.0f; // in U/sec + float acceleration_forward = 20.0f;// in U/sec^2 + float acceleration_reverse = 20.0f;// in U/sec^2 + string sound; + + // Runtime + float request = 0.0f; // -1.0 to 1.0 + float actual = 0.0f; // -1.0 to 1.0 +}; \ No newline at end of file diff --git a/src/components/internalrooms.cpp b/src/components/internalrooms.cpp new file mode 100644 index 0000000000..5e3c0b2fb5 --- /dev/null +++ b/src/components/internalrooms.cpp @@ -0,0 +1,35 @@ +#include "components/internalrooms.h" + + +glm::ivec2 InternalRooms::roomMin() +{ + if (rooms.empty()) + return {0, 0}; + auto min = rooms[0].position; + for(const auto& r : rooms) { + min.x = std::min(min.x, r.position.x); + min.y = std::min(min.y, r.position.y); + } + return min; +} + +glm::ivec2 InternalRooms::roomMax() +{ + if (rooms.empty()) + return {0, 0}; + auto max = rooms[0].position + rooms[0].size; + for(const auto& r : rooms) { + max.x = std::max(max.x, r.position.x + r.size.x); + max.y = std::max(max.y, r.position.y + r.size.y); + } + return max; +} + +ShipSystem::Type InternalRooms::getSystemAtRoom(glm::ivec2 pos) +{ + for(const auto& r : rooms) { + if (pos.x >= r.position.x && pos.x < r.position.x + r.size.x && pos.y >= r.position.y && pos.y < r.position.y + r.size.y) + return r.system; + } + return ShipSystem::Type::None; +} diff --git a/src/components/internalrooms.h b/src/components/internalrooms.h new file mode 100644 index 0000000000..b75e95b92c --- /dev/null +++ b/src/components/internalrooms.h @@ -0,0 +1,63 @@ +#pragma once + +#include "components/shipsystem.h" +#include + +// Internal composition of a ship. +class InternalRooms +{ +public: + struct Room { + glm::ivec2 position; + glm::ivec2 size; + ShipSystem::Type system = ShipSystem::Type::None; + }; + struct Door { + glm::ivec2 position; + bool horizontal; + }; + + std::vector rooms; + bool rooms_dirty = true; + std::vector doors; + bool doors_dirty = true; + + glm::ivec2 roomMin(); + glm::ivec2 roomMax(); + ShipSystem::Type getSystemAtRoom(glm::ivec2 pos); + + bool auto_repair_enabled = false; // Repair crew with auto target damaged rooms +}; + +class InternalCrew +{ +public: + enum class Action + { + Idle, + Move + }; + enum class Direction + { + None, + Up, + Down, + Left, + Right + }; + + float move_speed = 2.0f; + glm::vec2 position{-1,-1}; + glm::ivec2 target_position{0,0}; + Action action = Action::Idle; + Direction direction = Direction::None; + float action_delay = 0.0f; + sp::ecs::Entity ship; +}; + +class InternalRepairCrew +{ +public: + float repair_per_second = 0.007; + float unhack_per_second = 0.007; +}; diff --git a/src/components/jumpdrive.h b/src/components/jumpdrive.h new file mode 100644 index 0000000000..b02091e016 --- /dev/null +++ b/src/components/jumpdrive.h @@ -0,0 +1,25 @@ +#pragma once + +#include "stringImproved.h" +#include "shipsystem.h" +#include "tween.h" + +// Impulse engine component, indicate that this entity can move under impulse control. +class JumpDrive : public ShipSystem { +public: + constexpr static float charge_time = 90.0f; /*::linear(getSystemEffectiveness(), 0.0, 1.0, -0.25, 1.0); } +}; \ No newline at end of file diff --git a/src/components/lifetime.h b/src/components/lifetime.h new file mode 100644 index 0000000000..d054f38158 --- /dev/null +++ b/src/components/lifetime.h @@ -0,0 +1,10 @@ +#pragma once + +#include "script/callback.h" + +class LifeTime +{ +public: + float lifetime = 1.0; + sp::script::Callback on_expire; +}; diff --git a/src/components/maneuveringthrusters.h b/src/components/maneuveringthrusters.h new file mode 100644 index 0000000000..33b5c784e5 --- /dev/null +++ b/src/components/maneuveringthrusters.h @@ -0,0 +1,30 @@ +#pragma once + +#include "shipsystem.h" +#include + + +class ManeuveringThrusters : public ShipSystem { +public: + // Config + float speed = 10.0f; // [config] Speed of rotation, in deg/second + + // Runtime + float target = std::numeric_limits::min(); // [input] Ship will try to aim to this rotation. (degrees) + float rotation_request = std::numeric_limits::min(); // [input] Ship will rotate in this velocity. ([-1,1], overrides target_rotation) + + void stop() { target = std::numeric_limits::min(); rotation_request = std::numeric_limits::min(); } +}; + +class CombatManeuveringThrusters { +public: + float charge = 1.0f; // [output] How much charge there is in the combat maneuvering system (0.0-1.0) + + struct Thruster { + float request = 0.0f; // [input] How much boost we want at this moment (0.0-1.0) + float active = 0.0f; + float speed = 0.0f; /*< [config] Speed to indicate how fast we will fly forwards/sideways with a full boost/strafe */ + }; + Thruster boost; + Thruster strafe; +}; diff --git a/src/components/missile.h b/src/components/missile.h new file mode 100644 index 0000000000..57773b9306 --- /dev/null +++ b/src/components/missile.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include "systems/damage.h" + + +class MissileFlight +{ +public: + float speed=100.0f; + float timeout=0.0f; +}; + +class MissileHoming +{ +public: + float turn_rate=1.0f; + float range=100.0f; + sp::ecs::Entity target; + float target_angle = 0.0f; +}; + +//TODO: Not really part of missile.h, also part of asteroids +class ExplodeOnTouch +{ +public: + float damage_at_center = 35.0f; + float damage_at_edge = 35.0f; + float blast_range = 100.0f; + sp::ecs::Entity owner; + DamageType damage_type = DamageType::Kinetic; + string explosion_sfx; +}; +class ExplodeOnTimeout +{ +}; +class DelayedExplodeOnTouch : public ExplodeOnTouch +{ +public: + float delay = 1.0f; + bool triggered = false; +}; + +//TODO: Not really part of missile.h +class ConstantParticleEmitter +{ +public: + float interval = 0.1f; + float delay = 0.0f; + + float travel_random_range=0.0f; + glm::vec3 start_color = glm::vec3(1, 0.8, 0.8); + glm::vec3 end_color = glm::vec3(0, 0, 0); + float start_size = 5.0f; + float end_size = 20.0f; + float life_time = 5.0f; +}; diff --git a/src/components/missiletubes.cpp b/src/components/missiletubes.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/missiletubes.h b/src/components/missiletubes.h new file mode 100644 index 0000000000..7042b39657 --- /dev/null +++ b/src/components/missiletubes.h @@ -0,0 +1,47 @@ +#pragma once + +#include "ecs/entity.h" +#include "shipsystem.h" +#include "missileWeaponData.h" +#include + + +class MissileTubes : public ShipSystem { +public: + class MountPoint { + public: + enum class State + { + Empty, + Loading, + Loaded, + Unloading, + Firing + }; + + //Configuration + glm::vec3 position{};//Visual position on the 3D model where this beam is fired from. + float load_time = 8.0f; + uint32_t type_allowed_mask = (1 << MW_Count) - 1; + float direction = 0.0f; + EMissileSizes size = MS_Medium; + + //Runtime state + EMissileWeapons type_loaded = MW_None; + State state = State::Empty; + float delay = 0.0f; + int fire_count = 0; + + bool canLoad(EMissileWeapons type) { + return (type_allowed_mask & (1 << type)); + } + bool canOnlyLoad(EMissileWeapons type) { + return (type_allowed_mask == (1U << type)); + } + }; + + int storage[MW_Count] = {0}; + int storage_max[MW_Count] = {0}; + + std::vector mounts; +}; diff --git a/src/components/moveto.h b/src/components/moveto.h new file mode 100644 index 0000000000..4ed178005f --- /dev/null +++ b/src/components/moveto.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "script/callback.h" + + + +// Component to move to a specific spot at a fixed speed +class MoveTo +{ +public: + float speed = 1000.0; + glm::vec2 target{0, 0}; + sp::script::Callback on_arrival; +}; diff --git a/src/components/name.h b/src/components/name.h new file mode 100644 index 0000000000..5c61e2ba78 --- /dev/null +++ b/src/components/name.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class CallSign +{ +public: + string callsign; +}; + +class TypeName +{ +public: + string type_name; + string localized; +}; diff --git a/src/components/orbit.h b/src/components/orbit.h new file mode 100644 index 0000000000..9d63e1c85f --- /dev/null +++ b/src/components/orbit.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ecs/entity.h" + +// Component to orbit around another object or a fixed point +class Orbit +{ +public: + sp::ecs::Entity target; + glm::vec2 center; + float distance = 1000.0f; + float time = 60.0f; +}; diff --git a/src/components/pickup.h b/src/components/pickup.h new file mode 100644 index 0000000000..77d40f3640 --- /dev/null +++ b/src/components/pickup.h @@ -0,0 +1,21 @@ +#pragma once + +#include "script/callback.h" + + +// Simple component that allows a callback when an entity is touched, and destroys the entity after the callback. +class PickupCallback +{ +public: + bool player = true; // Only check for PlayerControl entities. + sp::script::Callback callback; + float give_energy = 0; +}; + +// Simple component that allows a callback when an entity is touched +class CollisionCallback +{ +public: + bool player = true; // Only check for PlayerControl entities. + sp::script::Callback callback; +}; diff --git a/src/components/player.cpp b/src/components/player.cpp new file mode 100644 index 0000000000..b10b0e6daa --- /dev/null +++ b/src/components/player.cpp @@ -0,0 +1,29 @@ +#include "player.h" +#include "i18n.h" + + +string alertLevelToString(AlertLevel level) +{ + // Convert an EAlertLevel to a string. + switch(level) + { + case AlertLevel::RedAlert: return "RED ALERT"; + case AlertLevel::YellowAlert: return "YELLOW ALERT"; + case AlertLevel::Normal: return "Normal"; + default: + return "???"; + } +} + +string alertLevelToLocaleString(AlertLevel level) +{ + // Convert an EAlertLevel to a translated string. + switch(level) + { + case AlertLevel::RedAlert: return tr("alert","RED ALERT"); + case AlertLevel::YellowAlert: return tr("alert","YELLOW ALERT"); + case AlertLevel::Normal: return tr("alert","Normal"); + default: + return "???"; + } +} diff --git a/src/components/player.h b/src/components/player.h new file mode 100644 index 0000000000..55448ad104 --- /dev/null +++ b/src/components/player.h @@ -0,0 +1,50 @@ +#pragma once + +#include "stringImproved.h" +#include "crewPosition.h" + + +enum class MainScreenSetting +{ + Front = 0, + Back, + Left, + Right, + Target, + Tactical, + LongRange +}; + +enum class MainScreenOverlay +{ + HideComms = 0, + ShowComms +}; + +enum class AlertLevel +{ + Normal, // No alert state + YellowAlert, // Yellow + RedAlert, // Red + MAX // ? +}; + + +class PlayerControl +{ +public: + // Main screen content + MainScreenSetting main_screen_setting = MainScreenSetting::Front; + // Content overlaid on the main screen, such as comms + MainScreenOverlay main_screen_overlay = MainScreenOverlay::HideComms; + + AlertLevel alert_level = AlertLevel::Normal; + + // Password to join a ship. Default is empty. + string control_code; + + CrewPositions allowed_positions = CrewPositions::all(); +}; + +string alertLevelToString(AlertLevel level); +string alertLevelToLocaleString(AlertLevel level); diff --git a/src/components/probe.h b/src/components/probe.h new file mode 100644 index 0000000000..1d3bd2c96a --- /dev/null +++ b/src/components/probe.h @@ -0,0 +1,13 @@ +#pragma once + +#include "script/callback.h" + +class ScanProbeLauncher +{ +public: + int max = 8; + int stock = 8; + float recharge = 0.0; + float charge_time = 10.0f; + sp::script::Callback on_launch; +}; diff --git a/src/components/radar.h b/src/components/radar.h new file mode 100644 index 0000000000..25e3b3733a --- /dev/null +++ b/src/components/radar.h @@ -0,0 +1,87 @@ +#pragma once + +#include "io/dataBuffer.h" +#include "script/callback.h" + + +class RadarTrace +{ +public: + static constexpr uint32_t Rotate = 1 << 0; + static constexpr uint32_t ColorByFaction = 1 << 1; + static constexpr uint32_t ArrowIfNotScanned = 1 << 2; + static constexpr uint32_t BlendAdd = 1 << 3; + static constexpr uint32_t LongRange = 1 << 4; + + string icon; + float min_size = 16.0; //Size in screen "pixels" + float max_size = 256.0; //Size in screen "pixels" + float radius = 0.0; // Size in world "units" + glm::u8vec4 color{255,255,255,255}; + + uint32_t flags = Rotate | LongRange; +}; + + +// Radar signature data, used by rawScannerDataOverlay. +class RawRadarSignatureInfo +{ +public: + float gravity; + float electrical; + float biological; + + RawRadarSignatureInfo() + : gravity(0), electrical(0), biological(0) {} + + RawRadarSignatureInfo(float gravity, float electrical, float biological) + : gravity(gravity), electrical(electrical), biological(biological) {} + + RawRadarSignatureInfo& operator+=(const RawRadarSignatureInfo& o) + { + gravity += o.gravity; + electrical += o.electrical; + biological += o.biological; + return *this; + } + + RawRadarSignatureInfo operator*(const float f) const + { + return RawRadarSignatureInfo(gravity * f, electrical * f, biological * f); + } +}; + +// Dynamic radar signature is added to entities that +// generate additional radar signature info by live systems (impulse engine, etc...) +class DynamicRadarSignatureInfo +{ +public: + float gravity = 0.0f; + float electrical = 0.0f; + float biological = 0.0f; +}; + +//TODO: This is currently a bit of a catch all components, and should be split up. +class LongRangeRadar +{ +public: + float short_range = 5000.0f; + float long_range = 30000.0f; + + bool waypoints_dirty = true; + std::vector waypoints; + sp::ecs::Entity radar_view_linked_entity; + + sp::script::Callback on_probe_link; + sp::script::Callback on_probe_unlink; +}; + +class ShareShortRangeRadar +{ +}; + +class AllowRadarLink +{ +public: + sp::ecs::Entity owner; +}; diff --git a/src/components/radarblock.h b/src/components/radarblock.h new file mode 100644 index 0000000000..de5a7defcd --- /dev/null +++ b/src/components/radarblock.h @@ -0,0 +1,14 @@ +#pragma once + +// Component that blocks long range radar, usually a nebula, but, potential for other things as well. +class RadarBlock +{ +public: + float range = 5000.0; + bool behind = true; //Also block everything behind this radar block. Setting this to false allow creating of "blackout spots" +}; + +// Entities with this component are never blocked on the long range rader by RadarBlock entities. +class NeverRadarBlocked +{ +}; diff --git a/src/components/reactor.h b/src/components/reactor.h new file mode 100644 index 0000000000..12d77f9142 --- /dev/null +++ b/src/components/reactor.h @@ -0,0 +1,18 @@ +#pragma once + +#include "shipsystem.h" + +// The reactor component stores and generates energy, any shipsystem can use energy and drain this. While the reactor generates energy. +class Reactor : public ShipSystem { +public: + Reactor() { can_be_hacked = false; } + + // Config + float max_energy = 1000.0f; + bool overload_explode = true; + + // Runtime + float energy = 1000.0f; + + bool useEnergy(float amount) { if (amount > energy) return false; energy -= amount; return true; } +}; \ No newline at end of file diff --git a/src/components/rendering.cpp b/src/components/rendering.cpp new file mode 100644 index 0000000000..9d78151aaa --- /dev/null +++ b/src/components/rendering.cpp @@ -0,0 +1,32 @@ +#include "mesh.h" +#include "textureManager.h" +#include "rendering.h" + +Mesh* MeshRenderComponent::getMesh() +{ + if (!mesh.ptr && !mesh.name.empty()) + mesh.ptr = Mesh::getMesh(mesh.name); + return mesh.ptr; +} + +sp::Texture* MeshRenderComponent::getTexture() +{ + if (!texture.ptr && !texture.name.empty()) + texture.ptr = textureManager.getTexture(texture.name); + return texture.ptr; +} + +sp::Texture* MeshRenderComponent::getSpecularTexture() +{ + if (!specular_texture.ptr && !specular_texture.name.empty()) + specular_texture.ptr = textureManager.getTexture(specular_texture.name); + return specular_texture.ptr; +} + +sp::Texture* MeshRenderComponent::getIlluminationTexture() +{ + if (!illumination_texture.ptr && !illumination_texture.name.empty()) + illumination_texture.ptr = textureManager.getTexture(illumination_texture.name); + return illumination_texture.ptr; +} + diff --git a/src/components/rendering.h b/src/components/rendering.h new file mode 100644 index 0000000000..3582fdf238 --- /dev/null +++ b/src/components/rendering.h @@ -0,0 +1,101 @@ +#pragma once +#include + +#include "io/dataBuffer.h" +#include "graphics/texture.h" +#include "mesh.h" +#include "shaderRegistry.h" + +struct MeshRef +{ + string name; + Mesh* ptr = nullptr; +}; +struct TextureRef +{ + string name; + sp::Texture* ptr = nullptr; +}; + +class MeshRenderComponent +{ +public: + MeshRef mesh; + TextureRef texture; + TextureRef specular_texture; + TextureRef illumination_texture; + glm::vec3 mesh_offset{}; + float scale = 1.0; + + Mesh* getMesh(); + sp::Texture* getTexture(); + sp::Texture* getSpecularTexture(); + sp::Texture* getIlluminationTexture(); +}; + +class EngineEmitter +{ +public: + float last_engine_particle_time = 0.0f; + + struct Emitter { + glm::vec3 position{}; + glm::vec3 color{}; + float scale; + }; + std::vector emitters; + bool emitters_dirty = true; +}; + +class BillboardRenderer +{ +public: + string texture; + float size = 512.0f; +}; + +class NebulaRenderer +{ +public: + struct Cloud + { + glm::vec2 offset{0, 0}; + TextureRef texture; + float size = 512.0f; + }; + + float render_range = 10000.0f; + std::vector clouds; + bool clouds_dirty = true; +}; + +class ExplosionEffect +{ +public: + constexpr static float max_lifetime = 2.f; + constexpr static int particle_count = 1000; + + float lifetime = max_lifetime; + float size = 1.0; + glm::vec3 particle_directions[particle_count]; + bool radar = false; + bool electrical = false; + + // Fit elements in a uint8 - at 4 vertices per quad, that's (256 / 4 =) 64 quads. + static constexpr size_t max_quad_count = particle_count * 4; + std::shared_ptr> particles_buffers; +}; + + +class PlanetRender +{ +public: + float size; + float cloud_size; + float atmosphere_size; + string texture; + string cloud_texture; + string atmosphere_texture; + glm::vec3 atmosphere_color{}; + float distance_from_movement_plane; +}; diff --git a/src/components/scanning.cpp b/src/components/scanning.cpp new file mode 100644 index 0000000000..24eccfe692 --- /dev/null +++ b/src/components/scanning.cpp @@ -0,0 +1,35 @@ +#include "components/scanning.h" +#include "components/faction.h" + + +ScanState::State ScanState::getStateFor(sp::ecs::Entity entity) +{ + auto faction = entity.getComponent(); + return getStateForFaction(faction ? faction->entity : sp::ecs::Entity{}); +} + +void ScanState::setStateFor(sp::ecs::Entity entity, ScanState::State state) +{ + auto faction = entity.getComponent(); + setStateForFaction(faction ? faction->entity : sp::ecs::Entity{}, state); +} + +ScanState::State ScanState::getStateForFaction(sp::ecs::Entity faction_entity) +{ + for(const auto& it : per_faction) + if (it.faction == faction_entity) + return it.state; + return ScanState::State::NotScanned; +} + +void ScanState::setStateForFaction(sp::ecs::Entity faction_entity, ScanState::State state) +{ + for(auto& it : per_faction) { + if (it.faction == faction_entity) { + it.state = state; + return; + } + } + per_faction.push_back({faction_entity, state}); + per_faction_dirty = true; +} diff --git a/src/components/scanning.h b/src/components/scanning.h new file mode 100644 index 0000000000..b3969026c0 --- /dev/null +++ b/src/components/scanning.h @@ -0,0 +1,55 @@ +#pragma once + +#include "ecs/entity.h" +#include + + +class ScanState +{ +public: + enum class State { + NotScanned, + FriendOrFoeIdentified, + SimpleScan, + FullScan + }; + struct Entry + { + sp::ecs::Entity faction; + ScanState::State state; + }; + + /*! + * Scan state per FactionInfo. + * When the required faction is not in the vector, the scan state + * is SS_NotScanned + */ + bool per_faction_dirty = true; + std::vector per_faction; + + bool allow_simple_scan = false; // Does the first scan go to a full scan or a simple scan. + int complexity = -1; //Amount of bars each minigame has (-1 for default) + int depth = -1; //Amount of minigames that need to be finished (-1 for default) + + State getStateFor(sp::ecs::Entity entity); + void setStateFor(sp::ecs::Entity entity, State state); + State getStateForFaction(sp::ecs::Entity entity); + void setStateForFaction(sp::ecs::Entity entity, State state); +}; + +class ScienceDescription +{ +public: + string not_scanned; + string friend_or_foe_identified; + string simple_scan; + string full_scan; +}; + +class ScienceScanner +{ +public: + float delay = 0.0f; // When a delay based scan is done, this will count down. + float max_scanning_delay = 6.0f; + sp::ecs::Entity target; +}; \ No newline at end of file diff --git a/src/components/selfdestruct.h b/src/components/selfdestruct.h new file mode 100644 index 0000000000..27eb9f1e28 --- /dev/null +++ b/src/components/selfdestruct.h @@ -0,0 +1,20 @@ +#pragma once + +#include "crewPosition.h" + + +class SelfDestruct +{ +public: + // Maximum number of self-destruction confirmation codes + constexpr static int max_codes = 3; + + bool active = false; + uint32_t code[max_codes] = {0, 0, 0}; + bool confirmed[max_codes] = {false, false, false}; + CrewPosition entry_position[max_codes] = {CrewPosition::helmsOfficer, CrewPosition::helmsOfficer, CrewPosition::helmsOfficer}; + CrewPosition show_position[max_codes] = {CrewPosition::helmsOfficer, CrewPosition::helmsOfficer, CrewPosition::helmsOfficer}; + float countdown = 0.0f; + float damage = 150.0f; + float size = 1500.0f; +}; diff --git a/src/components/sfx.h b/src/components/sfx.h new file mode 100644 index 0000000000..b038dcd1ff --- /dev/null +++ b/src/components/sfx.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +class Sfx +{ +public: + string sound; + float power; + bool played = false; +}; + +/* + if (be.source && delta > 0 && !beam_sound_played) + { + float volume = 50.0f + (beam_fire_sound_power * 75.0f); + float pitch = (1.0f / beam_fire_sound_power) + random(-0.1f, 0.1f); + if (source) { + if (auto transform = source.getComponent()) + soundManager->playSound(beam_fire_sound, transform->getPosition(), 400.0, 0.6, pitch, volume); + } + beam_sound_played = true; + } +*/ \ No newline at end of file diff --git a/src/components/shields.cpp b/src/components/shields.cpp new file mode 100644 index 0000000000..866e979574 --- /dev/null +++ b/src/components/shields.cpp @@ -0,0 +1,22 @@ +#include "components/shields.h" +#include "vectorUtils.h" + + +ShipSystem& Shields::getSystemForIndex(int index) +{ + if (entries.size() < 2) + return front_system; + float angle = index * 360.0f / entries.size(); + if (std::abs(angleDifference(angle, 0.0f)) < 90) + return front_system; + return rear_system; +} + +float Shields::getDamageFactor(int index) +{ + auto system = getSystemForIndex(index); + float shield_damage_exponent = 1.6f; + float shield_damage_divider = 7.0f; + float shield_damage_factor = 1.0f + powf(1.0f, shield_damage_exponent) / shield_damage_divider-powf(system.getSystemEffectiveness(), shield_damage_exponent) / shield_damage_divider; + return shield_damage_factor; +} diff --git a/src/components/shields.h b/src/components/shields.h new file mode 100644 index 0000000000..fe45ffff40 --- /dev/null +++ b/src/components/shields.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ecs/entity.h" +#include "shipsystem.h" + + +class Shields { +public: + bool active = true; + + // Time in seconds it takes to recalibrate shields + float calibration_time = 25.0f; + float calibration_delay = 0.0f; + int frequency = -1; // Current frequency of the shield. -1 indicates that these shields have no frequency. + + float energy_use_per_second = 1.5f; + + struct Shield { + float level = 1.0f; + float max = 1.0f; + float hit_effect = 0.0f; + + int percentage() { if (max <= 0.0f) return 0; return int(100.0f * level / max); } + }; + std::vector entries; + ShipSystem front_system; + ShipSystem rear_system; + + ShipSystem& getSystemForIndex(int index); + float getDamageFactor(int index); +}; diff --git a/src/components/shiplog.cpp b/src/components/shiplog.cpp new file mode 100644 index 0000000000..40abd7c4ac --- /dev/null +++ b/src/components/shiplog.cpp @@ -0,0 +1,27 @@ +#include "components/shiplog.h" +#include "gameGlobalInfo.h" + + +void ShipLog::add(const string& message, glm::u8vec4 color) +{ + add(gameGlobalInfo->getMissionTime() + string(": "), message, color); +} + +void ShipLog::add(const string& prefix, const string& message, glm::u8vec4 color) +{ + // Cap the ship's log size to 100 entries. If it exceeds that limit, + // start erasing entries from the beginning. + if (entries.size() > 100) + entries.erase(entries.begin()); + + // Timestamp a log entry, color it, and add it to the end of the log. + entries.push_back({prefix, message, color}); + new_entry_count += 1; +} + +void ShipLog::clear() +{ + cleared = true; + new_entry_count = 0; + entries.clear(); +} \ No newline at end of file diff --git a/src/components/shiplog.h b/src/components/shiplog.h new file mode 100644 index 0000000000..3af8dce487 --- /dev/null +++ b/src/components/shiplog.h @@ -0,0 +1,33 @@ +#pragma once + +#include "stringImproved.h" +#include +#include + + +class ShipLog +{ +public: + class Entry + { + public: + string prefix; + string text; + glm::u8vec4 color; + + bool operator!=(const Entry& e) const { return prefix != e.prefix || text != e.text || color != e.color; } + }; + + void add(const string& message, glm::u8vec4 color); + void add(const string& prefix, const string& message, glm::u8vec4 color); + void clear(); + + size_t size() const { return entries.size(); } + const Entry& get(size_t index) const { return entries[index]; } + + // Info for replication + bool cleared = false; + size_t new_entry_count = 0; +private: + std::vector entries; +}; diff --git a/src/components/shipsystem.cpp b/src/components/shipsystem.cpp new file mode 100644 index 0000000000..5481ab529e --- /dev/null +++ b/src/components/shipsystem.cpp @@ -0,0 +1,131 @@ +#include "shipsystem.h" +#include "i18n.h" +#include "gameGlobalInfo.h" +#include "components/reactor.h" +#include "components/beamweapon.h" +#include "components/missiletubes.h" +#include "components/shields.h" +#include "components/impulse.h" +#include "components/maneuveringthrusters.h" +#include "components/jumpdrive.h" +#include "components/warpdrive.h" + + +// Overheat subsystem damage rate +constexpr static float damage_per_second_on_overheat = 0.08f; + + +float ShipSystem::getSystemEffectiveness() +{ + float power = power_level; + + // Substract the hacking from the power, making double hacked systems run at 25% efficiency. + power = std::max(0.0f, power - hacked_level * 0.75f); + + // Degrade damaged systems. + if (gameGlobalInfo && gameGlobalInfo->use_system_damage) + return std::max(0.0f, power * health); + + // If a system cannot be damaged, excessive heat degrades it. + return std::max(0.0f, power * (1.0f - heat_level)); +} + +void ShipSystem::addHeat(float amount) +{ + heat_level += amount; + + if (heat_level > 1.0f) + { + float overheat = heat_level - 1.0f; + heat_level = 1.0f; + + if (gameGlobalInfo->use_system_damage) + { + // Heat damage is specified as damage per second while overheating. + // Calculate the amount of overheat back to a time, and use that to + // calculate the actual damage taken. + health -= overheat / heat_add_rate_per_second * damage_per_second_on_overheat; + + if (health < -1.0f) + health = -1.0f; + } + } + + if (heat_level < 0.0f) + heat_level = 0.0f; +} + +ShipSystem* ShipSystem::get(sp::ecs::Entity entity, Type type) +{ + switch(type) + { + case Type::None: + case Type::COUNT: + return nullptr; + case Type::Reactor: + return entity.getComponent(); + case Type::BeamWeapons: + return entity.getComponent(); + case Type::MissileSystem: + return entity.getComponent(); + case Type::Maneuver: + return entity.getComponent(); + case Type::Impulse: + return entity.getComponent(); + case Type::Warp: + return entity.getComponent(); + case Type::JumpDrive: + return entity.getComponent(); + case Type::FrontShield: + { + auto shields = entity.getComponent(); + if (shields) + return &shields->front_system; + return nullptr; + } + case Type::RearShield: + { + auto shields = entity.getComponent(); + if (shields && shields->entries.size() > 1) + return &shields->rear_system; + return nullptr; + } + } + return nullptr; +} + +string getSystemName(ShipSystem::Type system) +{ + switch(system) + { + case ShipSystem::Type::Reactor: return "reactor"; + case ShipSystem::Type::BeamWeapons: return "beamweapons"; + case ShipSystem::Type::MissileSystem: return "missilesystem"; + case ShipSystem::Type::Maneuver: return "maneuvering"; + case ShipSystem::Type::Impulse: return "impulse"; + case ShipSystem::Type::Warp: return "warpdrive"; + case ShipSystem::Type::JumpDrive: return "jumpdrive"; + case ShipSystem::Type::FrontShield: return "frontshield"; + case ShipSystem::Type::RearShield: return "rearshield"; + default: + return "UNKNOWN"; + } +} + +string getLocaleSystemName(ShipSystem::Type system) +{ + switch(system) + { + case ShipSystem::Type::Reactor: return tr("system", "Reactor"); + case ShipSystem::Type::BeamWeapons: return tr("system", "Beam Weapons"); + case ShipSystem::Type::MissileSystem: return tr("system", "Missile System"); + case ShipSystem::Type::Maneuver: return tr("system", "Maneuvering"); + case ShipSystem::Type::Impulse: return tr("system", "Impulse Engines"); + case ShipSystem::Type::Warp: return tr("system", "Warp Drive"); + case ShipSystem::Type::JumpDrive: return tr("system", "Jump Drive"); + case ShipSystem::Type::FrontShield: return tr("system", "Front Shield Generator"); + case ShipSystem::Type::RearShield: return tr("system", "Rear Shield Generator"); + default: + return "UNKNOWN"; + } +} diff --git a/src/components/shipsystem.h b/src/components/shipsystem.h new file mode 100644 index 0000000000..47bb902572 --- /dev/null +++ b/src/components/shipsystem.h @@ -0,0 +1,58 @@ +#pragma once + +#include "ecs/entity.h" +#include + +//Base class for ship systems, ever created directly, use as base class for other components. +class ShipSystem +{ +public: + enum class Type + { + None = -1, + Reactor = 0, + BeamWeapons, + MissileSystem, + Maneuver, + Impulse, + Warp, + JumpDrive, + FrontShield, + RearShield, + COUNT + }; + static constexpr int COUNT = static_cast(Type::COUNT); + + static constexpr float power_factor_rate = 0.08f; + static constexpr float default_add_heat_rate_per_second = 0.05f; + static constexpr float default_power_rate_per_second = 0.3f; + static constexpr float default_coolant_rate_per_second = 1.2f; + + float health = 1.0f; //1.0-0.0, where 0.0 is fully broken. + float health_max = 1.0f; //1.0-0.0, where 0.0 is fully broken. + float power_level = 1.0f; //0.0-3.0, default 1.0 + float power_request = 1.0f; + float heat_level = 0.0f; //0.0-1.0, system will damage at 1.0 + float coolant_level = 0.0f; //0.0-10.0 + float coolant_request = 0.0f; + bool can_be_hacked = true; + float hacked_level = 0.0f; //0.0-1.0 + float power_factor = 1.0f; + float coolant_change_rate_per_second = default_coolant_rate_per_second; + float heat_add_rate_per_second = default_add_heat_rate_per_second; + float power_change_rate_per_second = default_power_rate_per_second; + float auto_repair_per_second = 0.0f; // TODO = 0.005f; for CPU ships + + float getSystemEffectiveness(); + void addHeat(float amount); + + float getHeatingDelta() const + { + return std::pow(1.7f, power_level - 1.0f) - (1.01f + coolant_level * 0.1f); + } + + static ShipSystem* get(sp::ecs::Entity entity, Type type); +}; + +string getSystemName(ShipSystem::Type system); +string getLocaleSystemName(ShipSystem::Type system); diff --git a/src/components/spin.h b/src/components/spin.h new file mode 100644 index 0000000000..c30877fbae --- /dev/null +++ b/src/components/spin.h @@ -0,0 +1,8 @@ +#pragma once + +// Component to spin around your axis all the time. +class Spin +{ +public: + float rate = 0.0f; +}; diff --git a/src/components/target.h b/src/components/target.h new file mode 100644 index 0000000000..01e9b7069a --- /dev/null +++ b/src/components/target.h @@ -0,0 +1,9 @@ +#pragma once + +#include "ecs/entity.h" + +class Target +{ +public: + sp::ecs::Entity entity; +}; diff --git a/src/components/warpdrive.h b/src/components/warpdrive.h new file mode 100644 index 0000000000..0493fe621c --- /dev/null +++ b/src/components/warpdrive.h @@ -0,0 +1,26 @@ +#pragma once + +#include "stringImproved.h" +#include "shipsystem.h" + +// Impulse engine component, indicate that this entity can move under impulse control. +class WarpDrive : public ShipSystem { +public: + constexpr static float charge_time = 4.0f; + constexpr static float decharge_time = 2.0f; + constexpr static float heat_per_warp = 0.02f; + + // Config + int max_level = 4; + float speed_per_level = 1000; + float energy_warp_per_second = 1.7f; + + // Runtime + int request = 0; // [input] Level of warp requested, from 0 to max_level + float current = 0.0f; // [output] Current active warp amount, from 0.0 to 4.0 +}; + +class WarpJammer { +public: + float range = 7000.0; +}; \ No newline at end of file diff --git a/src/components/zone.cpp b/src/components/zone.cpp new file mode 100644 index 0000000000..125852748e --- /dev/null +++ b/src/components/zone.cpp @@ -0,0 +1,17 @@ +#include "zone.h" +#include "math/centerOfMass.h" +#include "math/triangulate.h" + + +void Zone::updateTriangles() +{ + label_offset = centerOfMass(outline); + radius = 1; + for(auto p : outline) { + p -= label_offset; + radius = std::max(radius, glm::length(p)); + } + + triangles.clear(); + Triangulate::process(outline, triangles); +} \ No newline at end of file diff --git a/src/components/zone.h b/src/components/zone.h new file mode 100644 index 0000000000..807ca33de1 --- /dev/null +++ b/src/components/zone.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + + +class Zone +{ +public: + glm::u8vec4 color{255,255,255, 0}; + std::vector outline; + std::vector triangles; + string label; + glm::vec2 label_offset; + float radius; + bool zone_dirty = true; + + void updateTriangles(); +}; diff --git a/src/crewPosition.h b/src/crewPosition.h new file mode 100644 index 0000000000..9d8aa31f11 --- /dev/null +++ b/src/crewPosition.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + + +enum class CrewPosition +{ + //6/5 player crew + helmsOfficer, + weaponsOfficer, + engineering, + scienceOfficer, + relayOfficer, + //4/3 player crew + tacticalOfficer, //helms+weapons-shields + engineeringAdvanced,//engineering+shields + operationsOfficer, //science+comms + //1 player crew + singlePilot, + //extras + damageControl, + powerManagement, + databaseView, + altRelay, + commsOnly, + shipLog, + + MAX +}; + +static_assert(static_cast(CrewPosition::MAX) <= 64); +class CrewPositions +{ +public: + void add(CrewPosition cp) { mask |= 1 << static_cast(cp); } + void remove(CrewPosition cp) { mask &=~(1 << static_cast(cp)); } + bool has(CrewPosition cp) const { return mask & (1 << static_cast(cp)); } + + uint64_t mask = 0; + + bool operator==(const CrewPositions& other) { return mask == other.mask; } + bool operator!=(const CrewPositions& other) { return mask != other.mask; } + + class Iterator { + public: + Iterator(uint64_t _mask, CrewPosition _cp) : mask(_mask), cp(_cp) { + if(cp != CrewPosition::MAX && (mask & (1 << int(cp))) == 0) { + ++(*this); + } + } + bool operator!=(const Iterator& other) const { return cp != other.cp; } + void operator++() { + cp = CrewPosition(int(cp)+1); + while(cp != CrewPosition::MAX && (mask & (1 << int(cp))) == 0) { + cp = CrewPosition(int(cp)+1); + } + } + CrewPosition operator*() { return cp; } + private: + uint64_t mask; + CrewPosition cp; + }; + Iterator begin() { return {mask, CrewPosition(0)}; } + Iterator end() { return {mask, CrewPosition::MAX}; } + + static CrewPositions all() { return CrewPositions{(1 << int(CrewPosition::MAX)) - 1}; } +}; + +namespace sp::io { + static inline DataBuffer& operator << (DataBuffer& packet, const CrewPositions& cps) { packet << cps.mask; return packet;} + static inline DataBuffer& operator >> (DataBuffer& packet, CrewPositions& cps) { packet >> cps.mask; return packet; } +} diff --git a/src/discord.cpp b/src/discord.cpp index c7559ffb0c..70cbfd3854 100644 --- a/src/discord.cpp +++ b/src/discord.cpp @@ -1,7 +1,7 @@ #include "discord.h" #include "playerInfo.h" #include "gameGlobalInfo.h" -#include "spaceObjects/playerSpaceship.h" +#include "components/name.h" #include static IDiscordCore* core; @@ -63,23 +63,28 @@ void DiscordRichPresence::update(float delta) if (my_spaceship && my_player_info) { - auto name = my_spaceship->getCallSign() + " [" + my_spaceship->getTypeName() + "]"; + string name; + if (auto callsign = my_spaceship.getComponent()) + name = callsign->callsign; + if (auto type_name = my_spaceship.getComponent()) + name += " [" + type_name->type_name + "]"; strncpy(activity.details, name.c_str(), sizeof(activity.details)); - for(int idx=0; idxcrew_position[idx]) + auto cp = CrewPosition(idx); + if (my_player_info->hasPosition(cp)) { - strncpy(activity.state, getCrewPositionName(ECrewPosition(idx)).c_str(), sizeof(activity.state)); - if (idx == helmsOfficer) + strncpy(activity.state, getCrewPositionName(cp).c_str(), sizeof(activity.state)); + if (cp == CrewPosition::helmsOfficer) strcpy(activity.assets.small_image, "helms_white"); - if (idx == weaponsOfficer) + if (cp == CrewPosition::weaponsOfficer) strcpy(activity.assets.small_image, "weapons_white"); - if (idx == engineering) + if (cp == CrewPosition::engineering) strcpy(activity.assets.small_image, "engineering_white"); - if (idx == scienceOfficer) + if (cp == CrewPosition::scienceOfficer) strcpy(activity.assets.small_image, "science_white"); - if (idx == relayOfficer) + if (cp == CrewPosition::relayOfficer) strcpy(activity.assets.small_image, "relay_white"); break; } diff --git a/src/epsilonServer.cpp b/src/epsilonServer.cpp index fec65eb80c..64dca3c1bc 100644 --- a/src/epsilonServer.cpp +++ b/src/epsilonServer.cpp @@ -6,6 +6,8 @@ #include "preferenceManager.h" #include "GMActions.h" #include "main.h" +#include "config.h" + EpsilonServer::EpsilonServer(int server_port) : GameServer("Server", VERSION_NUMBER, server_port) @@ -66,14 +68,14 @@ std::unordered_set EpsilonServer::onVoiceChat(int32_t client_id, int32_ if (target_identifier == 0) { //Communicate to local ship. - int32_t ship_id = -1; + sp::ecs::Entity ship; foreach(PlayerInfo, i, player_info_list) if (i->client_id == client_id) - ship_id = i->ship_id; + ship = i->ship; std::unordered_set result; foreach(PlayerInfo, i, player_info_list) - if (i->ship_id == ship_id && i->client_id != client_id) + if (i->ship == ship && i->client_id != client_id) result.insert(i->client_id); return result; } diff --git a/src/factionInfo.cpp b/src/factionInfo.cpp deleted file mode 100644 index e457fb5fc9..0000000000 --- a/src/factionInfo.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "factionInfo.h" -#include "scriptInterface.h" -#include "multiplayer_server.h" - -/// A FactionInfo object contains presentation details and faction relationships for member SpaceObjects. -/// EmptyEpsilon has a hardcoded limit of 32 factions. -/// -/// SpaceObjects belong to a faction that determines which objects are friendly, neutral, or hostile toward them. -/// For example, these relationships determine whether a SpaceObject can be targeted by weapons, docked with, or receive comms from another SpaceObject. -/// If a faction doesn't have a relationship with another faction, it treats those factions as neutral. -/// Friendly and hostile faction relationships are automatically reciprocated when set with setEnemy() and setFriendly(). -/// -/// If this faction consideres another faction to be hostile, it can target and fire weapons at it, and CpuShips with certain orders might pursue it. -/// If neutral, this faction can't target and fire weapons at the other faction, and other factions can dock with its stations or dockable ships. -/// If friendly, this faction acts as neutral but also shares short-range radar with PlayerSpaceships in Relay, and can grant reputation points to PlayerSpaceships of the same faction. -/// -/// Many scenario and comms scripts also give friendly factions benefits at a reputation cost that netural factions do not. -/// Factions are loaded from resources/factionInfo.lua upon launching a scenario, and accessed by using the getFactionInfo() global function. -/// -/// Example: -/// human_navy = getFactionInfo("Human Navy") -/// exuari = getFactionInfo("Exuari") -/// faction = FactionInfo():setName("USN"):setLocaleName(_("USN")) -- sets the internal and translatable faction names -/// faction:setGMColor(255,128,255) -- uses purple icons for this faction's SpaceObjects in GM and Spectator views -/// faction:setFriendly(human_navy):setEnemy(exuari) -- sets this faction's friendly and hostile relationships -/// faction:setDescription(_("The United Stellar Navy, or USN...")) -- sets a translatable description for this faction -REGISTER_SCRIPT_CLASS(FactionInfo) -{ - /// Sets this faction's internal string name, used to reference this faction regardless of EmptyEpsilon's language setting. - /// If no locale name is defined, this sets the locale name to the same value. - /// Example: faction:setName("USN") - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setName); - /// Sets this faction's name as presented in the user interface. - /// Wrap the string in the _() function to make it available for translation. - /// Example: faction:setLocaleName(_("USN")) - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setLocaleName); - /// Sets the RGB color used for SpaceObjects of this faction as seen on the GM and Spectator views. - /// Defaults to white (255,255,255). - /// Example: faction:setGMColor(255,0,0) -- sets the color to red - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setGMColor); - /// Sets this faction's longform description as shown in its Factions ScienceDatabase child entry. - /// Wrap the string in the _() function to make it available for translation. - /// Example: faction:setDescription(_("The United Stellar Navy, or USN...")) -- sets a translatable description for this faction - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setDescription); - /// Sets the given faction to appear as hostile to SpaceObjects of this faction. - /// For example, Spaceships of this faction can target and fire at SpaceShips of the given faction. - /// Defaults to no hostile factions. - /// Warning: A faction can be designated as hostile to itself, but the behavior is not well-defined. - /// Example: faction:setEnemy(exuari) -- sets the Exuari to appear as hostile to this faction - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setEnemy); - /// Sets the given faction to appear as friendly to SpaceObjects of this faction. - /// For example, PlayerSpaceships of this faction can gain reputation with it. - /// Defaults to no friendly factions. - /// Example: faction:setFriendly(exuari) -- sets the Human Navy to appear as friendly to this faction - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setFriendly); - /// Sets the given faction to appear as neutral to SpaceObjects of this faction. - /// This removes any existing faction relationships between the two factions. - /// Example: faction:setNeutral(human_navy) -- sets the Human Navy to appear as neutral to this faction - REGISTER_SCRIPT_CLASS_FUNCTION(FactionInfo, setNeutral); -} - -std::array, 32> factionInfo; - -static int getFactionInfo(lua_State* L) -{ - auto name = luaL_checkstring(L, 1); - for(unsigned int n = 0; n < factionInfo.size(); n++) - if (factionInfo[n] && factionInfo[n]->getName() == name) - return convert>::returnType(L, factionInfo[n]); - return 0; -} -/// P getFactionInfo(string faction_name) -/// Returns a reference to the FactionInfo object with the given name. -/// Use this to modify faction details and relationships with FactionInfo functions. -/// Example: faction = getFactionInfo("Human Navy") -- faction = the Human Navy FactionInfo object -REGISTER_SCRIPT_FUNCTION(getFactionInfo); - -REGISTER_MULTIPLAYER_CLASS(FactionInfo, "FactionInfo"); -FactionInfo::FactionInfo() -: MultiplayerObject("FactionInfo") -{ - index = 255; - gm_color = {255,255,255,255}; - enemy_mask = 0; - friend_mask = 0; - - registerMemberReplication(&index); - registerMemberReplication(&gm_color); - registerMemberReplication(&name); - registerMemberReplication(&locale_name); - registerMemberReplication(&description); - registerMemberReplication(&enemy_mask); - registerMemberReplication(&friend_mask); - - if (game_server) { - for(size_t n=0; n other) -{ - if (!other) - { - LOG(WARNING) << "Tried to set an undefined faction to be an enemy of " << name; - return; - } - - friend_mask &=~(1U << other->index); - other->friend_mask &=~(1U << index); - enemy_mask |= (1 << other->index); - other->enemy_mask |= (1 << index); -} - -void FactionInfo::setFriendly(P other) -{ - if (!other) - { - LOG(WARNING) << "Tried to set an undefined faction to be friendly with " << name; - return; - } - - friend_mask |= (1U << other->index); - other->friend_mask |= (1U << index); - enemy_mask &=~(1 << other->index); - other->enemy_mask &=~(1 << index); -} - -void FactionInfo::setNeutral(P other) -{ - if (!other) - { - LOG(WARNING) << "Tried to set an undefined faction to be neutral with " << name; - return; - } - - friend_mask &=~(1 << other->index); - other->friend_mask &=~(1 << index); - enemy_mask &=~(1 << other->index); - other->enemy_mask &=~(1 << index); -} - -EFactionVsFactionState FactionInfo::getState(P other) -{ - if (!other) return FVF_Neutral; - if (enemy_mask & (1 << other->index)) return FVF_Enemy; - if (friend_mask & (1 << other->index)) return FVF_Friendly; - return FVF_Neutral; -} - -EFactionVsFactionState FactionInfo::getState(uint8_t idx0, uint8_t idx1) -{ - if (idx0 >= factionInfo.size()) return FVF_Neutral; - if (idx1 >= factionInfo.size()) return FVF_Neutral; - if (!factionInfo[idx0] || !factionInfo[idx1]) return FVF_Neutral; - return factionInfo[idx0]->getState(factionInfo[idx1]); -} - -unsigned int FactionInfo::findFactionId(string name) -{ - for(unsigned int n = 0; n < factionInfo.size(); n++) - if (factionInfo[n] && factionInfo[n]->name == name) - return n; - LOG(ERROR) << "Failed to find faction: " << name; - return 0; -} - -void FactionInfo::reset() -{ - for(unsigned int n = 0; n < factionInfo.size(); n++) - if (factionInfo[n]) - factionInfo[n]->destroy(); -} diff --git a/src/factionInfo.h b/src/factionInfo.h deleted file mode 100644 index c9ba92f6ff..0000000000 --- a/src/factionInfo.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef FACTION_INFO_H -#define FACTION_INFO_H - -#include "P.h" -#include "stringImproved.h" -#include "multiplayer.h" -#include -#include - - -class FactionInfo; -extern std::array, 32> factionInfo; - -enum EFactionVsFactionState -{ - FVF_Friendly, - FVF_Neutral, - FVF_Enemy -}; - -class FactionInfo : public MultiplayerObject, public Updatable -{ -public: - FactionInfo(); - virtual ~FactionInfo(); - - virtual void update(float delta) override; - /*! - * \brief Set name of faction. - * \param Name Name of the faction - */ - void setName(string name) { this->name = name; if (locale_name == "") locale_name = name; } - void setLocaleName(string name) { this->locale_name = name; } - - /*! - * \brief Get name of faction. - * \return String Name of the faction - */ - string getName() { return this->name; } - string getLocaleName() { return this->locale_name; } - - /*! - * \brief Get description of faction. - * \return String description of the faction - */ - string getDescription() {return this->description;} - /*! - * \brief Set color of faction on GM screen. - * \param r Red component. - * \param g Green component. - * \param b Blue component. - */ - void setGMColor(int r, int g, int b) { gm_color = glm::u8vec4(r, g, b, 255); } - glm::u8vec4 getGMColor() { return gm_color; } - /*! - * \brief Set description of faction. - * \param description - */ - void setDescription(string description) { this->description = description; } - /*! - * \brief Add another faction that this faction sees as an enemy. - * \param faction info object. - */ - void setEnemy(P other); - /*! - * \brief Add another faction that this faction sees as a friendly. - * \param faction info object. - */ - void setFriendly(P other); - void setNeutral(P other); - - EFactionVsFactionState getState(P other); - - static EFactionVsFactionState getState(uint8_t idx0, uint8_t idx1); - static unsigned int findFactionId(string name); - - static void reset(); //Destroy all FactionInfo objects -protected: - uint8_t index; - glm::u8vec4 gm_color; - string name; - string locale_name; - string description; - uint32_t enemy_mask; - uint32_t friend_mask; -}; - -#endif//FACTION_INFO_H diff --git a/src/gameGlobalInfo.cpp b/src/gameGlobalInfo.cpp index 9ccc7621e2..d537cccea4 100644 --- a/src/gameGlobalInfo.cpp +++ b/src/gameGlobalInfo.cpp @@ -3,12 +3,15 @@ #include "gameGlobalInfo.h" #include "preferenceManager.h" #include "scenarioInfo.h" -#include "scienceDatabase.h" #include "multiplayer_client.h" #include "soundManager.h" #include "random.h" #include "config.h" -#include "io/json.h" +#include "components/collision.h" +#include "systems/collision.h" +#include "ecs/query.h" +#include "menus/luaConsole.h" +#include "playerInfo.h" #include P gameGlobalInfo; @@ -20,15 +23,8 @@ GameGlobalInfo::GameGlobalInfo() SDL_assert(!gameGlobalInfo); callsign_counter = 0; - victory_faction = -1; gameGlobalInfo = this; - for(int n=0; n GameGlobalInfo::getPlayerShip(int index) -{ - SDL_assert(index >= 0 && index < max_player_ships); - if (game_server) - return game_server->getObjectById(playerShipId[index]); - return game_client->getObjectById(playerShipId[index]); -} - -void GameGlobalInfo::setPlayerShip(int index, P ship) -{ - SDL_assert(index >= 0 && index < max_player_ships); - SDL_assert(game_server); - - if (ship) - playerShipId[index] = ship->getMultiplayerId(); - else - playerShipId[index] = -1; -} - -int GameGlobalInfo::findPlayerShip(P ship) -{ - for(int n=0; n ship) +void GameGlobalInfo::onReceiveServerCommand(sp::io::DataBuffer& packet) { - for(int n=0; n> command; + switch(command) { - if (!getPlayerShip(n)) + case CMD_PLAY_CLIENT_SOUND:{ + CrewPosition position; + string sound_name; + sp::ecs::Entity entity; + packet >> entity >> position >> sound_name; + if (my_spaceship == entity && my_player_info) { - setPlayerShip(n, ship); - return n; + if ((position == CrewPosition::MAX && my_player_info->main_screen) || my_player_info->hasPosition(position)) + { + soundManager->playSound(sound_name); + } } + }break; } - return -1; +} + +void GameGlobalInfo::playSoundOnMainScreen(sp::ecs::Entity ship, string sound_name) +{ + sp::io::DataBuffer packet; + packet << CMD_PLAY_CLIENT_SOUND << ship << CrewPosition::MAX << sound_name; + broadcastServerCommand(packet); } void GameGlobalInfo::update(float delta) @@ -115,15 +96,28 @@ void GameGlobalInfo::update(float delta) if (my_player_info) { //Set the my_spaceship variable based on the my_player_info->ship_id - if ((my_spaceship && my_spaceship->getMultiplayerId() != my_player_info->ship_id) || (my_spaceship && my_player_info->ship_id == -1) || (!my_spaceship && my_player_info->ship_id != -1)) - { - if (game_server) - my_spaceship = game_server->getObjectById(my_player_info->ship_id); - else - my_spaceship = game_client->getObjectById(my_player_info->ship_id); - } + if (my_spaceship != my_player_info->ship) + my_spaceship = my_player_info->ship; } elapsed_time += delta; + + if (main_scenario_script && main_script_error_count < max_repeated_script_errors) { + auto res = main_scenario_script->call("update", delta); + if (res.isErr() && res.error() != "Not a function") { + LuaConsole::checkResult(res); + main_script_error_count += 1; + if (main_script_error_count == max_repeated_script_errors) { + LuaConsole::addLog("5 repeated script update errors, stopping updates."); + } + } else { + main_script_error_count = 0; + } + } + for(auto& as : additional_scripts) { + auto res = as->call("update", delta); + if (res.isErr() && res.error() != "Not a function") + LuaConsole::checkResult(res); + } } string GameGlobalInfo::getNextShipCallsign() @@ -145,12 +139,85 @@ string GameGlobalInfo::getNextShipCallsign() return "SS" + string(callsign_counter); } -void GameGlobalInfo::addScript(P