An Ink-Bleed Fade

I wanted to do something nice in Incredipede for transitioning between screens. Instead of just flipping boringly from the title screen to the game map, for example, I wanted it to look like the ink was being pulled off the paper and then reprinted with a map. Like you could see the press pushing the ink into the paper.

You can see what I came up with by watching the begining of the video in my “How do You Play Incredipede” post.

I asked my friend at Gaijin Games, Andrew Hynek, who wrote an amazing looking ink game called Drift Sumi-e for some suggestions. I ended up mangling his ideas into something more me. That is, something simpler and easier to implement.

Incredipede is written in Flash using Stage3d. I use the Starling 2d API because it makes my code simpler and faster. One of the things I love about Stage3d is that I get to write shaders! In assembly! I find this really fun and I wrote a few posts on pixel shaders here.

This fade effect is all with the shaders. The basic idea is simple: I just take the brightest pixels and don’t draw them, then I take the next brightest pixels and stop drawing them until I’ve stopped drawing even the darkest pixels. The problem with doing that is it ends up looking kind of shitty. Here is how it looks if I just take out half of the brightest pixels:

Kind of awful. If you click to enlarge it you can see how individual pixels end up being pulled out of solid spots of colour. The fade ends up looking super pixely, like something from the 80s. Another problem is that all the black pixels just disappear all at once at the very end of the fade.

This is pretty bad but it was a good enough idea to start with. Sarah and I happened to be in Nice with our friend Marc ten Bosch while I was working on this and while we were all walking along an old aqueduct we discussed how to make it better. We thought about using noise to make the effect more organic but eventually came up with the genius idea to use a blurred mask.

The shader code to do the bad, pixely effect is something like:

//get the pixel
tex ft0, v0, fs1 <2d,repeat,linear,nomip>
//save a copy of the pixel in ft3
mov ft3, ft0

//brightness of a pixel ~= (R+R+B+G+G+G)/6
mov ft1 ft0.x
add ft1 ft1 ft0.x
add ft1 ft1 ft0.y
add ft1 ft1 ft0.y
add ft1 ft1 ft0.y
add ft1 ft1 ft0.z
div ft1 ft1 fc2
//ft1 now holds the pixel brightness

//if the brightness is less than our fade constant (which moves between 0 and 1)
//then make ft1 = 0, otherwise make it = 1
sge ft1, fc0, ft1

//multiply the saved pixel by ft1, which will zero it out if it isn’t
//darker than our fade constant
mul oc, ft3, ft1

Even if you don’t know agal assembly this shouldn’t be too hard to read. Most of the code is getting perceived brightness of the pixel with a formula from some friendly people over on Stack Overflow. All it does is take the brightness, check to see if it’s greater than our fade constant and doesn’t draw the pixel if it is.

Now, here is the clever trick that makes it look good. Instead of looking at the pixel in the image I look at the pixel of a blurred version of the image. I still draw the pixel of the normal unblurry image but I use a blurred image to decide whether to draw it or not.

Here’s what the image looks like not faded at all (wonder at the amazing art of Thomas Shahan!):

And here’s what the blurred image looks like:

Now, the player never sees this blurred image, only the graphics card does. We use the pixels from this blurred image to decide what pixels to draw from the unblurred image. Only one line of the shader code has to change. We used to do this:

//get the pixel
tex ft0, v0, fs1 <2d,repeat,linear,nomip>
//save a copy of the pixel in ft3
mov ft3, ft0

Now we do this:

//grab the pixel from the blurry image to check the brightness
tex ft0, v0, fs1 <2d,repeat,linear,nomip>
//grab the pixel from the unblurred image to actually draw
tex ft3, v0, fs0 <2d,repeat,linear,nomip>

We pass both images into the shader and use one as a mask to draw the other. This gives us beautiful unpixely fades that look like this:

The Map Half Faded

So that’s how the Incredipede ink fade is done. I admit that there is a little hand-waving magic here. You have to know how to render-to-texture and write custom shaders in Starling. Which is not terribly easy. And you have to be able to blur your image on the fly. The blurring is actually way harder than the ink-fade transition. I based my blurring on the work of Lars Gerckens.

Mostly I hope this inspired you to try some cool shader tricks of your own! They are pretty fun and you can get some great effects.

This example is taken from my game Incredipede, if you don’t already know what Incredipede is you can head over to Incredipede.com to check it out.

How Do You Play Incredipede?

Have you been wondering exactly how you play Incredipede? How do you make limbs? How many muscles can you create? Here are some answers for you.

 

You can get more information about Incredipede as well as pre-order it at Incredipede.com.

Integrating PayPal for Digital Goods

Let’s face it: PayPal may be an unethical, bullying monopoly, but it’s a necessary evil when selling online. When we used PayPal to sell Fantastic Contraption, they suddenly froze our account because they were worried our digital goods (the full version of the game) might *poof* and disappear causing thousands of players to demand a refund.

Incredipede will use micropayments to sell cool custom creature skins by different artists.
PayPal has since then joined the modern age and added a new payment solution specifically for digital goods. It has lower rates for payments under $15 (5% + 5 cents, you just have to apply), and includes an optional login-free 2-click interface. You still need access to a browser window running Javascript, but it seems PayPal is making strides to support smaller, faster purchases.

I just added a PayPal micropayment system to Incredipede. I used to implement payment methods professionally, but for my own projects I prefer to keep things simple. The Java PayPal APIs I found were all too complicated, didn’t work with Google App Engine, or were out of date and incompatible with Digital Goods. So I rolled my own simple system which uses PayPal’s Express Checkout API. It has three public methods:

  1. startPurchase – Begins the transaction and sends the user to PayPal
  2. validateDetails – Verifies the payment when the user returns
  3. finishPurchase – Completes the payment

I’ve simplified it down to one file for you with a bunch of inline constants and other bad practices. You may download PayPalManager.java (or copy & paste from below) and do as you will with it:

package com.incredipede.store;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletResponse;

/**
 * Super simple implementation of Paypal for Digital Goods - Express Checkout API
 * Java NVP (name-value pairs) implementation (July 1012 version 69.0)
 * 
 * Using API and code samples from:
 * https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_ExpressCheckout_IntegrationGuide_DG.pdf
 * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_IntroducingExpressCheckoutDG
 * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_api_reference
 * https://www.paypal-labs.com/integrationwizard/ecpaypal/cart.php 
 *
 * Usage:
 *     When user presses a "Buy" button on your site:
 *         PayPalManager.startPurchase(2, 5, "Dapper Hat", A virtual item in the game Incredipede", "1.00", response);
 *     When user returns to RETURN_URL after authenitcating purchase on Paypal's website:
 *         String token = request.getParameter("token");
 *         int userId = Integer.parseInt(request.getParameter("userId"));
 *         int itemId = Integer.parseInt(request.getParameter("itemId"));
 *         // first check the status on paypal's system and make sure purchase is for reals
 *         String payerId = PayPalManager.validateDetails(token, userId, itemId);
 *         // next perform the purchase on your system to make sure it succeeds
 *         ...
 *         // finally take the user's money
 *         try {
 *             PayPalManager.finishPurchase(token, payerId, userId, itemId, "Dapper Hat", 
 *                 A virtual item in the game Incredipede", "1.00");
 *         } catch (Exception e) {
 *             // roll back the purchase on your system
 *             ...
 *         }
 * 
 * @author Sarah Northway
 */
public class PayPalManager
{
	// production creds
//	protected static String API_USERNAME = "xxxxxxxxxxxxxxxxxxxxxxxx";
//	protected static String API_PASSWORD = "XXXXXXXXXXXXX";
//	protected static String API_SIGNATURE = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
//	protected static String API_URL = "https://api-3t.paypal.com/nvp";
//	protected static String REDIRECT_URL = "https://www.paypal.com/cgibin/webscr?cmd=_express-checkout";
	
	// sandbox creds
	protected static String API_USERNAME = "xxxxxxxxxxxxxxxxxxxxxxxx";
	protected static String API_PASSWORD = "XXXXXXXXXXXXX";
	protected static String API_SIGNATURE = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
	protected static String API_URL = "https://api-3t.sandbox.paypal.com/nvp";
	protected static String REDIRECT_URL = "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout";
	
	/** User will return to this page after the sale is successful */
	protected static String RETURN_URL = "http://127.0.0.1:8888/return;
	
	/** User will return here if they hit the cancel button during purchase */
	protected static String CANCEL_URL = "http://127.0.0.1:8888/cancel;

	protected final static Logger log = Logger.getLogger(PayPalManager.class.getName());
	
	/**
	 * Step 1: SetExpressCheckout
	 * 
	 * The first step of Express Checkout for Digital Goods: send a SetExpressCheckout
	 * request to PayPal and receive a token in response. Redirect the user to Paypal,
	 * then wait for their return through either the returnUrl or cancelUrl.
	 * 
	 * As of version 69.0, digital payments must set L_PAYMENTREQUEST_0_ITEMCATEGORY0=Digital, 
	 * must specify NOSHIPPING=1 and REQCONFIRMSHIPPING=0, 
	 * must use both AMT and ITEMAMT, and must have exactly one 
	 * payment (PAYMENTREQUEST_0_[...]) and one item (L_PAYMENTREQUEST_0_[...]0).
	 * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_SetExpressCheckout
	 *
	 * @param userId The user's unique id in our system
	 * @param itemId The unique id in our system for the thing being bought
	 * @param itemName Shown in paypal as the name of the thing being bought eg "Dapper hat"
	 * @param itemDescription Shown in paypal beneath the item name eg "A virtual item in the game Incredipede"
	 * @param itemPriceDollars String price in USD must include decimal and two digits after eg "10.00"
	 */
	public static void startPurchase (int userId, int itemId, String itemName, String itemDescription, 
		String itemPriceDollars, HttpServletResponse resp)
	{
		// include the userId and itemId in the return urls so we can access them later
		String returnUrl = encodeValue(RETURN_URL + "&userId=" + userId + "&itemId=" + itemId);
		String cancelUrl = encodeValue(CANCEL_URL + "&userId=" + userId + "&itemId=" + itemId);
		
		String data = 
			"METHOD=SetExpressCheckout" +
			getAuthenticationData() +
			"&REQCONFIRMSHIPPING = 0" +
			"&NOSHIPPING = 1" +
			"&ALLOWNOTE = 0" +
			"&PAYMENTREQUEST_0_PAYMENTACTION=Sale" +
			"&PAYMENTREQUEST_0_CURRENCYCODE=USD" + 
			getPurchaseData(userId, itemId, itemName, itemDescription, itemPriceDollars) + 
			"&RETURNURL=" + returnUrl + 
			"&CANCELURL=" + cancelUrl +
			"";

		// tell paypal we want to start a purchase
		HashMap results = doServerCall(data);
		
		// forward the user on to payapal's site with the token identifying this transaction
		try {
			String token = results.get("TOKEN");
			String redirectUrl = resp.encodeRedirectURL(REDIRECT_URL + "&token=" + token);
			log.info("Sending user to paypal: " + redirectUrl);
			resp.sendRedirect(redirectUrl);
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException("Failed to open PayPal link: " + e.getMessage());
		}
	}
	
	/**
	 * Step 2: GetExpressCheckoutDetails
	 * 
	 * Second step, performed when the user returns from paypal to validate the transaction
	 * details. If we cared about shipping info, the user's name etc it would be fetched here.
	 * Throws an exception if userId or purchase details don't match paypal's values, or if
	 * there's a problem with the purchase itself.
	 * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_GetExpressCheckoutDetails
	 * 
	 * @param token The token created and returned by Paypal in step 1 (from the return url)
	 * @param userId The user's unique id in our system (from the return url)
	 * @param itemId The unique id in our system for the thing being bought (from the return url)
	 * @return Returns the user's paypal PayerId for use in the last step
	 */
	public static String validateDetails(String token, int userId, int itemId)
	{
		String data = 
			"METHOD=GetExpressCheckoutDetails" +
			getAuthenticationData() +
			"&TOKEN=" + encodeValue(token) +
			"";
	
		HashMap results = doServerCall(data);

		int resultsUserId = Integer.parseInt(results.get("PAYMENTREQUEST_0_CUSTOM"));
		if (resultsUserId != userId) {
			throw new RuntimeException("UserId does not match.");
		}
		
		int resultsItemId = Integer.parseInt(results.get("PAYMENTREQUEST_0_INVNUM"));
		if (resultsItemId != itemId) {
			throw new RuntimeException("ItemId does not match.");
		}
		
		String payerId = results.get("PAYERID");
		if (payerId == null || payerId.trim().length() == 0) {
			throw new RuntimeException("Payment has not been initiated by the user.");
		}
		
		return payerId;
	}
	
	/**
	 * Step 3: DoExpressCheckoutPayment
	 * 
	 * Completes the payment that has already been started and authorized by the user
	 * via the paypal website. Requires passing in purchase information again.
	 * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_DoExpressCheckoutPayment
	 * 
	 * @param userId The user's unique id in our system
	 * @param itemId The unique id in our system for the thing being bought
	 * @param itemName Shown in paypal as the name of the thing being bought eg "Dapper hat"
	 * @param itemDescription Shown in paypal beneath the item name eg "A virtual item in the game Incredipede"
	 * @param itemPriceDollars String price in USD must include decimal and two digits after eg "10.00"
	 */
	public static void finishPurchase(String token, String payerId, int userId, int itemId, String itemName, 
		String itemDescription, String itemPriceDollars)
	{
		try {
			String data = 
				"METHOD=DoExpressCheckoutPayment" +
				getAuthenticationData() +
				"&TOKEN=" + encodeValue(token) +
				"&PAYERID=" + encodeValue(payerId) +
				getPurchaseData(userId, itemId, itemName, itemDescription, itemPriceDollars) + 
				"";
		
			HashMap results = doServerCall(data);
			
			// warn if transaction type isn't completed or on the way to completed
			String status = results.get("PAYMENTINFO_0_PAYMENTSTATUS");
			if (status == null || !(status.equalsIgnoreCase("Completed") 
				|| status.equalsIgnoreCase("In-Progress")
				|| status.equalsIgnoreCase("Processed") 
				|| status.equalsIgnoreCase("Completed-Funds-Held"))) {
				ActionHandler.log.warning("Unexpected paypal purchase status: " + status 
					+ " for userId=" + userId + ", paypal payerId=" + payerId 
					+ ", transaction=" + results.get("PAYMENTINFO_0_TRANSACTIONID"));
			}
			
		// must rollback purchase if anything happens here, so make sure we catch them all
		} catch (RuntimeException e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}
	}
	
	/**
	 * Return the name-value-pair parameters required for SetExpressCheckout and 
	 * DoExpressCheckoutPayment steps.
	 *
	 * @param userId The user's unique id in our system
	 * @param itemId The unique id in our system for the thing being bought
	 * @param itemName Shown in paypal as the name of the thing being bought eg "Dapper hat"
	 * @param itemDescription Shown in paypal beneath the item name eg "A virtual item in the game Incredipede"
	 * @param itemPriceDollars String price in USD must include decimal and two digits after eg "10.00"
	 */
	protected static String getPurchaseData(int userId, int itemId, String itemName, 
		String itemDescription, String itemPriceDollars)
	{
		return 
			"&PAYMENTREQUEST_0_AMT=" + itemPriceDollars + 
			"&PAYMENTREQUEST_0_ITEMAMT=" + itemPriceDollars + 
			"&PAYMENTREQUEST_0_DESC=" + itemDescription + 
			"&PAYMENTREQUEST_0_CUSTOM=" + userId + 
			"&PAYMENTREQUEST_0_INVNUM=" + itemId + 
			"&L_PAYMENTREQUEST_0_NAME0=" + itemName + 
			"&L_PAYMENTREQUEST_0_DESC0=" + itemDescription + 
			"&L_PAYMENTREQUEST_0_AMT0=" + itemPriceDollars + 
			"&L_PAYMENTREQUEST_0_QTY0=" + 1 + 
			"&L_PAYMENTREQUEST_0_ITEMCATEGORY0=Digital" +
			"";
	}

	/**
	 * Return the name-value-pair parameters required for all paypal api calls 
	 * to authenticate the seller account.
	 */
	protected static String getAuthenticationData()
	{
		return 
			"&VERSION=69.0" +
			"&USER=" + API_USERNAME +
			"&PWD=" + API_PASSWORD +
			"&SIGNATURE=" + API_SIGNATURE +
			"";
	}
	
	/**
	 * Send off the given data to PayPal's API and return the result in key-value pairs.
	 * Validate the ACK return value from paypal and throw an exception if it isn't "Success".
	 */
	protected static HashMap doServerCall (String data)
	{
		log.info("Sending data to paypal: " + data);
	
		String response = "";
		try {
			URL postURL = new URL(API_URL);
			HttpURLConnection conn = (HttpURLConnection)postURL.openConnection();
			conn.setDoInput(true);
			conn.setDoOutput(true);
			conn.setConnectTimeout(3000);
			conn.setReadTimeout(7000);
			conn.setRequestMethod("POST");
			
			DataOutputStream output = new DataOutputStream(conn.getOutputStream());
			output.writeBytes(data);
			output.flush();
			output.close();

			// Read input from the input stream.
			int responseCode = conn.getResponseCode();
			if (responseCode != HttpURLConnection.HTTP_OK) {
				throw new RuntimeException("Error " + responseCode + ": " + conn.getResponseMessage());
			}
			
			BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			String line = null;
			while(((line = reader.readLine()) !=null)) {
				response = response + line;
			}
			reader.close();
		} catch(IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}

		log.info("Got response from paypal: " + response);
		if(response.length() <= 0) {
			throw new RuntimeException("Received empty response");
		}
		HashMap results = parsePaypalResponse(response);
		
		// first check for the new version (PAYMENTINFO_0_ACK)
		String ackString = results.get("PAYMENTINFO_0_ACK");
		if (ackString == null || !(ackString.equalsIgnoreCase("Success") || ackString.equalsIgnoreCase("SuccessWithWarning"))) {
			String errorCode = results.get("PAYMENTINFO_0_ERRORCODE");
			String errorLongMsg = results.get("PAYMENTINFO_0_LONGMESSAGE");
			if (errorCode != null && errorCode.trim().length() > 0) {
				throw new RuntimeException("Purchase Failed (code " + errorCode + "): " + errorLongMsg);
			}
			
			// sometimes API returns old version ACK instead of PAYMENTINFO_0_ACK
			ackString = results.get("ACK");
			if (ackString == null || !(ackString.equalsIgnoreCase("Success") || ackString.equalsIgnoreCase("SuccessWithWarning"))) {
				errorCode = results.get("L_ERRORCODE0");
				errorLongMsg = results.get("L_LONGMESSAGE0");
				throw new RuntimeException("Purchase Failed (code " + errorCode + "): " + errorLongMsg);
			}
		}
		
		return results;
	}
	
	/**
	 * Parse results from PayPal to a map of name/value pairs. Their format looks like: 
	 * "TOKEN=EC%2d80X519901R8632201&TIMESTAMP=2012%2d07%2d13T09%3a57%3a44Z&ACK=Success"
	 */
	protected static HashMap parsePaypalResponse (String data)
	{
		HashMap results = new HashMap();
		StringTokenizer tokenizer = new StringTokenizer(data, "&");
		while (tokenizer.hasMoreTokens()) {
			StringTokenizer tokenizer2 = new StringTokenizer(tokenizer.nextToken(), "=");
			if (tokenizer2.countTokens() != 2) {
				continue;
			}
			String key = decodeValue(tokenizer2.nextToken());
			String value = decodeValue(tokenizer2.nextToken());
			results.put(key.toUpperCase(), value);
		}
		return results;
	}
	
	/**
	 * Prepare a given string for transmission via HTTP. Spaces become %20, etc.
	 */
	protected static String encodeValue(String value)
	{
		try {
			return URLEncoder.encode(value, "UTF-8");
		} catch (Exception e) {
			e.printStackTrace();
			return "";
		}
	}
	
	/**
	 * Undo encoding of string that was sent via HTTP. %20 becomes a space, etc.
	 */
	protected static String decodeValue(String value)
	{
		try {
			return URLDecoder.decode(value, "UTF-8");
		} catch (Exception e) {
			e.printStackTrace();
			return "";
		}
	}
}


Kittehs


Lost Cat
Originally uploaded by apes_abroad.

We often make friends with stray dogs when we travel, because, well, there are a lot of them in the world. In Panagia, it’s cats. Cats on the rooftops, cats in the alleys, cats in the gutters caterwauling at dusk. They’re all quite skittish so I took it as a challenge to get one of them to trust me.

I started feeding tiny bits of sausage once a day to a squirlish black and white cat with beautiful green eyes that just stare and stare into mine when I speak to her. The first day I only got her halfway up the steps. The second day she came to the front door and almost ate out of my hand. On the third day she lept to the deck, bolted across the floor and ran into the kitchen, earning herself much yelling and commotion, and the nickname “Houserunner”.

Some days Houserunner brings another cat with her who I call “Little Brother” (but is more likely her sister or daughter). This one has the same beautiful eyes and inquisitive stare, and enjoys hiding in the “cat cave” underneath my seat on the deck.

During our first week in Panagia we passed a very pregnant calico, but soon forgot her. Last week she reappeared with three kittens in tow, presumably now 3-4 weeks old. They moved in to a drainage hole in the house across the street, and we had a clear view from our deck of them frolicking in the bushes and clambering around the ruined building next door.

This morning we came out to find only a single black kitten alone in front of their home. He sat there and mewed tiny mournful mews for hours. Where was mom? Where were brother and sister? It was heartbreaking, and made it hard to concentrate. If happiness is a kitten, then sadness is a lost kitten. Eventually there came an answering meow from down the road, and the little guy perked up and scrambled off in that direction.

Several hours later we heard the same mewing and he returned. I brought him a saucer of milk and a nibblet of sausage, mostly to see if he was eating solid food. He went for the sausage, so that answered that. More mewing, then finally came that answering meow again and off he went. What’s going on? Is the little kitten just getting lost? Or is mom ditching him on purpose?

Colin doesn’t like it when I feed strays. Not because they’ll become pests, though that’s part of it. He feels bad for how confused and lost they’ll be when we stop feeding them. Like they’ll get used to free food and forget how to forage for themselves, then after we go they’ll pine for that happy full feeling. I’m not sure if Colin’s right, but it sure is sad. *sniff*

We love strays but for god’s sake people, spay and neuter your pets!

Edit: But wait there’s moar! A young spotted mom (The Lynx) just arrived in the ruined house next door with a small army of kittens in tow. Houserunner and Little Bro are there too playing with the kittens. It’s a monstrous pile of adorable. How the hell are they going to feed them all??

Edit #2: Lynx and Little Sister (definitely female) seem to be living as a family unit with at least 10 kittens. I didn’t know cats did this! They’re both nursing and don’t seem to know or care whose kittens are whose anymore. Today we watched them move their offspring up onto the hill, which took hours of back and forth and responding to the cries of kittens who got lost along the way (so that’s how that happens). Little Sis got fed up by the end and carried the stragglers up by the scruff of their necks.

Rebuild’s on sale. Why? #BecauseWeMay

Rebuild 99 cent saleThis weekend I and a host of other indie developers put our games on sale #BecauseWeMay. Last week Amazon had the same idea and put Rebuild on sale for 99 cents, and although I’m personally fine with whatever they do and it did make me money, I didn’t have the option to say no. Other platforms do the opposite: they set a price in stone and don’t let developers put their own games on sale. So #BecauseWeMay is an acknowledgement of those platforms (like the Apple App Store, Google Play, and Steam) that let developers choose their own prices. Super Office Stress took the message to heart and actually raised their price to 99 bucks.

In other Rebuild news, I’ve done a few more interviews recently, and wrote a postmortem where I describe the ups and downs of writing Flash games. All this keeps getting me thinking about a sequel…

But mostly I’ve been beavering away with Colin at Incredipede. I hope he’ll want to post something soon because it’s looking great!

EDIT: Amazon’s decided to feature/discount Rebuild again this weekend, so you can go get Rebuild for your Kindle Fire. How much will it cost? Only Amazon knows!