Je me suis quelque peu énervé avec les dernières versions de Twine, les 2.1 et 2.1.1. Impossible de les faire tourner convenablement sur l’un quelconque de mes ordinateurs, que ce soit à la maison ou au bureau. Sous Linux ou Windows. Je me suis même laissé aller à un commentaire peu amène sur le forum…
Pourquoi Twee2 ?
Du coup, j’en reviens à Twee2. En gros, il s’agit d’un compilateur en ligne de commande pour des jeux Twine. Il présente quelques avantages en terme de souplesse d’utilisation :
- Possibilité de scinder l’aventure en fichiers multiples
- Facilité d’un travail collectif dans un dépôt Github
- Utilisation possible de Sass pour les feuilles de style
- Utilisation possible de Coffeescript pour le javascript
- Édition du code dans Atom, autrement plus confortable que l’éditeur des passages dans Twine
Pour peu que Ruby soit installé sur votre machine, rien de plus simple que d’installer Twee2 :
$ gem install twee2
Remarquez que je n’ai pas utilisé sudo
. Car installer des gems au niveau du système peut poser de sérieux problèmes de sécurité. Je reviendrais sur l’installation de Ruby dans un billet ultérieur.
Les soucis
Si vous compilez en l’état un fichier Twine, vous allez avoir deux problèmes :
- L’auteur de Twee2, Dan-Q a choisi de ne pas traiter des passages aux noms spéciaux, comme StoryMenu. Ils étaient utilisés dans Twine 1.4, abandonnés dans les premières versions de Sugarcube, puis réintroduits dans les plus récentes.
- La version de Sugarcube utilisée n’est pas non plus à jour. Les dernières livraisons ont apporté quelques ajouts que j’apprécie de pouvoir utiliser, comme la macro
<<capture>>
.
Où est Twee2 ?
Pour réaliser cette mise à jour, il faut d’abord localiser où est installé Twee2 sur votre ordinateur. La procédure est identique sous Windows et Linux, seuls les noms des dossiers vont changer. Ouvrez un terminal de commande et entrez :
$ gem env
Vous obtiendrez un écran qui va ressembler à ceci :
Twee2 se trouve dans l’un des dossiers intitulés INSTALLATION DIRECTORY ou USER INSTALLATION DITECTORY. Ouvrez les l’un après l’autre jusqu’à repérer notre gem Twee2.
Deux fichiers sont ici à remplacer
format.js
et story_file.rb
.
Mettre à jour Sugarcube
Le premier fichier, format.js
est simplement la dernière version de Sugarcube2. Pour mettre à jour :
- Allez sur le site de Sugarcube
- Dans la section Downloads, récupérez l’archive de la dernière version pour Twine 2.0. Au 7/03/17, c’est la V2.14.
- Décompressez l’archive sur votre ordinateur
- Remplacez le fichier
format.js
dans le dossier Sugarcube2 de votre installation de Twee2 par celui extrait de l’archive. Et voilà.
Prise en compte des noms de passage spéciaux
Sur le dépôt Github de Twee2, un utilisateur, MCDemarco a posté une modification du logiciel permettant de faire accepter les noms de passage spéciaux comme StoryMenu. Malheureusement, Dan-Q, ne l’a pas prise en compte pour l’instant. Nous allons le faire nous-mêmes, à la main.
- Copiez le contenu du fichier ci-dessous (après avoir cliqué sur view raw)
- Ouvrez le fichier
story_file.rb
présent sur votre ordinateur - Remplacez-y le code d’origine par celui que vous venez de copier. Enregistrez. Et voilà…
- Vous pouvez également télécharger ce fichier sous forme d’archive en cliquant sur story_file.rb en bas du cadre de code.
require 'rubygems' | |
require 'haml' | |
require 'coffee_script' | |
require 'sass' | |
require 'builder' | |
module Twee2 | |
class StoryFileNotFoundException < Exception; end | |
class StoryFile | |
attr_accessor :passages | |
attr_reader :child_story_files | |
HAML_OPTIONS = { | |
remove_whitespace: true | |
} | |
Tilt::CoffeeScriptTemplate.default_bare = true # bare mode for HAML :coffeescript blocks | |
COFFEESCRIPT_OPTIONS = { | |
bare: true | |
} | |
# Loads the StoryFile with the given name | |
def initialize(filename) | |
raise(StoryFileNotFoundException) if !File::exists?(filename) | |
@passages, current_passage = {}, nil | |
@child_story_files = [] | |
# Load file into memory to begin with | |
lines = File::read(filename).split(/\r?\n/) | |
# First pass - go through and perform 'includes' | |
i, in_story_includes_section = 0, false | |
while i < lines.length | |
line = lines[i] | |
if line =~ /^:: *StoryIncludes */ | |
in_story_includes_section = true | |
elsif line =~ /^::/ | |
in_story_includes_section = false | |
elsif in_story_includes_section && (line.strip != '') | |
child_file = line.strip | |
# include a file here because we're in the StoryIncludes section | |
if File::exists?(child_file) | |
lines.push(*File::read(child_file).split(/\r?\n/)) # add it on to the end | |
child_story_files.push(child_file) | |
else | |
puts "WARNING: tried to include file '#{line.strip}' via StoryIncludes but file was not found." | |
end | |
elsif line =~ /^( *)::@include (.*)$/ | |
# include a file here because an @include directive was spotted | |
prefix, filename = $1, $2.strip | |
if File::exists?(filename) | |
lines[i,1] = File::read(filename).split(/\r?\n/).map{|l|"#{prefix}#{l}"} # insert in-place, with prefix of appropriate amount of whitespace | |
i-=1 # process this line again, in case of ::@include nesting | |
else | |
puts "WARNING: tried to ::@include file '#{filename}' but file was not found." | |
end | |
end | |
i+=1 | |
end | |
# Second pass - parse the file | |
lines.each do |line| | |
if line =~ /^:: *([^\[]*?) *(\[(.*?)\])? *(<(.*?)>)? *$/ | |
@passages[current_passage = $1.strip] = { tags: ($3 || '').split(' '), position: $5, content: '', exclude_from_output: false, pid: nil} | |
elsif current_passage | |
@passages[current_passage][:content] << "#{line}\n" | |
end | |
end | |
@passages.each_key{|k| @passages[k][:content].strip!} # Strip excessive trailing whitespace | |
# Run each passage through a preprocessor, if required | |
run_preprocessors | |
# Extract 'special' passages and mark them as not being included in output | |
story_css, pid, @story_js, @story_start_pid, @story_start_name = '', 0, '', nil, 'Start' | |
@passages.each_key do |k| | |
if k == 'StoryTitle' | |
Twee2::build_config.story_name = @passages[k][:content] | |
@passages[k][:exclude_from_output] = true | |
elsif k == 'StoryIncludes' | |
@passages[k][:exclude_from_output] = true # includes should already have been handled above | |
elsif @passages[k][:tags].include? 'stylesheet' | |
story_css << "#{@passages[k][:content]}\n" | |
@passages[k][:exclude_from_output] = true | |
elsif @passages[k][:tags].include? 'script' | |
@story_js << "#{@passages[k][:content]}\n" | |
@passages[k][:exclude_from_output] = true | |
elsif @passages[k][:tags].include? 'twee2' | |
eval @passages[k][:content] | |
@passages[k][:exclude_from_output] = true | |
else | |
@passages[k][:pid] = (pid += 1) | |
end | |
end | |
@story_start_pid = (@passages[@story_start_name] || {pid: 1})[:pid] | |
# Generate XML in Twine 2 format | |
@story_data = Builder::XmlMarkup.new | |
# TODO: what is tw-storydata's "options" attribute for? | |
@story_data.tag!('tw-storydata', { | |
name: Twee2::build_config.story_name, | |
startnode: @story_start_pid, | |
creator: 'Twee2', | |
'creator-version' => Twee2::VERSION, | |
ifid: Twee2::build_config.story_ifid, | |
format: '{{STORY_FORMAT}}', | |
options: '' | |
}) do | |
@story_data.style(story_css, role: 'stylesheet', id: 'twine-user-stylesheet', type: 'text/twine-css') | |
@story_data.script('{{STORY_JS}}', role: 'script', id: 'twine-user-script', type: 'text/twine-javascript') | |
@passages.each do |k,v| | |
unless v[:exclude_from_output] | |
@story_data.tag!('tw-passagedata', { pid: v[:pid], name: k, tags: v[:tags].join(' '), position: v[:position] }, v[:content]) | |
end | |
end | |
end | |
end | |
# Returns the rendered XML that represents this story | |
def xmldata | |
data = @story_data.target! | |
data.gsub('{{STORY_JS}}', @story_js) | |
end | |
# Runs HAML, Coffeescript etc. preprocessors across each applicable passage | |
def run_preprocessors | |
@passages.each_key do |k| | |
# HAML | |
if @passages[k][:tags].include? 'haml' | |
@passages[k][:content] = Haml::Engine.new(@passages[k][:content], HAML_OPTIONS).render | |
@passages[k][:tags].delete 'haml' | |
end | |
# Coffeescript | |
if @passages[k][:tags].include? 'coffee' | |
@passages[k][:content] = CoffeeScript.compile(@passages[k][:content], COFFEESCRIPT_OPTIONS) | |
@passages[k][:tags].delete 'coffee' | |
end | |
# SASS / SCSS | |
if @passages[k][:tags].include? 'sass' | |
@passages[k][:content] = Sass::Engine.new(@passages[k][:content], :syntax => :sass).render | |
end | |
if @passages[k][:tags].include? 'scss' | |
@passages[k][:content] = Sass::Engine.new(@passages[k][:content], :syntax => :scss).render | |
end | |
end | |
end | |
end | |
end |
Si ! Ça marche
Et pour vous montrez que tout ça fonctionne de façon satisfaisante, j’ai mis sur Github un dépôt contenant la version à la sauce Twee2 de mon démonstrateur Le mini-monde.
Mes excuses enfin aux lecteurs pour lesquels une bonne partie de cet article n’était que charabia indigeste. J’écris mes billets suivant deux rythmes, deux planifications différentes. Il y a d’abord ceux d’actualité : une manip’ que je viens de mener, une découverte du moment que j’ai envie de partager. Il y a (aura) aussi des billets à la parution plus raisonnée, plus pédagogique, prenant les choses par leur commencement. Je reviendrais donc dans cette série là sur l’installation de Ruby ou le choix des formats d’histoire pour Twine.
Laisser un commentaire