Index: ps/trunk/source/tools/entconvert/entconvert.pl
===================================================================
--- ps/trunk/source/tools/entconvert/entconvert.pl (revision 7351)
+++ ps/trunk/source/tools/entconvert/entconvert.pl (revision 7352)
@@ -1,247 +1,248 @@
use strict;
use warnings;
use File::Find;
use XML::Simple;
use Data::Dumper;
my $dir = '../../../binaries/data/mods/public/entities';
my $dir2 = '../../../binaries/data/mods/public/simulation/templates';
my @xml;
find({ wanted => sub {
push @xml, $_ if /\.xml$/ and !/template_entity(|_full|_quasi)\.xml$/;
}, no_chdir => 1 }, $dir);
s~\Q$dir/~~ for @xml;
my %xml = ('template_entity_full.xml' => 1, 'template_entity_quasi.xml' => 1);
$xml{$_} = 1 for @xml;
my (%dot_actor, %dot_inherit);
for my $xml (@xml) {
print "$xml\n";
my $name = $xml;
$name =~ s/\.xml$//;
my %opt = (KeyAttr => []);
my $data = XMLin("$dir/$xml", %opt, ForceArray => 1);
my $c = convert($name, $data);
my $out = $c;
# print "$out\n\n";
open my $fo, "> $dir2/$xml" or die $!; print $fo $out;
}
sub convert {
my ($name, $data) = @_;
#print Dumper $data if $name eq 'template_unit_infantry';
my $out = qq{\n};
my $i = " ";
$out .= qq{{Parent}) {
my $p = $data->{Parent};
$p = "units/$p" if $p =~ /^(celt|cart|hele|iber|pers|rome)_(cavalry|infantry)/;
warn "Unknown parent $p\n" unless $xml{"$p.xml"};
$out .= qq{ parent="$p"};
$dot_inherit{$name}{$p} = 1;
}
$out .= qq{>\n};
my $civ;
$civ = $1 if $name =~ /^units\/([a-z]{4})_/;
my $needs_explicit_civ = ($civ and $data->{Parent} !~ /^${civ}_/);
if ($data->{Traits}[0]{Id} or $needs_explicit_civ) {
$out .= qq{$i\n};
$out .= qq{$i$i$civ\n} if $needs_explicit_civ;
my @map = (
[Generic => 'GenericName'],
[Specific => 'SpecificName'],
[Icon => 'IconSheet'],
[Icon_Cell => 'IconCell'],
);
for my $m (@map) {
$out .= qq{$i$i<$m->[1]>$data->{Traits}[0]{Id}[0]{$m->[0]}[0]$m->[1]>\n} if $data->{Traits}[0]{Id}[0]{$m->[0]};
}
for my $k (keys %{$data->{Traits}[0]{Id}[0]}) {
next if $k =~ /^(Civ)$/; # we do civ based on the filename instead, since it's more reliable
next if $k =~ /^(Tooltip|History|Internal_Only|Classes|Rollover|Civ_Code)$/; # TODO: convert these somehow
warn "Unrecognised field <$k>" unless grep $_->[0] eq $k, @map;
}
$out .= qq{$i\n};
}
if ($name eq 'template_unit') {
$out .= qq{$i\n};
$out .= qq{$i\n};
$out .= qq{$i$i1\n};
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Population} or $data->{Traits}[0]{Creation}[0]{Resource}) {
$out .= qq{$i\n};
$out .= qq{$i$i$data->{Traits}[0]{Population}[0]{Rem}[0]\n} if $data->{Traits}[0]{Population}[0]{Rem}
and $data->{Traits}[0]{Population}[0]{Rem}[0] != 1;
$out .= qq{$i$i$data->{Traits}[0]{Population}[0]{Add}[0]\n} if $data->{Traits}[0]{Population}[0]{Add};
if ($data->{Traits}[0]{Creation}[0]{Resource}) {
$out .= qq{$i$i\n};
for (qw(Food Wood Stone Metal)) {
$out .= qq{$i$i$i<\l$_>$data->{Traits}[0]{Creation}[0]{Resource}[0]{$_}[0]\l$_>\n} if $data->{Traits}[0]{Creation}[0]{Resource}[0]{$_}[0];
}
$out .= qq{$i$i\n};
}
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Supply} and $name =~ /template_gaia/) {
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Supply}) {
$out .= qq{$i\n};
$out .= qq{$i$i$data->{Traits}[0]{Supply}[0]{Max}[0]\n};
$out .= qq{$i$i$data->{Traits}[0]{Supply}[0]{Type}[0]\n};
$out .= qq{$i$i$data->{Traits}[0]{Supply}[0]{SubType}[0]\n} if $data->{Traits}[0]{Supply}[0]{SubType};
$out .= qq{$i\n};
}
if ($data->{Actions}[0]{Gather}) {
$out .= qq{$i\n};
$out .= qq{$i$i$data->{Actions}[0]{Gather}[0]{Speed}[0]\n};
if ($data->{Actions}[0]{Gather}[0]{Resource}) {
$out .= qq{$i$i\n};
my $r = $data->{Actions}[0]{Gather}[0]{Resource}[0];
for my $t (sort keys %$r) {
if (ref $r->{$t}[0]) {
for my $s (sort keys %{$r->{$t}[0]}) {
$out .= qq{$i$i$i<\L$t.$s>$r->{$t}[0]{$s}[0]$t.$s>\n};
}
} else {
$out .= qq{$i$i$i<\L$t>$r->{$t}[0]$t>\n};
}
}
$out .= qq{$i$i\n};
}
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Health}) {
$out .= qq{$i\n};
+ $out .= qq{$i$icorpse\n} if $name eq 'template_unit';
$out .= qq{$i$i$data->{Traits}[0]{Health}[0]{Max}[0]\n} if $data->{Traits}[0]{Health}[0]{Max};
$out .= qq{$i$i$data->{Traits}[0]{Health}[0]{RegenRate}[0]\n} if $data->{Traits}[0]{Health}[0]{RegenRate};
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Armour}) {
$out .= qq{$i\n};
for my $n (qw(Hack Pierce Crush)) {
$out .= qq{$i$i<$n>$data->{Traits}[0]{Armour}[0]{$n}[0]$n>\n} if $data->{Traits}[0]{Armour}[0]{$n};
}
$out .= qq{$i\n};
}
if ($data->{Actions}[0]{Move}) {
$out .= qq{$i\n};
$out .= qq{$i$i$data->{Actions}[0]{Move}[0]{Speed}[0]\n} if $data->{Actions}[0]{Move}[0]{Speed};
$out .= qq{$i\n};
}
die if $data->{Actions}[0]{Attack}[0]{Melee} and $data->{Actions}[0]{Attack}[0]{Ranged}; # only allow one at once
my $attack = $data->{Actions}[0]{Attack}[0]{Melee} || $data->{Actions}[0]{Attack}[0]{Ranged};
if ($attack) {
$out .= qq{$i\n};
for my $n (qw(Hack Pierce Crush Range MinRange ProjectileSpeed)) {
$out .= qq{$i$i<$n>$attack->[0]{$n}[0]$n>\n} if $attack->[0]{$n};
}
if ($attack->[0]{Speed}) {
my $s = $attack->[0]{Speed}[0];
# TODO: are these values sane?
if ($s eq '1000') {
$out .= qq{$i$i600\n};
$out .= qq{$i$i1000\n};
} elsif ($s eq '1500' or $s eq '1520' or $s eq '1510') {
$out .= qq{$i$i900\n};
$out .= qq{$i$i1500\n};
} elsif ($s eq '2000') {
$out .= qq{$i$i1200\n};
$out .= qq{$i$i2000\n};
} else {
die $s;
}
}
$out .= qq{$i\n};
}
$dot_actor{$name} = $data->{Actor};
if ($data->{Actor}) {
$out .= qq{$i\n};
$out .= qq{$i$i$data->{Actor}[0]\n};
$out .= qq{$i\n};
}
if ($data->{Traits}[0]{Footprint}) {
$out .= qq{$i\n};
if ($data->{Traits}[0]{Footprint}[0]{Radius}) {
$out .= qq{$i$i\n};
}
if ($data->{Traits}[0]{Footprint}[0]{Width}) {
$out .= qq{$i$i\n};
}
if ($data->{Traits}[0]{Footprint}[0]{Height}) {
$out .= qq{$i$i$data->{Traits}[0]{Footprint}[0]{Height}[0]\n};
}
$out .= qq{$i\n};
}
if ($name =~ /^template_(structure|gaia)$/) {
$out .= qq{$i\n};
}
if ($name =~ /^template_structure_resource_field$/) {
$out .= qq{$i\n};
}
if ($data->{Actions}[0]{Create}[0]{List}[0]{StructCiv} or $data->{Actions}[0]{Create}[0]{List}[0]{StructMil}) {
$out .= qq{$i\n};
$out .= qq{$i$i\n};
for (sort (keys %{$data->{Actions}[0]{Create}[0]{List}[0]{StructCiv}[0]}, keys %{$data->{Actions}[0]{Create}[0]{List}[0]{StructMil}[0]})) {
my $n = "structures/" . ($civ || "{civ}") . "_" . (lc $_);
$out .= qq{$i$i$i$n\n};
}
$out .= qq{$i$i\n};
$out .= qq{$i\n};
}
$out .= qq{\n};
return $out;
}
open my $dot, '> entities.dot' or die $!;
print $dot < "$_";\n};
}
for my $p (sort keys %dot_inherit) {
for my $c (sort keys %{$dot_inherit{$p}}) {
print $dot qq{"$p" -> "$c";\n};
}
}
print $dot "}\n";
Index: ps/trunk/source/simulation2/components/ICmpTemplateManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpTemplateManager.h (revision 7351)
+++ ps/trunk/source/simulation2/components/ICmpTemplateManager.h (revision 7352)
@@ -1,92 +1,96 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_ICMPTEMPLATEMANAGER
#define INCLUDED_ICMPTEMPLATEMANAGER
#include "simulation2/system/Interface.h"
#include
/**
* Template manager: Handles the loading of entity template files for the initialisation
* and deserialization of entity components.
*/
class ICmpTemplateManager : public IComponent
{
public:
/**
* Loads the template XML file identified by 'templateName' (including inheritance
* from parent XML files), and applies the techs that are currently active for
* player 'playerID', for use with a new entity 'ent'.
* The returned CParamNode must not be used for any entities other than 'ent'.
*
* If templateName is of the form "actor|foo" then it will load a default
* stationary entity template that uses actor "foo". (This is a convenience to
* avoid the need for hundreds of tiny decorative-object entity templates.)
*
* If templateName is of the form "preview|foo" then it will load a template
* based on entity template "foo" with the non-graphical components removed.
* (This is for previewing construction/placement of units.)
*
+ * If templateName is of the form "foundation|foo" then it will load a template
+ * based on entity template "foo" with various components removed and a few changed
+ * and added. (This is for constructing foundations of buildings.)
+ *
* @return NULL on error
*/
virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::wstring& templateName, int playerID) = 0;
/**
* Loads the template XML file identified by 'templateName' (including inheritance
* from parent XML files). The templateName syntax is the same as LoadTemplate.
*
* @return NULL on error
*/
virtual const CParamNode* GetTemplate(std::wstring templateName) = 0;
/**
* Returns the template most recently specified for the entity 'ent'.
* Used during deserialization.
*
* @return NULL on error
*/
virtual const CParamNode* LoadLatestTemplate(entity_id_t ent) = 0;
/**
* Returns the name of the template most recently specified for the entity 'ent'.
*/
virtual std::wstring GetCurrentTemplateName(entity_id_t ent) = 0;
/**
* Returns a list of strings that could be validly passed as @c templateName to LoadTemplate.
* (This includes "actor|foo" etc names).
* Intended for use by the map editor. This is likely to be quite slow.
*/
virtual std::vector FindAllTemplates() = 0;
/*
* TODO:
* When an entity changes template (e.g. upgrades) or player ownership, it
* should call some Reload(ent, templateName, playerID) function to load its new template.
* When a player researches new techs, it should call Reload(playerID).
* When a file changes on disk, something should call Reload(templateName).
*
* Reloading should happen by sending a message to affected components (containing
* their new CParamNode), then automatically updating this.template of scripted components.
*/
DECLARE_INTERFACE_TYPE(TemplateManager)
};
#endif // INCLUDED_ICMPTEMPLATEMANAGER
Index: ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 7351)
+++ ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 7352)
@@ -1,341 +1,393 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpTemplateManager.h"
#include "simulation2/MessageTypes.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/";
static const wchar_t ACTOR_ROOT[] = L"art/actors/";
class CCmpTemplateManager : public ICmpTemplateManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeGloballyToMessageType(MT_Destroy);
}
DEFAULT_COMPONENT_ALLOCATOR(TemplateManager)
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode))
{
}
virtual void Deinit(const CSimContext& UNUSED(context))
{
}
virtual void Serialize(ISerializer& serialize)
{
// TODO: refactor the common bits of this? also need nicer debug output
serialize.NumberU32_Unbounded("num entities", (u32)m_LatestTemplates.size());
std::map::const_iterator it = m_LatestTemplates.begin();
for (; it != m_LatestTemplates.end(); ++it)
{
serialize.NumberU32_Unbounded("id", it->first);
serialize.String("template", it->second, 0, 256);
}
// TODO: will need to serialize techs too, because we need to be giving out
// template data before other components (like the tech components) have been deserialized
}
virtual void Deserialize(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode), IDeserializer& deserialize)
{
u32 numEntities;
deserialize.NumberU32_Unbounded(numEntities);
for (u32 i = 0; i < numEntities; ++i)
{
entity_id_t ent;
std::wstring templateName;
deserialize.NumberU32_Unbounded(ent);
deserialize.String(templateName, 0, 256);
m_LatestTemplates[ent] = templateName;
}
}
virtual void HandleMessage(const CSimContext& UNUSED(context), const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Destroy:
{
const CMessageDestroy& msgData = static_cast (msg);
// Clean up m_LatestTemplates so it doesn't record any data for destroyed entities
m_LatestTemplates.erase(msgData.entity);
break;
}
}
}
virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::wstring& templateName, int playerID);
virtual const CParamNode* GetTemplate(std::wstring templateName);
virtual const CParamNode* LoadLatestTemplate(entity_id_t ent);
virtual std::wstring GetCurrentTemplateName(entity_id_t ent);
virtual std::vector FindAllTemplates();
private:
// Map from template name (XML filename or special |-separated string) to the most recently
// loaded valid template data.
// (Failed loads won't remove valid entries under the same name, so we behave more nicely
// when hotloading broken files)
std::map m_TemplateFileData;
// Remember the template used by each entity, so we can return them
// again for deserialization.
// TODO: should store player ID etc.
std::map m_LatestTemplates;
// (Re)loads the given template, regardless of whether it exists already,
// and saves into m_TemplateFileData. Also loads any parents that are not yet
// loaded. Returns false on error.
// @param templateName XML filename to load (not a |-separated string)
bool LoadTemplateFile(const std::wstring& templateName, int depth);
// Constructs a standard static-decorative-object template for the given actor
void ConstructTemplateActor(const std::wstring& actorName, CParamNode& out);
// Copy the non-interactive components of an entity template (position, actor, etc) into
// a new entity template
void CopyPreviewSubset(CParamNode& out, const CParamNode& in);
+
+ // Copy the components of an entity necessary for a construction foundation
+ // (position, actor, armour, health, etc) into a new entity template
+ void CopyFoundationSubset(CParamNode& out, const CParamNode& in);
};
REGISTER_COMPONENT_TYPE(TemplateManager)
const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std::wstring& templateName, int UNUSED(playerID))
{
m_LatestTemplates[ent] = templateName;
const CParamNode* templateRoot = GetTemplate(templateName);
if (!templateRoot)
return NULL;
// TODO: Eventually we need to support techs in here, and return a different template per playerID
return templateRoot;
}
const CParamNode* CCmpTemplateManager::GetTemplate(std::wstring templateName)
{
// Load the template if necessary
if (!LoadTemplateFile(templateName, 0))
{
LOGERROR(L"Failed to load entity template '%ls'", templateName.c_str());
return NULL;
}
const CParamNode& templateRoot = m_TemplateFileData[templateName].GetChild("Entity");
if (!templateRoot.IsOk())
{
LOGERROR(L"Invalid root element in entity template '%ls'", templateName.c_str());
return NULL;
}
// TODO: the template ought to be validated with some schema, so we don't
// need to nicely report errors like invalid root elements here
return &templateRoot;
}
const CParamNode* CCmpTemplateManager::LoadLatestTemplate(entity_id_t ent)
{
std::map::const_iterator it = m_LatestTemplates.find(ent);
if (it == m_LatestTemplates.end())
return NULL;
return LoadTemplate(ent, it->second, -1);
}
std::wstring CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent)
{
std::map::const_iterator it = m_LatestTemplates.find(ent);
if (it == m_LatestTemplates.end())
return L"";
return it->second;
}
bool CCmpTemplateManager::LoadTemplateFile(const std::wstring& templateName, int depth)
{
// If this file was already loaded, we don't need to do anything
if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end())
return true;
// Handle infinite loops more gracefully than running out of stack space and crashing
if (depth > 100)
{
LOGERROR(L"Probable infinite inheritance loop in entity template '%ls'", templateName.c_str());
return false;
}
// Handle special case "actor|foo"
if (templateName.find(L"actor|") == 0)
{
ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]);
return true;
}
// Handle special case "preview|foo"
if (templateName.find(L"preview|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::wstring baseName = templateName.substr(8);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%ls'", baseName.c_str());
return NULL;
}
// Copy a subset to the requested template
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
return true;
}
+ // Handle special case "foundation|foo"
+ if (templateName.find(L"foundation|") == 0)
+ {
+ // Load the base entity template, if it wasn't already loaded
+ std::wstring baseName = templateName.substr(11);
+ if (!LoadTemplateFile(baseName, depth+1))
+ {
+ LOGERROR(L"Failed to load entity template '%ls'", baseName.c_str());
+ return NULL;
+ }
+ // Copy a subset to the requested template
+ CopyFoundationSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
+ return true;
+ }
+
// Normal case: templateName is an XML file:
VfsPath path = VfsPath(TEMPLATE_ROOT) / (templateName + L".xml");
CXeromyces xero;
PSRETURN ok = xero.Load(path);
if (ok != PSRETURN_OK)
return false; // (Xeromyces already logged an error with the full filename)
int attr_parent = xero.GetAttributeID("parent");
utf16string parentStr = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent);
if (!parentStr.empty())
{
std::wstring parentName(parentStr.begin(), parentStr.end());
// To prevent needless complexity in template design, we don't allow |-separated strings as parents
if (parentName.find('|') != parentName.npos)
{
LOGERROR(L"Invalid parent '%ls' in entity template '%ls'", parentName.c_str(), templateName.c_str());
return false;
}
// Ensure the parent is loaded
if (!LoadTemplateFile(parentName, depth+1))
{
LOGERROR(L"Failed to load parent '%ls' of entity template '%ls'", parentName.c_str(), templateName.c_str());
return false;
}
CParamNode& parentData = m_TemplateFileData[parentName];
// Initialise this template with its parent
m_TemplateFileData[templateName] = parentData;
}
// Load the new file into the template data (overriding parent values)
CParamNode::LoadXML(m_TemplateFileData[templateName], xero);
return true;
}
void CCmpTemplateManager::ConstructTemplateActor(const std::wstring& actorName, CParamNode& out)
{
std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(actorName));
std::string xml = ""
""
""
"upright"
"0"
"false"
""
""
"" + name + ""
""
"";
out.LoadXMLString(out, xml.c_str());
}
static LibError AddToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector& templates = *(std::vector*)cbData;
// Strip the .xml extension
VfsPath pathstem = change_extension(pathname, L"");
// Strip the root from the path
std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1);
// We want to ignore template_*.xml templates, since they should never be built in the editor
if (name.substr(0, 9) == L"template_")
return INFO::OK;
templates.push_back(name);
return INFO::OK;
}
static LibError AddActorToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector& templates = *(std::vector*)cbData;
// Strip the root from the path
std::wstring name = pathname.string().substr(ARRAY_SIZE(ACTOR_ROOT)-1);
templates.push_back(L"actor|" + name);
return INFO::OK;
}
std::vector CCmpTemplateManager::FindAllTemplates()
{
// TODO: eventually this should probably read all the template files and look for flags to
// determine which should be displayed in the editor (and in what categories etc); for now we'll
// just return all the files
std::vector templates;
LibError ok;
// Find all the normal entity templates first
ok = fs_util::ForEachFile(g_VFS, TEMPLATE_ROOT, AddToTemplates, (uintptr_t)&templates, L"*.xml", fs_util::DIR_RECURSIVE);
WARN_ERR(ok);
// Add all the actors too
ok = fs_util::ForEachFile(g_VFS, ACTOR_ROOT, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", fs_util::DIR_RECURSIVE);
WARN_ERR(ok);
return templates;
}
void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& in)
{
// We only want to include components which are necessary (for the visual previewing of an entity)
// and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
// to this list should be carefully considered
std::set permittedComponentTypes;
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("VisualActor");
// (This could be initialised once and reused, but it's not worth the effort)
CParamNode::LoadXMLString(out, "");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
// In the future, we might want to add some extra flags to certain components, to indicate they're
// running in 'preview' mode and should not e.g. register with global managers
}
+
+void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode& in)
+{
+ // TODO: this is all kind of yucky and hard-coded; it'd be nice to have a more generic
+ // extensible scriptable way to define these subsets
+
+ std::set permittedComponentTypes;
+ permittedComponentTypes.insert("Ownership");
+ permittedComponentTypes.insert("Position");
+ permittedComponentTypes.insert("Identity");
+ permittedComponentTypes.insert("Obstruction");
+ permittedComponentTypes.insert("Selectable");
+ permittedComponentTypes.insert("Footprint");
+ permittedComponentTypes.insert("Armour");
+ permittedComponentTypes.insert("Health");
+ permittedComponentTypes.insert("Cost");
+
+ CParamNode::LoadXMLString(out, "");
+ out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
+
+ // TODO: the foundation shouldn't be considered an obstruction by default, until construction has
+ // really started, to prevent players abusing it to block their opponents
+
+ // TODO: Use the appropriate actor
+ CParamNode::LoadXMLString(out, "structures/fndn_4x4.xml");
+
+ // Add the Foundation component, to deal with the construction process
+ CParamNode::LoadXMLString(out, "");
+
+ // Initialise health to 1
+ CParamNode::LoadXMLString(out, "1");
+}
+
Index: ps/trunk/binaries/data/mods/public/gui/session_new/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session_new/session.xml (revision 7351)
+++ ps/trunk/binaries/data/mods/public/gui/session_new/session.xml (revision 7352)
@@ -1,236 +1,239 @@
Index: ps/trunk/binaries/data/mods/public/gui/session_new/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session_new/input.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/gui/session_new/input.js (revision 7352)
@@ -1,324 +1,352 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
var INPUT_NORMAL = 0;
var INPUT_SELECTING = 1;
var INPUT_BANDBOXING = 2;
var INPUT_BUILDING_PLACEMENT = 3;
var inputState = INPUT_NORMAL;
var placementEntity = "";
var mouseX = 0;
var mouseY = 0;
function updateCursor()
{
if (inputState == INPUT_NORMAL)
{
var action = determineAction(mouseX, mouseY);
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
return;
}
}
}
Engine.SetCursor("arrow-default");
}
function findGatherType(gatherer, supply)
{
if (!gatherer || !supply)
return;
if (gatherer[supply.type.generic+"."+supply.type.specific])
return supply.type.specific;
if (gatherer[supply.type.generic])
return supply.type.generic;
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y)
{
var selection = g_Selection.toList();
// No action if there's no selection
if (!selection.length)
return;
// If the selection doesn't exist, no action
var entState = Engine.GuiInterfaceCall("GetEntityState", selection[0]);
if (!entState)
return;
// If the selection isn't friendly units, no action
var player = Engine.GetPlayerID();
if (entState.player != player && !g_DevSettings.controlAll)
return;
var targets = Engine.PickEntitiesAtPoint(x, y);
// If there's no unit, just walk
if (!targets.length)
return {"type": "move"};
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
var targetState = Engine.GuiInterfaceCall("GetEntityState", targets[0]);
// Different owner -> attack
if (entState.attack && targetState.player != entState.player)
return {"type": "attack", "cursor": "action-attack", "target": targets[0]};
+ // Resource -> gather
var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply);
if (resource)
return {"type": "gather", "cursor": "action-gather-"+resource, "target": targets[0]};
+ // If a builder, then: Foundation -> build
+ if (entState.buildEntities && targetState.foundation)
+ return {"type": "build", "cursor": "action-build", "target": targets[0]};
+
// TODO: need more actions
// If we don't do anything more specific, just walk
return {"type": "move"};
}
/*
Selection methods: (not all currently implemented)
- Left-click on entity to select (always chooses the 'closest' one if the mouse is over several).
Includes non-controllable units (e.g. trees, enemy units).
- Double-left-click to select entity plus all of the same type on the screen.
- Triple-left-click to select entity plus all of the same type in the world.
- Left-click-and-drag to select all in region. Only includes controllable units.
- Left-click on empty space to deselect all.
- Hotkeys to select various groups.
- Shift plus left-click on entity to toggle selection of that unit. Only includes controllable.
- Shift plus any other selection method above, to add them to current selection.
*/
// TODO: it'd probably be nice to have a better state-machine system
function handleInputBeforeGui(ev)
{
// Capture mouse position so we can use it for displaying cursors
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
switch (inputState)
{
case INPUT_BANDBOXING:
switch (ev.type)
{
case "mousemotion":
var x0 = selectionDragStart[0];
var y0 = selectionDragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = getGUIObjectByName("bandbox");
bandbox.size = [x0, y0, x1, y1].join(" ");
bandbox.hidden = false;
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var x0 = selectionDragStart[0];
var y0 = selectionDragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = getGUIObjectByName("bandbox");
bandbox.hidden = true;
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
g_Selection.setHighlightList([]);
g_Selection.reset();
g_Selection.addList(ents);
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
var bandbox = getGUIObjectByName("bandbox");
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
}
return false;
}
var selectionDragStart;
function handleInputAfterGui(ev)
{
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
g_Selection.setHighlightList(ents);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
selectionDragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
var selection = g_Selection.toList();
switch (action.type)
{
case "move":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z});
return true;
case "attack":
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target});
return true;
+ case "build": // (same command as repair)
+ case "repair":
+ Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target});
+ return true;
+
case "gather":
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target});
return true;
+
+ default:
+ throw new Error("Invalid action.type "+action.type);
}
}
break;
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
var dragDeltaX = ev.x - selectionDragStart[0];
var dragDeltaY = ev.y - selectionDragStart[1];
var maxDragDelta = 4;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
if (!ents.length)
{
g_Selection.reset();
inputState = INPUT_NORMAL;
return true;
}
g_Selection.reset();
g_Selection.addList([ents[0]]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
var angle = Math.PI;
- Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": placementEntity, "x": target.x, "z": target.z, "angle": angle});
+ Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
+ "template": placementEntity,
+ "x": target.x,
+ "z": target.z,
+ "angle": angle
+ });
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
+ var selection = g_Selection.toList();
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
var angle = Math.PI;
+ // Remove the preview
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
- Engine.PostNetworkCommand({"type": "construct", "template": placementEntity, "x": target.x, "z": target.z, "angle": angle});
+ // Start the construction
+ Engine.PostNetworkCommand({
+ "type": "construct",
+ "template": placementEntity,
+ "x": target.x,
+ "z": target.z,
+ "angle": angle,
+ "entities": selection
+ });
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
}
return false;
}
function testBuild(ent)
{
placementEntity = ent;
inputState = INPUT_BUILDING_PLACEMENT;
}
Index: ps/trunk/binaries/data/mods/public/gui/session_new/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session_new/session.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/gui/session_new/session.js (revision 7352)
@@ -1,217 +1,245 @@
+// Cache dev-mode settings that are frequently or widely used
var g_DevSettings = {
controlAll: false
};
function init(initData, hotloadData)
{
if (hotloadData)
{
g_Selection.selected = hotloadData.selection;
}
onSimulationUpdate();
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { selection: g_Selection.selected };
}
function onTick()
{
g_DevSettings.controlAll = getGUIObjectByName("devControlAll").checked;
// TODO: at some point this controlAll needs to disable the simulation code's
// player checks (once it has some player checks)
updateCursor();
}
function onSimulationUpdate()
{
var simState = Engine.GuiInterfaceCall("GetSimulationState");
// If we're called during init when the game is first loading, there will be
// no simulation yet, so do nothing
if (!simState)
return;
- // updateDebug(simState);
+ updateDebug(simState);
updatePlayerDisplay(simState);
updateUnitDisplay();
}
function updateDebug(simState)
{
var debug = getGUIObjectByName("debug");
+
+ if (getGUIObjectByName("devDisplayState").checked)
+ {
+ debug.hidden = false;
+ }
+ else
+ {
+ debug.hidden = true;
+ return;
+ }
+
var text = uneval(simState);
var selection = g_Selection.toList();
if (selection.length)
{
var entState = Engine.GuiInterfaceCall("GetEntityState", selection[0]);
- var template = Engine.GuiInterfaceCall("GetTemplateData", entState.template);
- text += "\n\n" + uneval(entState) + "\n\n" + uneval(template);
+ if (entState)
+ {
+ var template = Engine.GuiInterfaceCall("GetTemplateData", entState.template);
+ text += "\n\n" + uneval(entState) + "\n\n" + uneval(template);
+ }
}
debug.caption = text;
}
function updatePlayerDisplay(simState)
{
var playerState = simState.players[Engine.GetPlayerID()];
getGUIObjectByName("resourceFood").caption = playerState.resourceCounts.food;
getGUIObjectByName("resourceWood").caption = playerState.resourceCounts.wood;
getGUIObjectByName("resourceStone").caption = playerState.resourceCounts.stone;
getGUIObjectByName("resourceMetal").caption = playerState.resourceCounts.metal;
getGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit;
}
function damageTypesToText(dmg)
{
if (!dmg)
return "(None)";
return dmg.hack + " Hack\n" + dmg.pierce + " Pierce\n" + dmg.crush + " Crush";
}
var g_unitConstructionButtons = 0; // the number currently visible
// The unitSomethingPanel objects, which are displayed in a stack at the bottom of the screen,
// ordered with *lowest* first
var g_unitPanels = ["Stance", "Formation", "Construction", "Research", "Training", "Queue"];
function updateUnitDisplay()
{
var detailsPanel = getGUIObjectByName("selectionDetails");
var commandsPanel = getGUIObjectByName("unitCommands");
var selection = g_Selection.toList();
if (selection.length == 0)
{
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
var entState = Engine.GuiInterfaceCall("GetEntityState", selection[0]);
// If the unit has no data (e.g. it was killed), don't try displaying any
// data for it. (TODO: it should probably be removed from the selection too;
// also need to handle multi-unit selections)
if (!entState)
{
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
var template = Engine.GuiInterfaceCall("GetTemplateData", entState.template);
detailsPanel.hidden = false;
commandsPanel.hidden = false;
getGUIObjectByName("selectionDetailsIcon").sprite = "snPortraitSheetHele";
getGUIObjectByName("selectionDetailsIcon").cell_id = template.icon_cell;
var healthSize = getGUIObjectByName("selectionDetailsHealthBar").size;
healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
getGUIObjectByName("selectionDetailsHealthBar").size = healthSize;
getGUIObjectByName("selectionDetailsHealth").tooltip = "Hitpoints " + entState.hitpoints + " / " + entState.maxHitpoints;
getGUIObjectByName("selectionDetailsSpecific").caption = template.name.specific;
if (template.name.generic == template.name.specific)
{
getGUIObjectByName("selectionDetailsGeneric").hidden = true;
}
else
{
getGUIObjectByName("selectionDetailsGeneric").hidden = false;
getGUIObjectByName("selectionDetailsGeneric").caption = template.name.generic;
}
getGUIObjectByName("selectionDetailsAttack").caption = damageTypesToText(entState.attack);
getGUIObjectByName("selectionDetailsArmour").caption = damageTypesToText(entState.armour);
var usedPanels = {};
if (entState.attack) // TODO - this should be based on some AI properties
{
usedPanels["Stance"] = 1;
usedPanels["Formation"] = 1;
}
else // TODO - this should be based on various other things
{
usedPanels["Queue"] = 1;
usedPanels["Training"] = 1;
usedPanels["Research"] = 1;
}
// Set up the unit construction buttons
// (TODO: abstract this to apply to the other button panels)
if (entState.buildEntities && entState.buildEntities.length)
{
usedPanels["Construction"] = 1;
var i = 0;
for each (var build in entState.buildEntities)
{
var button = getGUIObjectByName("unitConstructionButton["+i+"]");
var icon = getGUIObjectByName("unitConstructionIcon["+i+"]");
var template = Engine.GuiInterfaceCall("GetTemplateData", build);
var name;
if (template.name.specific && template.name.generic)
name = template.name.specific + " (" + template.name.generic + ")";
else
name = template.name.specific || template.name.generic || "???";
+ var tooltip = "[font=trebuchet14b]" + name + "[/font]";
+
+ if (template.cost)
+ {
+ var costs = [];
+ if (template.cost.food) costs.push("[font=tahoma10b]Food:[/font] " + template.cost.food);
+ if (template.cost.wood) costs.push("[font=tahoma10b]Wood:[/font] " + template.cost.wood);
+ if (template.cost.metal) costs.push("[font=tahoma10b]Metal:[/font] " + template.cost.metal);
+ if (template.cost.stone) costs.push("[font=tahoma10b]Stone:[/font] " + template.cost.stone);
+ if (costs.length)
+ tooltip += "\n" + costs.join(", ");
+ }
+
button.hidden = false;
- button.tooltip = "Construct " + name;
+ button.tooltip = tooltip;
button.onpress = (function(b) { return function() { testBuild(b) } })(build);
// (need nested functions to get the closure right)
icon.sprite = "snPortraitSheetHele";
icon.cell_id = template.icon_cell;
++i;
}
var numButtons = i;
// Position the visible buttons
// (TODO: if there's lots, maybe they should be squeezed together to fit)
for (i = 0; i < numButtons; ++i)
{
var button = getGUIObjectByName("unitConstructionButton["+i+"]");
var size = button.size;
size.left = 40*i;
size.right = 40*i + size.bottom;
button.size = size;
}
// Hide any buttons we're no longer using
for (i = numButtons; i < g_unitConstructionButtons; ++i)
getGUIObjectByName("unitConstructionButton["+i+"]").hidden = true;
g_unitConstructionButtons = numButtons;
}
var offset = 0;
for each (var panelName in g_unitPanels)
{
var panel = getGUIObjectByName("unit"+panelName+"Panel");
if (usedPanels[panelName])
{
var size = panel.size;
var h = size.bottom - size.top;
size.bottom = offset;
size.top = offset - h;
panel.size = size;
panel.hidden = false;
offset -= (h + 12);
}
else
{
panel.hidden = true;
}
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 7352)
@@ -1,56 +1,105 @@
function ProcessCommand(player, cmd)
{
// print("command: " + player + " " + uneval(cmd) + "\n");
switch (cmd.type)
{
case "walk":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
ai.Walk(cmd.x, cmd.z);
}
break;
case "attack":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
ai.Attack(cmd.target);
}
break;
+ case "repair":
+ // This covers both repairing damaged buildings, and constructing unfinished foundations
+ for each (var ent in cmd.entities)
+ {
+ var ai = Engine.QueryInterface(ent, IID_UnitAI);
+ if (!ai)
+ continue;
+ ai.Repair(cmd.target);
+ }
+ break;
+
case "gather":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
ai.Gather(cmd.target);
}
break;
case "construct":
- // TODO: this should do all sorts of stuff with foundations and resource costs etc
- var ent = Engine.AddEntity(cmd.template);
- if (ent)
+ /*
+ * Construction process:
+ * . Take resources away immediately.
+ * . Create a foundation entity with 1hp, 0% build progress.
+ * . Increase hp and build progress up to 100% when people work on it.
+ * . If it's destroyed, an appropriate fraction of the resource cost is refunded.
+ * . If it's completed, it gets replaced with the real building.
+ */
+
+ // Find the player
+ var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ var playerEnt = cmpPlayerMan.GetPlayerByID(player);
+ var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
+
+ // Tentatively create the foundation (we don't know yet if the player can really afford it)
+ var ent = Engine.AddEntity("foundation|" + cmd.template);
+ // TODO: report errors (e.g. invalid template names)
+
+ var cmpCost = Engine.QueryInterface(ent, IID_Cost);
+ if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
- var pos = Engine.QueryInterface(ent, IID_Position);
- if (pos)
- {
- pos.JumpTo(cmd.x, cmd.z);
- pos.SetYRotation(cmd.angle);
- }
+ // TODO: report error to player (they ran out of resources)
+
+ // Remove the foundation because the construction was aborted
+ Engine.DestroyEntity(ent);
+
+ break;
}
+
+ // Move the foundation to the right place
+ var cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpPosition.JumpTo(cmd.x, cmd.z);
+ cmpPosition.SetYRotation(cmd.angle);
+
+ // Make it owned by the current player
+ var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ cmpOwnership.SetOwner(player);
+
+ // Initialise the foundation
+ var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
+ cmpFoundation.InitialiseConstruction(player, cmd.template);
+
+ // Tell the units to start building this new entity
+ ProcessCommand(player, {
+ "type": "repair",
+ "entities": cmd.entities,
+ "target": ent
+ });
+
break;
default:
print("Ignoring unrecognised command type '" + cmd.type + "'\n");
}
}
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 7352)
@@ -1,183 +1,203 @@
function GuiInterface() {}
GuiInterface.prototype.Init = function()
{
// TODO: need to not serialise this value
this.placementEntity = undefined; // = undefined or [templateName, entityID]
};
GuiInterface.prototype.GetSimulationState = function(player)
{
var ret = {
"players": []
};
-
+
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
- var player = {
+ var playerData = {
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"resourceCounts": cmpPlayer.GetResourceCounts()
};
- ret.players.push(player);
+ ret.players.push(playerData);
}
-
+
return ret;
};
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTempMan.GetCurrentTemplateName(ent);
if (!template)
return null;
var ret = {
"template": template,
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
ret.position = cmpPosition.GetPosition();
}
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
}
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
ret.attack = cmpAttack.GetAttackStrengths();
}
var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
{
ret.armour = cmpArmour.GetArmourStrengths();
}
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
{
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
+ var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
+ if (cmpFoundation)
+ {
+ ret.foundation = {
+ "progress": cmpFoundation.GetBuildPercentage()
+ };
+ }
+
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
{
ret.resourceSupply = {
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType()
};
}
var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
return ret;
};
GuiInterface.prototype.GetTemplateData = function(player, name)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(name);
var ret = {};
if (template.Identity)
{
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon_cell = template.Identity.IconCell;
}
+ if (template.Cost)
+ {
+ ret.cost = {};
+ if (template.Cost.Resources)
+ {
+ if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
+ if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
+ if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
+ if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
+ }
+ }
+
return ret;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
for each (var ent in cmd.entities)
{
var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (cmpSelectable)
cmpSelectable.SetSelectionHighlight(cmd.colour);
}
};
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
{
this.placementEntity = undefined;
}
else
{
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
}
// Move the preview into the right location
if (this.placementEntity)
{
var pos = Engine.QueryInterface(this.placementEntity[1], IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
}
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
var exposedFunctions = {
"GetSimulationState": 1,
"GetEntityState": 1,
"GetTemplateData": 1,
"SetSelectionHighlight": 1,
"SetBuildingPlacementPreview": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 7352)
@@ -1,72 +1,94 @@
function Health() {}
Health.prototype.Init = function()
{
- this.hitpoints = this.GetMaxHitpoints();
+ // Default to , but use if it's undefined or zero
+ // (Allowing 0 initial HP would break our death detection code)
+ this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints());
};
//// Interface functions ////
Health.prototype.GetHitpoints = function()
{
return this.hitpoints;
};
Health.prototype.GetMaxHitpoints = function()
{
return +this.template.Max;
};
+Health.prototype.SetHitpoints = function(value)
+{
+ // If we're already dead, don't allow resurrection
+ if (this.hitpoints == 0)
+ return;
+
+ this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
+}
+
Health.prototype.Reduce = function(amount)
{
if (amount >= this.hitpoints)
{
// If this is the first time we reached 0, then die.
// (The entity will exist a little while after calling DestroyEntity so this
// might get called multiple times)
if (this.hitpoints)
{
- this.CreateCorpse();
+ if (this.template.DeathType == "corpse")
+ this.CreateCorpse();
+
Engine.DestroyEntity(this.entity);
}
this.hitpoints = 0;
}
else
{
this.hitpoints -= amount;
}
}
+Health.prototype.Increase = function(amount)
+{
+ // If we're already dead, don't allow resurrection
+ if (this.hitpoints == 0)
+ return;
+
+ this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
+}
+
//// Private functions ////
Health.prototype.CreateCorpse = function()
{
// Create a static local version of the current entity
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var templateName = cmpTempMan.GetCurrentTemplateName(this.entity);
var corpse = Engine.AddLocalEntity("preview|" + templateName);
// (Maybe this should be some kind of "corpse|" instead of "preview|", if we want
// to add things like corpse-removal timers and change the terrain conformance mode)
// Copy various parameters so it looks just like us
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
var cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position);
var pos = cmpPosition.GetPosition();
cmpCorpsePosition.JumpTo(pos.x, pos.z);
var rot = cmpPosition.GetRotation();
cmpCorpsePosition.SetYRotation(rot.y);
cmpCorpsePosition.SetXZRotation(rot.x, rot.z);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership);
cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner());
// Make it fall over
var cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual);
cmpCorpseVisual.SelectAnimation("death", true);
};
Engine.RegisterComponentType(IID_Health, "Health", Health);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 7352)
@@ -0,0 +1,114 @@
+function Foundation() {}
+
+Foundation.prototype.Init = function()
+{
+ this.buildProgress = 0.0; // 0 <= progress <= 1
+};
+
+Foundation.prototype.InitialiseConstruction = function(owner, template)
+{
+ var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+
+ this.finalTemplateName = template;
+ this.addedHitpoints = cmpHealth.GetHitpoints();
+ this.maxHitpoints = cmpHealth.GetMaxHitpoints();
+
+ // We need to know the owner in OnDestroy, but at that point the entity has already been
+ // decoupled from its owner, so we need to remember it in here (and assume it won't change)
+ this.owner = owner;
+};
+
+Foundation.prototype.GetBuildPercentage = function()
+{
+ return Math.floor(this.buildProgress * 100);
+};
+
+Foundation.prototype.OnDestroy = function()
+{
+ // Refund a portion of the construction cost, proportional to the amount of build progress remaining
+
+ if (this.buildProgress == 1.0)
+ return;
+
+ var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(this.owner), IID_Player);
+
+ var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
+ var costs = cmpCost.GetResourceCosts();
+ for (var r in costs)
+ {
+ var scaled = Math.floor(costs[r] * (1.0 - this.buildProgress));
+ if (scaled)
+ cmpPlayer.AddResource(r, scaled);
+ }
+};
+
+/**
+ * Perform some amount of construction work.
+ * Returns true if the construction is completed.
+ */
+Foundation.prototype.Build = function(builderEnt, work)
+{
+ // Do nothing if we've already finished building
+ if (this.buildProgress == 1.0)
+ return true;
+
+ var amount = work; // TODO: adjust by time cost of this building
+
+ // TODO: implement some kind of diminishing returns for multiple builders.
+ // e.g. record the set of entities that build this, then every ~2 seconds
+ // count them (and reset the list), and apply some function to the count to get
+ // a factor, and apply that factor here.
+
+ this.buildProgress += amount;
+ if (this.buildProgress > 1.0)
+ this.buildProgress = 1.0;
+
+ // Add an appropriate proportion of hitpoints
+ var targetHP = Math.max(0, Math.min(this.maxHitpoints, Math.floor(this.maxHitpoints * this.buildProgress)));
+ var deltaHP = targetHP - this.addedHitpoints;
+ if (deltaHP > 0)
+ {
+ var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ cmpHealth.Increase(deltaHP);
+ this.addedHitpoints += deltaHP;
+ }
+
+ if (this.buildProgress >= 1.0)
+ {
+ // Finished construction
+
+ // Create the real entity
+ var building = Engine.AddEntity(this.finalTemplateName);
+
+ // Copy various parameters from the foundation
+
+ var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
+ var pos = cmpPosition.GetPosition();
+ cmpBuildingPosition.JumpTo(pos.x, pos.z);
+ var rot = cmpPosition.GetRotation();
+ cmpBuildingPosition.SetYRotation(rot.y);
+ cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
+ // TODO: should add a ICmpPosition::CopyFrom() instead of all this
+
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
+ cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
+
+ var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
+ cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints());
+
+ Engine.DestroyEntity(this.entity);
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+};
+
+Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);
+
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Foundation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Foundation.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Foundation.js (revision 7352)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Foundation");
Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 7352)
@@ -1,49 +1,49 @@
function ResourceGatherer() {}
ResourceGatherer.prototype.Init = function()
{
};
ResourceGatherer.prototype.GetGatherRates = function()
{
var ret = {};
for (var r in this.template.Rates)
ret[r] = this.template.Rates[r] * this.template.BaseSpeed;
return ret;
};
ResourceGatherer.prototype.GetRange = function()
{
return { "max": 4, "min": 0 };
// maybe this should depend on the unit or target or something?
}
/**
* Gather from the target entity. This should only be called after a successful range check,
* and if the target has a compatible ResourceSupply.
* It should be called at a rate of once per second.
*/
ResourceGatherer.prototype.PerformGather = function(target)
{
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
var rate;
if (type.specific && this.template.Rates[type.generic+"."+type.specific])
rate = this.template.Rates[type.generic+"."+type.specific] * this.template.BaseSpeed;
else
rate = this.template.Rates[type.generic] * this.template.BaseSpeed;
var status = cmpResourceSupply.TakeResources(rate);
// Give the gathered resources to the player
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(cmpOwnership.GetOwner()), IID_Player);
- cmpPlayer.AddResources(type.generic, status.amount);
+ cmpPlayer.AddResource(type.generic, status.amount);
return status;
};
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 7352)
@@ -1,19 +1,48 @@
function Builder() {}
Builder.prototype.Init = function()
{
};
Builder.prototype.GetEntitiesList = function()
{
var string = this.template.Entities._string;
// Replace the "{civ}" codes with this entity's civ ID
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (cmpIdentity)
string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv());
return string.split(/\s+/);
};
+Builder.prototype.GetRange = function()
+{
+ return { "max": 16, "min": 0 };
+ // maybe this should depend on the unit or target or something?
+}
+
+/**
+ * Build/repair the target entity. This should only be called after a successful range check.
+ * It should be called at a rate of once per second.
+ */
+Builder.prototype.PerformBuilding = function(target)
+{
+ var rate = 0.1; // XXX
+
+ // If it's a foundation, then build it
+ var cmpFoundation = Engine.QueryInterface(target, IID_Foundation);
+ if (cmpFoundation)
+ {
+ var finished = cmpFoundation.Build(this.entity, rate);
+ return { "finished": finished };
+ }
+ else
+ {
+ // TODO: do some kind of repairing
+
+ return { "finished": true };
+ }
+};
+
Engine.RegisterComponentType(IID_Builder, "Builder", Builder);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 7352)
@@ -1,63 +1,64 @@
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
+Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("GuiInterface.js");
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; },
});
AddMock(100, IID_Player, {
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetResourceCounts: function() { return { "food": 100 }; }
});
AddMock(101, IID_Player, {
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetResourceCounts: function() { return { "food": 200 }; }
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [{popCount:10, popLimit:20, resourceCounts:{food:100}}, {popCount:40, popLimit:30, resourceCounts:{food:200}}]
});
AddMock(10, IID_Position, {
GetPosition: function() {
return {x:1, y:2, z:3};
}
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
}
});
var state = cmp.GetEntityState(-1, 10);
TS_ASSERT_UNEVAL_EQUALS(state, {
template: "example",
position: {x:1, y:2, z:3},
hitpoints: 50,
maxHitpoints: 60,
buildEntities: ["test1", "test2"]
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7352)
@@ -1,355 +1,459 @@
/*
This is currently just a very simplistic state machine that lets units be commanded around
and then autonomously carry out the orders. It might need to be entirely redesigned.
*/
const STATE_IDLE = 0;
const STATE_WALKING = 1;
const STATE_ATTACKING = 2;
-const STATE_GATHERING = 3;
+const STATE_REPAIRING = 3;
+const STATE_GATHERING = 4;
/* Attack process:
* When starting attack:
* Activate attack animation (with appropriate repeat speed and offset)
* Set this.attackTimer to run at maximum of:
* GetTimers().prepare msec from now
* this.attackRechargeTime
* Loop:
* Wait for the timer
* Perform the attack
* Set this.attackRechargeTime to now plus GetTimers().recharge
* Set this.attackTimer to run after GetTimers().repeat
* At any point it's safe to cancel the attack and switch to a different action
* (The rechargeTime is to prevent people spamming the attack command and getting
* faster-than-normal attacks)
*/
-/* Gather process is about the same, except with less synchronisation - the action
+/* Repeat/Gather process is about the same, except with less synchronisation - the action
* is just performed 1sec after initiated, and then repeated every 1sec.
- * (TODO: it'd be nice to avoid most of the duplication between Attack and Gather code)
+ * (TODO: it'd be nice to avoid most of the duplication between Attack and Repeat and Gather code)
*/
function UnitAI() {}
UnitAI.prototype.Init = function()
{
this.state = STATE_IDLE;
// The earliest time at which we'll have 'recovered' from the previous attack, and
// can start preparing a new attack
this.attackRechargeTime = 0;
// Timer for AttackTimeout
this.attackTimer = undefined;
// Current target entity ID
this.attackTarget = undefined;
+ // Timer for RepairTimeout
+ this.repairTimer = undefined;
+ // Current target entity ID
+ this.repairTarget = undefined;
+
// Timer for GatherTimeout
this.gatherTimer = undefined;
// Current target entity ID
this.gatherTarget = undefined;
};
//// Interface functions ////
UnitAI.prototype.Walk = function(x, z)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpMotion)
return;
this.SelectAnimation("walk", false, cmpMotion.GetSpeed());
cmpMotion.MoveToPoint(x, z, 0, 0);
this.state = STATE_WALKING;
};
UnitAI.prototype.Attack = function(target)
{
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
// TODO: verify that this is a valid target
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.attackTarget = target;
this.MoveToTarget(target, cmpAttack.GetRange());
this.state = STATE_ATTACKING;
};
+UnitAI.prototype.Repair = function(target)
+{
+ // Verify that we're able to respond to Repair commands
+ var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
+ if (!cmpBuilder)
+ return;
+
+ // TODO: verify that this is a valid target
+
+ // Stop any previous action timers
+ this.CancelTimers();
+
+ // Remember the target, and start moving towards it
+ this.repairTarget = target;
+ this.MoveToTarget(target, cmpBuilder.GetRange());
+ this.state = STATE_REPAIRING;
+};
+
UnitAI.prototype.Gather = function(target)
{
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return;
// TODO: verify that this is a valid target
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.gatherTarget = target;
this.MoveToTarget(target, cmpResourceGatherer.GetRange());
this.state = STATE_GATHERING;
};
//// Message handlers ////
UnitAI.prototype.OnDestroy = function()
{
// Clean up any timers that are now obsolete
this.CancelTimers();
};
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (msg.speed)
{
// Started moving
// => play the appropriate animation
this.SelectAnimation("walk", false, msg.speed);
}
else
{
if (this.state == STATE_WALKING)
{
// Stopped walking
this.state = STATE_IDLE;
this.SelectAnimation("idle");
}
else if (this.state == STATE_ATTACKING)
{
// We were attacking, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoAttackRange())
return;
// In range, so perform the attack,
// after the prepare time but not before the previous attack's recharge
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timers = cmpAttack.GetTimers();
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {});
// Start the idle animation before we switch to the attack
this.SelectAnimation("idle");
}
+ else if (this.state == STATE_REPAIRING)
+ {
+ // We were repairing, and have stopped moving
+ // => check if we can still reach the target now
+
+ if (!this.MoveIntoRepairRange())
+ return;
+
+ // In range, so perform the repairing
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
+
+ // Start the repair/build animation
+ this.SelectAnimation("build");
+ }
else if (this.state == STATE_GATHERING)
{
// We were gathering, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoGatherRange())
return;
// In range, so perform the gathering
var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {});
// Start the gather animation
var type = cmpResourceSupply.GetType();
var anim = "gather_" + (type.specific || type.generic);
this.SelectAnimation(anim);
}
}
};
//// Private functions ////
function hypot2(x, y)
{
return x*x + y*y;
}
UnitAI.prototype.CheckRange = function(target, range)
{
// Target must be in the world
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
return { "error": "not-in-world" };
// We must be in the world
var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld())
return { "error": "not-in-world" };
// Target must be within range
var posTarget = cmpPositionTarget.GetPosition();
var posSelf = cmpPositionSelf.GetPosition();
var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z);
// TODO: ought to be distance to closest point in footprint, not to center
// The +4 is a hack to give a ~1 tile tolerance, because the pathfinder doesn't
// always get quite close enough to the target
if (dist2 > (range.max+4)*(range.max+4))
return { "error": "out-of-range" };
return {};
}
UnitAI.prototype.CancelTimers = function()
{
if (this.attackTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.attackTimer);
this.attackTimer = undefined;
}
+ if (this.repairTimer)
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.repairTimer);
+ this.repairTimer = undefined;
+ }
+
if (this.gatherTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.gatherTimer);
this.gatherTimer = undefined;
}
};
/**
* Tries to move into range of the attack target.
* Returns true if it's already in range.
*/
UnitAI.prototype.MoveIntoAttackRange = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange();
var rangeStatus = this.CheckRange(this.attackTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.attackTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return false;
}
return true;
};
+UnitAI.prototype.MoveIntoRepairRange = function()
+{
+ var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
+ var range = cmpBuilder.GetRange();
+
+ var rangeStatus = this.CheckRange(this.repairTarget, range);
+ if (rangeStatus.error)
+ {
+ if (rangeStatus.error == "out-of-range")
+ {
+ // Out of range => need to move closer
+ // (The target has probably moved while we were chasing it)
+ this.MoveToTarget(this.repairTarget, range);
+ return false;
+ }
+
+ // Otherwise it's impossible to reach the target, so give up
+ // and switch back to idle
+ this.state = STATE_IDLE;
+ this.SelectAnimation("idle");
+ return false;
+ }
+
+ return true;
+};
+
UnitAI.prototype.MoveIntoGatherRange = function()
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var range = cmpResourceGatherer.GetRange();
var rangeStatus = this.CheckRange(this.gatherTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.gatherTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return false;
}
return true;
};
+// TODO: refactor all this repetitive code
+
UnitAI.prototype.SelectAnimation = function(name, once, speed)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation(name, once, speed);
};
UnitAI.prototype.MoveToTarget = function(target, range)
{
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
return;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
var pos = cmpPositionTarget.GetPosition();
cmpMotion.MoveToPoint(pos.x, pos.z, range.min, range.max);
};
UnitAI.prototype.AttackTimeout = function(data)
{
// If we stopped attacking before this timeout, then don't do any processing here
if (this.state != STATE_ATTACKING)
return;
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
-
// Check if we can still reach the target
if (!this.MoveIntoAttackRange())
return;
+ var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+
// Play the attack animation
this.SelectAnimation("melee", false, 1);
// Hit the target
cmpAttack.PerformAttack(this.attackTarget);
// Set a timer to hit the target again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timers = cmpAttack.GetTimers();
this.attackRechargeTime = cmpTimer.GetTime() + timers.recharge;
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, data);
};
+UnitAI.prototype.RepairTimeout = function(data)
+{
+ // If we stopped repairing before this timeout, then don't do any processing here
+ if (this.state != STATE_REPAIRING)
+ return;
+
+ // Check if we can still reach the target
+ if (!this.MoveIntoRepairRange())
+ return;
+
+ var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
+
+ // Repair/build the target
+ var status = cmpBuilder.PerformBuilding(this.repairTarget);
+
+ // If the target is fully built and repaired, then stop and go back to idle
+ if (status.finished)
+ {
+ this.state = STATE_IDLE;
+ this.SelectAnimation("idle");
+ return;
+ }
+
+ // Set a timer to gather again
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, data);
+};
+
UnitAI.prototype.GatherTimeout = function(data)
{
// If we stopped gathering before this timeout, then don't do any processing here
if (this.state != STATE_GATHERING)
return;
- var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
-
// Check if we can still reach the target
if (!this.MoveIntoGatherRange())
return;
+ var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
+
// Gather from the target
var status = cmpResourceGatherer.PerformGather(this.gatherTarget);
// If the resource is exhausted, then stop and go back to idle
if (status.exhausted)
{
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return;
}
// Set a timer to gather again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, data);
};
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 7352)
@@ -1,66 +1,80 @@
function Player() {}
Player.prototype.Init = function()
{
this.playerID = undefined;
this.playerName = "Unknown";
this.civ = "celt";
this.popCount = 0;
this.popLimit = 50;
this.resourceCount = {
- "food": 100,
- "wood": 50,
- "metal": 0,
- "stone": 0
+ "food": 2000,
+ "wood": 1500,
+ "metal": 500,
+ "stone": 1000
};
};
Player.prototype.SetPlayerID = function(id)
{
this.playerID = id;
};
Player.prototype.GetPopulationCount = function()
{
return this.popCount;
};
Player.prototype.GetPopulationLimit = function()
{
return this.popLimit;
};
Player.prototype.GetResourceCounts = function()
{
return this.resourceCount;
};
-Player.prototype.AddResources = function(type, amount)
+Player.prototype.AddResource = function(type, amount)
{
this.resourceCount[type] += (+amount);
};
+Player.prototype.TrySubtractResources = function(amounts)
+{
+ // Check we can afford it all
+ for (var type in amounts)
+ if (amounts[type] > this.resourceCount[type])
+ return false;
+
+ // Subtract the resources
+ for (var type in amounts)
+ this.resourceCount[type] -= amounts[type];
+
+ return true;
+};
+
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
if (msg.from == this.playerID)
{
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
this.popCount -= cost.GetPopCost();
this.popLimit += cost.GetPopBonus();
}
}
if (msg.to == this.playerID)
{
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
this.popCount += cost.GetPopCost();
this.popLimit -= cost.GetPopBonus();
}
}
};
Engine.RegisterComponentType(IID_Player, "Player", Player);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 7351)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 7352)
@@ -1,24 +1,25 @@
UnitPortraitSheet1
+ corpse1001.07.02.5