• Please note: In an effort to ensure that all of our users feel welcome on our forums, we’ve updated our forum rules. You can review the updated rules here: http://forums.wolflair.com/showthread.php?t=5528.

    If a fellow Community member is not following the forum rules, please report the post by clicking the Report button (the red yield sign on the left) located on every post. This will notify the moderators directly. If you have any questions about these new rules, please contact support@wolflair.com.

    - The Lone Wolf Development Team

"Lifepath" style creation - Thoughts?

TCArknight

Well-known member
Howdy all!

Recently several games have come up, most notably Cyberpunk Red and the new Twilight 2000 (v4), that involve Lifepath-style components to the character creation.

While specific Lifepath creation in these games involves die rolls to determine the specifics of each stage, at it's simplest level, the user can just make a choice for which entry to take (as I did with the Star Trek Adventures dataset).

As such, I wanted to put together this thread to get folk's thoughts on how best to implement this sort of "Stage" oriented selection. Each stage breaks down to selecting one or more items for the character. This may be simple notes on clothing style, hair styles, tattoos, etc. These are one off selections that can be done with a chooser_table portal and specific components tailored to the stage. In STA, this would be the Environment, Upbringing, and Academy components for example

However, with Twilight 2000 v4 (T2K) for example, the Lifepath is a series of "Terms" where you choose a career (which is either military or civilian) that last 1-6 years (which could be entered manually). Each term, you can improve two skills (or same twice) and possibly gain a Special Ability.

Once a certain number of terms are taken, one final term is given (which functions like the others but has certain requirements in choices made during it to represent the draft before WWIII).

This same "Term"-like setup could appear in any number of games with Lifepath versions.

What are folks thoughts on how best to represent this selection of "Terms" and the associated skills/abilities?
 
If you have SR5 and the Run Faster add-on, the "Life Modules System" character creation method that book offers is an example of implementing something much like that in HL. That's implemented with the life modules being gizmos, and all the skills and qualities they offer being shadowed to the hero, so that you can do all the proper customizations within the gizmo (so that you can enforce all that particular module's rules on what may be chosen). Making everything stack once shadowed to the hero was the annoying part in setting that up - you have to figure out how to pick one copy of each thing that is the master copy, and have all the non-master copies add their values to the master, then hide themselves, but then within their original gizmos, each individual copy needs to show how much was gained from that particular life module.
 
Agent tables could also be used in place of gizmos - that's an alternative to how you can enforce each individual module's rules on what can and can't be taken, separately from the rules on a different one.
 
Thanks Mathias. :)

It seems to me that the agent tab and tables work best in this situation.

Are these properties valid for all tables or just for the table_dynamic?
useagentadd="yes"
useagentcandidate="yes"
useagentlinkage="yes"
 
Hmm - never actually tried to add a chooser to an agent table. I'm not sure if it would work, or if you would just need to have dynamic tables, but only allowing one pick per table.
 
It does look like this works when a separate portal on the agent tab. :)
Code:
<portal id="childSpecialty" 
  style="chsNormal" 
  width="110">
  <chooser_table 
    component="Specialty" 
	choosetemplate="SimpleItem"
	[COLOR="Blue"][B]useagentcandidate="yes"
        candidatefield="specExpr"[/B][/COLOR]>
    <chosen><![CDATA[
      if (@ispick = 0) then
        @text = "{text ff0000}Select Specialty"
      else
        @text = "Specialty: " & field[name].text
        endif
      ]]></chosen>
    <titlebar>
      @text = "Choose your Specialty"
      </titlebar>
    </chooser_table>
  </portal>
(In this case, specExpr is a field on the Childhood component (the agent) holding the candidate expression of the available Specialties for that Childhood selection.)

If this didn't work, is there a way to limit the number of picks on a table_dynamic ? The only way I figured to do it was to have an autotag on the table that got forwarded to the hero with the first selection. If that tag was present, it changed the font of the additem statement and did a freeze on the table. (I also just spotted the showfrozenfixed="yes" property for the table_dynamic that I could use for this as well...)
 
Using validation errors is my preference for this sort of thing. With the lifepaths, I can see someone convincing their GM "but my character had really bad teenage years - he was living on the streets and then he was working for criminals, so I should get to take both of those".


I don't have any good ideas for how to force a table_dynamic to accept a maximum number of selections (in this case, 1).
 
Good point. :)

Since a chooser_table works with the agent (either in a template or alone) and an associated expression, it seems like a better option in cases like this.

As I think about it, an agent doesn't seem the way to go for the tours in T2K. From what I can tell, the compset for an agent has to be forced unique, and the hero can have multiple tours.

Breaking it down -
The hero can have anywhere from 1 - 8 Tours.
A Tour consists of:
(index number based on how many tours have been chosen)
1 career choice (can be the same across multiple tours) which consists of:
2 skill selections
1 specialty selection
+1d6 years

The hero can have anywhere from 1 - 8 Tours.

I can see a table_dynamic to add a list of tours. The showtemplate for a single entry would show index, a chooser for the career, and years of the tour.

Any thoughts on the best way to handle this?
 
Each tour has to be unique, so you'd add one pick for each environment option, another pick for each upbringing choice, etc. So the user adds the lifepaths on one tab, and then as they add each one, a new tab pops up where they customize that particular lifepath.

However, this is not an option if there's ways to repeat a tour. Like in SR5, there's the paths that reflect what you've done as an adult, and those can be repeated. At that point you have to switch to gizmos, or do something hacky like creating "Tour of Duty (Repeat #2)" style picks, which is just clutter in the selection list for most users, who won't be repeating these, so if your system includes this, I'd recommend picks with gizmos.

Oh, and another thing from when we designed this in Shadowrun - another reason we went with gizmos over agents is that once the user sets up a lifepath during character creation, they'll never touch it again during play. If it's an agent, that remains as a tab on the character, taking up screen real estate, but as a gizmo, all of the lifepaths are on the same tab where the user chose their race and other basics, and the user won't be visiting that tab again during play, but customizing them is done on a pop-up form, so the only screen real-estate they need is for a name and form-launching button.
 
I think I see how you’re saying...

1) Lifepath component bootstrapped to the actor
2) Stages component as a child of the Lifepath
3) Module component which gets added to the Stages entity. This would be the Environment, Upbringing, Childhood, Tour, etc.

Am I seeing that right?
 
No, tables on the hero, just on a normal tab, where the user adds each career choice pick. All of those are set up with gizmos, so the user then clicks the button to open the customization form, and within that form, they choose their two skills, choose a specialty, and fill in a box for how many years they spent on that one.
 
Thanks Mathias!

That pointed me in the right direction. I have a Lifepath component with a child entity, lpCustom using the LifepathHelper (a thing based on the LifepathHelper component).
Code:
  <!-- Lifepath Module -->
  <!-- form="lifepathgiz"> -->
  <entity
    id="lpCustom"
    form="lifepathgiz">
    <bootstrap thing="LifepathHelper"/>
    </entity>

  <component
    id="LifepathHelper"
    name="Lifepath Helper">
	  
    <field
      id="lpSpecExpr"
      name="Specialty Expression"
      type="derived"
	  maxlength="500"
	  defvalue="TRUE">
	  </field>
    
    <!-- build the expression strings based on tags -->
    <eval index="1" phase="Render" priority="5000"><![CDATA[
	  var expr as string

      ~ get the tags from the container for what Specialties are available	  
	  expr = container.tagids[Specialty.?,"|"]
	  expr = replace(expr, "Specialty", "thingid", 0)
	  
      field[lpSpecExpr].text = expr
	  
      ]]></eval>
    </component>

This seems to work fine. :) When I add a module (Street Kid), the lifepathgiz form pops up with the table to select the Specialty (and the list is correct based on the tags on the Street Kid module).

When a Specialty is selected (doing an autotag with Helper.Shadow) this error pops up:
Live state of gizmo 'lpCustom' is being tested during phase 'Initialization' at priority 10000 by pick 'Runner' (spcT2KRunner) before live state of parent pick 'lpT2KStreetKid' is resolved

The phase being tested is apparently on the SpecialTab component. When I remove that from the Specialty compset though, it pops the same aerror with a different "tested during" timing.

Is there something I'm missing or how is this handled for SR5 and the Life Modules?
 
To see that, use the "debug selection tasks" to examine when the very first task is on the lpT2KStreetKid pick is - it'll be after Initialization/10000, and it needs to be at or before that time. A blank script works to solve this:

Code:
<eval index="1" phase="Initialization" priority="9500">
   <before name="the script on lpCustom that's at 10000"/><![CDATA[
   ~empty script, but earlier than scripts on the picks in this gizmo
   ]]></eval>
 
Worked perfectly. :)

I can add the specialty to the gizmo with no issue.

However, even though the Specialty pick is on the gizmo with the Helper.Shadow tag, it's not reflecting on the hero.

Is there something I need to implement to have that reflect on the actor itself? or would that need to be a Displace?

EDIT: I found it. needed to add this to the Specialty component definition.
Code:
    <shadow target="hero">Helper.Shadow</shadow>
 
Last edited:
Second part of the Lifepath gizmo question -

For example, there are a small number of skills (and they are defined as unique) in the system so they are all bootstrapped to the hero from the beginning.

If I want to make a selection of a skill in the lifepath module and the table has an autotag entry so that the selected skill gets a particular tag (SkillRankIncrease), it appears that the skill is copied to the gizmo and that copy is tagged instead of the skill already existing on the hero.

Making the skill Shadow to the hero results in a new copy of the Skill, one with the SkillRankIncrease tag, and one without. Is this what you were referring to about having to pick a Master and hide any other copies?
 
Yep - even if they're unique, you'll need to use useronce uniqueness to make this work - then, yeah, shadow them to the hero, and then you need to set up scripts that can make one copy the main copy, and add together each one's bonuses to get a final value that's from all your lifepaths.


Here are the relevant scripts in SR5, although you'll have to wade through a lot of code relating to skill groups and skillsofts that are shadowrun-specific, and probably not something you have to worry about. So you'll want to pull all that out. I'm afraid I don't remember right now what caused us to have to split "Shadowed skills find their normal versions" into two scripts, and whether you'll need to retain that.


Code:
    <eval index="3" phase="Initialize" priority="29999" name="Determine FirstCopy"><![CDATA[
      ~if we're not user once unique, then every copy is the FirstCopy
      if (tagis[User.Unique] = 0) then
        perform assign[Helper.FirstCopy]
        done
        endif

      if (quickadd <> 0) then
        perform assign[Helper.FirstCopy]
        endif
      ]]></eval>

    <eval index="4" phase="Initialize" priority="30000" name="Not FirstCopy"><![CDATA[
      if (tagis[Helper.FirstCopy] = 0) then
        perform quickfind.redirect
        perform assign[Hide.Skill]
        endif
      ]]></eval>
Code:
    <eval index="8" phase="Traits" priority="8500" name="Shadowed skills find their normal versions">
      <before name="Calc trtFinal Final"/>
      <after name="Calc grRating #3"/><![CDATA[
      ~this only applies to shadowed skills
      ~those will be the ones added as skillsofts
      doneif (shadowed + tagexpr[SkillHelp.Autosoft | SkillHelp.Personafix] = 0)

      ~exclude new skill advancements
      doneif (tagis[Advance.Gizmo] <> 0)

      ~if we have a root pick, and our root pick is an advancement, stop now
      if (isroot <> 0) then
        doneif (root.tagis[Advance.Gizmo] <> 0)

        ~if that root pick has a root, and that's an advancement, stop now
        ~(this happens when Drake is added as an advancement - the Elemental
        ~Attack skill is two bootstraps down from the actual pick)
        if (root.isroot <> 0) then
          doneif (root.root.tagis[Advance.Gizmo] <> 0)
          endif
        endif

      ~handling for if we're added by a life module
      if (parent.tagis[component.LifeModule] <> 0) then
        ~no special handling needed in this case

      ~if we weren't added by a life module, and we're a skill group, stop now
      elseif (tagis[component.SkillGroup] <> 0) then
        done

      ~if we're a skillsoft/autosoft, our rating is our parent's rating
      elseif (parent.tagis[component.Gear] <> 0) then
        ~our replacement value is the rating of the program that added us
        field[trtReplace].value = parent.field[grRating].value

        ~if we're attached to the targeting autosoft, we'll need to grab some
        ~tags in order for the weapons to find us
        perform parent.pulltags[AutosoType.Targeting]
        perform parent.pulltags[ExoticProf.?]

        ~if we're not in a device, or not running, hide ourselves and don't
        ~proceed to looking for existing copies of the skill to replace
        if (hero.tagexpr[Hero.MatrixOnly | Hero.Vehicle] <> 0) then
          if (parent.field[grIsEquip].value = 0) then
            perform assign[Hide.Skill]
            perform assign[Helper.Disable]
            done
            endif
        elseif (tagis[SkillHelp.Personafix] <> 0) then
          ~nothing we need to do in this case
        elseif (tagis[SkillHelp.Autosoft] <> 0) then
          if (hero.tagis[Hero.Vehicle] = 0) then
            perform assign[Hide.Skill]
            perform assign[Helper.Disable]
            done
            endif
        elseif (parent.tagis[Equipment.KnowInfus] <> 0) then
          perform field[trtBase].modify[+,1,"Knowledge Infusion"]
          perform assign[SkillHelp.KnowInfus]
        elseif (parent.tagis[Equipment.GearSkill] <> 0) then
          if (parent.field[grIsEquip].value = 0) then
            perform assign[Hide.Skill]
            perform assign[Helper.Disable]
            done
            endif
        elseif (parent.container.ishero = 0) then
          if (container.parent.field[grIsEquip].value = 0) then
            perform assign[Hide.Skill]
            perform assign[Helper.Disable]
            done
            endif
        else
          perform assign[Hide.Skill]
          perform assign[Helper.Disable]
          done
          endif

        ~if the thing we're bootstrapped by requires activation, but it's not
        ~active, then we're hidden
        if (parent.tagis[User.Activation] <> 0) then
          if (parent.field[abilActive].value = 0) then
            perform assign[Hide.Skill]
            perform assign[Helper.Disable]
            done
            endif
          endif


      ~if we're a skill that came from a possessing spirit, there are some
      ~changes that need to be applied
      elseif (parent.tagis[component.Adjustment] <> 0) then
        if (hero.tagis[Hero.Channeling] <> 0) then
          perform assign[Hide.Skill]
          done
          endif

        perform assign[SkillHelp.FixSkill]
        perform assign[SkillHelp.NoSpecial]
        perform assign[SkillHelp.SpiritSkl]

        ~if our parent isn't activated, we're hidden.  We also don't want to run
        ~the rest of this script, since that's when our value is added to the
        ~regular version of our skill
        if (parent.activated = 0) then
          perform assign[Hide.Skill]
          done
          endif

        ~normally, this is an overriding value
        perform field[trtOver].modify[=,container.child[attrFor].field[trtFinal].value,"Possessing Spirit's Force"]
        endif

      var ismatch as number
      var cantest as number

      ~now, check to see if a non-skillsoft copy of this skill is present on the hero
      foreach pick in hero from Skill where "!Hide.Skill & " & tagids[thingid.?]
        ismatch = 0
        cantest = 0

        ~if the skill we found is shadowed, then we can only make use of it if
        ~we're a skill from a life module, and it's also from a life module
        if (eachpick.shadowed <> 0) then

          ~if it's shadowed because it was originally added as an advancement,
          ~then that's OK - we can use that copy
          if (eachpick.tagis[Advance.Gizmo] <> 0) then
            cantest = 1
          elseif (tagis[SkillHelp.LifeSkill] <> 0) then
            if (eachpick.tagexpr[SkillHelp.LifeSkill & !SkillHelp.LaterLMSkl] <> 0) then
              cantest = 1
              endif
            endif
        else
          cantest = 1
          endif

        ~if the skill we found is ourself, discard it as an option
        if (eachpick.uniqindex = uniqindex) then
          cantest = 0
          endif

        if (cantest <> 0) then
          ~if we need a menu selection, make sure that the target's menu
          ~menu selection is the same as ours
          if (field[usrCandid1].isempty = 0) then
            if (field[usrChosen1].ischosen <> 0) then
              if (eachpick.field[usrChosen1].ischosen <> 0) then
                if (compare(eachpick.field[usrChosen1].chosen.idstring, field[usrChosen1].chosen.idstring) = 0) then
                  ismatch = 1
                  endif
                endif
              endif
          elseif (tagis[User.NeedDomain] <> 0) then
            if (eachpick.field[domDomain].isempty = 0) then
              if (field[domDomain].isempty = 0) then
                if (compare(eachpick.field[domDomain].text,field[domDomain].text) = 0) then
                  ismatch = 1
                  endif
                endif
              endif
          else
            ismatch = 1
            endif

          ~for a knowledge skill, make sure that the knowledge skill categories
          ~are the same.  For example, the Academic Knowledge: Buildings skill
          ~from a university architecture module would not stack with the
          ~Professional Knowledge: Buildings from a Trade school architecture
          ~life module
          if (tagis[component.SkillKnow] <> 0) then
            if (eachpick.field[sklKnowCat].value <> field[sklKnowCat].value) then
              ismatch = 0
              endif
            endif
          endif

        if (ismatch <> 0) then
          if (field[trtOver].value >= 0) then
            eachpick.field[trtOver].value = maximum(eachpick.field[trtOver].value,field[trtOver].value)
            perform eachpick.assign[SkillHelp.DisableSpc]
            endif
          eachpick.field[trtReplace].value = maximum(eachpick.field[trtReplace].value,field[trtReplace].value)
          perform assign[Hide.Skill]
          perform assign[SkillHelp.LaterLMSkl]
          endif
        nexteach
      ]]></eval>

    <eval index="9" phase="Traits" priority="950" name="Shadowed skills find their normal versions - early changes">
      <before name="Bound trtUser"/><![CDATA[
      ~this only applies to shadowed skills
      ~those will be the ones added as skillsofts
      doneif (shadowed + tagexpr[SkillHelp.Autosoft | SkillHelp.Personafix] = 0)

      ~exclude new skill advancements
      doneif (tagis[Advance.Gizmo] <> 0)

      ~if we have a root pick, and our root pick is an advancement, stop now
      if (isroot <> 0) then
        doneif (root.tagis[Advance.Gizmo] <> 0)

        ~if that root pick has a root, and that's an advancement, stop now
        ~(this happens when Drake is added as an advancement - the Elemental
        ~Attack skill is two bootstraps down from the actual pick)
        if (root.isroot <> 0) then
          doneif (root.root.tagis[Advance.Gizmo] <> 0)
          endif
        endif

      ~this script is specific to those added as life modules
      doneif (parent.tagis[component.LifeModule] = 0)

      perform assign[SkillHelp.LifeSkill]

      var ismatch as number
      var cantest as number

      ~now, check to see if a non-skillsoft copy of this skill is present on the hero
      foreach pick in hero from Skill where tagids[thingid.?]
        ismatch = 0
        cantest = 0

        ~if the skill we found is shadowed, then we can only make use of it if
        ~we're a skill from a life module, and it's
        if (eachpick.shadowed <> 0) then
          if (tagis[SkillHelp.LifeSkill] <> 0) then
            if (eachpick.tagis[SkillHelp.LifeSkill] <> 0) then
              cantest = 1
              endif
            endif
        else
          cantest = 1
          endif

        ~if the skill we found is ourself, discard it as an option
        if (eachpick.uniqindex = uniqindex) then
          cantest = 0
          endif

        if (cantest <> 0) then
          ~if we need a menu selection, make sure that the target's menu
          ~menu selection is the same as ours
          if (field[usrCandid1].isempty = 0) then
            if (field[usrChosen1].ischosen <> 0) then
              if (eachpick.field[usrChosen1].ischosen <> 0) then
                if (compare(eachpick.field[usrChosen1].chosen.idstring, field[usrChosen1].chosen.idstring) = 0) then
                  ismatch = 1
                  endif
                endif
              endif
          elseif (tagis[User.NeedDomain] <> 0) then
            if (eachpick.field[domDomain].isempty = 0) then
              if (field[domDomain].isempty = 0) then
                if (compare(eachpick.field[domDomain].text,field[domDomain].text) = 0) then
                  ismatch = 1
                  endif
                endif
              endif
          else
            ismatch = 1
            endif

          ~for a knowledge skill, make sure that the knowledge skill categories
          ~are the same.  For example, the Academic Knowledge: Buildings skill
          ~from a university architecture module would not stack with the
          ~Professional Knowledge: Buildings from a Trade school architecture
          ~life module
          if (tagis[component.SkillKnow] <> 0) then
            if (eachpick.field[sklKnowCat].value <> field[sklKnowCat].value) then
              ismatch = 0
              endif
            endif
          endif

        if (ismatch <> 0) then
          ~what we're going to do here is make sure that both copies - ourselves
          ~and the copy we found - have identical trtInitial fields - both the
          ~value and the history should be the same.  So, we start by recording
          ~the history that's currently in eachpick's field, then add our value
          ~to the copy we found, and then set our own value = to the new total,
          ~but we only add the history we recorded to our own history, meaning
          ~that at the end, from either copy, you'll see each thing that
          ~contributed to the history recorded here.

          if (field[trtLifMod].value <> 0) then
            perform eachpick.field[trtInitial].modify[+,field[trtLifMod].value,"Life Module: " & parent.field[name].text]
            endif
          if (eachpick.field[trtLifMod].value <> 0) then
            perform field[trtInitial].modify[+,eachpick.field[trtLifMod].value,"Life Module: " & eachpick.parent.field[name].text]
            endif
          endif
        nexteach
      ]]></eval>
 
New question. :)

I've gotten the Skill selection/improvement process working where if the skill is selected for a module, it improves the version of itself on the hero which has the Helper.MasterSkill tag I've added.

I'm using a table_dynamic for the selections of the skill. However, I've found that (when I have multiple selections available) if I add a skill to the list the first time, that is isn't available to select a second time (or more).

In order to have the skill available for multiple selections, would I need to have the skills set as not unique? What impact would that have on the quickadd process?
 
This is for something along the lines of knowledge skills in PF, where you need to fill in some text or make a choice in a menu? If so, then check out all of the rows dealing with the ismatch variable in the SR5 examples - that's where it's looking to make sure the same text is filled in, both on the copy on the master, and on the copy in the module, or if it's a drop-down, that the same choice was made on both. But yeah, those skills would not be unique.
 
Mathias,

I appreciate all the help on this. :)

Is it possible to bootstrap a Resource on the gizmo child that can be used to keep track of the number of selections of that type that are available?

I have the Lifepath module with this as the child:
Code:
  <!-- Lifepath Module -->
  <!-- form="lifepathgiz"> -->
  <entity
    id="lpCustom"
    form="lifepathgiz">
    <bootstrap thing="LifepathHelper"/>
    </entity>

LifepathHelper has a bootstrap of resLPSpecialty. It also has this on the script:
Code:
	  foreach bootstrap in this where "thingid.resLPSpecialty"
	    eachthing.field[resMax].value += parent.field[lpNumSpec].value
		nexteach

This is the portal on the lifepathgiz form where I'm trying to display the resLPSpecialty summary:
Code:
  <portal
    id="lpSpecialty"
    style="tblNormal">
    <table_dynamic
      component="Specialty"
      showtemplate="SimpleItem"
      choosetemplate="SimpleItem"
      alwaysupdate="yes"
      scrollable="yes"
      addpick="LifepathHelper"
      headerpick="LifepathHelper"
      candidatepick="LifepathHelper"
	  candidatefield="lpSpecExpr">
      <autotag group="Helper" tag="Shadow"/>
      <headertitle><![CDATA[
        @text = parent.tagnames[LifepathCat.?,","] &" (" & parent.field[name].text & ") Specialty - " & container.childfound[resLPSpecialty].field[resShort].text
        ]]></headertitle>
      <additem><![CDATA[
        @text = "Select Specialty"
        ]]></additem>
      </table_dynamic>
    </portal>
The summary is always showing a 0 of 0 value however. Am I missing timings or something?
 
Back
Top