+4 votes
by (280 points)

Hello.

This is my first time asking a question in this forum. I've been browsing around for about a week now looking for some way of implementing a visual red health bar to complement the simple HP-text I am currently using in my header (I currently have something like: 20/100 HP) that gets updated with a replace call in the footer of every passage.

I've found many functional ways of implementing this that all seem to work in SugarCube, but I haven't been able to get any of them to work in Harlowe 2.0.1 as of yet. I've tried HTML5-solutions, CSS and JavaScript. Ideally I would also be able to use the same code to setup an XP-bar as well in green.

I'm creating something similar to a tiny dungeon crawler. Considering that the player will level up over time and the maxHP will increase by increments of 10 every time, I will need the HP-bar to be able to reflect this and show a percentage of maxHP in red while also letting the player know that they have 45/130 HP etc. I currently have player stats listed in a left-side menu that is a header and is placed there with the following CSS in case that matters. I'll give some example code (with bits in Swedish) just to give you an idea of what I currently use:

tw-passage tw-include[title="Vänstermenyn"] {
    width: 190px;
    position: absolute;
    top: 0px;
    left: -190px;
    padding: 2em; 
    text-overflow: clip;
    border: solid Black 0.05em; 
    border-radius: 25px;
    font-family: 'Roboto', sans-serif;
    font-size: 16px;
    line-height: 140%;
    color: Black; 
    text-align:left;
    background-color: #FFFFFC;
    box-shadow: 0.5em 0.5em 0 #330000;
    margin: 0 -0.8em 1em;  
}

I use this simple code to print the player stats:

<RUBRIK4>''Nivå:''</RUBRIK4> []<level|<BR>
<RUBRIK4>''XP:''</RUBRIK4> []<xp|/[]<xpToLevel|<BR>
<RUBRIK4>''HP:''</RUBRIK4> []<hp|/[]<hpMax|<BR>
<RUBRIK4>''Guld:''</RUBRIK4> []<guld|<BR>

And this in the footer to keep it updated:

(replace: ?level)[$playerLevel]
(replace: ?xp)[$xp]
(replace: ?xpToLevel)[$xpToLevel]
(replace: ?hp)[$hp]
(replace: ?hpMax)[$hpMax]
(replace: ?guld)[ $guld]

Also, just to be clear, I am developing a game for use in a school where I work to inspire kids to start coding in Twine. It is important that the game will work on iPads running Safari as well as Chromebooks running Chrome. I'm just throwing that out there in case there are multiple ways of solving this and some might cause compatability issues for certain browsers.

Hope someone can help!

Sincerely,
Magister52

3 Answers

+6 votes
by (159k points)
selected by
 
Best answer

The following prototype is loosely based on answers given in the Forum Health bar in right side bar or in a status passage thread. It consists of five parts.

 

1. Defining the variables used to store the max and current values of the health bar.
The best places to initialise these variables would be within your story's startup tagged passage.

(set: $maxHealth to 100)
(set: $health to $maxHealth)

 

2. The HTML being used to simulate a health bar.
Ideally this would be placed within your header tagged passage, make sure there are no line-breaks within the HTML structure as they can effect the visual layout.

<div id="health-bar">\
	<div class="bar"></div>\
</div>

 

3. The CSS being used to style the inner and outer sections that make up the health bar.
I have 'borrowed' the CSS from the linked thread for this prototype, you will obviously want to change it to suit your own needs. The CSS also hides the footer tagged passage defined later in this prototype, the CSS needs to be placed within your story's Story Stylesheet area.

#health-bar {
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box;
	width: 200px;
	height: 20px;
	padding: 5px;
	background: #ddd;
	-webkit-border-radius: 5px;
	-moz-border-radius: 5px;
	border-radius: 5px;
	position: relative;
}
.bar {
	background: #c54;
	width: 100%;
	height: 10px;
	position: relative;
	transition: width .5s linear;
}

tw-include[title="Update Health Bar"] {
	display: none;
}

 

4. The Javascript method used to update the visual state of the Heath Bar.
The following Javascript needs to be placed within your story's Story Javascript area, it defines a unique GE namespace to contain your story's custom Javascript as well as the updateHeathBar method which will be used to update the Heath Bar.

/* Create an unique private namespace to contain custom code. */
if (!window.GE) {
	window.GE = {};
}

/* Update Heath Bar elements. */
window.GE.updateHeathBar = function (maxHealth, health) {
	var
		outer = $('#health-bar'),
		inner = outer.find('.bar'),
		barWidth = (health / maxHealth) * 100;

	outer.data('total', maxHealth);
	outer.data('value', health);
	inner.css('width', barWidth + "%");
};

 

5. A footer tagged passage named Update Health Bar
This passage serves two purposes: firstly it automatically refreshes the Health Bar after each passage transition; secondly it can be manual invoked using a (display:) macro whenever you need to cause the Health Bar to be refreshed after a non passage-transitioning link has been selected.

(print: "<script>GE.updateHeathBar(" + (text: $maxHealth) + "," + (text: $health) + ");")

 

To test the above prototype simply create a new Story Project containing all of the above, then place the following TwineScript within the main starting-point Passage.

<!-- The following health change will automatically be applied to the Health Bar due to the footer tagged passage. -->

(set: $health to it - 10)


(link: "Do some Damage")[
	<!-- This health change will be applied after the link is selected, it uses an manual invokation of the footer tagged passage to cause the current Health Bar to visually be updated. -->
	(set: $health to 40)
	(display: "Update Health Bar")
]

note: I suggest you change the name of the GE namespace defined in point 4 to something more meaningful to your project, make sure the new name is UNIQUE and that you update its usage in point 4's code as well as the reference in point 5.

WARNING: The Javascript method defined in point 4 does not include any invalid parameter value or missing HTML element error catching, I strongly suggest you add some.

by (280 points)
Awesome!

Your code worked wonders greyelf!
It looks even better than I was hoping for.

I see what you mean about it missing some error catching though. However, I'm not quite sure how to go about solving that issue. Should I run Twine IF-macros to make sure it never sends faulty values to the JS or is it better to modify the JS to get the job done? I'm not skilled at JS so I wouldn't know where to begin.

Thanks a bunch for helping out though.

Sincerely,
Magister52
by (390 points)
reshown by

I'd like to force the player to a different passage once the health bar reaches 0, but I'd like for the new passage to be loaded after the health bar animation is updated (i.e.- you watch it run down to 0, and then the page refreshes to a "You are dead" passage). 

I've tried playing around with adding...

(if: $health < 1)[(go-to: "dead")]

...after

(display: "Update Health Bar")

in your TwineScript above, which indeed takes me to the "dead" passage, but we unfortunately don't see the health bar deplete and then be taken to the passage.

Is there any way to force the animation to finish and then go to a different passage?

+1 vote
by (940 points)

The biggest obstacle is the fact that Harlowe does not have any way of communicating variables to Javascript.

A progress bar is not too difficult to set up, I suggest having a look at the W3 tutorial, the problem is knowing what the current health value is (or Xp).

Sugarcube can do this via the script state.variables(), but Harlowe does not have a correspondent.

 

Now I'm not saying it's impossible, but pretty difficult. I too spent a few days bashing my head on this, but my very limited experience with Javascript (and Twine itself) limits what I can realistically solve.

by (280 points)
Thanks for answering so quickly.

I've already checked out that W3-tutorial and it's great. However, getting those variables inserted into the CSS-code without messing things up was beyond me. I tried using the (print:)-macro to get it to work, but I failed.
0 votes
by (1.1k points)
edited by

For another method, use harlowe to create the bar.

In a passage, put something like this:

{(set: $redRoughAmount to $heroHP / $maxHP)
(set: $redPercent to (round: $redRoughAmount * 100))
(set: $greenPercent to 100 - $redPercent)
(set: $red5per to (round: $redPercent * .20)) <!-- break it into 5% increments -->
(set: $green5per to 20 - $red5per)
(set: $barLength to 20)
(set: $healthBar to "")

(for: each _block, ...(range: 1 , $barLength))[
	(if: _block is <= $red5per)[
		(set: $healthBar to it + "❤️")
		]
	(else:)[
		(set: $healthBar to it + "❣️")
		]
	]
(print: $healthBar)
}

I named the passage "func:healthbar". The code can be highly compacted, of course. While the (for: ) loop only uses the $red5per variable, I left the greens in there for error checking while I was working on this.

Then put (display: "func:healthbar") where you want (header, footer, in the text, etc)

❤️ is https://emojipedia.org/heavy-black-heart/
❣️ is https://emojipedia.org/heavy-heart-exclamation-mark-ornament/

Which can be replaced with various other things as needed.

The bar can be wrapped in a <div> and controlled as needed with CSS/javascript as well!

(it took me 8 tries to find another heart symbol which would display in the answer. They work fine in game, though!)

...