Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Live Countdown Bar (Sugarcube 2.0)

edited February 2017 in Help! with 2.0
So I had this idea for a mechanic in the game I'm writing. But I'm a bit at a loss how to write and implement it.

The premise is this. In battle, you're presented with a full bar that slowly depletes (Indicating the time you have). There is also a button. You click this button to deal damage, so you want to furiously click this as much as possible before the timer bar depletes.

When the bar depletes, you're sent to another page. For the amount of times you've clicked, you deal a proportional amount of damage to the enemy, and this is described on the second page. Basically it's a bit more interactive for the player than turn-based combat where damage is rolled automatically.

I had some other ideas as variants for different sort of attacks, using a similar countdown function. E.g. A timer, a rune displayed, and you've got to click on the same rune from a set of eight before the timer runs out to cast, etc. It all would use the same underlying 'timer bar' mechanic that depletes in real time, then sends you to a different page once the bar runs out.

Anyone know how I should go about this?

Alternatively, I was also thinking about how one would do a similar coundown (or count UP) bar where you have to hit a button when the bar rests within a certain section, much like your 'Dance Dance Revolution' kind of thing. E.g. Hit when the bar rests between 60% and 80%. But I've been thinking this would be too hard to implement and may not even be possible.

renzokuken.jpg

Comments

  • edited February 2017
    I'm sure it's probably not the best way, but something like this can get you started:

    In your css:
    .time-bar {
        width: 60%;
        height: 20px;
        border-radius: 5px;
        border: 2px solid #fff;
    }
    .time-bar span {
        position: relative;
        top: 0px;
        background: #fff;
        height: 18px;
        border-radius: 5px;
        display: inline-block;
    }
    .time-bar span {
        -webkit-animation: w0 10s ease forwards;
        animation: w0 10s ease forwards;
    }
    .time-bar .w0 {
        width: 1%;
    }
    @keyframes w0 {
        from { width: 100%; }
        to { width: 1%; }
    }
    

    Then:
    ::timed_battle
    <div class="time-bar"><span class="w0"></span></div>
    
    <<button "Attack">><<set $atk++>><</button>>
    /%use a button--mashing on a link will highlight text%/
    <<timed 10s>>
    <<goto "battle_next">>
    <</timed>>
    
    ::battle_next
    <<set $dam to $atk*10>>Dealt $dam damage!<<set $atk to 0>>
    
    [[Next|timed battle]]
    

    I attached a sample.
  • As for your second question, here's more code:

    In css:
    .time-bar {
        width: 60%;
        height: 20px;
        border-radius: 5px;
        border: 2px solid #fff;
    }
    .time-bar span {
    	position: relative;
    	top: 0px;
        background: #fff;
        height: 18px;
        border-radius: 5px;
        display: inline-block;
    }
    .time-bar .w0 {
        animation: w0 3s linear forwards;
    }
    .time-bar .w0 {
        width: 1%;
    }
    
    
    @keyframes w0 {
        0% { width: 100%; }
        33% { width: 66%; background: #fff }
    	34% { width: 65%; background: red;}
    	65% { width: 34%; background: red;}
    	66% { width: 33%; background: #fff;}
    	100% { width: 1%; }
    }
    

    In Twine:
    ::timed battle
    <div class="time-bar"><span class="w0"></span></div>
    
    <span id="timedpress"> </span>
    <<timed 1s>>
    <<replace "#timedpress">><<button "Click now!">><<set $success to true>><</button>><</replace>>
    <<next 1s>>
    <<replace "#timedpress">> <</replace>>
    <<next 1s>>
    <<goto "battle_next">>
    <</timed>>
    
    ::battle_next
    <<if $success>>You clicked at the right time!<<else>>You missed!<<endif>><<unset $success>>
    
    [[Next|timed battle]]
    
  • edited February 2017
    Thanks heaps! This code looks interesting and I'm going to mess around with it today. :)

    EDIT: One obstacle I can see is keeping the CSS 10 secs and the in-passage 10 secs stat as two separate items, which means if you were to ever try and modify the timer value (E.g. A talent that increases the time you have), you'd have to do some real messing around.

    Also, for some reason I've yet to figure out, the bar really slows down as it reaches depletion instead of depleting at a static pace.

    BTW, not complaining, just thinking out loud as I play around with it. ^_^
  • DairynGM wrote: »
    Thanks heaps! This code looks interesting and I'm going to mess around with it today. :)

    EDIT: One obstacle I can see is keeping the CSS 10 secs and the in-passage 10 secs stat as two separate items, which means if you were to ever try and modify the timer value (E.g. A talent that increases the time you have), you'd have to do some real messing around.

    Also, for some reason I've yet to figure out, the bar really slows down as it reaches depletion instead of depleting at a static pace.

    BTW, not complaining, just thinking out loud as I play around with it. ^_^

    For the second one, that's easy to fix. Change "ease" to "linear" below:
        -webkit-animation: w0 10s ease forwards;
        animation: w0 10s ease forwards;
    

    to
        -webkit-animation: w0 10s linear forwards;
        animation: w0 10s linear forwards;
    

    As for the first thing, the animation and the timer actually have nothing to do with each other, as you pointed out. If you do want to line everything up with variables, you could use JQuery to pace out the animation. I'm not sure precisely how to do that off the top of my head, but I'll see if I can whip something up later.
  • edited February 2017
    Thanks again for the quick and thorough feedback. I've been playing around with this most of the morning. :)

    So far I've added a live damage counter in the same passage as the countdown timer so the player can see how much much damage they're doing before the second passage, and I'm playing around with a way to make said damage pulse once every time you click. So far it's just flashing in an epileptic fit inducing kind of way, but hey, progress!

    EDIT: Adding the Shakescreen add-on TheMadExile made and attaching it to the button with a 0.5 sec duration seems to have done the trick instead of a pulse. It gives a rather satisfying 'screenshake' every time you 'land' a hit. :)
  • edited February 2017
    Glad you like it! Here's the variable controlled bar:

    In CSS:
    .time-bar {
        width: 60%;
        height: 20px;
        border-radius: 5px;
        border: 2px solid #fff;
    }
    .time-bar span {
        position: relative;
        top: 0px;
        background: #fff;
        height: 18px;
        border-radius: 5px;
        display: inline-block;
    }
    
    .time-bar .w0 {
        position: relative;
        width: 100%;
    }
    

    In passages:
    ::StoryInit
    <<set $battleSec to 10>> /%change to time you want, in seconds (integers only)%/
    <<set $battleTime to $battleSec + "s">>
    
    ::timed battle
    <div class="time-bar"><span class="w0"></span></div>
    <<script>>!function(){
    	$(document).ready(function(){
    	var dur = State.variables.battleSec * 1000;
    	$('.time-bar .w0').animate({width: '1%'}, dur, 'linear');
    	});
    }();<</script>>
    <<button "Attack">><<set $atk++>><</button>>
    /%use a button--mashing on a link will highlight text%/
    <<timed $battleTime>>
    <<goto "battle_next">>
    <</timed>>
    
    ::battle_next
    <<set $dam to $atk*10>>Dealt $dam damage!<<set $atk to 0>>
    
    [[Next|timed battle]]
    

    I didn't test it as extensively as I probably should have, so let me know if you have any problems.

    It will probably only work with integers (so no half-seconds) because of how the <<timed>> macro works.
  • Chapel wrote: »
    In passages:
    ::timed battle
    <div class="time-bar"><span class="w0"></span></div>
    <<script>>!function(){
    	$(document).ready(function(){
    	var dur = State.variables.battleSec * 1000;
    	$('.time-bar .w0').animate({width: '1%'}, dur, 'linear');
    	});
    }();<</script>>
    <<button "Attack">><<set $atk++>><</button>>
    /%use a button--mashing on a link will highlight text%/
    <<timed $battleTime>>
    <<goto "battle_next">>
    <</timed>>
    
    No.

    jQuery's ready() method does nothing useful when used inside a story format. The main entry functions of every story format I know of is registered via ready() or equivalent. By the time any user code can be executed the document has been ready for a significant amount of, computer, time. Using the ready() method, or equivalent, in user code does little useful as it immediately executes its callback. Beyond that, you're attempting to animate an element possibly before it's even on the page. The correct way to handle this would be with a postrender or, probably better, postdisplay task.

    Further, if you're going to use jQuery to animate the bar, then you'd be better off using the animate() method's complete callback parameter, rather than a separate <<timed>>. That way the animation and passage navigation are perfectly synchronized.

    I'd also suggest hiding, or disabling, the button immediately, so the player can't get any last second hits in as navigation begins.

    I would recommend something like the following:
    :: StoryInit
    <<set $battleSec to 10>> /* in seconds, non-integer values are okay */
    
    :: timed battle
    <div class="time-bar"><span class="w0"></span></div>
    <div class="time-btn"><<button "Attack">><<set $atk++>><</button>></div>
    <<script>>
    postdisplay['attack-timer'] = function (taskName) {
    	delete postdisplay[taskName]; // single-use task
    	$('.time-bar .w0').animate({ width : '0%' }, {
    		duration : State.variables.battleSec * 1000,
    		easing   : 'linear',
    		complete : function () {
    			$('.time-btn').hide();
    			Engine.play('battle_next');
    		}
    	});
    };
    <</script>>
    


    Chapel wrote: »
    It will probably only work with integers (so no half-seconds) because of how the <<timed>> macro works.
    The <<timed>> macro has no issues working with fractional- or sub-second delays—either specified in seconds or milliseconds; e.g. 1.5s, 1500ms, 0.5s, 500ms.

    Where did you get the idea that it couldn't?
  • Chapel wrote: »
    In passages:
    ::timed battle
    <div class="time-bar"><span class="w0"></span></div>
    <<script>>!function(){
    	$(document).ready(function(){
    	var dur = State.variables.battleSec * 1000;
    	$('.time-bar .w0').animate({width: '1%'}, dur, 'linear');
    	});
    }();<</script>>
    <<button "Attack">><<set $atk++>><</button>>
    /%use a button--mashing on a link will highlight text%/
    <<timed $battleTime>>
    <<goto "battle_next">>
    <</timed>>
    
    No.

    jQuery's ready() method does nothing useful when used inside a story format. The main entry functions of every story format I know of is registered via ready() or equivalent. By the time any user code can be executed the document has been ready for a significant amount of, computer, time. Using the ready() method, or equivalent, in user code does little useful as it immediately executes its callback. Beyond that, you're attempting to animate an element possibly before it's even on the page. The correct way to handle this would be with a postrender or, probably better, postdisplay task.

    Further, if you're going to use jQuery to animate the bar, then you'd be better off using the animate() method's complete callback parameter, rather than a separate <<timed>>. That way the animation and passage navigation are perfectly synchronized.

    I'd also suggest hiding, or disabling, the button immediately, so the player can't get any last second hits in as navigation begins.

    I would recommend something like the following:
    :: StoryInit
    <<set $battleSec to 10>> /* in seconds, non-integer values are okay */
    
    :: timed battle
    <div class="time-bar"><span class="w0"></span></div>
    <div class="time-btn"><<button "Attack">><<set $atk++>><</button>></div>
    <<script>>
    postdisplay['attack-timer'] = function (taskName) {
    	delete postdisplay[taskName]; // single-use task
    	$('.time-bar .w0').animate({ width : '0%' }, {
    		duration : State.variables.battleSec * 1000,
    		easing   : 'linear',
    		complete : function () {
    			$('.time-btn').hide();
    			Engine.play('battle_next');
    		}
    	});
    };
    <</script>>
    
    Apologies (mostly to DairynGM for giving bad code), didn't know that about ready(), but it does make sense. I was wondering if I should be using postrender, but ready() worked (I realize it probably didn't "work" in the sense that it was running right) so I didn't want to rock the boat.

    I feel like I should probably stop answering these questions, haha. Thanks for the code.

    Disabling the button and forwarding with JQuery; strange how it always seems so obvious after the fact.
    Chapel wrote: »
    It will probably only work with integers (so no half-seconds) because of how the <<timed>> macro works.
    The <<timed>> macro has no issues working with fractional- or sub-second delays—either specified in seconds or milliseconds; e.g. 1.5s, 1500ms, 0.5s, 500ms.

    Where did you get the idea that it couldn't?

    Probably a half-baked internet blog. I thought that <<timed>> used CSS time values, and I read somewhere, sometime that CSS time values had to be integers. Looking it up real quick and it's clear that the thing I read at some point (or made up and convinced myself of, who knows) about time in CSS was absolutely not true.

    Should've double checked before posting that.
  • edited February 2017
    Regardless of if it's bad code or not, it shows me the ways to do it and the ways to maybe not do it (and why), which is all helping me understand stuff a lot more. So it's hardly wasted. :)

    This is what I threw together from the stuff you guys gave me. For ref:

    Javascript:
    /*! <<shake>> macro set for SugarCube 2.x */
    !function(){"use strict";if("undefined"==typeof version||"undefined"==typeof version.title||"SugarCube"!==version.title||"undefined"==typeof version.major||version.major<2||"undefined"==typeof version.minor||version.minor<5)throw new Error("<<shake>> macro requires SugarCube 2.5.0 or greater, aborting load");Macro.add("shake",{tags:null,handler:function(){var duration=this.args.length>0?this.args[0]:1/0,shakeClass="shake";if(1/0!==duration)try{duration=Math.max(Engine.minDomActionDelay,Util.fromCssTime(duration))}catch(e){return this.error(e.message)}Config.debug&&this.debugView.modes({block:!0});var $wrapper=jQuery(document.createElement("span"));$wrapper.addClass("macro-"+this.name+" "+shakeClass).wiki(this.payload[0].contents).appendTo(this.output),1/0!==duration&&setTimeout(function(){$wrapper.removeClass(shakeClass)},Engine.minDomActionDelay+duration)}}),Macro.add(["shakescreen","shaketarget"],{handler:function(){var $targets,duration,shakeClass;if("shakescreen"===this.name)$targets=jQuery("#passages"),duration=this.args.length>0?this.args[0]:1/0,shakeClass="shake-block";else{if(0===this.args.length)return this.error("no selector specified");if($targets=jQuery(this.args[0]),0===$targets.length)return this.error('no elements matched the selector "'+this.args[0]+'"');duration=this.args.length>1?this.args[1]:1/0,shakeClass="block"===jQuery($targets[0]).css("display")?"shake-block":"shake"}if("stop"===duration)return void $targets.removeClass(shakeClass);if(1/0!==duration)try{duration=Math.max(Engine.minDomActionDelay,Util.fromCssTime(duration))}catch(e){return this.error(e.message)}$targets.addClass(shakeClass),1/0!==duration&&setTimeout(function(){$targets.removeClass(shakeClass)},Engine.minDomActionDelay+duration)}})}();
    

    CSS Stylesheet:
    .time-bar {
        width: 60%;
        height: 20px;
        border-radius: 5px;
        border: 2px solid #fff;
    }
    .time-bar span {
        position: relative;
        top: 0px;
        background: #fff;
        height: 18px;
        border-radius: 5px;
        display: inline-block;
    }
    
    .time-bar .w0 {
        position: relative;
        width: 100%;
    }
    
    
    @keyframes w0 {
        from { width: 100%; }
        to { width: 1%; }
    }
    /*! <<shake>> macro set for SugarCube 2.x */
    @-webkit-keyframes shakeanim {
    	0% { -webkit-transform: translate(2px, 1px) rotate(0deg); }
    	10% { -webkit-transform: translate(-1px, -2px) rotate(-1deg); }
    	20% { -webkit-transform: translate(-3px, 0px) rotate(1deg); }
    	30% { -webkit-transform: translate(0px, 2px) rotate(0deg); }
    	40% { -webkit-transform: translate(1px, -1px) rotate(1deg); }
    	50% { -webkit-transform: translate(-1px, 2px) rotate(-1deg); }
    	60% { -webkit-transform: translate(-3px, 1px) rotate(0deg); }
    	70% { -webkit-transform: translate(2px, 1px) rotate(-1deg); }
    	80% { -webkit-transform: translate(-1px, -1px) rotate(1deg); }
    	90% { -webkit-transform: translate(2px, 2px) rotate(0deg); }
    	100% { -webkit-transform: translate(1px, -2px) rotate(-1deg); }
    }
    @-o-keyframes shakeanim {
    	0% { -o-transform: translate(2px, 1px) rotate(0deg); }
    	10% { -o-transform: translate(-1px, -2px) rotate(-1deg); }
    	20% { -o-transform: translate(-3px, 0px) rotate(1deg); }
    	30% { -o-transform: translate(0px, 2px) rotate(0deg); }
    	40% { -o-transform: translate(1px, -1px) rotate(1deg); }
    	50% { -o-transform: translate(-1px, 2px) rotate(-1deg); }
    	60% { -o-transform: translate(-3px, 1px) rotate(0deg); }
    	70% { -o-transform: translate(2px, 1px) rotate(-1deg); }
    	80% { -o-transform: translate(-1px, -1px) rotate(1deg); }
    	90% { -o-transform: translate(2px, 2px) rotate(0deg); }
    	100% { -o-transform: translate(1px, -2px) rotate(-1deg); }
    }
    @keyframes shakeanim {
    	0% { transform: translate(2px, 1px) rotate(0deg); }
    	10% { transform: translate(-1px, -2px) rotate(-1deg); }
    	20% { transform: translate(-3px, 0px) rotate(1deg); }
    	30% { transform: translate(0px, 2px) rotate(0deg); }
    	40% { transform: translate(1px, -1px) rotate(1deg); }
    	50% { transform: translate(-1px, 2px) rotate(-1deg); }
    	60% { transform: translate(-3px, 1px) rotate(0deg); }
    	70% { transform: translate(2px, 1px) rotate(-1deg); }
    	80% { transform: translate(-1px, -1px) rotate(1deg); }
    	90% { transform: translate(2px, 2px) rotate(0deg); }
    	100% { transform: translate(1px, -2px) rotate(-1deg); }
    }
    .shake-block,
    .shake {
    	-webkit-animation-duration: 0.8s;
    	     -o-animation-duration: 0.8s;
    	        animation-duration: 0.8s;
    	-webkit-animation-iteration-count: infinite;
    	     -o-animation-iteration-count: infinite;
    	        animation-iteration-count: infinite;
    	-webkit-animation-name: shakeanim;
    	     -o-animation-name: shakeanim;
    	        animation-name: shakeanim;
    	-webkit-animation-timing-function: linear;
    	     -o-animation-timing-function: linear;
    	        animation-timing-function: linear;
    	-webkit-transform-origin: 50% 50%;
    	    -ms-transform-origin: 50% 50%;
    	     -o-transform-origin: 50% 50%;
    	        transform-origin: 50% 50%;
    }
    .shake-block {
    	display: block;
    }
    .shake {
    	display: inline-block;
    }
    
    ::StoryInit
    <<set $battleSec to 10>> /* in seconds, non-integer values are okay */
    <<set $dam to 0>>
    <<set $atk to 0>>
    
    ::Timed Battle
    Damage: <span id="damage">$dam</span>!\
    <<silently>><<repeat 0.1s>><<if $dam neq $attk>><<set $dam to $atk*10>><<replace "#damage">>$dam<</replace>><</if>><</repeat>><</silently>>
    
    <div class="time-bar"><span class="w0"></span></div>
    <div class="time-btn"><<button "Attack">><<shakescreen 0.5s>><<set $atk++>><</button>></div>
    <<script>>
    postdisplay['attack-timer'] = function (taskName) {
    	delete postdisplay[taskName]; // single-use task
    	$('.time-bar .w0').animate({ width : '0%' }, {
    		duration : State.variables.battleSec * 1000,
    		easing   : 'linear',
    		complete : function () {
    			$('.time-btn').hide();
    			Engine.play('Battle Next');
    		}
    	});
    };
    <</script>>
    
    ::Battle Next
    <<set $dam to $atk*10>>Dealt $dam damage!<<set $atk to 0>>
    
    [[Next|Timed Battle]]
    
  • edited February 2017
    First seems to be working great, so I'm moving onto the second idea: The 'click in a certain % zone' Renzokuken one, as mentioned in the first post.

    Meshing together the code from TheLastExile with the stuff from Chapel in one of my usual Frankenstein experiments. Aiming to create a timer where the button is always visible, you've got to click when it rests within a certain range (60% to 80%), and if you click at the wrong or right time it triggers failure or success instantly. I'm aiming for the speed in which the timer depletes to be a variable, like the first bit of code.

    EDIT: Trying this has resulted in another flashy mess. o_o I'll try again later.
  • edited February 2017
    Chapel wrote: »
    I feel like I should probably stop answering these questions, haha. Thanks for the code.
    Don't let me discourage you, as that certainly wasn't the point of my post. I was simply attempting to correct errors and bad practices, because they're like plagues. Unless you step on them quickly and decisively, they spread like wildfire affecting more and more people because they get passed on like a disease. Losing the kingdom, all for the want of a horseshoe nail.

    The jQuery readiness callback normally gets the hammer from me fairly quickly because of how pointless it generally is within user code and how often people recommend it. I also have a particular distaste of cargo cult programming, which is often the reason behind its recommendation, so that's likely a factor as well.
  • Not just helping with code. Also educating on proverbs. :)

    Which is great, because 'for want of a nail' is perfectly applicable to an upcoming tabletop session I'm running.
  • @DairynGM You shouldn't need the @keyframes stuff in your css anymore since you're handling the animation in jQuery.

    Not a huge deal, but might as well minimize code anywhere you can. Otherwise it looks good.
Sign In or Register to comment.